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.
This commit is contained in:
1 parent
b792ad256e
commit
0a6023e942
10 files changed
+4138
No files matched your search
@@ -0,0 +1,92 @@
|
||||
// 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 });
|
||||
});
|
||||
Reference in new issue
Block a user