161 lines
8.1 KiB
JavaScript
161 lines
8.1 KiB
JavaScript
// Real Prisma/service integration in an isolated schema. No public business writes or SMS.
|
|
import assert from 'node:assert/strict';
|
|
import { randomUUID } from 'node:crypto';
|
|
import pg from '../../api/node_modules/pg/lib/index.js';
|
|
import prisma from '../../api/node_modules/@prisma/client/default.js';
|
|
import adapter from '../../api/node_modules/@prisma/adapter-pg/dist/index.js';
|
|
import service from '../../api/dist/sending-monitor/sending-monitor.module.js';
|
|
import management from '../../api/dist/sending-monitor/monitor-rule-management.service.js';
|
|
|
|
process.env.TZ = 'UTC';
|
|
const connectionString = process.env.QA_DATABASE_URL || process.env.DATABASE_URL;
|
|
if (!connectionString) throw Error('QA_DATABASE_URL is required');
|
|
const schema = `qa_monitor_rules_${randomUUID().replaceAll('-', '')}`;
|
|
assert.match(schema, /^qa_monitor_rules_[a-f0-9]{32}$/);
|
|
const admin = new pg.Client({ connectionString });
|
|
let client,
|
|
checks = 0;
|
|
await admin.connect();
|
|
try {
|
|
await admin.query(`CREATE SCHEMA "${schema}"`);
|
|
for (const table of [
|
|
'Tenant',
|
|
'SmsApplication',
|
|
'SmsSignature',
|
|
'SendingMonitorRule',
|
|
'SendingMonitorRuleVersion',
|
|
'OperationLog',
|
|
])
|
|
await admin.query(`CREATE TABLE "${schema}"."${table}" (LIKE public."${table}" INCLUDING ALL)`);
|
|
const pool = new pg.Pool({ connectionString, max: 2, options: `-c search_path=${schema} -c timezone=UTC` });
|
|
client = new prisma.PrismaClient({ adapter: new adapter.PrismaPg(pool, { schema, disposeExternalPool: true }) });
|
|
assert.equal((await client.$queryRawUnsafe('SELECT current_schema() name'))[0].name, schema);
|
|
for (const id of ['t1', 't2']) await client.tenant.create({ data: { id, name: `企业${id}`, code: id } });
|
|
for (let i = 0; i < 23; i++)
|
|
await client.smsApplication.create({
|
|
data: {
|
|
id: `a${i}`,
|
|
tenantId: 't1',
|
|
name: `应用${String(i).padStart(2, '0')}`,
|
|
cmppAccount: `isolated${i}`,
|
|
cmppEnterpriseCode: 'qa',
|
|
secretHash: 'not-a-real-credential',
|
|
},
|
|
});
|
|
await client.smsApplication.create({
|
|
data: {
|
|
id: 'b1',
|
|
tenantId: 't2',
|
|
name: '企业乙应用',
|
|
cmppAccount: 'isolated-b',
|
|
cmppEnterpriseCode: 'qa',
|
|
secretHash: 'not-a-real-credential',
|
|
},
|
|
});
|
|
await client.smsSignature.create({ data: { id: 's1', tenantId: 't1', applicationId: 'a0', name: '同名签名' } });
|
|
await client.smsSignature.create({ data: { id: 's2', tenantId: 't2', applicationId: 'b1', name: '同名签名' } });
|
|
const sut = new service.SendingMonitorService(client),
|
|
reads = new management.MonitorRuleManagementService(client);
|
|
const options = await reads.options({ kind: 'application', keyword: '企业t1' });
|
|
assert.equal(options.items.length, 20);
|
|
assert.equal(options.hasMore, true);
|
|
assert(options.items.every((x) => x.name.includes('企业t1') && x.tenantId === 't1'));
|
|
checks++;
|
|
const next = await reads.options({ kind: 'application', keyword: '企业t1', page: '2' });
|
|
assert.equal(next.items.length, 3);
|
|
assert.equal(next.hasMore, false);
|
|
assert(next.items.every((x) => !options.items.some((old) => old.id === x.id)));
|
|
checks++;
|
|
assert.deepEqual(
|
|
(await reads.options({ kind: 'signature', tenantId: 't1', applicationId: 'a0' })).items.map((x) => x.id),
|
|
['s1'],
|
|
);
|
|
assert.equal((await reads.options({ kind: 'signature', tenantId: 't1', applicationId: 'b1' })).items.length, 0);
|
|
checks++;
|
|
await assert.rejects(reads.options({ kind: 'signature' }), (e) => e.getStatus() === 400);
|
|
await assert.rejects(reads.list({ page: '0' }), (e) => e.getStatus() === 400);
|
|
await assert.rejects(reads.editor({ tenantId: 't1' }), (e) => e.getStatus() === 400);
|
|
checks++;
|
|
const config = { enabled: true, minSamples: 100, thresholds: [90, 95, 98], consecutiveBad: 1, consecutiveGood: 2 };
|
|
const save = async (scope, values = config, version = 0) =>
|
|
(await sut.saveRule({ type: 'overall', scope, config: values, version }, 'qa-rule-manager'))[0];
|
|
const global = await save({});
|
|
for (let i = 0; i < 23; i++) await save({ tenantId: 't1', applicationId: `a${i}` });
|
|
const signature = await save({ tenantId: 't1', signatureId: 's1' });
|
|
const combinedScope = { tenantId: 't1', applicationId: 'a0', signatureId: 's1' };
|
|
const combined = await save(combinedScope);
|
|
assert.equal((await reads.list({})).total, 25);
|
|
assert.equal((await reads.list({ page: '2' })).items.length, 5);
|
|
assert.equal((await reads.list({ keyword: '应用00', kind: 'combined' })).items[0].names.signatureName, '同名签名');
|
|
checks++;
|
|
const pending = await reads.editor(combinedScope);
|
|
assert.equal(pending.current.version, 1);
|
|
assert.equal(pending.matched.length, 0);
|
|
checks++;
|
|
await client.$executeRawUnsafe(
|
|
`UPDATE "SendingMonitorRuleVersion" SET "effectiveAt"=(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')-interval '1 minute'`,
|
|
);
|
|
await client.$executeRawUnsafe(
|
|
`UPDATE "SendingMonitorRule" SET "effectiveAt"=(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')-interval '1 minute'`,
|
|
);
|
|
const effective = await reads.editor(combinedScope);
|
|
assert.deepEqual(
|
|
effective.matched.map((r) => Object.keys(r.scope).length),
|
|
[3, 2, 2, 0],
|
|
);
|
|
assert.equal(effective.inherited[0].ruleId, signature.id);
|
|
assert.equal(effective.matched[3].ruleId, global.id);
|
|
checks++;
|
|
const newScope = await reads.editor({ tenantId: 't2', applicationId: 'b1' });
|
|
assert.equal(newScope.current, null);
|
|
assert.equal(newScope.inherited[0].ruleId, global.id);
|
|
checks++;
|
|
await assert.rejects(save({ tenantId: 't1', applicationId: 'b1' }), (e) => e.getStatus() === 400);
|
|
await assert.rejects(save({ tenantId: 't1', applicationId: 'a0', signatureId: 's2' }), (e) => e.getStatus() === 400);
|
|
checks++;
|
|
const disabled = await save(combinedScope, { ...config, enabled: false }, 1);
|
|
const pendingDisable = await reads.list({ kind: 'combined' });
|
|
assert.equal(pendingDisable.items[0].config.enabled, false);
|
|
assert.equal(pendingDisable.items[0].active.config.enabled, true);
|
|
checks++;
|
|
await client.$executeRawUnsafe(
|
|
`UPDATE "SendingMonitorRuleVersion" SET "effectiveAt"=(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')-interval '1 minute'`,
|
|
);
|
|
assert.equal((await reads.editor(combinedScope)).matched[0].config.enabled, false);
|
|
checks++;
|
|
const race = await Promise.allSettled([
|
|
sut.restoreRule(combined.id, disabled.version, 'qa-rule-manager'),
|
|
sut.restoreRule(combined.id, disabled.version, 'qa-rule-manager'),
|
|
]);
|
|
assert.equal(race.filter((r) => r.status === 'fulfilled').length, 1);
|
|
assert.equal(race.find((r) => r.status === 'rejected').reason.getStatus(), 409);
|
|
checks++;
|
|
await client.$executeRawUnsafe(
|
|
`UPDATE "SendingMonitorRuleVersion" SET "effectiveAt"=(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')-interval '1 minute'`,
|
|
);
|
|
assert.equal((await reads.editor(combinedScope)).matched[0].ruleId, signature.id);
|
|
checks++;
|
|
await client.smsApplication.update({ where: { id: 'a1' }, data: { status: 'deleted' } });
|
|
await client.smsSignature.update({ where: { id: 's1' }, data: { auditStatus: 'deleted' } });
|
|
assert(!(await reads.options({ kind: 'application' })).items.some((r) => r.id === 'a1'));
|
|
assert.equal((await reads.options({ kind: 'signature', tenantId: 't1' })).items.length, 0);
|
|
const historical = (await reads.list({ keyword: '应用01' })).items[0];
|
|
assert.equal(historical.names.applicationStatus, 'deleted');
|
|
assert.equal(historical.names.applicationName, '应用01');
|
|
await assert.rejects(save(historical.scope, config, historical.version), (e) => e.getStatus() === 400);
|
|
await sut.restoreRule(historical.id, historical.version, 'qa-rule-manager');
|
|
checks++;
|
|
const oldCount = await client.sendingMonitorRuleVersion.count();
|
|
await client.$executeRawUnsafe(
|
|
`ALTER TABLE "OperationLog" ADD CONSTRAINT qa_reject CHECK ("resource"<>'sending_monitor') NOT VALID`,
|
|
);
|
|
await assert.rejects(sut.restoreRule(signature.id, signature.version, 'qa-rule-manager'));
|
|
assert.equal(await client.sendingMonitorRuleVersion.count(), oldCount);
|
|
checks++;
|
|
console.log(JSON.stringify({ checks, realPrisma: true, schemaIsolated: true, publicBusinessWrites: 0 }));
|
|
} finally {
|
|
if (client) await client.$disconnect();
|
|
await admin.query(`DROP SCHEMA "${schema}" CASCADE`);
|
|
await admin.end();
|
|
}
|