import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { randomBytes, randomUUID } from 'node:crypto'; import { mkdirSync } from 'node:fs'; import path from 'node:path'; const url = new URL(process.env.OPERATIONS_TEST_DATABASE_URL || ''); assert(['127.0.0.1', 'localhost'].includes(url.hostname) && url.pathname.startsWith('/cmpp_qa_operations_six_')); const redis = new URL(process.env.OPERATIONS_TEST_REDIS_URL || 'redis://127.0.0.1:16452'); assert(['127.0.0.1', 'localhost'].includes(redis.hostname)); Object.assign(process.env, { NODE_ENV: 'test', DATABASE_URL: url.toString(), REDIS_URL: redis.toString(), HTTP_API_MASTER_KEY: randomBytes(32).toString('hex'), MINIO_ENDPOINT: '127.0.0.1:19400', GATEWAY_CONTROL_URL: 'http://127.0.0.1:19401', SIGNATURE_ANALYTICS_ENABLED: 'false', HOME_DASHBOARD_ENABLED: 'false', GATEWAY_STARTUP_RECONNECT_DELAY_MS: '3600000', GATEWAY_CONNECTION_RECONCILER_DISABLED: 'true', GATEWAY_CONNECTING_TIMEOUT_SCANNER_DISABLED: 'true', SMS_RECEIPT_TIMEOUT_SCAN_ENABLED: 'false', SMS_SCHEDULED_DISPATCH_SCAN_ENABLED: 'false', CMPP_INBOUND_LONG_MESSAGE_SCAN_ENABLED: 'false', UPSTREAM_RECEIPT_INBOX_SCAN_ENABLED: 'false', CMPP_DOWNSTREAM_REQUEUE_TASK_SCAN_ENABLED: 'false', CMPP_PROCESS_ROLE: 'api', API_ENABLE_SEND_WORKER: 'false', CMPP_INBOUND_WORKFLOW_WORKER_ENABLED: 'false', }); // Only isolated fixtures are written. No send API, worker, or Gateway is started. const require = createRequire(new URL('../../api/package.json', import.meta.url)); require('reflect-metadata'); Object.defineProperty(BigInt.prototype, 'toJSON', { value() { return Number(this); }, configurable: true, }); const { NestFactory } = require('@nestjs/core'); const { AppModule } = require('./dist/app.module'); const { PrismaService } = require('./dist/prisma/prisma.service'); const { UsersService } = require('./dist/users/users.service'); const { SessionService } = require('./dist/auth/session.service'); const { HomeProjection } = require('./dist/home-dashboard/home-projection'); const { todayKey, startOfDay } = require('./dist/signature-analytics/analytics-date'); const { hourlySendTrend } = require('./dist/home-dashboard/home-read'); const app = await NestFactory.create(AppModule, { logger: ['error'] }); app.setGlobalPrefix('api'); let browser; const pass = (name) => console.log('PASS', name); try { await app.listen(Number(process.env.OPERATIONS_TEST_PORT || 17451), '127.0.0.1'); const base = (await app.getUrl()) + '/api'; const db = app.get(PrismaService), sessions = app.get(SessionService), users = app.get(UsersService); const stamp = randomUUID().slice(0, 8), key = () => randomUUID(); const tenant = await db.tenant.create({ data: { name: '六项验收企业' + stamp, code: key() } }); const makeApp = (name) => db.smsApplication.create({ data: { tenantId: tenant.id, name, cmppAccount: key(), cmppEnterpriseCode: '000001', secretHash: 'unused', interfaceEnabled: false, }, }); const application = await makeApp('通知应用' + stamp), otherApp = await makeApp('其他应用' + stamp); const makeSignature = (applicationId, name) => db.smsSignature.create({ data: { tenantId: tenant.id, applicationId, name, auditStatus: 'approved' } }); const signature = await makeSignature(application.id, '【验收甲】'), signature2 = await makeSignature(application.id, '【验收乙】'); const otherSignature = await makeSignature(otherApp.id, '【其他应用】'); const user = await users.create({ username: 'operations' + stamp, email: stamp + '@example.invalid', displayName: '隔离验收', password: randomBytes(24).toString('hex'), roleCode: 'platform_admin', }); const session = await sessions.create(user.id, 'admin', 0), cookie = sessions.cookieName('admin'); const headers = { 'content-type': 'application/json', cookie: cookie + '=' + session.token }; const req = async (route, body, method = body === undefined ? 'GET' : 'PUT', head = headers) => { const response = await fetch(base + route, { method, headers: head, ...(body === undefined ? {} : { body: JSON.stringify(body) }), }); return { status: response.status, data: await response.json() }; }; const templateBody = (name, sig = signature) => ({ tenantId: tenant.id, applicationId: sig.applicationId, signatureId: sig.id, name, content: sig.name + '验证码${code123}', variables: [{ name: 'code123', example: '中文示例', required: true }], }); const create = (body) => req('/admin/enterprise-templates', body, 'POST'); const customer = await users.create({ username: 'client' + stamp, email: 'c' + stamp + '@example.invalid', displayName: '隔离客户', password: randomBytes(24).toString('hex'), roleCode: 'enterprise_admin', tenantId: tenant.id, }); const clientSession = await sessions.create(customer.id, 'client', 0); const clientHeaders = { 'content-type': 'application/json', cookie: sessions.cookieName('client') + '=' + clientSession.token, }; const first = await create(templateBody('通知模板' + stamp)); assert.equal(first.status, 201, JSON.stringify(first)); const duplicate = await create(templateBody(' 通知模板' + stamp + ' ', signature2)); assert.equal(duplicate.status, 400); assert.match(duplicate.data.message, /同一.*应用|当前.*应用/); assert.equal((await create(templateBody('通知模板' + stamp, otherSignature))).status, 201); assert.equal((await req('/admin/enterprise-templates/' + first.data.id, { name: '通知模板' + stamp })).status, 200); const concurrent = await Promise.all([ create(templateBody('并发' + stamp)), create(templateBody('并发' + stamp, signature2)), ]); assert.deepEqual(concurrent.map((r) => r.status).sort(), [201, 400]); for (const variable of ['中文', 'a_b', 'a-b', 'a b', '12']) { const body = templateBody('非法' + key()); body.content = signature.name + '${' + variable + '}'; body.variables[0].name = variable; assert.equal((await create(body)).status, 400); } const numeric = templateBody('数字变量' + stamp); numeric.content = signature.name + '${123}'; numeric.variables[0].name = '123'; assert.equal((await create(numeric)).status, 201); const extra = await create(templateBody('修改前' + stamp)); assert.equal((await req('/admin/enterprise-templates/' + extra.data.id, { name: first.data.name })).status, 400); await db.smsTemplate.update({ where: { id: extra.data.id }, data: { auditStatus: 'deleted' } }); assert.equal((await create(templateBody('修改前' + stamp))).status, 201); assert.equal( (await req('/admin/enterprise-templates/' + extra.data.id + '/status', { status: 'approved' }, 'POST')).status, 400, ); assert.equal((await req('/admin/enterprise-templates', undefined, 'GET', {})).status, 401); const clientBody = templateBody('客户模板' + stamp); delete clientBody.tenantId; assert.equal((await req('/client/templates', clientBody, 'POST', clientHeaders)).status, 201); assert.equal( ( await req( '/client/templates', { ...clientBody, signatureId: signature2.id, content: signature2.name + '验证码${code123}' }, 'POST', clientHeaders, ) ).status, 400, ); assert.equal( ( await req( '/client/templates', { ...clientBody, name: '非法客户模板', content: signature.name + '${中文}', variables: [{ name: '中文' }] }, 'POST', clientHeaders, ) ).status, 400, ); assert.equal((await req('/admin/enterprise-templates', undefined, 'GET', clientHeaders)).status, 401); pass( 'real API application/name uniqueness across signatures, concurrent saves, rename, restore, cross-application and variable validation', ); const channel = await db.smsChannel.create({ data: { code: key(), name: '验收通道' + stamp, carrier: 'mobile', carriers: ['mobile', 'unicom', 'telecom'], status: 'active', gatewayHost: '127.0.0.1', gatewayPort: 1, account: key(), passwordCipher: 'unused', srcId: '1069', }, }); const groups = []; for (const [carrier, province] of [ ['mobile', '全国'], ['unicom', '全国'], ['unicom', '湖北'], ['telecom', '全国'], ]) { const group = await db.smsChannelGroup.create({ data: { code: key(), name: carrier + province + stamp, carrier, items: { create: { channelId: channel.id, carrier, province } }, }, }); groups.push(group); await db.channelRouteRule.create({ data: { tenantId: tenant.id, applicationId: application.id, groupId: group.id, carrier }, }); } const filters = { enterpriseKeyword: tenant.name, applicationKeyword: application.name, channelKeyword: channel.name, objectKeyword: signature.name, pageSize: '1', }; const details = await req('/admin/report-details?' + new URLSearchParams(filters)); assert.equal(details.status, 200); assert.equal(details.data.total, 3); assert.equal(details.data.items.length, 1); assert.equal(details.data.items[0].virtual, true); for (const field of ['enterpriseKeyword', 'applicationKeyword', 'channelKeyword', 'objectKeyword']) { const empty = await req('/admin/report-details?' + new URLSearchParams({ ...filters, [field]: '不匹配' })); assert.equal(empty.data.total, 0, field); } const page2 = await req('/admin/report-details?' + new URLSearchParams({ ...filters, page: '2' })); assert.notEqual(page2.data.items[0].id, details.data.items[0].id); pass('four independent AND search fields apply before pagination and retain virtual unreported rows'); const date = todayKey(new Date()), start = startOfDay(date); const beforeTrend = await hourlySendTrend(db, date); const messages = []; for (const [index, status] of ['submitted', 'unknown', 'delivered', 'failed', 'queued', 'rejected'].entries()) { messages.push( await db.smsMessageRecord.create({ data: { tenantId: tenant.id, applicationId: application.id, signatureId: signature.id, channelId: channel.id, messageId: key(), phoneNumber: '13800000000', content: signature.name + '隔离记录', status, queuedAt: new Date(+start + (index ? 3600000 : 0)), }, }), ); } await db.smsMessageRecord.create({ data: { messageId: key(), phoneNumber: '13800000000', content: '前日', status: 'delivered', queuedAt: new Date(+start - 1), }, }); await db.smsSubmitRecord.createMany({ data: [1, 2].map(() => ({ messageRecordId: messages[2].id, channelId: channel.id, submitId: key(), submitStatus: 'submitted', })), }); const records = await req( '/admin/operations/messages?' + new URLSearchParams({ applicationId: application.id, status: 'unknown', page: '1', pageSize: '20' }), ); assert.equal(records.status, 200); assert.deepEqual(records.data.items.map((r) => r.status).sort(), ['submitted', 'unknown']); const trend = await hourlySendTrend(db, date); assert.equal(trend.length, 24); assert.equal(trend[0].submittedCount - beforeTrend[0].submittedCount, 1); assert.equal(trend[1].submittedCount - beforeTrend[1].submittedCount, 5); assert.equal(trend[1].successCount - beforeTrend[1].successCount, 1); assert.equal(trend[2].submittedCount - beforeTrend[2].submittedCount, 0); await app.get(HomeProjection).tick(); const home = await req('/admin/operations/home/summary'); assert.equal(home.status, 200, JSON.stringify(home)); assert.deepEqual(home.data.hourlySendTrend, trend); pass( 'unknown maps to submitted and legacy unknown; home uses Beijing hours, zero fill and business rows, not supplier retries', ); // Force a failure after member deletion: the entire configuration transaction must roll back. await db.$executeRawUnsafe( `CREATE FUNCTION qa_reject_channel_audit() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.action='sms_channel.update' THEN RAISE EXCEPTION 'qa rollback'; END IF; RETURN NEW; END $$`, ); await db.$executeRawUnsafe( `CREATE TRIGGER qa_reject_channel_audit BEFORE INSERT ON "OperationLog" FOR EACH ROW EXECUTE FUNCTION qa_reject_channel_audit()`, ); assert.equal((await req('/admin/channels/' + channel.id, { carriers: ['mobile', 'telecom'] })).status, 500); assert.equal(await db.smsChannelGroupItem.count({ where: { channelId: channel.id } }), 4); assert.deepEqual((await db.smsChannel.findUnique({ where: { id: channel.id } })).carriers, [ 'mobile', 'unicom', 'telecom', ]); await db.$executeRawUnsafe('DROP TRIGGER qa_reject_channel_audit ON "OperationLog"'); await db.$executeRawUnsafe('DROP FUNCTION qa_reject_channel_audit()'); assert.equal((await req('/admin/channels/' + channel.id, { carriers: ['mobile', 'telecom'] })).status, 200); assert.equal(await db.smsChannelGroupItem.count({ where: { channelId: channel.id } }), 2); assert.equal(await db.channelRouteRule.count({ where: { applicationId: application.id } }), 4); const audit = await db.operationLog.findFirst({ where: { resourceId: channel.id, action: 'sms_channel.update' }, orderBy: { createdAt: 'desc' }, }); assert.equal(audit.detail.removedGroupItems.length, 2); await assert.rejects( db.smsChannelGroupItem.create({ data: { channelId: channel.id, groupId: groups[1].id, carrier: 'unicom' } }), /not compatible/, ); pass( 'carrier reduction is atomic, removes national/province memberships, preserves routes and other carriers, records audit and rejects stale membership writes', ); const { Client } = require('pg'); const writer = new Client({ connectionString: url.toString() }); await writer.connect(); let settled = false, insertion; try { await db.$transaction(async (tx) => { await tx.smsChannel.update({ where: { id: channel.id }, data: { carriers: ['mobile'] } }); insertion = writer .query('INSERT INTO "SmsChannelGroupItem" (id,"groupId","channelId",carrier) VALUES ($1,$2,$3,$4)', [ key(), groups[3].id, channel.id, 'telecom', ]) .then( () => ({ success: true }), (error) => ({ code: error.code }), ) .finally(() => { settled = true; }); await new Promise((resolve) => setTimeout(resolve, 100)); assert.equal(settled, false); await tx.smsChannelGroupItem.deleteMany({ where: { channelId: channel.id, groupId: groups[3].id } }); }); assert.equal((await insertion).code, '23514'); } finally { await writer.end(); } await db.smsChannel.update({ where: { id: channel.id }, data: { carriers: ['mobile', 'telecom'] } }); await db.smsChannelGroupItem.create({ data: { channelId: channel.id, groupId: groups[3].id, carrier: 'telecom' } }); pass('real concurrent member insertion waits for carrier change then rejects stale capability'); if (process.env.OPERATIONS_TEST_BROWSER_URL) { const uiUrl = new URL(process.env.OPERATIONS_TEST_BROWSER_URL); assert(['localhost', '127.0.0.1'].includes(uiUrl.hostname)); const { chromium } = await import(process.env.PLAYWRIGHT_MODULE || 'playwright'); const evidence = process.env.OPERATIONS_TEST_EVIDENCE_DIR; assert(evidence && path.isAbsolute(evidence)); mkdirSync(evidence, { recursive: true }); browser = await chromium.launch({ channel: 'msedge', headless: true }); const context = await browser.newContext(); await context.addCookies([ { name: cookie, value: session.token, url: uiUrl.origin, httpOnly: true, sameSite: 'Lax' }, ]); const auth = await req('/admin/auth/session'); assert.equal(auth.status, 200); await context.addInitScript( (value) => localStorage.setItem('cmpp-auth-session:admin', JSON.stringify(value)), auth.data, ); const page = await context.newPage(), errors = []; page.on('pageerror', (e) => errors.push(e.message)); for (const [width, height] of [ [1600, 1000], [1366, 768], [390, 844], ]) { await page.setViewportSize({ width, height }); await page.goto(uiUrl.origin + '/admin'); await page.getByRole('heading', { name: '小时发送曲线', exact: true }).waitFor(); await page.locator('[aria-label="小时发送曲线"] canvas').waitFor(); const curve = await page.getByRole('heading', { name: '小时发送曲线', exact: true }).boundingBox(); const rank = await page.getByRole('heading', { name: /企业消费排行/ }).boundingBox(); assert(curve.y < rank.y); await page.screenshot({ path: path.join(evidence, `home-${width}.png`), fullPage: true }); await page.reload(); await page.locator('[aria-label="小时发送曲线"] canvas').waitFor(); await page.goto(uiUrl.origin + '/admin/report-tasks'); for (const label of ['企业', '应用', '通道', '报备对象']) await page.getByRole('textbox', { name: label, exact: true }).waitFor(); await page.getByRole('textbox', { name: '企业', exact: true }).fill(tenant.name); await page.getByRole('button', { name: '查询', exact: true }).click(); await page.screenshot({ path: path.join(evidence, `reports-${width}.png`), fullPage: true }); await page.goto(uiUrl.origin + '/admin/enterprise-templates'); await page.getByRole('textbox', { name: '企业应用', exact: true }).fill(application.name); await page.getByRole('textbox', { name: '模板名称', exact: true }).fill(first.data.name); await page.getByRole('button', { name: '查询', exact: true }).click(); await page .getByRole('article') .filter({ hasText: first.data.name }) .filter({ hasText: application.name }) .getByRole('button', { name: '编辑', exact: true }) .click(); await page.getByRole('dialog').getByRole('button', { name: /^签名/ }).click(); await page.getByPlaceholder('搜索短信签名').fill('验收甲'); await page.getByRole('option', { name: signature.name, exact: true }).waitFor(); await page.screenshot({ path: path.join(evidence, `template-search-${width}.png`), fullPage: true }); await page.getByRole('option', { name: signature.name, exact: true }).click(); await page.getByRole('textbox', { name: /模板内容/ }).fill(signature.name + '${中文}'); assert(await page.getByRole('button', { name: '保存', exact: true }).isDisabled()); await page.getByRole('button', { name: '取消', exact: true }).click(); await page.getByRole('button', { name: '放弃并关闭', exact: true }).click(); await page.goto(uiUrl.origin + '/admin/channels'); await page.getByRole('textbox', { name: '通道名称', exact: true }).fill(channel.name); await page.getByRole('button', { name: '查询', exact: true }).click(); await page .getByRole('article') .filter({ hasText: channel.name }) .getByRole('button', { name: '编辑', exact: true }) .click(); await page.getByRole('checkbox', { name: '电信', exact: true }).uncheck(); await page .getByRole('alert') .filter({ hasText: /通道组/ }) .waitFor(); assert(await page.getByRole('button', { name: '确认', exact: true }).isDisabled()); await page.screenshot({ path: path.join(evidence, `carrier-warning-${width}.png`), fullPage: true }); await page.getByRole('checkbox', { name: /我已确认/ }).check(); assert(await page.getByRole('button', { name: '确认', exact: true }).isEnabled()); await page.getByRole('dialog').getByRole('button', { name: '关闭', exact: true }).last().click(); assert.equal(await db.smsChannelGroupItem.count({ where: { channelId: channel.id } }), 2); await page.goto(uiUrl.origin + '/admin/analytics'); await page.getByRole('heading', { name: '签名通道发送质量', exact: true }).waitFor(); await page .getByRole('columnheader', { name: '业务短信', exact: true, includeHidden: true }) .waitFor({ state: 'attached' }); assert.equal( await page.getByRole('columnheader', { name: '通道提交', exact: true, includeHidden: true }).count(), 0, ); await page.screenshot({ path: path.join(evidence, `quality-${width}.png`), fullPage: true }); await page.goto(uiUrl.origin + '/admin/sms-records'); await page.getByRole('button', { name: '发送状态', exact: true }).click(); await page.getByRole('option', { name: '未知', exact: true }).click(); const filteredResponse = page.waitForResponse( (r) => r.url().includes('/api/admin/operations/messages?') && new URL(r.url()).searchParams.get('status') === 'unknown', ); await page.getByRole('button', { name: '查询', exact: true }).click(); const filtered = await (await filteredResponse).json(); assert(filtered.items.some((r) => r.status === 'submitted')); await page.getByText('提交成功', { exact: true }).first().waitFor(); await page.screenshot({ path: path.join(evidence, `unknown-${width}.png`), fullPage: true }); } assert.deepEqual(errors, []); pass( 'real authenticated UI: three viewport sizes, refresh, routing, searchable signatures, validation, channel warning and cancel', ); } } finally { await browser?.close(); await app.close(); }