import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import { createRequire } from 'node:module'; // Run once per encoding against a fresh, fully migrated local QA database. const url = new URL(process.env.SIGNATURE_TEST_DATABASE_URL || ''); assert(['127.0.0.1', 'localhost'].includes(url.hostname)); assert(url.pathname.startsWith('/cmpp_qa_signature_encoding_')); process.env.DATABASE_URL = url.toString(); process.env.CMPP_PROCESS_ROLE = 'api'; process.env.NODE_ENV = 'test'; const require = createRequire(new URL('../../api/package.json', import.meta.url)); require('reflect-metadata'); const { Prisma } = require('@prisma/client'); const { Module } = require('@nestjs/common'); const { NestFactory } = require('@nestjs/core'); const { PrismaService } = require('./dist/prisma/prisma.service'); const { leadingSignatureSql } = require('./dist/signature-analytics/signature-extraction'); const { unreportedRows } = require('./dist/signature-analytics/analytics-aggregate'); const { SignatureRetirementService } = require('./dist/signature-retirement/signature-retirement.service'); const { SignatureRetirementController } = require('./dist/signature-retirement/signature-retirement.controller'); const { SignatureAnalyticsService } = require('./dist/signature-analytics/signature-analytics.service'); const { todayKey, addDays, startOfDay } = require('./dist/signature-analytics/analytics-date'); const db = new PrismaService(); const service = new SignatureRetirementService(db); const writer = new SignatureAnalyticsService(db); const today = todayKey(); const yesterday = addDays(today, -1); const target = '【宜都市万商市场投资有限公司】'; const pass = (name) => console.log('PASS', name); let app; try { assert.equal(await db.tenant.count(), 0, 'A fresh isolated database is required; never clear existing data'); const [{ server_encoding: encoding }] = await db.$queryRaw`SHOW server_encoding`; assert(['UTF8', 'SQL_ASCII'].includes(encoding)); console.log('Database encoding:', encoding); const cases = [ [target + '正文', target], ['【湘银物业】正文', '【湘银物业】'], ['【A】正文', '【A】'], ['【天地中文—123】正文', '【天地中文—123】'], ['【😀𠀀】正文', '【😀𠀀】'], ["【O'Reilly\\示例】正文", "【O'Reilly\\示例】"], ['【一\n二】正文', '【一\n二】'], ['【一\r\n二】正文', '【一\r\n二】'], ['【 】正文', '【 】'], ['【第一】正文【第二】', '【第一】'], ['【第一】【第二】', '【第一】'], ['【第二】', '【第二】'], ['【' + '中'.repeat(10000) + '】正文', '【' + '中'.repeat(10000) + '】'], ['', null], ['无签名正文', null], [' 正文【签名】', null], [' 【签名】正文', null], ['\n【签名】正文', null], ['\uFEFF【签名】正文', null], ['[签名]正文', null], ['【】正文', null], ['【未关闭', null], ['】错误【签名】', null], ['【外【内】外】', null], ['【【嵌套】', null], ["'; SELECT 1; --【签名】", null], ]; const extracted = await db.$queryRaw(Prisma.sql` SELECT v.value AS input,${leadingSignatureSql(Prisma.sql`v.value`)} AS signature FROM jsonb_array_elements_text(${JSON.stringify(cases.map(([input]) => input))}::jsonb) WITH ORDINALITY AS v(value,position) ORDER BY v.position`); assert.deepEqual( extracted.map((r) => [r.input, r.signature]), cases, ); const [old] = await db.$queryRaw(Prisma.sql`SELECT SUBSTRING(${target} FROM '^【[^【】]+】') AS signature`); assert.equal(old.signature, encoding === 'SQL_ASCII' ? null : target); pass('26 extraction cases; original SQL_ASCII failure reproduced; UTF8 semantics retained'); const tenant = await db.tenant.create({ data: { name: '编码验收企业', code: randomUUID() } }); const foreign = await db.tenant.create({ data: { name: '其他企业', code: randomUUID() } }); const createApplication = (owner, name) => db.smsApplication.create({ data: { tenantId: owner.id, name, cmppAccount: randomUUID(), cmppEnterpriseCode: '000001', secretHash: 'isolated-no-login', interfaceEnabled: false, }, }); const own = await createApplication(tenant, '应用甲'); const other = await createApplication(tenant, '应用乙'); const foreignApp = await createApplication(foreign, '其他企业应用'); const signature = (application, name, auditStatus = 'approved') => db.smsSignature.create({ data: { tenantId: application.tenantId, applicationId: application.id, name, auditStatus }, }); const registered = await signature(own, '【已登记无通道报备】'); await signature(own, '【待审核签名】', 'pending'); await signature(own, '【已删除签名】', 'deleted'); await signature(other, target); await signature(foreignApp, target); await signature(own, '【跨应用隔离】'); const message = (application, content, queuedAt, signatureId = null) => ({ messageId: randomUUID(), tenantId: application.tenantId, applicationId: application.id, signatureId, phoneNumber: '13800000000', content, queuedAt, status: 'failed', errorCode: 'SIGNATURE', }); const expected = [ { applicationId: own.id, signatureName: target, messageCount: 64 }, { applicationId: own.id, signatureName: '【已删除签名】', messageCount: 1 }, { applicationId: other.id, signatureName: '【跨应用隔离】', messageCount: 1 }, ...Array.from({ length: 10 }, (_, i) => ({ applicationId: other.id, signatureName: `【分页${i}】`, messageCount: 1, })), ]; const summarize = (items) => items .map(({ applicationId, signatureName, messageCount }) => ({ applicationId, signatureName, messageCount })) .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); for (const date of [today, yesterday]) { const start = startOfDay(date); const end = startOfDay(addDays(date, 1)); const rows = Array.from({ length: 64 }, (_, i) => message(own, target + '隔离正文', i === 63 ? new Date(end.getTime() - 1) : start), ); rows.push( message(own, '【已删除签名】正文', start), message(other, '【跨应用隔离】正文', start), message(own, registered.name + '正文', start), message(own, '【待审核签名】正文', start), message(other, target + '正文', start), message(foreignApp, target + '正文', start), message(own, '【已关联记录】正文', start, registered.id), { ...message(own, '【无应用】正文', start), applicationId: null }, ...expected.slice(3).map((item) => message(other, item.signatureName + '正文', start)), ...cases.filter(([, expectedSignature]) => expectedSignature === null).map(([text]) => message(own, text, start)), ); await db.smsMessageRecord.createMany({ data: rows }); assert.deepEqual(summarize((await service.unreportedSignaturesLive({ date })).items), summarize(expected)); assert.deepEqual(summarize(await unreportedRows(db, date)), summarize(expected)); } await db.smsMessageRecord.createMany({ data: [ message(own, '【下一天】正文', startOfDay(addDays(today, 1))), message(own, '【前一天】正文', new Date(startOfDay(yesterday).getTime() - 1)), ], }); assert.deepEqual(summarize((await service.unreportedSignatures({ date: today })).items), summarize(expected)); const generated = await writer.generate(yesterday); assert.equal(generated.skipped, false); const historical = await service.unreportedSignatures({ date: yesterday }); assert.equal(historical.dataSource, 'report'); assert.deepEqual(summarize(historical.items), summarize(expected)); assert.equal(await db.unreportedSignatureDaily.count(), expected.length); pass( '64-message reproduction, date boundaries, tenant/application isolation, existing/deleted/linked signatures, real daily persistence', ); // Real controller and service over HTTP, without the application schedulers or sending processes. // Authentication is unchanged and belongs to the full application, not this isolated controller harness. class Harness {} Module({ controllers: [SignatureRetirementController], providers: [ { provide: PrismaService, useValue: db }, { provide: SignatureRetirementService, useValue: service }, ], })(Harness); app = await NestFactory.create(Harness, { logger: false }); await app.listen(0, '127.0.0.1'); const base = await app.getUrl(); const get = async (query, status = 200) => { const response = await fetch( `${base}/admin/signature-retirement/unreported-signatures?${new URLSearchParams(query)}`, { signal: AbortSignal.timeout(15000) }, ); assert.equal(response.status, status); return response.json(); }; for (const date of [today, yesterday]) { const first = await get({ date, page: '1', pageSize: '10' }); const second = await get({ date, page: '2', pageSize: '10' }); assert.equal(first.total, expected.length); assert.equal(second.total, expected.length); assert.deepEqual(summarize([...first.items, ...second.items]), summarize(expected)); for (const keyword of ['宜都市万商市场投资有限公司', '应用甲', '编码验收企业']) { const result = await get({ date, keyword }); const wanted = keyword.startsWith('宜都') ? expected.slice(0, 1) : keyword === '应用甲' ? expected.slice(0, 2) : expected; assert.deepEqual(summarize(result.items), summarize(wanted)); } assert.equal((await get({ date, keyword: '不存在的签名' })).total, 0); assert.equal((await get({ date, keyword: "' OR 1=1 --" })).total, 0); } await get({ date: '2026-02-30' }, 400); await get({ date: today, page: '0' }, 400); pass( 'real HTTP live/report results, independent pagination, signature/company/application search, empty results and invalid parameters', ); const frozenDate = addDays(today, -5); await db.smsMessageRecord.create({ data: message(own, target, startOfDay(frozenDate)) }); await writer.generate(frozenDate, true); const before = await service.unreportedSignatures({ date: frozenDate }); await db.smsMessageRecord.create({ data: message(own, target, startOfDay(frozenDate)) }); assert.equal((await writer.generate(frozenDate)).skipped, true); await assert.rejects(() => writer.generate(frozenDate, true), /历史补建不得覆盖/); const after = await service.unreportedSignatures({ date: frozenDate }); assert.deepEqual(after.items, before.items); assert.equal(after.items[0].messageCount, 1); pass('frozen history stays unchanged; no automatic historical rebuild'); assert.equal(await db.smsSubmitRecord.count(), 0); pass('no supplier submissions created'); } finally { if (app) await app.close(); await db.$disconnect(); }