import { createHash } from 'node:crypto'; import { PrismaPg } from '../../api/node_modules/@prisma/adapter-pg/dist/index.js'; import { PrismaClient } from '../../api/node_modules/@prisma/client/index.js'; const prisma = new PrismaClient({ adapter: new PrismaPg( process.env.DATABASE_URL ?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public', ), }); const apiBaseUrl = process.env.API_BASE_URL ?? 'http://127.0.0.1:3101/api'; const tenantCode = 'smoke-tenant'; const username = 'smoke_client'; const email = 'smoke-client@example.com'; const phone = '13800138099'; const password = 'SmokePass123!'; function hashPassword(value) { return createHash('sha256').update(value).digest('hex'); } async function ensureSmokeData() { const tenant = await prisma.tenant.upsert({ where: { code: tenantCode }, update: { name: 'Smoke Test Enterprise', status: 'active' }, create: { code: tenantCode, name: 'Smoke Test Enterprise', status: 'active' }, }); const user = await prisma.user.upsert({ where: { username }, update: { tenantId: tenant.id, email, phone, displayName: 'Smoke Client Admin', passwordHash: hashPassword(password), status: 'active', failedLoginCount: 0, lockedUntil: null, deletedAt: null, }, create: { tenantId: tenant.id, username, email, phone, displayName: 'Smoke Client Admin', passwordHash: hashPassword(password), status: 'active', }, }); const enterpriseAdminRole = await prisma.role.upsert({ where: { code: 'enterprise_admin' }, update: { name: '企业管理员', scope: 'tenant' }, create: { code: 'enterprise_admin', name: '企业管理员', scope: 'tenant' }, }); await prisma.userRole.upsert({ where: { userId_roleId: { userId: user.id, roleId: enterpriseAdminRole.id } }, update: {}, create: { userId: user.id, roleId: enterpriseAdminRole.id }, }); await prisma.tenantAccount.upsert({ where: { tenantId: tenant.id }, update: { balanceCents: { increment: 0 }, smsUnits: { increment: 0 }, creditCents: 5000, status: 'active' }, create: { tenantId: tenant.id, balanceCents: 100000, smsUnits: 10000, creditCents: 5000, status: 'active' }, }); const channel = await prisma.smsChannel.upsert({ where: { code: 'SMOKE_CMPP' }, update: { name: 'Smoke CMPP Channel', carrier: 'all', sendRegion: '全国', gatewayHost: '127.0.0.1', gatewayPort: 17890, account: 'smoke-account', passwordCipher: 'smoke-password', srcId: '10690000', rateLimitPerSecond: 500, unitPrice: 5, status: 'active', }, create: { code: 'SMOKE_CMPP', name: 'Smoke CMPP Channel', carrier: 'all', protocol: 'CMPP', gatewayHost: '127.0.0.1', gatewayPort: 17890, account: 'smoke-account', passwordCipher: 'smoke-password', srcId: '10690000', rateLimitPerSecond: 500, unitPrice: 5, status: 'active', }, }); const group = await prisma.smsChannelGroup.upsert({ where: { code: 'SMOKE_GROUP' }, update: { name: 'Smoke Channel Group', carrier: 'mobile', status: 'active' }, create: { code: 'SMOKE_GROUP', name: 'Smoke Channel Group', carrier: 'mobile', status: 'active' }, }); const groupItem = await prisma.smsChannelGroupItem.findFirst({ where: { groupId: group.id, channelId: channel.id }, }); if (!groupItem) { await prisma.smsChannelGroupItem.create({ data: { groupId: group.id, channelId: channel.id, carrier: 'mobile', priority: 1, weight: 1, rateLimitPerSecond: 500 }, }); } else { await prisma.smsChannelGroupItem.update({ where: { id: groupItem.id }, data: { carrier: 'mobile', province: null, priority: 1, weight: 1, rateLimitPerSecond: 500 }, }); } let application = (await prisma.smsApplication.findFirst({ where: { tenantId: tenant.id, name: 'Smoke SMS App' } })) ?? (await prisma.smsApplication.create({ data: { tenantId: tenant.id, name: 'Smoke SMS App', scene: 'smoke', secretHash: hashPassword('smoke-secret'), dailyLimit: 100000, customerUnitPrice: 5, maxPhonesPerTask: 100000, templateMismatchMode: 'reject', status: 'active', }, })); application = await prisma.smsApplication.update({ where: { id: application.id }, data: { customerUnitPrice: 5, status: 'active' }, }); const routeRule = await prisma.channelRouteRule.findFirst({ where: { tenantId: tenant.id, applicationId: application.id, groupId: group.id, carrier: 'mobile', status: 'active' }, }); if (!routeRule) { await prisma.channelRouteRule.create({ data: { tenantId: tenant.id, applicationId: application.id, groupId: group.id, carrier: 'mobile', priority: 1, status: 'active' }, }); } await prisma.cmppConnectionState.upsert({ where: { applicationId_channelId_connectionId: { applicationId: application.id, channelId: channel.id, connectionId: 'smoke-conn-1' } }, update: { tenantId: tenant.id, applicationId: application.id, status: 'connected', desiredConnections: 1, currentConnections: 1, lastHeartbeatAt: new Date(), lastError: null, }, create: { tenantId: tenant.id, applicationId: application.id, channelId: channel.id, connectionId: 'smoke-conn-1', status: 'connected', desiredConnections: 1, currentConnections: 1, lastConnectedAt: new Date(), lastHeartbeatAt: new Date(), }, }); const signature = (await prisma.smsSignature.findFirst({ where: { tenantId: tenant.id, applicationId: application.id, name: '烟测签名' } })) ?? (await prisma.smsSignature.create({ data: { tenantId: tenant.id, applicationId: application.id, name: '烟测签名', purpose: 'smoke', auditStatus: 'approved', reportStatus: 'approved', }, })); const template = (await prisma.smsTemplate.findFirst({ where: { tenantId: tenant.id, applicationId: application.id, name: 'Smoke Verify Code' } })) ?? (await prisma.smsTemplate.create({ data: { tenantId: tenant.id, applicationId: application.id, signatureId: signature.id, name: 'Smoke Verify Code', content: '您的验证码为${code},5分钟内有效。', category: 'verification', auditStatus: 'approved', billingUnits: 1, variables: { create: [{ name: 'code', example: '123456', required: true }] }, }, })); const reportTask = await prisma.channelSignatureReportTask.findFirst({ where: { tenantId: tenant.id, signatureId: signature.id, channelId: channel.id }, }); if (!reportTask) { await prisma.channelSignatureReportTask.create({ data: { tenantId: tenant.id, signatureId: signature.id, channelId: channel.id, status: 'approved' }, }); } else if (reportTask.status !== 'approved') { await prisma.channelSignatureReportTask.update({ where: { id: reportTask.id }, data: { status: 'approved', reason: null } }); } return { tenant, user, channel, application, signature, template }; } async function request(path, options = {}) { const response = await fetch(`${apiBaseUrl}${path}`, { ...options, headers: { 'content-type': 'application/json', ...(options.headers ?? {}), }, }); const text = await response.text(); let body; try { body = text ? JSON.parse(text) : null; } catch { body = text; } if (!response.ok) { throw new Error(`${options.method ?? 'GET'} ${path} failed: ${response.status} ${text}`); } return body; } function assert(condition, message) { if (!condition) { throw new Error(message); } } async function run() { const data = await ensureSmokeData(); const tenantHeader = { 'x-tenant-id': data.tenant.id }; const health = await request('/health'); assert(health.status === 'ok', 'health should return ok'); const captcha = await request('/client/auth/captcha'); const captchaText = String(captcha.challenge.split('=')[0].split('+').map((part) => Number(part.trim())).reduce((sum, value) => sum + value, 0)); const login = await request('/client/auth/login', { method: 'POST', body: JSON.stringify({ login: email, password, captchaId: captcha.captchaId, captchaText }), }); assert(login.accessToken && login.user?.tenantId === data.tenant.id, 'login should return smoke tenant user'); const tenants = await request('/admin/tenants'); assert(tenants.some((tenant) => tenant.code === tenantCode), 'tenant list should contain smoke tenant'); const recharge = await request('/admin/billing/manual-recharges', { method: 'POST', body: JSON.stringify({ tenantId: data.tenant.id, amountCents: 1234, smsUnits: 10, operatorId: data.user.id, remark: 'real env smoke manual recharge', }), }); assert(recharge.id, 'manual recharge should create an order'); const transactions = await request('/client/billing/transactions', { headers: tenantHeader }); assert(transactions.some((item) => item.relatedId === recharge.id), 'transactions should include recharge delta'); const check = await request('/admin/billing/check', { method: 'POST', body: JSON.stringify({ tenantId: data.tenant.id, amountCents: 1, smsUnits: 1 }), }); assert(check.canSend === true, 'billing check should allow the smoke send'); const task = await request('/client/send/batch-tasks', { method: 'POST', body: JSON.stringify({ tenantId: data.tenant.id, applicationId: data.application.id, templateId: data.template.id, content: '您的验证码为123456,5分钟内有效。', category: 'verification', phones: ['13800138000', '13900139000'], variables: { code: '123456' }, createdById: data.user.id, sourceIp: '127.0.0.1', userAgent: 'real-env-smoke', }), }); assert(task.id && task.messages?.length === 2, 'send batch task should create two messages'); const taskList = await request('/client/operations/batch-tasks', { headers: tenantHeader }); assert(taskList.some((item) => item.id === task.id), 'client task list should include smoke task'); const upload = await request('/admin/files/presigned-upload', { method: 'POST', body: JSON.stringify({ objectKey: `smoke/${Date.now()}-upload.txt`, expiresInSeconds: 300 }), }); assert(upload.uploadUrl?.startsWith('http'), 'presigned upload should return an upload URL'); const putResponse = await fetch(upload.uploadUrl, { method: 'PUT', body: 'cmpp real env smoke upload', headers: { 'content-type': 'text/plain' }, }); assert(putResponse.ok, `MinIO presigned PUT should succeed, got ${putResponse.status}`); const log = await request('/admin/operation-logs', { method: 'POST', body: JSON.stringify({ tenantId: data.tenant.id, userId: data.user.id, action: 'smoke.verify', resource: 'real-env-smoke', resourceId: task.id, ipAddress: '127.0.0.1', userAgent: 'real-env-smoke', detail: { taskNo: task.taskNo, rechargeOrderId: recharge.id, objectKey: upload.objectKey }, }), }); assert(log.id, 'operation log should be created'); const logs = await request('/admin/operations/audit-logs', { headers: tenantHeader }); assert(logs.some((item) => item.id === log.id), 'operations audit logs should include smoke log'); const trace = await request(`/admin/operations/trace?tenantId=${data.tenant.id}&taskId=${task.id}`); assert(trace.messages?.length >= 2, 'trace should include smoke messages'); const reconciliation = await request(`/admin/operations/reconciliation?tenantId=${data.tenant.id}&taskId=${task.id}`); assert(reconciliation.messages?._count?._all >= 2, 'reconciliation should return message aggregate'); console.log( JSON.stringify( { ok: true, apiBaseUrl, tenantId: data.tenant.id, userId: data.user.id, rechargeOrderId: recharge.id, batchTaskId: task.id, messageCount: task.messages.length, uploadedObjectKey: upload.objectKey, operationLogId: log.id, }, null, 2, ), ); } run() .catch((error) => { console.error(error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });