Files
dpsapi/e2e/tests/host/population.spec.ts
T
bryanpedini 0a6023e942 feat(cli): add help, token and audit commands
Entry point and dispatch, token list [all] / new / delete, audit [all | n | n<unit>] with padded table output, help, usage and exit codes; unit and e2e tests for commands, configuration precedence, logging and token lifecycle.
2026-10-06 10:52:25 +02:00

93 lines
4.4 KiB
TypeScript

// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
/**
* PLAN §12 core item 13: a large token population (300 tokens, created only through the binary)
* with listing, deletion, authentication and audit checks (SQLite and the `audit` command) against
* that population.
*/
import {
test,
expect,
assertDbInvariants,
expectCliFailure,
expectContainersResponse,
expectErrorResponse,
formatAuditOutput,
mapLimit,
readOperations,
readTokens,
rfc3339ToNanos,
} from '../../harness';
import { expectedAuditStdout } from '../../harness/personas';
const N = 300;
test('300 tokens: create, list, delete, authenticate and audit', async ({ cli, startServer, http, ws, rng }) => {
test.setTimeout(240_000);
const server = await startServer();
server.fakeDocker!.many(1);
// Created through the binary, 12 CLI processes at a time, while the server is running
const descriptions = Array.from({ length: N }, (_, i) => (i % 10 === 0 ? `pop ${i} ünïcødé ✓` : i % 7 === 0 ? '' : `pop ${i}`));
const tokens = await mapLimit(descriptions, 12, (d) => cli.tokenNew(d));
expect(new Set(tokens).size, 'all tokens distinct').toBe(N);
// list
const rows = await cli.tokenList();
expect(rows).toHaveLength(N);
const byToken = new Map(rows.map((r) => [r.token, r]));
tokens.forEach((t, i) => expect(byToken.get(t)?.description).toBe(descriptions[i]));
for (let i = 1; i < rows.length; i++) {
const a = rfc3339ToNanos(rows[i - 1].createdAt);
const b = rfc3339ToNanos(rows[i].createdAt);
expect(a < b || (a === b && rows[i - 1].token < rows[i].token)).toBe(true);
}
expect(rows.every((r) => r.active && r.deletedAt === null)).toBe(true);
// delete a random third, 8 at a time
const shuffled = rng.shuffle(tokens);
const deleted = shuffled.slice(0, 100);
const alive = shuffled.slice(100);
await mapLimit(deleted, 8, (t) => cli.tokenDelete(t));
const after = await cli.tokenListAll();
expect(after.filter((r) => !r.active).map((r) => r.token).sort()).toEqual([...deleted].sort());
expect(after.filter((r) => r.active)).toHaveLength(200);
expect(after.filter((r) => !r.active).every((r) => r.deletedAt !== null)).toBe(true);
// plain `token list`: exactly the 200 alive tokens, in the same relative order as `token list all`
const activeOnly = await cli.tokenList();
expect(activeOnly.map((r) => r.token)).toEqual(after.filter((r) => r.active).map((r) => r.token));
expect(new Set(activeOnly.map((r) => r.token))).toEqual(new Set(alive));
// authenticate: 60 alive (200), 40 deleted (401), 20 unknown (401), concurrently
const probes = rng.shuffle([
...alive.slice(0, 60).map((t) => ({ t, ok: true })),
...deleted.slice(0, 40).map((t) => ({ t, ok: false })),
...Array.from({ length: 20 }, () => ({ t: rng.string(43, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'), ok: false })),
]);
await mapLimit(probes, 10, async (p) => {
const r = await http.get(server.url(), { token: p.t });
if (p.ok) expectContainersResponse(r);
else expectErrorResponse(r, 401);
});
// deleting already-deleted tokens fails and changes nothing
const snapshot = JSON.stringify(readTokens(ws.dbPath));
for (const t of deleted.slice(0, 10)) expectCliFailure(await cli.run(['token', 'delete', t]), { exitCode: 1, message: 'token already inactive' });
expect(JSON.stringify(readTokens(ws.dbPath))).toBe(snapshot);
expect((await server.stop()).exitCode).toBe(0);
// audit: exactly one row per successful probe, each for the right token
const ops = readOperations(ws.dbPath);
expect(ops.map((o) => o.token).sort()).toEqual(alive.slice(0, 60).sort());
expect(server.logs().requests().filter((r) => r.attrs.status === '200')).toHaveLength(60);
expect(server.logs().requests().filter((r) => r.attrs.status === '401')).toHaveLength(60);
// `audit` over the population: all 60 rows, the default 10, and a window holding every row
expect(formatAuditOutput(await cli.audit('60'))).toBe(expectedAuditStdout(ops, { limit: 60n }));
expect(formatAuditOutput(await cli.audit())).toBe(expectedAuditStdout(ops, { limit: 10n }));
expect((await cli.audit('1h')).map((r) => r.token).sort()).toEqual(alive.slice(0, 60).sort());
const report = assertDbInvariants(ws.dbPath, { tokenCount: N, operationCount: 60, auditBeforeDeletion: true });
expect(report).toEqual({ tokens: N, activeTokens: 200, inactiveTokens: 100, operations: 60 });
});