230 lines
11 KiB
JavaScript
230 lines
11 KiB
JavaScript
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();
|
|
}
|