import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { randomBytes, randomUUID } from 'node:crypto'; import { readFileSync } from 'node:fs'; const url = new URL(process.env.SIGNATURE_TEST_DATABASE_URL || ''); assert( ['127.0.0.1', 'localhost'].includes(url.hostname) && url.pathname.startsWith('/cmpp_qa_signature_unique_'), 'isolated local database required', ); const redisUrl = new URL(process.env.SIGNATURE_TEST_REDIS_URL || 'redis://127.0.0.1:16436'); assert(['127.0.0.1', 'localhost'].includes(redisUrl.hostname), 'isolated local Redis required'); Object.assign(process.env, { NODE_ENV: 'test', DATABASE_URL: url.toString(), REDIS_URL: redisUrl.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', }); 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 { SmsConfigService } = require('./dist/sms-config/sms-config.service'); const { SessionService } = require('./dist/auth/session.service'); const { UsersService } = require('./dist/users/users.service'); const { ReportImportReviewService } = require('./dist/report-materials/import-review.service'); const { FilesService } = require('./dist/files/files.service'); const { ReportImportParserService } = require('./dist/report-materials/import-parser.service'); const { SignatureNameConflict } = require('./dist/sms-config/signature-uniqueness'); const { Client } = require('pg'); const pass = (name) => console.log('PASS', name); const app = await NestFactory.create(AppModule, { logger: ['error'] }); app.setGlobalPrefix('api'); try { const db = app.get(PrismaService), sms = app.get(SmsConfigService); assert.equal(await db.smsSignature.count(), 0, 'fresh database required; do not delete existing data'); await app.listen(0, '127.0.0.1'); const base = (await app.getUrl()) + '/api'; const stamp = randomUUID().slice(0, 8); const tenant = await db.tenant.create({ data: { name: '签名唯一性隔离企业', code: stamp } }); const other = await db.tenant.create({ data: { name: '另一个隔离企业', code: stamp + 'b' } }); const makeApplication = (n) => db.smsApplication.create({ data: { tenantId: tenant.id, name: '隔离应用' + n, cmppAccount: stamp + n, cmppEnterpriseCode: 'test', secretHash: 'unused', interfaceEnabled: false, }, }); const a = await makeApplication('a'), b = await makeApplication('b'); const users = app.get(UsersService), sessions = app.get(SessionService); const admin = await users.create({ username: stamp, email: stamp + '@example.invalid', displayName: '隔离审核员', password: randomBytes(24).toString('hex'), roleCode: 'platform_admin', }); const client = await users.create({ username: stamp + 'c', email: stamp + 'c@example.invalid', displayName: '隔离客户', password: randomBytes(24).toString('hex'), roleCode: 'enterprise_admin', tenantId: tenant.id, }); async function headersFor(user, portal) { const session = await sessions.create(user.id, portal, 0); return { 'content-type': 'application/json', cookie: `${sessions.cookieName(portal)}=${session.token}` }; } const adminHeaders = await headersFor(admin, 'admin'), clientHeaders = await headersFor(client, 'client'); const body = { tenantId: tenant.id, applicationId: a.id, name: '【唯一验证】' }; const request = (path, data, headers = adminHeaders, method = 'POST') => fetch(base + path, { method, headers, body: JSON.stringify(data) }); const create = (data, headers) => request('/admin/enterprise-signatures', data, headers); assert.equal((await create(body, {})).status, 401); const first = await create(body); assert.equal(first.status, 201); const signature = await first.json(); const duplicate = await create(body); assert.equal(duplicate.status, 409); assert.match((await duplicate.json()).message, /同名有效签名/); const clientBody = { applicationId: a.id, name: body.name }; assert.equal((await request('/client/signatures', clientBody, clientHeaders)).status, 409); assert.equal( (await request('/client/signatures', clientBody, { ...clientHeaders, 'x-tenant-id': other.id })).status, 403, ); assert.equal((await request('/client/signatures', { name: '无括号' }, clientHeaders)).status, 400); pass('admin/client HTTP duplicate 409, authentication, tenant header isolation and format validation'); const unbound = await sms.createSignature({ tenantId: tenant.id, name: body.name }); await assert.rejects(() => sms.createSignature({ tenantId: tenant.id, name: body.name }), SignatureNameConflict); const secondApp = await sms.createSignature({ ...body, applicationId: b.id }); await sms.createSignature({ tenantId: other.id, name: body.name }); await sms.updateSignature(signature.id, { name: body.name, purpose: '保留用途' }); await assert.rejects(() => sms.updateSignature(secondApp.id, { applicationId: a.id }), SignatureNameConflict); await assert.rejects(() => sms.updateSignature(secondApp.id, { applicationId: null }), SignatureNameConflict); const renamed = await sms.createSignature({ ...body, name: '【另一个名称】' }); assert.equal( (await request('/admin/enterprise-signatures/' + renamed.id, { name: body.name }, adminHeaders, 'PUT')).status, 409, ); await assert.rejects( () => sms.updateSignature(signature.id, { purpose: '禁止跨租户' }, other.id), /Signature not found/, ); pass('self edit, rename, application change, null application scope and cross-tenant separation'); await sms.changeSignatureStatus(unbound.id, { status: 'disabled' }); const replacement = await sms.createSignature({ tenantId: tenant.id, name: body.name }); for (const restore of [ () => sms.changeSignatureStatus(unbound.id, { status: 'approved' }), () => sms.approveSignature(unbound.id, { reviewerId: admin.id }), () => sms.submitSignature(unbound.id), ]) { await assert.rejects(restore, SignatureNameConflict); } await sms.changeSignatureStatus(replacement.id, { status: 'deleted' }); await sms.changeSignatureStatus(unbound.id, { status: 'approved' }); pass('disable/delete release names; status restoration, review and submit cannot bypass uniqueness'); const races = await Promise.all(Array.from({ length: 12 }, () => create({ ...body, name: '【并发名称】' }))); assert.equal(races.filter((r) => r.status === 201).length, 1); assert.equal(races.filter((r) => r.status === 409).length, 11); assert.equal( await db.smsSignature.count({ where: { tenantId: tenant.id, applicationId: a.id, name: '【并发名称】' } }), 1, ); for (const applicationId of [a.id, null]) { await assert.rejects( () => db.smsSignature.create({ data: { tenantId: tenant.id, applicationId, name: body.name } }), (e) => e.code === 'P2002', ); } pass('12 concurrent real HTTP creates produce one row; both database indexes reject direct writes'); const files = app.get(FilesService), importer = new ReportImportReviewService(db, files, sms, new ReportImportParserService(db, files, sms)); await sms.updateSignature(signature.id, { drainageInfo: { signatureReportValues: { old: '必须保留', changed: '旧值' }, links: [{ url: 'https://example.invalid' }], }, }); const mappings = [ { sourceHeader: '签名', sourceColumnIndex: 0, targetFieldCode: 'name', targetKind: 'signatureName', fieldType: 'string', }, { sourceHeader: '新资料', sourceColumnIndex: 1, targetFieldCode: 'changed', targetKind: 'dynamic', fieldType: 'string', }, ]; const staged = await importer.stageSignatureRow(tenant.id, a.id, mappings, { name: body.name, changed: '新值' }); assert.equal(staged.operation, 'update'); assert.equal(staged.targetId, signature.id); const batch = { tenantId: tenant.id, applicationId: a.id, reportType: 'signature' }; const item = { reportType: 'signature', targetId: staged.targetId, payload: staged.payload }; assert.equal(await importer.applyImportItem(batch, item, admin.id), signature.id); const saved = await db.smsSignature.findUniqueOrThrow({ where: { id: signature.id } }); assert.equal(saved.purpose, '保留用途'); assert.equal(saved.drainageInfo.signatureReportValues.old, '必须保留'); assert.equal(saved.drainageInfo.signatureReportValues.changed, '新值'); assert.equal(saved.drainageInfo.links.length, 1); assert.equal(await importer.applyImportItem(batch, { ...item, targetId: null }, admin.id), signature.id); // A stopped historical duplicate must not be chosen over the effective signature. await db.smsSignature.create({ data: { ...body, auditStatus: 'disabled' } }); assert.equal( (await importer.stageSignatureRow(tenant.id, a.id, mappings, { name: body.name })).targetId, signature.id, ); pass('batch staging/apply preserves ID, unmapped materials, purpose and links; delayed create becomes update'); const concurrentItem = { ...item, targetId: null, payload: { ...item.payload, name: '【并发导入】' } }; const importedIds = await Promise.all( Array.from({ length: 3 }, () => importer.applyImportItem(batch, concurrentItem, admin.id)), ); assert.equal(new Set(importedIds).size, 1); assert.equal( await db.smsSignature.count({ where: { tenantId: tenant.id, applicationId: a.id, name: '【并发导入】' } }), 1, ); const { writeUniqueSignature } = require('./dist/sms-config/signature-uniqueness'); const raceIdentity = { ...body, name: '【数据库竞态】' }; await assert.rejects( () => writeUniqueSignature(db, raceIdentity, async () => { await db.smsSignature.create({ data: raceIdentity }); return db.smsSignature.create({ data: raceIdentity }); }), SignatureNameConflict, ); pass('concurrent batch imports converge to one signature and real P2002 maps to HTTP conflict'); // Exercise migration failure on a session-local shadow table, never modify application history. const pg = new Client({ connectionString: url.toString() }); await pg.connect(); try { await pg.query( 'CREATE TEMP TABLE "SmsSignature" ("tenantId" text, "applicationId" text, "name" text, "auditStatus" text)', ); await pg.query( `INSERT INTO "SmsSignature" VALUES ('history', NULL, 'duplicate', 'approved'), ('history', NULL, 'duplicate', 'pending')`, ); const sql = readFileSync( new URL('../../api/prisma/migrations/20260917120000_signature_active_name_unique/migration.sql', import.meta.url), 'utf8', ); await assert.rejects(() => pg.query(sql), /duplicate active signatures exist/); await pg.query('ROLLBACK'); assert.equal((await pg.query('SELECT count(*) FROM "SmsSignature"')).rows[0].count, '2'); assert.equal( (await pg.query("SELECT count(*) FROM pg_indexes WHERE schemaname LIKE 'pg_temp_%' AND tablename='SmsSignature'")) .rows[0].count, '0', ); } finally { await pg.end(); } pass('historical duplicates block migration and roll back without modifying records'); console.log('Signature uniqueness acceptance complete; no SMS, external notifications or live environment writes.'); } finally { await app.close(); }