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:
bryanpedini committed 2026-10-06 10:52:25 +02:00
1 parent b792ad256e
commit 0a6023e942
10 files changed
+4138

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
/**
* PLAN §12 core item 11 (server side): deleting tokens through the binary while the server runs —
* deleted_at populated, active=0, history retained, 401 afterwards, audit rows keep satisfying the
* foreign key; credential rotation; tokens created while the server runs are usable immediately.
*/
import {
test,
expect,
assertDbInvariants,
expectCliFailure,
expectContainersResponse,
expectErrorResponse,
msToNanos,
readOperations,
readToken,
rfc3339ToNanos,
tokenId,
} from '../../harness';
test('delete while serving: 401 afterwards, history and audit rows retained', async ({ cli, startServer, http, ws }) => {
const a = await cli.tokenNew('to delete');
const b = await cli.tokenNew('survivor');
const server = await startServer({ logLevel: 'DEBUG' });
expectContainersResponse(await http.get(server.url(), { token: a }));
expectContainersResponse(await http.curl({ url: server.url(), token: a }));
expectContainersResponse(await http.get(server.url(), { token: b }));
const createdA = readToken(ws.dbPath, a)!.created_at;
const t0 = msToNanos(Date.now() - 1);
await cli.tokenDelete(a);
const t1 = msToNanos(Date.now() + 1);
expectErrorResponse(await http.get(server.url(), { token: a }), 401);
expectErrorResponse(await http.curl({ url: server.url(), token: a }), 401);
expectContainersResponse(await http.get(server.url(), { token: b }));
const rowA = readToken(ws.dbPath, a)!;
expect(rowA.active).toBe(0);
expect(rowA.created_at).toBe(createdA);
expect(rowA.description).toBe('to delete');
const del = rfc3339ToNanos(rowA.deleted_at!);
expect(del >= t0 && del <= t1).toBe(true);
expect(readToken(ws.dbPath, b)!.active).toBe(1);
const ops = readOperations(ws.dbPath);
expect(ops.map((o) => o.token)).toEqual([a, a, b, b]);
for (const o of ops.filter((x) => x.token === a)) expect(rfc3339ToNanos(o.time) < del).toBe(true);
const list = await cli.tokenListAll();
expect(list.map((r) => [r.token, r.active, r.deletedAt])).toEqual([
[a, false, rowA.deleted_at],
[b, true, null],
]);
expect((await cli.tokenList()).map((r) => [r.token, r.active, r.deletedAt]), 'token list: the active token only').toEqual([[b, true, null]]);
expectCliFailure(await cli.run(['token', 'delete', a]), { exitCode: 1, message: 'token already inactive' });
expect((await server.stop()).exitCode).toBe(0);
const classes = server.logs().filter({ msg: 'authentication result' }).map((r) => [r.attrs.result, r.attrs.token_id]);
expect(classes).toEqual([
['ok', tokenId(a)],
['ok', tokenId(a)],
['ok', tokenId(b)],
['inactive_token', undefined],
['inactive_token', undefined],
['ok', tokenId(b)],
]);
assertDbInvariants(ws.dbPath, { tokenCount: 2, operationCount: 4, auditBeforeDeletion: true });
});
test('credential rotation: new token works immediately, old one stops working, nothing restarts', async ({ cli, startServer, http, ws }) => {
const server = await startServer();
const old = await cli.tokenNew('rotation v1');
expectContainersResponse(await http.get(server.url(), { token: old }));
for (let v = 2; v <= 4; v++) {
const next = await cli.tokenNew(`rotation v${v}`); // created while the server runs
expectContainersResponse(await http.get(server.url(), { token: next }));
await cli.tokenDelete(v === 2 ? old : (await cli.tokenList()).find((r) => r.description === `rotation v${v - 1}`)!.token);
expectContainersResponse(await http.get(server.url(), { token: next }));
}
expectErrorResponse(await http.get(server.url(), { token: old }), 401);
const rows = await cli.tokenListAll();
expect(rows.map((r) => [r.description, r.active])).toEqual([
['rotation v1', false],
['rotation v2', false],
['rotation v3', false],
['rotation v4', true],
]);
expect((await cli.tokenList()).map((r) => r.description), 'token list: only the current credential').toEqual(['rotation v4']);
expect(server.pid, 'same process throughout').toBe(server.pid);
expect(server.generation).toBe(1);
expect((await server.stop()).exitCode).toBe(0);
assertDbInvariants(ws.dbPath, { tokenCount: 4, operationCount: 7, auditBeforeDeletion: true });
});