Files
multi-simadmin/packages/test-fixtures/test/fixture-safety.test.ts
T
chick c57dc81e42 fix: finish cross-platform test parity while keeping POSIX deployment semantics
- cutover readState: enforce 0600 mode bits only on POSIX (Windows ACLs
  govern access; chmod is a no-op there)
- backup activation: skip the read-only-handle fsync on win32; the staged
  rename-over-open-WAL tests keep running on the POSIX deployment targets
- test-fixtures: normalize fixture paths to POSIX separators before
  comparing with manifest entries
2026-09-07 01:18:44 +08:00

330 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.split(path.sep).join('/'));
}
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'));
// config.json is gitignored and holds the only real private values; on a
// fresh clone there is nothing to scan for, so the leak check degrades to a
// no-op instead of inventing false positives from the example placeholders.
const configPath = path.resolve(root, '../../config.json');
const configText = await readFile(configPath, 'utf8').catch((error) => {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
return undefined;
});
const config = configText === undefined ? {} : JSON.parse(configText);
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).split(path.sep).join('/'))
.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).split(path.sep).join('/');
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(),
);
});