// 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 }); });