test(e2e): add playwright e2e harness

Process and API-only Playwright harness: binary and image setup, server/CLI runners, fake Docker Engine socket, read-only database inspection, secret redaction in reports, seeded fuzzing, leak detection and harness self-checks.
This commit is contained in:
bryanpedini committed 2026-10-06 10:52:25 +02:00
1 parent a02eef13a7
commit fe0cd4bf6d
33 files changed
+6370

No files matched your search

+225
View File
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
/**
* Self-checks of the harness guarantees the other suites rely on: infrastructure failures are
* HarnessErrors that can never satisfy an expected-failure assertion; parsers and redaction work.
*/
import {
test,
expect,
EMPTY_AUDIT,
EMPTY_TOKEN_LIST,
expectCliFailure,
expectContainersResponse,
formatAuditOutput,
formatTokenList,
padTable,
parseAuditOutput,
parseTokenList,
redactValue,
safeStringify,
goQuote,
goUnquote,
HarnessError,
isHarnessError,
parseLogLine,
redactText,
Rng,
runProcess,
toolEnv,
tokenId,
printsTokens,
USAGE_TEXT,
type ProcResult,
} from '../../harness';
test('timeouts, signals and spawn failures are HarnessErrors, never results', async ({ cli }) => {
const timeout = await runProcess('sleep', ['5'], { env: toolEnv(), timeoutMs: 200 }).catch((e) => e);
expect(isHarnessError(timeout) && timeout.kind).toBe('timeout');
const signal = await runProcess('sh', ['-c', 'kill -SEGV $$'], { env: toolEnv() }).catch((e) => e);
expect(isHarnessError(signal) && signal.kind).toBe('signal');
const spawn = await runProcess('/nonexistent/binary', [], { env: toolEnv() }).catch((e) => e);
expect(isHarnessError(spawn) && spawn.kind).toBe('spawn');
// A run that does not finish is killed and reported as a timeout, never as a failure exit
const reallyHung = await cli.shell('sleep 5', { timeoutMs: 200 }).catch((e) => e);
expect(isHarnessError(reallyHung) && reallyHung.kind).toBe('timeout');
// expectCliFailure refuses abnormal terminations even if the "exit code" would match
const fake: ProcResult = { command: 'x', argv: [], stdout: '', stderr: 'dpsapi: x\n', stdoutBuffer: Buffer.alloc(0), stderrBuffer: Buffer.alloc(0), exitCode: null, signal: 'SIGKILL', timedOut: false, durationMs: 1 };
expect(() => expectCliFailure(fake, { exitCode: 1 })).toThrow(HarnessError);
expect(() => expectCliFailure({ ...fake, signal: null, exitCode: 1, timedOut: true }, { exitCode: 1 })).toThrow(HarnessError);
});
test('Go quoting round-trips and matches strconv.Quote conventions', () => {
const cases: Array<[string | Buffer, string]> = [
['', '""'],
['plain', '"plain"'],
['a"b\\c', '"a\\"b\\\\c"'],
['\x07\b\f\n\r\t\v', '"\\a\\b\\f\\n\\r\\t\\v"'],
['\x00\x01\x1f\x7f', '"\\x00\\x01\\x1f\\x7f"'],
['é日🚀', '"é日🚀"'],
[' ​
\u0085', '"\\u00a0\\u200b\\u2028\\u0085"'],
['\u{E0001}', '"\\U000e0001"'],
[Buffer.from([0x61, 0xff, 0xc0, 0x80]), '"a\\xff\\xc0\\x80"'],
];
for (const [input, quoted] of cases) {
expect(goQuote(input)).toBe(quoted);
if (typeof input === 'string') expect(goUnquote(quoted)).toBe(input);
}
});
test('slog text parser handles quoting, escapes and non-record lines', () => {
const r = parseLogLine('time=2026-10-05T20:03:15.908Z level=INFO msg="this request has been made" method=GET path="/a b\\"c" status=200 empty="" eq="a=b"')!;
expect(r.level).toBe('INFO');
expect(r.msg).toBe('this request has been made');
expect(r.attrs).toEqual({ method: 'GET', path: '/a b"c', status: '200', empty: '', eq: 'a=b' });
expect(parseLogLine('dpsapi: token not found')).toBeUndefined();
expect(parseLogLine('usage:')).toBeUndefined();
});
test('redaction replaces registered secrets anywhere and token-shaped strings', ({ secrets }) => {
const tok = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ';
expect(tok).toHaveLength(43);
secrets.register(tok);
expect(secrets.redact(`x${tok}y`)).toBe(`x[REDACTED:${tokenId(tok)}]y`);
expect(redactText(`Bearer ${'Z'.repeat(43)}`, [])).toBe(`Bearer [REDACTED:${tokenId('Z'.repeat(43))}]`);
expect(secrets.findIn(`... ${tok} ...`)).toHaveLength(1);
expect(secrets.findIn('nothing')).toEqual([]);
});
test('seeded PRNG is deterministic per seed and independent per fork', ({ rng }) => {
const a = new Rng('s');
const b = new Rng('s');
expect(Array.from({ length: 5 }, () => a.nextUint32())).toEqual(Array.from({ length: 5 }, () => b.nextUint32()));
expect(new Rng('s').fork('x').next()).not.toBe(new Rng('s').fork('y').next());
expect(rng.seed).toContain('seeded PRNG is deterministic');
expect(test.info().annotations.find((x) => x.type === 'seed')).toBeTruthy();
});
test('padTable reproduces the tabwriter layout of the spec examples byte for byte; the strict parsers round-trip and reject deviations', () => {
const t1 = 'whEsbopp2tB2xXN7IC3LBnHhVge2F_-Gf910hIDL2pQ';
const t2 = 'S_muyryYKV3fegqvOSlEbHJ8ZvHz_s5tPov3dkY49Bo';
const list =
'TOKEN DESCRIPTION ACTIVE CREATED_AT DELETED_AT\n' +
`${t1} "smoke test" false 2026-10-05T20:03:15.894338161Z 2026-10-05T20:03:15.94012Z\n` +
`${t2} "" true 2026-10-05T20:05:01.042950671Z -\n`;
const entries = [
{ token: t1, description: 'smoke test', active: false, createdAt: '2026-10-05T20:03:15.894338161Z', deletedAt: '2026-10-05T20:03:15.94012Z' },
{ token: t2, description: '', active: true, createdAt: '2026-10-05T20:05:01.042950671Z', deletedAt: null },
];
expect(formatTokenList(entries)).toBe(list);
expect(parseTokenList(list).map((r) => [r.token, r.description, r.active, r.createdAt, r.deletedAt])).toEqual(entries.map((e) => [e.token, e.description, e.active, e.createdAt, e.deletedAt]));
const audit = 'TIME TOKEN ORIGIN OPERATION\n' + `2026-10-05T20:03:16.001Z ${t1} 127.0.0.1 GET containers\n`;
expect(formatAuditOutput([{ time: '2026-10-05T20:03:16.001Z', token: t1, origin: '127.0.0.1', operation: 'GET containers' }])).toBe(audit);
expect(parseAuditOutput(audit).map((r) => [r.time, r.token, r.origin, r.operation])).toEqual([['2026-10-05T20:03:16.001Z', t1, '127.0.0.1', 'GET containers']]);
expect(EMPTY_TOKEN_LIST).toBe('TOKEN DESCRIPTION ACTIVE CREATED_AT DELETED_AT\n');
expect(EMPTY_AUDIT).toBe('TIME TOKEN ORIGIN OPERATION\n');
expect(parseTokenList(EMPTY_TOKEN_LIST)).toEqual([]);
expect(parseAuditOutput(EMPTY_AUDIT)).toEqual([]);
// widths count runes (code points), not UTF-16 units, bytes or display width; the last cell is never padded
// (an empty last cell leaves the padding of the cell before it, as tabwriter does; the app never prints one)
expect(padTable([['a', '🚀🚀', 'x y'], ['日本語', 'b', '']])).toBe('a 🚀🚀 x y\n日本語 b \n');
expect(padTable([['e\u0301', 'z'], ['ab', 'z']])).toBe('e\u0301 z\nab z\n');
// descriptions with runs of spaces and escaped quotes are parsed as Go-quoted strings, not split on blanks
const tricky = formatTokenList([{ token: t1, description: 'a "b" c\\', active: true, createdAt: '2026-10-05T20:05:01Z', deletedAt: null }]);
expect(parseTokenList(tricky)[0].description).toBe('a "b" c\\');
// deviations are rejected: tabs (old format), trailing blanks, wrong padding, unterminated quote, wrong header
const bad = [
'TOKEN\tDESCRIPTION\tACTIVE\tCREATED_AT\tDELETED_AT\n',
list.replace('DELETED_AT\n', 'DELETED_AT \n'),
list.replace('"smoke test" false', '"smoke test" false '),
list.replace(`${t2} ""`, `${t2} "`),
list.replace('ACTIVE', 'ACTIV'),
list.slice(0, -1),
list.replace(`${t2} `, `${t2.slice(1)} `),
];
for (const b of bad) expect(() => parseTokenList(b), JSON.stringify(b.slice(0, 60))).toThrow();
expect(() => parseAuditOutput(audit.replace('127.0.0.1 GET', '127.0.0.1 GET '))).toThrow();
expect(() => parseAuditOutput('TIME\tTOKEN\tORIGIN\tOPERATION\n')).toThrow();
});
test('BigInt values (audit selections) serialize in recorded fuzz cases and redacted artifacts', ({ fuzz }) => {
const sel = { kind: 'audit', exitCode: 0, selection: { windowNs: 3_600_000_000_000n } };
expect(() => JSON.stringify(sel)).toThrow(/BigInt/);
expect(safeStringify(sel)).toBe('{"kind":"audit","exitCode":0,"selection":{"windowNs":"3600000000000"}}');
expect(JSON.stringify(redactValue({ cases: [sel] }, []))).toBe('{"cases":[{"kind":"audit","exitCode":0,"selection":{"windowNs":"3600000000000"}}]}');
const c = fuzz.record({ i: 0, expected: sel });
expect(c.expected.selection.windowNs).toBe(3_600_000_000_000n);
expect(JSON.stringify(fuzz.cases)).toContain('"windowNs":"3600000000000"');
});
test('tokens printed by token list all and audit are registered as secrets by the runner (help registers nothing)', async ({ cli, secrets, startServer, http }) => {
// created and deleted behind the harness runners' back (plain runProcess): not registered yet
const outside = async (args: string[]) => {
const r = await runProcess(cli.binary, args, { env: cli.env() });
expect(r.exitCode, args.slice(0, 2).join(' ')).toBe(0);
return r.stdout.trim();
};
const listed = await outside(['token', 'new', 'only in list all']);
const audited = await outside(['token', 'new', 'only in audit']);
expect(secrets.has(listed) || secrets.has(audited)).toBe(false);
const server = await startServer();
expectContainersResponse(await http.get(server.url(), { token: audited }));
expect((await server.stop()).exitCode).toBe(0);
await outside(['token', 'delete', listed]);
await outside(['token', 'delete', audited]);
await cli.help();
await cli.tokenList(); // inactive tokens are not printed by plain token list
expect(secrets.has(listed) || secrets.has(audited), 'nothing printed, nothing registered').toBe(false);
await cli.tokenListAll();
expect(secrets.has(listed) && secrets.has(audited), 'token list all registers every printed token').toBe(true);
const third = await outside(['token', 'new', 'audit only']);
const s2 = await startServer();
expectContainersResponse(await http.get(s2.url(), { token: third }));
expect((await s2.stop()).exitCode).toBe(0);
await outside(['token', 'delete', third]);
expect(secrets.has(third)).toBe(false);
expect((await cli.audit()).map((r) => r.token)).toEqual([audited, third]);
expect(secrets.has(third), 'audit registers the printed tokens').toBe(true);
// `audit all` (via the runner's audit('all') and via plain run()) registers every printed token
const fourth = await outside(['token', 'new', 'audit all only']);
const s3 = await startServer();
expectContainersResponse(await http.get(s3.url(), { token: fourth }));
expect((await s3.stop()).exitCode).toBe(0);
await outside(['token', 'delete', fourth]);
expect(secrets.has(fourth)).toBe(false);
expect(printsTokens(['audit', 'all'])).toBe(true);
expect((await cli.audit('all')).map((r) => r.token)).toEqual([audited, third, fourth]);
expect(secrets.has(fourth), 'audit all registers the printed tokens').toBe(true);
const fifth = await outside(['token', 'new', 'audit all via run']);
const s4 = await startServer();
expectContainersResponse(await http.get(s4.url(), { token: fifth }));
expect((await s4.stop()).exitCode).toBe(0);
expect(secrets.has(fifth)).toBe(false);
expect((await cli.run(['audit', 'all'])).exitCode).toBe(0);
expect(secrets.has(fifth), 'run([audit, all]) registers the printed tokens').toBe(true);
});
test('usage text: one line per command form, descriptions aligned at offset 34, `audit all` last', () => {
const lines = USAGE_TEXT.split('\n');
expect(lines.shift()).toBe('usage:');
expect(lines.pop(), 'ends with a newline').toBe('');
expect(lines.map((l) => l.slice(0, 34).trimEnd())).toEqual([
' dpsapi',
' dpsapi help',
' dpsapi token list [all]',
' dpsapi token new [description]',
' dpsapi token delete <token>',
' dpsapi audit',
' dpsapi audit <n>',
' dpsapi audit <n><unit>',
' dpsapi audit all',
]);
for (const l of lines) expect(l[34] !== ' ' && l[33] === ' ', `description starts at offset 34: ${JSON.stringify(l)}`).toBe(true);
expect(lines.at(-1)).toBe(' dpsapi audit all' + ' '.repeat(16) + 'print every audit row');
});