import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { randomBytes, randomUUID } from 'node:crypto'; import { writeFileSync } from 'node:fs'; const url = new URL(process.env.OPT_OUT_TEST_DATABASE_URL || ''); assert(['127.0.0.1', 'localhost'].includes(url.hostname) && url.pathname.startsWith('/cmpp_qa_optout_')); const redis = new URL(process.env.OPT_OUT_TEST_REDIS_URL || 'redis://127.0.0.1:16436'); 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', SEND_SUBMIT_OUTBOX_PUBLISH_ENABLED: 'true', }); // No workers/publishers/Gateway are started. Commands stay in this isolated database. const require = createRequire(new URL('../../api/package.json', import.meta.url)); Object.assign(process.env, { 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', }); require('reflect-metadata'); Object.defineProperty(BigInt.prototype, 'toJSON', { value() { return Number(this); }, configurable: true, }); const { NestFactory } = require('@nestjs/core'), { AppModule } = require('./dist/app.module'); const { PrismaService } = require('./dist/prisma/prisma.service'), { SessionService } = require('./dist/auth/session.service'); const { UsersService } = require('./dist/users/users.service'), { SendChainService } = require('./dist/send-chain/send-chain.service'); const { completionContext } = require('./dist/send-chain/completion-context'); const app = await NestFactory.create(AppModule, { logger: ['error'] }); app.setGlobalPrefix('api'); const pass = (name) => console.log('PASS', name); try { await app.listen(Number(process.env.OPT_OUT_TEST_PORT || 0), '127.0.0.1'); const base = (await app.getUrl()) + '/api', db = app.get(PrismaService), chain = app.get(SendChainService); const stamp = randomUUID().slice(0, 8), key = () => randomUUID(); const tenant = await db.tenant.create({ data: { name: '拒收策略验收企业', code: key() } }); const application = await db.smsApplication.create({ data: { tenantId: tenant.id, name: '拒收策略隔离应用', cmppAccount: key(), cmppEnterpriseCode: '000001', secretHash: 'unused', interfaceEnabled: false, templateMismatchMode: 'direct_send', }, }); const signature = await db.smsSignature.create({ data: { tenantId: tenant.id, applicationId: application.id, name: '【拒收验收】', auditStatus: 'approved' }, }); const content = signature.name + '文'.repeat(65); // 71 characters, two parts. assert.equal(content.length, 71); const template = await db.smsTemplate.create({ data: { tenantId: tenant.id, applicationId: application.id, signatureId: signature.id, name: '拒收策略验收模板' + stamp, content, auditStatus: 'approved', billingUnits: 2, }, }); const channel = await db.smsChannel.create({ data: { name: '隔离通道甲' + stamp, code: key(), carrier: 'mobile', carriers: ['mobile', 'unicom'], status: 'active', gatewayHost: '127.0.0.1', gatewayPort: 1, account: key(), passwordCipher: 'unused', srcId: '1069', sendRegion: '全国', unitPrice: 325, config: { serviceId: 'SMS' }, }, }); const backup = await db.smsChannel.create({ data: { name: '隔离通道乙', code: key(), carrier: 'mobile', carriers: ['mobile'], status: 'active', gatewayHost: '127.0.0.1', gatewayPort: 1, account: key(), passwordCipher: 'unused', srcId: '1069', sendRegion: '全国', unitPrice: 325, config: { serviceId: 'SMS' }, }, }); for (const c of [channel, backup]) { await db.cmppConnectionState.create({ data: { channelId: c.id, connectionId: key(), status: 'connected', currentConnections: 1 }, }); await db.channelSignatureReportTask.create({ data: { tenantId: tenant.id, signatureId: signature.id, channelId: c.id, carrier: 'mobile', approvalScope: 'carrier_specific', status: 'approved', }, }); } const group = await db.smsChannelGroup.create({ data: { code: key(), name: '隔离移动组', carrier: 'mobile', items: { create: [ { channelId: channel.id, carrier: 'mobile', priority: 1 }, { channelId: backup.id, carrier: 'mobile', priority: 2 }, ], }, }, }); const unicom = await db.smsChannelGroup.create({ data: { code: key(), name: '隔离联通组', carrier: 'unicom', items: { create: { channelId: channel.id, carrier: 'unicom' } }, }, }); await db.channelRouteRule.create({ data: { tenantId: tenant.id, applicationId: application.id, groupId: group.id, carrier: 'mobile' }, }); await db.channelRouteRule.create({ data: { tenantId: tenant.id, applicationId: application.id, groupId: unicom.id, carrier: 'unicom' }, }); const users = app.get(UsersService), sessions = app.get(SessionService); const user = await users.create({ username: 'optout' + 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 = (path, body, method = body === undefined ? 'GET' : 'PUT', head = headers) => fetch(base + path, { method, headers: head, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }); const path = '/admin/enterprise-templates/' + template.id + '/opt-out-policy'; assert.equal((await req(path, undefined, 'GET', {})).status, 401); 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); assert.equal( (await req(path, undefined, 'GET', { cookie: sessions.cookieName('client') + '=' + clientSession.token })).status, 401, ); assert.equal((await req(path, { rules: [], preserveFragments: false })).status, 400); assert.equal( (await req(path, { rules: [{ channelId: 'foreign', action: 'add' }], preserveFragments: true })).status, 400, ); const rules = [ { channelId: channel.id, action: 'add' }, { channelId: backup.id, action: 'remove' }, ]; assert.equal((await req(path, { rules, preserveFragments: true })).status, 200); assert.deepEqual((await req(path).then((r) => r.json())).rules, rules); assert.equal( await db.operationLog.count({ where: { resourceId: template.id, action: 'sms_template.opt_out_policy.update' } }), 1, ); pass('real HTTP policy save/read, authentication, scope, mandatory fragment protection and durable audit'); const reduced = await req('/admin/channels/' + channel.id, { carriers: ['mobile'] }); assert.equal(reduced.status, 200, await reduced.text()); assert.deepEqual((await db.smsChannel.findUnique({ where: { id: channel.id } })).carriers, ['mobile']); assert.equal(await db.smsChannelGroupItem.count({ where: { groupId: unicom.id, channelId: channel.id } }), 0); assert.equal(await db.smsChannelGroupItem.count({ where: { groupId: group.id, channelId: channel.id } }), 1); pass('carrier reduction removes incompatible members and preserves supported group members'); const batch = await db.smsBatchTask.create({ data: { tenantId: tenant.id, applicationId: application.id, taskNo: key(), content, phoneTotal: 1 }, }); const message = await db.smsMessageRecord.create({ data: { tenantId: tenant.id, applicationId: application.id, signatureId: signature.id, batchTaskId: batch.id, messageId: key(), phoneNumber: '13800138000', carrier: 'mobile', content, billingUnits: 2, unitPrice: 425, amountCents: 850, }, }); // No templateId intentionally: direct_send must still match its template. const route = await chain.selectChannelForMessage(message); assert.equal(route.contentPolicy.content, content + '拒收请回复R'); await chain.submitMessageToGateway(message, route, 0); const first = await db.smsSubmitRecord.findFirstOrThrow({ where: { messageRecordId: message.id } }); const written = await db.smsMessageRecord.findUniqueOrThrow({ where: { id: message.id } }); const outbox = await db.gatewaySubmitOutbox.findUniqueOrThrow({ where: { submitId: first.submitId } }); assert.equal(written.originalContent, content); assert.equal(written.content, outbox.payload.content); assert.equal(first.sentContent, written.content); assert.equal(written.billingUnits, 2); assert.equal(Number(written.amountCents), 850); assert.equal(Number(first.costAmountCents), 650); const route2 = await chain.selectChannelForMessage(written, { excludeChannelIds: [channel.id] }); await chain.submitMessageToGateway(written, route2, 1, first.id); const retried = await db.smsMessageRecord.findUniqueOrThrow({ where: { id: message.id } }); assert.equal(retried.content, content); assert.equal(retried.originalContent, content); assert.equal( (await db.gatewaySubmitOutbox.findUniqueOrThrow({ where: { submitId: first.submitId } })).payload.content, content + '拒收请回复R', ); assert.equal(await db.gatewaySubmitOutbox.count({ where: { messageRecordId: message.id, status: 'pending' } }), 2); pass('single submit and alternate-channel retry: real transactional content/Outbox snapshots and unchanged charges'); const before = await db.smsSubmitRecord.count(); await assert.rejects( db.$transaction((tx) => completionContext.run({ tx, messageRecordId: message.id }, async () => { await chain.submitMessageToGateway(retried, route, 2); throw new Error('rollback verification'); }), ), /rollback verification/, ); assert.equal(await db.smsSubmitRecord.count(), before); assert.equal((await db.smsMessageRecord.findUniqueOrThrow({ where: { id: message.id } })).content, content); pass('failed transaction rolls back content, submit and Outbox together'); const gateway = chain.submission.gatewaySubmit; const many = []; for (let i = 0; i < 2; i++) many.push( await db.smsMessageRecord.create({ data: { tenantId: tenant.id, applicationId: application.id, signatureId: signature.id, batchTaskId: batch.id, messageId: key(), phoneNumber: '13800138000', carrier: 'mobile', content, billingUnits: 2, }, }), ); await gateway.processSendJobBatch(many.map((m) => ({ messageRecordId: m.id }))); for (const m of many) { const row = await db.smsMessageRecord.findUniqueOrThrow({ where: { id: m.id } }); assert.equal(row.content, content + '拒收请回复R'); assert.equal(row.originalContent, content); } pass('microbatch uses the same policy and persists original text'); for (const state of [ { status: 'delivered', submitStatus: 'accepted', receiptStatus: 'delivered' }, { status: 'submit_failed', submitStatus: 'rejected' }, { status: 'failed', submitStatus: 'accepted', receiptStatus: 'undelivered' }, { status: 'timeout', submitStatus: 'accepted' }, ]) { await db.smsMessageRecord.create({ data: { tenantId: tenant.id, applicationId: application.id, signatureId: signature.id, messageId: key(), phoneNumber: '13800138000', content, ...state, }, }); } const quality = await req('/admin/operations/signature-quality?keyword=' + encodeURIComponent(tenant.name)).then( (r) => r.json(), ); const stat = quality.items.find((i) => i.signatureId === signature.id); assert.equal(stat.total, stat.successCount + stat.submitFailureCount + stat.failureCount + stat.unknownCount); assert.equal(stat.failureCount, 1); assert(stat.unknownCount >= 1); pass('real quality query yields four exclusive categories, including timeout without receipt'); if (process.env.OPT_OUT_TEST_BROWSER === 'true') { const { chromium, } = require('C:/Users/hectorzhao/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); const browser = await chromium.launch({ channel: 'msedge', headless: true }); try { const page = await browser.newPage(); const errors = []; page.on('pageerror', (e) => errors.push(e.message)); const ui = process.env.OPT_OUT_TEST_UI_URL || 'http://127.0.0.1:17438'; assert.equal(new URL(ui).hostname, '127.0.0.1'); const auth = await req('/admin/auth/session').then((r) => r.json()); await page .context() .addCookies([{ name: cookie, value: session.token, url: ui, httpOnly: true, sameSite: 'Lax' }]); await page.addInitScript((data) => localStorage.setItem('cmpp-auth-session:admin', JSON.stringify(data)), auth); for (const [width, height] of [ [1600, 1000], [1366, 768], [390, 844], ]) { await page.setViewportSize({ width, height }); await page.goto(ui + '/#/admin/enterprise-templates'); const row = page.locator('.admin-enterprise-template-row').filter({ hasText: template.name }).first(); await row.getByRole('button', { name: '拒收指令', exact: true }).click(); await page.getByLabel(channel.name + '的拒收指令').waitFor(); assert(await page.getByLabel('避免影响消息分片数').isDisabled()); await page.screenshot({ path: `.local-data/template-optout-20260920/template-${width}.png`, fullPage: true }); await page.getByRole('button', { name: '保存策略', exact: true }).click(); await page.getByRole('heading', { name: '模板拒收指令', exact: true }).waitFor({ state: 'hidden' }); await page.goto(ui + '/#/admin/analytics'); await page.locator('.quality-status-bar__track').first().waitFor(); assert.match(await page.locator('.quality-status-bar__track').first().getAttribute('title'), /未收到回执/); await page.screenshot({ path: `.local-data/template-optout-20260920/quality-${width}.png`, fullPage: true }); await page.reload(); await page.locator('.quality-status-bar__track').first().waitFor(); await page.goto(ui + '/#/admin/sms-records'); await page .locator('.admin-sms-record-card') .filter({ hasText: '拒收请回复R' }) .first() .getByRole('button', { name: '查看发送详情' }) .click(); await page.getByRole('heading', { name: '原始短信内容', exact: true }).waitFor(); await page.getByRole('heading', { name: /第 1 次提交通道内容/ }).waitFor(); await page.screenshot({ path: `.local-data/template-optout-20260920/detail-${width}.png`, fullPage: true }); await req('/admin/channels/' + channel.id, { carriers: ['mobile', 'unicom'] }); await page.goto(ui + '/#/admin/channels'); await page.getByLabel('通道名称', { exact: true }).fill(channel.name); await page.getByRole('button', { name: '查询', exact: true }).click(); await page .locator('.sms-channel-table__row') .filter({ hasText: channel.name }) .getByRole('button', { name: '编辑', exact: true }) .click(); await page.getByRole('heading', { name: '编辑通道', exact: true }).waitFor(); await page.getByRole('checkbox', { name: '联通', exact: true }).uncheck(); assert(await page.getByRole('button', { name: '确认', exact: true }).isDisabled()); await page.getByRole('checkbox', { name: /我已确认移除运营商/ }).check(); await page.screenshot({ path: `.local-data/template-optout-20260920/channel-${width}.png`, fullPage: true }); await page.getByRole('button', { name: '确认', exact: true }).click(); await page.getByRole('heading', { name: '编辑通道', exact: true }).waitFor({ state: 'hidden' }); assert.deepEqual((await db.smsChannel.findUniqueOrThrow({ where: { id: channel.id } })).carriers, ['mobile']); } assert.deepEqual(errors, []); pass('real API browser saves, locked checkbox, route/refresh, four-segment bar and three sizes'); } finally { await browser.close(); } } assert.equal( await db.gatewaySubmitOutbox.count({ where: { messageRecordId: { in: [message.id, ...many.map((m) => m.id)] }, status: { not: 'pending' } }, }), 0, ); writeFileSync( '.local-data/template-optout-20260920/fixture.json', JSON.stringify({ tenantId: tenant.id, templateId: template.id, messageId: message.id }), ); pass('no command published, no Gateway/SMSC started, no external SMS sent'); } finally { await app.close(); }