Files
multi-simadmin/packages/test-fixtures/test/fixture-safety.test.ts
T
chick f11877f13e feat(contracts): share notification, organization and device capability models
Move the Hub channel table, organization tags and device capability definitions
into the workspace packages so the control plane and the console validate the
same contract, and refresh the frozen upstream evidence for the new paths.
2026-09-05 18:52:54 +08:00

320 lines
12 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile, readdir, stat } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import path from 'node:path';
import { upstream58e2204Operations } from '../../operation-registry/src/upstream-58e2204.ts';
import {
collectOne,
selectReadonlyOperations,
validateReadonlyOperation,
DENY_REASONS,
} from '../scripts/collector.ts';
import { redact } from '../src/redactor.ts';
import {
payloadShape,
validateShapeNode,
validateSensitivePayload,
validateStatusContract,
} from '../src/verifier.ts';
const root = path.resolve(import.meta.dirname, '..');
const fixtureRoot = path.join(root, 'src/simadmin');
const allowedEnvelope = [
'schemaVersion',
'upstreamBaseline',
'capturedAt',
'sourceInstanceAlias',
'operationId',
'method',
'pathTemplate',
'statusCategory',
'httpStatus',
'contentType',
'latencyBucket',
'redacted',
'payload',
];
// Preserve business-schema keys such as `url` and channel `headers`; sensitive values
// are typed-redacted. The exact top-level envelope already excludes transport metadata.
const forbiddenKeys = /^(raw(response)?|instance(id|name))$/i;
const DENIED_PATHS = new Set(Object.keys(DENY_REASONS));
const leakPatterns = [
/https?:\/\//i,
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
/\b(?:\d{1,3}\.){3}\d{1,3}\b/,
/(?:[0-9a-f]{2}:){5}[0-9a-f]{2}/i,
/\+?\d(?:[ ()-]*\d){9,14}/,
/\b\d{14,22}\b/,
/\b(?:bearer\s+)?[A-Za-z0-9_-]{32,}\b/i,
];
async function jsonFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const e of await readdir(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...(await jsonFiles(p)));
else if (e.name.endsWith('.json')) out.push(p);
}
return out;
}
function walkKeys(v: any, cb: (k: string) => void) {
if (Array.isArray(v)) return v.forEach((x) => walkKeys(x, cb));
if (v && typeof v === 'object')
for (const [k, x] of Object.entries(v)) {
cb(k);
walkKeys(x, cb);
}
}
function stringLeaves(v: any, out: string[] = []) {
if (Array.isArray(v)) v.forEach((x) => stringLeaves(x, out));
else if (v && typeof v === 'object') Object.values(v).forEach((x) => stringLeaves(x, out));
else if (typeof v === 'string' && v.length >= 3) out.push(v);
return out;
}
test('selector is registry-derived passive GET R0 only with explicit denials', () => {
const { selected, denied } = selectReadonlyOperations(upstream58e2204Operations);
assert.ok(selected.length > 20);
assert.ok(
denied.some((x: any) => x.pathTemplate === '/api/network/operators/scan' && x.denyReason),
);
for (const op of selected) {
assert.equal(op.method, 'GET');
assert.equal(op.riskLevel, 'R0');
assert.doesNotMatch(op.pathTemplate, /[{}]/);
assert.notEqual(op.pathTemplate, '/api/network/operators/scan');
}
});
test('collector source has no write-method or authentication escape hatch', async () => {
const source = await readFile(path.join(root, 'scripts/collect-readonly-fixtures.ts'), 'utf8');
assert.doesNotMatch(source, /ensureAuthenticated|login\s*\(|--method|--path|--origin/i);
assert.doesNotMatch(source, /['"](?:POST|PUT|PATCH|DELETE)['"]/);
});
test('transport can only receive credential-free GET, manual redirect and timeout', async () => {
let init: any;
const fake = async (_url: any, i: any) => {
init = i;
return new Response(
JSON.stringify({ phone: '+15551234567', message: 'private', ip: '10.1.2.3' }),
{ status: 200, headers: { 'content-type': 'application/json', 'set-cookie': 'bad=1' } },
);
};
const op = upstream58e2204Operations.find((x) => x.operationId === 'getHealth')!;
const f = await collectOne(
{ origin: 'http://192.168.1.2', alias: 'instance-1' },
op,
fake as any,
);
assert.deepEqual(
{
method: init.method,
body: init.body,
redirect: init.redirect,
credentials: init.credentials,
},
{ method: 'GET', body: undefined, redirect: 'manual', credentials: 'omit' },
);
assert.deepEqual(init.headers, { accept: 'application/json' });
assert.ok(init.signal);
assert.equal(f.payload.phone, '[REDACTED]');
assert.equal(f.payload.message, 'private');
assert.equal(f.payload.ip, '[REDACTED]');
});
test('redactor covers sensitive keys and value patterns deterministically', () => {
const x = redact({
password: 'p',
TOKEN: 't',
content: 'body',
phone: 'x',
other: [
'+155****4567',
'+155****4567',
'a@b.example',
'https://private.example/x',
'aa:bb:cc:dd:ee:ff',
'2001:db8::1',
],
});
assert.equal(x.password, '[REDACTED]');
assert.equal(x.TOKEN, '[REDACTED]');
assert.equal(x.content, '[REDACTED]');
assert.equal(x.phone, '[REDACTED]');
for (const p of leakPatterns) assert.doesNotMatch(JSON.stringify(x), p);
});
test('Registry operation identity also rejects in-place structural mutation', () => {
const op: any = upstream58e2204Operations.find((x) => x.operationId === 'getHealth')!;
const original = structuredClone(op);
const mutations = [
(x: any) => x.sensitiveFields.push({ direction: 'response', path: '$.response.data.secret' }),
(x: any) => {
delete x.pathTemplate;
},
(x: any) => {
x.method = 'POST';
},
(x: any) => {
x.sensitiveFields = 'changed';
},
];
for (const mutate of mutations) {
try {
mutate(op);
assert.throws(() => validateReadonlyOperation(op), /Registry integrity/);
} finally {
for (const key of Object.keys(op)) delete op[key];
Object.assign(op, structuredClone(original));
}
}
validateReadonlyOperation(op);
});
test('collector categorizes status and JSON parse outcomes consistently', async () => {
const op = upstream58e2204Operations.find((x) => x.operationId === 'getHealth')!;
const run = (status: number, type: string, body: string | null) =>
collectOne(
{ origin: 'http://192.168.1.2', alias: 'instance-1' },
op,
async () => new Response(body, { status, headers: { 'content-type': type } }),
);
for (const [status, type, body, category] of [
[200, 'application/problem+json', '{}', 'success'],
[200, 'application/json', 'bad', 'non-json'],
[204, 'text/plain', null, 'non-json'],
[401, 'application/json', '{}', 'auth-required'],
[405, 'application/json', '{}', 'unsupported'],
[500, 'application/json', '{}', 'http-error'],
] as const) {
const f = await run(status, type, body);
assert.equal(f.statusCategory, category);
validateStatusContract(f);
}
const redirect = await run(302, 'application/json', '{}');
assert.equal(redirect.statusCategory, 'http-error');
validateStatusContract(redirect);
assert.throws(() =>
validateStatusContract({
statusCategory: 'success',
httpStatus: 401,
contentType: 'application/json',
payload: {},
latencyBucket: '<250ms',
}),
);
});
test('real fixtures satisfy envelope, registry, leak scan, and manifest integrity', async () => {
const manifest = JSON.parse(await readFile(path.join(root, 'src/manifest.json'), 'utf8'));
assert.deepEqual(Object.keys(manifest).sort(), [
'domainCoverage',
'files',
'realFixtureCount',
'schemaVersion',
'shapeBaseline',
'syntheticFixtureCount',
'upstreamBaseline',
]);
const files = (await jsonFiles(fixtureRoot)).filter((x) => !x.includes('/synthetic-errors/'));
const srcEntries = await readdir(path.join(root, 'src'));
assert.ok(!srcEntries.includes('synthetic-errors'));
const config = JSON.parse(await readFile(path.resolve(root, '../../config.json'), 'utf8'));
const privateValues = stringLeaves(config);
const expected = selectReadonlyOperations(upstream58e2204Operations).selected;
assert.equal(files.length, expected.length * 2);
assert.equal(manifest.realFixtureCount, files.length);
assert.equal(manifest.syntheticFixtureCount, 0);
assert.equal(manifest.schemaVersion, 1);
assert.equal(manifest.upstreamBaseline, '58e2204');
const shapePath = path.join(root, 'src/response-shapes-58e2204.json');
const shapeText = await readFile(shapePath, 'utf8');
const shapeBaseline = JSON.parse(shapeText);
assert.deepEqual(Object.keys(shapeBaseline).sort(), [
'schemaVersion',
'shapes',
'upstreamBaseline',
]);
assert.equal(shapeBaseline.schemaVersion, 1);
assert.equal(shapeBaseline.upstreamBaseline, '58e2204');
assert.equal(Object.keys(shapeBaseline.shapes).length, 78);
assert.deepEqual(Object.keys(manifest.shapeBaseline).sort(), ['path', 'sha256', 'size']);
assert.equal(manifest.shapeBaseline.path, 'src/response-shapes-58e2204.json');
assert.equal(manifest.shapeBaseline.size, (await stat(shapePath)).size);
assert.equal(manifest.shapeBaseline.sha256, createHash('sha256').update(shapeText).digest('hex'));
assert.ok(!manifest.files.some((x: any) => x.path === manifest.shapeBaseline.path));
for (const node of Object.values(shapeBaseline.shapes)) validateShapeNode(node);
const registry = new Map(upstream58e2204Operations.map((x) => [x.operationId, x]));
const domains = new Map<string, number>();
const pairs = new Set<string>();
const disk = files.map((file) => path.relative(root, file)).sort();
const listed = manifest.files.map((x: any) => x.path).sort();
assert.deepEqual(listed, disk);
assert.equal(new Set(listed).size, listed.length);
for (const file of files) {
const text = await readFile(file, 'utf8');
const f = JSON.parse(text);
assert.deepEqual(Object.keys(f).sort(), [...allowedEnvelope].sort());
assert.equal(f.schemaVersion, 1);
assert.equal(f.upstreamBaseline, '58e2204');
assert.match(f.capturedAt, /^20(?:2[4-9]|[3-9]\d)-\d{2}-\d{2}$/);
assert.ok(!Number.isNaN(Date.parse(`${f.capturedAt}T00:00:00Z`)));
assert.ok(['instance-1', 'instance-2'].includes(f.sourceInstanceAlias));
assert.equal(f.redacted, true);
assert.equal(f.method, 'GET');
assert.ok(!('synthetic' in f));
assert.ok(
[
'success',
'non-json',
'auth-required',
'unsupported',
'http-error',
'timeout',
'network-error',
].includes(f.statusCategory),
);
if (['timeout', 'network-error'].includes(f.statusCategory)) assert.equal(f.httpStatus, null);
else assert.ok(Number.isInteger(f.httpStatus));
const op: any = registry.get(f.operationId);
assert.ok(op);
validateStatusContract(f);
validateSensitivePayload(f.payload, op);
const shapeKey = `${f.sourceInstanceAlias}::${f.operationId}`;
assert.deepEqual(payloadShape(f.payload), shapeBaseline.shapes[shapeKey]);
assert.equal(op?.riskLevel, 'R0');
assert.equal(op?.method, 'GET');
assert.equal(op?.pathTemplate, f.pathTemplate);
assert.ok(!DENIED_PATHS.has(f.pathTemplate));
domains.set(op.upstreamDomain, (domains.get(op.upstreamDomain) || 0) + 1);
const pair = `${f.sourceInstanceAlias}:${f.operationId}`;
assert.ok(!pairs.has(pair));
pairs.add(pair);
walkKeys(f, (k) => assert.doesNotMatch(k, forbiddenKeys));
for (const p of leakPatterns) assert.doesNotMatch(text, p);
for (const value of privateValues)
assert.ok(!text.includes(value), 'fixture contains configured value');
const rel = path.relative(root, file);
const m = manifest.files.find((x: any) => x.path === rel);
assert.ok(m);
assert.equal(m.size, (await stat(file)).size);
assert.match(m.sha256, /^[a-f0-9]{64}$/);
assert.equal(m.sha256, createHash('sha256').update(text).digest('hex'));
}
assert.deepEqual(Object.fromEntries([...domains].sort()), manifest.domainCoverage);
assert.deepEqual(
Object.keys(shapeBaseline.shapes).sort(),
[...pairs].map((x) => x.replace(':', '::')).sort(),
);
assert.deepEqual(
[...pairs].sort(),
expected
.flatMap((op: any) =>
['instance-1', 'instance-2'].map((alias) => `${alias}:${op.operationId}`),
)
.sort(),
);
});