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.
373 lines
19 KiB
TypeScript
373 lines
19 KiB
TypeScript
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
/**
|
|
* PLAN §12 core item 16: every startup/configuration failure and every CLI usage failure, with
|
|
* exact stderr, exit code (2 usage/config, 1 runtime) and proof that the database was not
|
|
* created or modified inappropriately.
|
|
*/
|
|
import fs from 'node:fs';
|
|
import {
|
|
test,
|
|
expect,
|
|
assertDbInvariants,
|
|
expectCliFailure,
|
|
expectCliSuccess,
|
|
fileDigest,
|
|
goQuote,
|
|
isPortFree,
|
|
occupyPort,
|
|
parseLogs,
|
|
readTokens,
|
|
USAGE_ERROR_MESSAGES,
|
|
USAGE_TEXT,
|
|
} from '../../harness';
|
|
import { cliOutcome, StateModel } from '../../harness/personas';
|
|
|
|
/** Server mode ([]) and a CLI command must both reject a bad configuration identically. */
|
|
const MODES: Array<[string, string[]]> = [
|
|
['server mode', []],
|
|
['token list', ['token', 'list']],
|
|
['token new', ['token', 'new', 'x']],
|
|
['audit', ['audit', '5m']],
|
|
];
|
|
|
|
/** Configuration is validated before dispatch: also `help` and `token list all` (which never open the database). */
|
|
const CONFIG_MODES: Array<[string, string[]]> = [...MODES, ['token list all', ['token', 'list', 'all']], ['audit all', ['audit', 'all']], ['help', ['help']], ['help with an argument', ['help', 'x']]];
|
|
|
|
const addrMsg = (v: string) => `configuration error: invalid HTTP_ADDR ${goQuote(v)}: must be an IPv4 or IPv6 address`;
|
|
const portMsg = (v: string) => `configuration error: invalid HTTP_PORT ${goQuote(v)}: must be an integer between 1 and 65535`;
|
|
const levelMsg = (v: string) => `configuration error: invalid LOG_LEVEL ${goQuote(v)}: must be one of ERROR, WARN, INFO, DEBUG, TRACE`;
|
|
|
|
const CONFIG_CASES: Array<[string, Record<string, string | undefined>, string]> = [
|
|
['DB_PATH unset', { DB_PATH: undefined }, 'configuration error: DB_PATH is required'],
|
|
['DB_PATH empty', { DB_PATH: '' }, 'configuration error: DB_PATH must not be empty'],
|
|
['HTTP_ADDR empty', { HTTP_ADDR: '' }, 'configuration error: HTTP_ADDR must not be empty'],
|
|
...['localhost', 'example.com', '127.0.0.1:8080', '[::1]', '256.0.0.1', '1.2.3', '010.0.0.1', ' 127.0.0.1', '127.0.0.1 ', 'fe80::1%lo', '0x7f.0.0.1', '127.0.0.1\n', '*'].map(
|
|
(v) => [`HTTP_ADDR=${JSON.stringify(v)}`, { HTTP_ADDR: v }, addrMsg(v)] as [string, Record<string, string>, string],
|
|
),
|
|
['HTTP_PORT empty', { HTTP_PORT: '' }, 'configuration error: HTTP_PORT must not be empty'],
|
|
...['0', '00000', '-1', '65536', '99999999999999999999', 'abc', ' 80', '80 ', '+80', '1e3', '0x50', '80', '80\n', '8O'].map(
|
|
(v) => [`HTTP_PORT=${JSON.stringify(v)}`, { HTTP_PORT: v }, portMsg(v)] as [string, Record<string, string>, string],
|
|
),
|
|
['LOG_LEVEL empty', { LOG_LEVEL: '' }, 'configuration error: LOG_LEVEL must not be empty'],
|
|
...['VERBOSE', 'INFO ', ' info', 'warning', 'ERR', 'trace\n', 'İNFO', 'INFO', 'debug2', 'FATAL', '0'].map(
|
|
(v) => [`LOG_LEVEL=${JSON.stringify(v)}`, { LOG_LEVEL: v }, levelMsg(v)] as [string, Record<string, string>, string],
|
|
),
|
|
// precedence: DB_PATH, HTTP_ADDR, HTTP_PORT, LOG_LEVEL — first problem wins
|
|
['DB_PATH missing beats everything', { DB_PATH: undefined, HTTP_ADDR: 'x', HTTP_PORT: 'y', LOG_LEVEL: 'z' }, 'configuration error: DB_PATH is required'],
|
|
['HTTP_ADDR beats HTTP_PORT and LOG_LEVEL', { HTTP_ADDR: 'x', HTTP_PORT: 'y', LOG_LEVEL: 'z' }, addrMsg('x')],
|
|
['HTTP_PORT beats LOG_LEVEL', { HTTP_PORT: 'y', LOG_LEVEL: 'z' }, portMsg('y')],
|
|
];
|
|
|
|
test.describe('configuration errors → exit 2, exact message, no database', () => {
|
|
for (const [name, env, message] of CONFIG_CASES) {
|
|
test(name, async ({ cli, ws }) => {
|
|
for (const [mode, args] of CONFIG_MODES) {
|
|
await test.step(mode, async () => {
|
|
const r = await cli.run(args, { env: { HTTP_ADDR: '127.0.0.1', HTTP_PORT: '1', ...env }, timeoutMs: 10_000 });
|
|
expectCliFailure(r, { exitCode: 2, message, usage: false });
|
|
expect(fs.existsSync(ws.dbPath), 'database not created on configuration error').toBe(false);
|
|
expect(fs.readdirSync(ws.dir), 'nothing written to the workspace').toEqual([]);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
test('configuration errors take precedence over usage errors (no usage text)', async ({ cli, ws }) => {
|
|
const r = await cli.run(['bogus'], { env: { DB_PATH: undefined } });
|
|
expectCliFailure(r, { exitCode: 2, message: 'configuration error: DB_PATH is required', usage: false });
|
|
const r2 = await cli.run(['token', 'new', 'a', 'b'], { env: { LOG_LEVEL: 'nope' } });
|
|
expectCliFailure(r2, { exitCode: 2, message: levelMsg('nope'), usage: false });
|
|
const r3 = await cli.run(['audit', '0'], { env: { HTTP_PORT: '0' } });
|
|
expectCliFailure(r3, { exitCode: 2, message: portMsg('0'), usage: false });
|
|
expect(fs.existsSync(ws.dbPath)).toBe(false);
|
|
});
|
|
});
|
|
|
|
const USAGE_CASES: Array<[string[], string]> = [
|
|
[['bogus'], 'unknown command'],
|
|
[['-h'], 'unknown command'],
|
|
[['--help'], 'unknown command'],
|
|
[['Help'], 'unknown command'],
|
|
[['HELP'], 'unknown command'],
|
|
[['-help'], 'unknown command'],
|
|
[['help '], 'unknown command'],
|
|
[['help', 'x'], 'help takes no arguments'],
|
|
[['help', ''], 'help takes no arguments'],
|
|
[['help', 'help'], 'help takes no arguments'],
|
|
[['help', 'token', 'list'], 'help takes no arguments'],
|
|
[['token', 'help'], 'unknown token subcommand'],
|
|
[['audit', 'help'], 'invalid audit argument'],
|
|
[['Token', 'list'], 'unknown command'],
|
|
[['TOKEN'], 'unknown command'],
|
|
[[''], 'unknown command'],
|
|
[['tokens', 'list'], 'unknown command'],
|
|
[['token'], 'missing token subcommand'],
|
|
[['token', 'bogus'], 'unknown token subcommand'],
|
|
[['token', 'LIST'], 'unknown token subcommand'],
|
|
[['token', 'New'], 'unknown token subcommand'],
|
|
[['token', ''], 'unknown token subcommand'],
|
|
[['token', 'remove', 'x'], 'unknown token subcommand'],
|
|
[['token', 'list', 'extra'], 'unknown token list argument'],
|
|
[['token', 'list', ''], 'unknown token list argument'],
|
|
[['token', 'list', 'ALL'], 'unknown token list argument'],
|
|
[['token', 'list', 'All'], 'unknown token list argument'],
|
|
[['token', 'list', '--all'], 'unknown token list argument'],
|
|
[['token', 'list', 'all '], 'unknown token list argument'],
|
|
[['token', 'list', 'help'], 'unknown token list argument'],
|
|
[['token', 'list', 'all', 'all'], 'token list takes at most one argument'],
|
|
[['token', 'list', 'all', ''], 'token list takes at most one argument'],
|
|
[['token', 'list', 'x', 'y'], 'token list takes at most one argument'],
|
|
[['token', 'new', 'a', 'b'], 'token new takes at most one argument'],
|
|
[['token', 'new', '', ''], 'token new takes at most one argument'],
|
|
[['token', 'delete'], 'token delete requires exactly one argument'],
|
|
[['token', 'delete', 'a', 'b'], 'token delete requires exactly one argument'],
|
|
// audit is case-sensitive and singular; one representative per invalid-argument class
|
|
// (the exhaustive table is in cli-audit.spec.ts)
|
|
[['AUDIT'], 'unknown command'],
|
|
[['Audit', '10'], 'unknown command'],
|
|
[['audits'], 'unknown command'],
|
|
[['audit', '1', 'h'], 'audit takes at most one argument'],
|
|
[['audit', '0'], 'invalid audit argument'],
|
|
[['audit', '0h'], 'invalid audit argument'],
|
|
[['audit', '-5'], 'invalid audit argument'],
|
|
[['audit', ' 5'], 'invalid audit argument'],
|
|
[['audit', ''], 'invalid audit argument'],
|
|
[['audit', '5H'], 'invalid audit argument'],
|
|
[['audit', '5y'], 'invalid audit argument'],
|
|
[['audit', 'h'], 'invalid audit argument'],
|
|
[['audit', '1.5'], 'invalid audit argument'],
|
|
[['audit', '9223372036854775808'], 'invalid audit argument'],
|
|
[['audit', '15251w'], 'invalid audit argument'],
|
|
// `audit all`: `all` is exact and lowercase only, and takes no further argument
|
|
[['audit', 'ALL'], 'invalid audit argument'],
|
|
[['audit', 'All'], 'invalid audit argument'],
|
|
[['audit', 'all1'], 'invalid audit argument'],
|
|
[['audit', '--all'], 'invalid audit argument'],
|
|
[['audit', 'all', 'x'], 'audit takes at most one argument'],
|
|
[['audit', 'all', 'all'], 'audit takes at most one argument'],
|
|
];
|
|
|
|
test('the usage table covers every usage error message, and the CLI oracle agrees with it', () => {
|
|
const model = new StateModel();
|
|
for (const [args, msg] of USAGE_CASES) expect(cliOutcome(args, model), JSON.stringify(args)).toEqual({ kind: 'error', exitCode: 2, message: `usage error: ${msg}`, usage: true });
|
|
expect([...new Set(USAGE_CASES.map(([, m]) => m))].sort()).toEqual([...USAGE_ERROR_MESSAGES].sort());
|
|
expect(cliOutcome(['help'], model)).toEqual({ kind: 'help', exitCode: 0 });
|
|
expect(cliOutcome(['token', 'list'], model)).toEqual({ kind: 'list', exitCode: 0, scope: 'active' });
|
|
expect(cliOutcome(['token', 'list', 'all'], model)).toEqual({ kind: 'list', exitCode: 0, scope: 'all' });
|
|
expect(cliOutcome(['audit', 'all'], model)).toEqual({ kind: 'audit', exitCode: 0, selection: { all: true } });
|
|
expect(cliOutcome(['audit'], model)).toEqual({ kind: 'audit', exitCode: 0, selection: { limit: 10n } });
|
|
});
|
|
|
|
test.describe('help', () => {
|
|
test('help prints exactly the usage text on stdout, nothing on stderr, exit 0; no database created, an existing one byte-identical', async ({ cli, ws }) => {
|
|
const r = await cli.help();
|
|
expect(r.stdout).toBe(USAGE_TEXT);
|
|
expect(fs.existsSync(ws.dbPath), 'help never opens/creates the database').toBe(false);
|
|
expect(fs.readdirSync(ws.dir), 'nothing written to the workspace').toEqual([]);
|
|
// the same bytes as the usage text printed after a usage error
|
|
const bad = await cli.run(['bogus']);
|
|
expect(bad.stderr).toBe(`dpsapi: usage error: unknown command\n${r.stdout}`);
|
|
|
|
await cli.tokenNew('existing');
|
|
const digest = fileDigest(ws.dbPath);
|
|
const rows = JSON.stringify(readTokens(ws.dbPath));
|
|
await cli.help();
|
|
expect(fileDigest(ws.dbPath)).toBe(digest);
|
|
expect(JSON.stringify(readTokens(ws.dbPath))).toBe(rows);
|
|
// DB_PATH pointing somewhere unusable does not matter: the database is never opened
|
|
for (const db of [ws.path('missing-dir', 'x.db'), ws.mkdir('a-directory')]) await cli.help({ env: { DB_PATH: db } });
|
|
expect(fs.existsSync(ws.path('missing-dir'))).toBe(false);
|
|
});
|
|
|
|
test('a configuration error still wins over help: exit 2, the configuration error line only, no usage text, nothing on stdout', async ({ cli, ws }) => {
|
|
const cases: Array<[Record<string, string | undefined>, string]> = [
|
|
[{ DB_PATH: undefined }, 'configuration error: DB_PATH is required'],
|
|
[{ DB_PATH: '' }, 'configuration error: DB_PATH must not be empty'],
|
|
[{ HTTP_ADDR: 'localhost' }, addrMsg('localhost')],
|
|
[{ HTTP_PORT: '0' }, portMsg('0')],
|
|
[{ LOG_LEVEL: 'verbose' }, levelMsg('verbose')],
|
|
[{ LOG_LEVEL: '' }, 'configuration error: LOG_LEVEL must not be empty'],
|
|
];
|
|
for (const [env, message] of cases) {
|
|
for (const args of [['help'], ['help', 'x']]) {
|
|
const r = await cli.run(args, { env });
|
|
expectCliFailure(r, { exitCode: 2, message, usage: false });
|
|
expect(r.stderr, `${JSON.stringify(args)} ${JSON.stringify(env)}: exact stderr`).toBe(`dpsapi: ${message}\n`);
|
|
expect(r.stderr.includes('usage:'), 'no usage text').toBe(false);
|
|
}
|
|
}
|
|
expect(fs.readdirSync(ws.dir)).toEqual([]);
|
|
});
|
|
|
|
test('log levels: at DEBUG/TRACE stderr carries only log records (dispatch mode=cli, cli dispatch command=help) while stdout is still exactly the usage text; silent at INFO and above', async ({ cli, ws }) => {
|
|
for (const level of ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'debug', 'Trace']) {
|
|
await test.step(level, async () => {
|
|
const r = await cli.run(['help'], { env: { LOG_LEVEL: level } });
|
|
expectCliSuccess(r, { stdout: USAGE_TEXT });
|
|
const verbose = /^(debug|trace)$/i.test(level);
|
|
if (!verbose) {
|
|
expect(r.stderr).toBe('');
|
|
return;
|
|
}
|
|
const logs = parseLogs(r.stderr);
|
|
expect(logs.unparsed.map((u) => u.text), 'every stderr line is a log record (no usage text, no dpsapi: line)').toEqual([]);
|
|
expect(logs.records.length).toBeGreaterThan(0);
|
|
expect(logs.find({ msg: 'dispatch' })!.attrs).toEqual({ mode: 'cli' });
|
|
expect(logs.filter({ msg: 'cli dispatch' }).map((x) => x.attrs)).toEqual([{ command: 'help' }]);
|
|
expect(logs.has({ msg: /^database / }), 'no database record').toBe(false);
|
|
expect(logs.count({ level: 'TRACE' }) > 0, 'TRACE records only at TRACE').toBe(/^trace$/i.test(level));
|
|
expect(r.stderr.includes('usage:'), 'the usage text is never on stderr').toBe(false);
|
|
});
|
|
}
|
|
expect(fs.existsSync(ws.dbPath)).toBe(false);
|
|
});
|
|
});
|
|
|
|
test.describe('usage errors → exit 2, error line + usage text, database untouched', () => {
|
|
for (const [args, msg] of USAGE_CASES) {
|
|
test(`${JSON.stringify(args)} → ${msg}`, async ({ cli, ws }) => {
|
|
// fresh workspace: the database must not be created
|
|
const r = await cli.run(args);
|
|
expectCliFailure(r, { exitCode: 2, message: `usage error: ${msg}`, usage: true });
|
|
expect(r.stderr).toBe(`dpsapi: usage error: ${msg}\n${USAGE_TEXT}`);
|
|
expect(fs.existsSync(ws.dbPath), 'usage errors never open/create the database').toBe(false);
|
|
|
|
// existing database: byte-identical afterwards
|
|
await cli.tokenNew('existing');
|
|
const digest = fileDigest(ws.dbPath);
|
|
const rows = JSON.stringify(readTokens(ws.dbPath));
|
|
const r2 = await cli.run(args);
|
|
expectCliFailure(r2, { exitCode: 2, message: `usage error: ${msg}`, usage: true });
|
|
expect(fileDigest(ws.dbPath)).toBe(digest);
|
|
expect(JSON.stringify(readTokens(ws.dbPath))).toBe(rows);
|
|
});
|
|
}
|
|
});
|
|
|
|
test.describe('runtime failures → exit 1', () => {
|
|
test('DB_PATH in a missing directory: not created, exit 1 (server and CLI)', async ({ cli, ws }) => {
|
|
const db = ws.path('missing-dir', 'x.db');
|
|
for (const [mode, args] of MODES) {
|
|
await test.step(mode, async () => {
|
|
const r = await cli.run(args, { env: { DB_PATH: db, HTTP_ADDR: '127.0.0.1', HTTP_PORT: '1' }, timeoutMs: 10_000 });
|
|
expectCliFailure(r, { exitCode: 1, message: 'initialize database schema: unable to open database file (14)' });
|
|
expect(fs.existsSync(ws.path('missing-dir'))).toBe(false);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('DB_PATH is a directory → exit 1', async ({ cli, ws }) => {
|
|
const dir = ws.mkdir('a-directory');
|
|
for (const [mode, args] of MODES) {
|
|
await test.step(mode, async () => {
|
|
const r = await cli.run(args, { env: { DB_PATH: dir, HTTP_ADDR: '127.0.0.1', HTTP_PORT: '1' }, timeoutMs: 10_000 });
|
|
expectCliFailure(r, { exitCode: 1, message: 'initialize database schema: unable to open database file (14)' });
|
|
expect(fs.readdirSync(dir)).toEqual([]);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('DB_PATH is not a SQLite database → exit 1, file left untouched', async ({ cli, ws, rng }) => {
|
|
const garbage = ws.writeFile('garbage.db', rng.bytes(8192));
|
|
const digest = fileDigest(garbage);
|
|
for (const [mode, args] of MODES) {
|
|
await test.step(mode, async () => {
|
|
const r = await cli.run(args, { env: { DB_PATH: garbage, HTTP_ADDR: '127.0.0.1', HTTP_PORT: '1' }, timeoutMs: 10_000 });
|
|
expectCliFailure(r, { exitCode: 1, message: 'initialize database schema: file is not a database (26)' });
|
|
expect(fileDigest(garbage)).toBe(digest);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('DB_PATH in a read-only directory → exit 1, nothing created', async ({ cli, ws }) => {
|
|
const dir = ws.mkdir('readonly');
|
|
fs.chmodSync(dir, 0o555);
|
|
try {
|
|
for (const [mode, args] of MODES) {
|
|
await test.step(mode, async () => {
|
|
const r = await cli.run(args, { env: { DB_PATH: `${dir}/x.db`, HTTP_ADDR: '127.0.0.1', HTTP_PORT: '1' }, timeoutMs: 10_000 });
|
|
expectCliFailure(r, { exitCode: 1, message: 'initialize database schema: unable to open database file (14)' });
|
|
expect(fs.readdirSync(dir)).toEqual([]);
|
|
});
|
|
}
|
|
} finally {
|
|
fs.chmodSync(dir, 0o755);
|
|
}
|
|
});
|
|
|
|
test('port already in use → exit 1 with the listen error; database still valid', async ({ cli, ws, e2e }) => {
|
|
const port = await e2e.allocatePort();
|
|
const holder = await occupyPort(port, '127.0.0.1');
|
|
try {
|
|
const r = await cli.run([], { env: { HTTP_ADDR: '127.0.0.1', HTTP_PORT: String(port) }, timeoutMs: 10_000 });
|
|
expectCliFailure(r, { exitCode: 1, message: `listen tcp4 127.0.0.1:${port}: bind: address already in use` });
|
|
assertDbInvariants(ws.dbPath, { tokenCount: 0, operationCount: 0 });
|
|
} finally {
|
|
await holder.close();
|
|
}
|
|
});
|
|
|
|
test('privileged port as an unprivileged user → exit 1 (permission denied)', async ({ cli }) => {
|
|
const r = await cli.run([], { env: { HTTP_ADDR: '127.0.0.1', HTTP_PORT: '1' }, timeoutMs: 10_000 });
|
|
expectCliFailure(r, { exitCode: 1, message: 'listen tcp4 127.0.0.1:1: bind: permission denied' });
|
|
});
|
|
|
|
test('address not assigned to this host → exit 1 (cannot assign requested address)', async ({ cli, e2e }) => {
|
|
const port = await e2e.allocatePort();
|
|
const r = await cli.run([], { env: { HTTP_ADDR: '192.0.2.1', HTTP_PORT: String(port) }, timeoutMs: 10_000 });
|
|
expectCliFailure(r, { exitCode: 1, message: `listen tcp4 192.0.2.1:${port}: bind: cannot assign requested address` });
|
|
});
|
|
});
|
|
|
|
test.describe('valid edge-case configuration', () => {
|
|
test('HTTP_PORT with leading zeros and the 65535 upper bound are accepted', async ({ startServer, e2e }) => {
|
|
const port = await e2e.allocatePort();
|
|
const s = await startServer({ port: `000${port}`, logLevel: 'INFO' });
|
|
expect(s.logs().find({ msg: 'server listening' })!.attrs.addr).toBe(`127.0.0.1:${port}`);
|
|
expect((await s.stop()).exitCode).toBe(0);
|
|
|
|
expect(await isPortFree(65535), 'port 65535 must be free on this host for this check').toBe(true);
|
|
const top = await startServer({ port: 65535 });
|
|
expect(top.logs().find({ msg: 'server listening' })!.attrs.addr).toBe('127.0.0.1:65535');
|
|
expect((await top.stop()).exitCode).toBe(0);
|
|
});
|
|
|
|
test('HTTP_ADDR unset defaults to 0.0.0.0 (IPv4 only)', async ({ startServer, http, cli }) => {
|
|
const token = await cli.tokenNew();
|
|
const s = await startServer({ addr: null });
|
|
expect(s.logs().find({ msg: 'server listening' })!.attrs.addr).toBe(`0.0.0.0:${s.port}`);
|
|
expect((await http.get(s.url(), { token })).status).toBe(200);
|
|
// tcp4 listener: not reachable over IPv6 loopback
|
|
const v6 = await http.raw({ host: '::1', port: s.port, data: 'GET / HTTP/1.1\r\nHost: x\r\n\r\n' });
|
|
expect(v6.outcome).toBe('refused');
|
|
expect((await s.stop()).exitCode).toBe(0);
|
|
});
|
|
|
|
test('all defaults (0.0.0.0:80, INFO) in a private network namespace', async ({ startServer }) => {
|
|
const s = await startServer({ addr: null, port: null, logLevel: null, privateNetwork: true });
|
|
const rec = await s.waitForLog({ msg: 'server listening' });
|
|
expect(rec.level).toBe('INFO');
|
|
expect(rec.attrs.addr).toBe('0.0.0.0:80');
|
|
expect(s.env.HTTP_ADDR).toBeUndefined();
|
|
expect(s.env.HTTP_PORT).toBeUndefined();
|
|
expect(s.env.LOG_LEVEL).toBeUndefined();
|
|
const exit = await s.stop('SIGTERM');
|
|
expect(exit.exitCode).toBe(0);
|
|
expect(s.logs().count({ level: ['DEBUG', 'TRACE'] }), 'default level is INFO').toBe(0);
|
|
});
|
|
|
|
test('an existing database is reused by every mode (repeated schema initialization)', async ({ cli, ws, startServer }) => {
|
|
const t = await cli.tokenNew('first');
|
|
for (let i = 0; i < 3; i++) expectCliSuccess(await cli.run(['token', 'list']));
|
|
const s = await startServer();
|
|
expect((await s.stop()).exitCode).toBe(0);
|
|
const s2 = await startServer();
|
|
expect((await s2.stop()).exitCode).toBe(0);
|
|
const rows = readTokens(ws.dbPath);
|
|
expect(rows.map((r) => r.token)).toEqual([t]);
|
|
assertDbInvariants(ws.dbPath, { tokenCount: 1, operationCount: 0 });
|
|
});
|
|
});
|