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,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 });
|
||||
});
|
||||
Reference in new issue
Block a user