feat(image): add scratch container image

Two-stage build producing a static /app on scratch; e2e tests for image contents, server and CLI through docker exec, socket handling, lifecycle and host/image parity.
This commit is contained in:
bryanpedini committed 2026-10-06 10:52:25 +02:00
1 parent 0a6023e942
commit 581a672fcf
9 files changed
+1858

No files matched your search

+568
View File
@@ -0,0 +1,568 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
/**
* PLAN §12 "Container-image E2E tests", §13 (Dockerfile) and §15 (runtime contract) against the
* final image ($DPSAPI_IMAGE): image contents and config, the scratch/two-stage build, the static
* binary, default entrypoint/server mode with the default bind address, `docker exec /app token …`,
* `/app audit` and `/app help` with stdout/stderr/exit captured separately and verified against the
* database, usage and
* configuration failures through `docker exec` and `docker run`, no shell / no Docker CLI, and
* `curl` from outside the container against the real Engine.
*/
import fs from 'node:fs';
import path from 'node:path';
import {
test,
expect,
REPO_ROOT,
AUDIT_COLUMNS,
EMPTY_AUDIT,
EMPTY_TOKEN_LIST,
TOKEN_LIST_COLUMNS,
USAGE_TEXT,
assertDbInvariants,
dockerCli,
expectCliFailure,
expectCliSuccess,
expectContainersResponse,
expectErrorResponse,
goQuote,
mapLimit,
msToNanos,
parseAuditOutput,
parseLogs,
parseTokenList,
padTable,
readOperations,
readTokens,
rfc3339ToNanos,
RFC3339NANO_UTC_RE,
runProcess,
shellQuote,
toolEnv,
} from '../../harness';
import { chronological, expectedAuditStdout, expectedTokenListStdout, stateDigest } from '../../harness/personas';
import { EM_BY_ARCH, PT_DYNAMIC, PT_INTERP, execApp, exportCreatedContainer, expectedOrigins, hex, imageTag, readElf, saveImage } from './image-helpers';
const TOKEN_LINE = /^[A-Za-z0-9_-]{43}\n$/;
test.describe('image contents and build', () => {
test('image layers (docker save) contain exactly one regular file: /app', async ({ image, ws }) => {
const saved = await saveImage(ws, image.image);
// FROM scratch: no base layers, the COPY of the binary is the only filesystem layer
expect(saved.manifest[0].Layers, 'filesystem layers').toHaveLength(1);
expect(saved.config.rootfs.diff_ids).toHaveLength(1);
const entries = saved.layers.flat();
expect(entries.map((e) => [e.type, e.name]), 'every entry of every layer').toEqual([['-', 'app']]);
const [app] = entries;
expect(app.perms, 'executable').toMatch(/^r.x/);
expect(app.owner).toBe('0/0');
const bin = await image.extractBinary();
expect(app.size, 'layer file is the extracted binary').toBe(fs.statSync(bin).size);
// history: one layer-producing step (COPY /app) + the ENTRYPOINT metadata step — nothing else
const history = saved.config.history;
expect(history.filter((h) => !h.empty_layer).map((h) => h.created_by), 'layer-producing history steps').toEqual([expect.stringMatching(/^COPY \/app \/app\b/)]);
expect(history.map((h) => h.created_by)).toEqual([expect.stringMatching(/^COPY \/app \/app\b/), 'ENTRYPOINT ["/app"]']);
expect(history.some((h) => /\b(RUN|apk|apt|sh -c)\b/.test(h.created_by ?? '')), 'no RUN step in the final stage').toBe(false);
// image config
const cfg = saved.config.config;
expect(cfg.Entrypoint).toEqual(['/app']);
expect(cfg.Cmd ?? null, 'no CMD: no arguments → server mode').toBeNull();
expect(cfg.User ?? '', 'runs as root (README: so it can open the socket and write /data)').toBe('');
// As observed (PLAN does not define these): buildkit's default PATH only, no VOLUME/EXPOSE/WORKDIR beyond "/"
expect(cfg.Env).toEqual(['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin']);
expect(cfg.Volumes ?? null).toBeNull();
expect(cfg.ExposedPorts ?? null).toBeNull();
expect(saved.config.os).toBe('linux');
// the same through `docker image inspect`
const inspect = JSON.parse((await dockerCli(['image', 'inspect', image.image])).stdout)[0] as { Config: { Entrypoint: string[]; Cmd: unknown }; RootFS: { Layers: string[] } };
expect(inspect.Config.Entrypoint).toEqual(['/app']);
expect(inspect.Config.Cmd ?? null).toBeNull();
expect(inspect.RootFS.Layers).toEqual(saved.config.rootfs.diff_ids);
});
test('container filesystem (docker export): /app plus only what Docker injects at creation', async ({ image, ws, testKey }) => {
const entries = await exportCreatedContainer(ws, testKey, image.image);
// Docker adds these to every container's init layer at `docker create` (empty mount targets for
// /etc/hosts, /etc/hostname, /etc/resolv.conf, /dev/console, the .dockerenv marker, the
// /etc/mtab symlink and the /dev, /proc, /sys mount points). They are not in the image layers
// (previous test) and are recognisable here as zero-byte placeholders / directories / symlink.
const injected = new Map<string, string>([
['.dockerenv', '-'],
['dev/', 'd'],
['dev/console', '-'],
['dev/pts/', 'd'],
['dev/shm/', 'd'],
['etc/', 'd'],
['etc/hostname', '-'],
['etc/hosts', '-'],
['etc/mtab', 'l'],
['etc/resolv.conf', '-'],
['proc/', 'd'],
['sys/', 'd'],
]);
const unexpected = entries.filter((e) => e.name !== 'app' && injected.get(e.name) !== e.type);
expect(unexpected.map((e) => `${e.type} ${e.name}`), 'entries that are neither /app nor Docker-injected').toEqual([]);
const nonEmptyFiles = entries.filter((e) => e.type === '-' && e.size > 0).map((e) => e.name);
expect(nonEmptyFiles, 'the only file with content is /app').toEqual(['app']);
expect(entries.find((e) => e.name === 'etc/mtab')!.linkTarget).toBe('/proc/mounts');
const bins = entries.filter((e) => /(^|\/)(sh|bash|ash|busybox|docker|dockerd|apk|curl|wget)$/.test(e.name));
expect(bins.map((e) => e.name), 'no shell, docker CLI, package manager or HTTP client').toEqual([]);
});
test('the binary from the image is statically linked (no PT_INTERP, no PT_DYNAMIC)', async ({ image }) => {
const bin = await image.extractBinary();
const arch = JSON.parse((await dockerCli(['image', 'inspect', '--format', '{{json .Architecture}}', image.image])).stdout) as string;
const elf = readElf(bin);
expect(elf.machine, `ELF machine matches image architecture ${arch}`).toBe(EM_BY_ARCH[arch]);
expect(elf.type, 'ET_EXEC (a static Go binary is not a PIE)').toBe(2);
expect(elf.programHeaderTypes.length).toBeGreaterThan(0);
expect(elf.programHeaderTypes.includes(PT_INTERP), 'no program interpreter (dynamic loader)').toBe(false);
expect(elf.programHeaderTypes.includes(PT_DYNAMIC), 'no dynamic section').toBe(false);
});
test('dockerfile: two stages, CGO_ENABLED=0 with version/commit ldflags, final FROM scratch copies only /app', async () => {
const text = fs.readFileSync(path.join(REPO_ROOT, 'dockerfile'), 'utf8');
// join continuation lines, drop comments and blanks
const lines = text
.replace(/\\\n/g, ' ')
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('#'));
const froms = lines.map((l, i) => [l, i] as const).filter(([l]) => /^FROM\s/i.test(l));
expect(froms.map(([l]) => l), 'exactly two stages').toEqual([expect.stringMatching(/^FROM \$\{GO_BUILDER\}:\$\{GO_VERSION\} AS build$/), 'FROM scratch']);
const builder = lines.slice(froms[0][1] + 1, froms[1][1]);
const final = lines.slice(froms[1][1] + 1);
expect(final, 'final stage: copy the binary, set the entrypoint, nothing else').toEqual(['COPY --from=build /app /app', 'ENTRYPOINT ["/app"]']);
const build = builder.find((l) => /^RUN .*go build/.test(l))!;
expect(build, 'go build step').toBeTruthy();
expect(build).toMatch(/\bCGO_ENABLED=0\b/);
expect(build).toMatch(/-X 'main\.APP_VERSION=\$\{APP_VERSION\}'/);
expect(build).toMatch(/-X 'main\.COMMIT_ID=\$\{COMMIT_ID\}'/);
expect(build).toMatch(/-o \/app\b/);
// builder image selected by build arguments (.vars / makefile convention)
expect(lines.slice(0, froms[0][1]).map((l) => l.replace(/=.*/, '')), 'global ARGs before the first FROM').toEqual(['ARG GO_BUILDER', 'ARG GO_VERSION']);
});
});
test.describe('server mode (default entrypoint)', () => {
test('no arguments, HTTP_ADDR/HTTP_PORT/LOG_LEVEL unset → listens on 0.0.0.0:80; version/commit in "server listening"', async ({ image, http }) => {
const c = await image.start({ logLevel: null, suffix: 'defaults' });
const info = (await c.inspect()) as { Path: string; Args: string[]; Config: { Env: string[]; Cmd: unknown } };
expect(info.Path).toBe('/app');
expect(info.Args, 'no arguments').toEqual([]);
expect(info.Config.Env.filter((e) => /^(HTTP_ADDR|HTTP_PORT|LOG_LEVEL)=/.test(e)), 'optional variables unset').toEqual([]);
expect(info.Config.Env).toContain('DB_PATH=/data/dpsapi.db');
const created = await c.exec(['token', 'new', 'defaults']);
expectCliSuccess(created, { stdout: TOKEN_LINE, stderr: '' });
const token = created.stdout.trim();
c.fakeDocker!.many(2);
// reached through the port published from container port 80
expect(expectContainersResponse(await http.get(c.url(), { token }))).toHaveLength(2);
expectErrorResponse(await http.get(c.url()), 401);
const exit = await c.stop();
expect(exit.exitCode).toBe(0);
expect(exit.stdout, 'the server never writes to stdout').toBe('');
const logs = parseLogs(exit.stderr);
expect(logs.unparsed).toEqual([]);
expect(logs.levels(), 'LOG_LEVEL unset → INFO').toEqual(['INFO']);
const listening = logs.filter({ msg: 'server listening' });
expect(listening).toHaveLength(1);
expect(listening[0].attrs.addr).toBe('0.0.0.0:80');
const tag = imageTag(image.image);
if (tag !== undefined) expect(listening[0].attrs.version, 'APP_VERSION embedded = the image tag (make docker tags with APP_VERSION)').toBe(tag);
expect(listening[0].attrs.version).toMatch(/^\S+$/);
expect(listening[0].attrs.commit, 'COMMIT_ID embedded (short hash, or "undefined" without commits)').toMatch(/^([0-9a-f]{4,40}|undefined)$/);
expect(logs.requests().map((r) => r.attrs.status)).toEqual(['200', '401']);
expect(logs.has({ msg: 'server stopped' })).toBe(true);
});
test('explicit HTTP_ADDR/HTTP_PORT are honoured inside the container', async ({ image, http }) => {
const [v4, v6] = await Promise.all([
image.start({ addr: '0.0.0.0', containerPort: 8080, suffix: 'p8080' }),
image.start({ addr: '::', containerPort: 1, suffix: 'p1' }),
]);
for (const [c, addr] of [
[v4, '0.0.0.0:8080'],
[v6, '[::]:1'],
] as const) {
const token = (await c.exec(['token', 'new'])).stdout.trim();
expect(expectContainersResponse(await http.get(c.url(), { token }))).toEqual([]);
const exit = await c.stop();
expect(exit.exitCode).toBe(0);
expect(parseLogs(exit.stderr).find({ msg: 'server listening' })!.attrs.addr).toBe(addr);
}
});
});
test.describe('docker exec /app token …', () => {
test('token new / list / delete: stdout, stderr and exit code separately; rows verified in the DB snapshot', async ({ image, secrets }) => {
const c = await image.start({ suffix: 'cli' });
const cases: Array<[string, string | undefined]> = [
['no description', undefined],
['empty argument', ''],
['normal', 'ci pipeline'],
['spaces', ' two words and trailing '],
['Unicode', 'café ☕ 日本語 עברית 🚀 Ωmega'],
['special characters', `!"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`],
['newline and tab', 'line1\nline2\tcol'],
['looks like a flag', '--help'],
];
const created = new Map<string, string>(); // token -> description
const windows = new Map<string, [bigint, bigint]>();
for (const [name, d] of cases) {
await test.step(`token new: ${name}`, async () => {
const before = msToNanos(Date.now() - 1);
const r = await c.exec(d === undefined ? ['token', 'new'] : ['token', 'new', d]);
const after = msToNanos(Date.now() + 1);
expectCliSuccess(r, { stdout: TOKEN_LINE, stderr: '' });
const token = r.stdout.trim();
expect(secrets.has(token), 'token registered as a secret').toBe(true);
created.set(token, d ?? '');
windows.set(token, [before, after]);
});
}
await test.step('quoting through a host shell: docker exec … token new "<quoted>"', async () => {
const d = `it's "quoted" $HOME \`id\` ; | &`;
const r = await runProcess('/bin/sh', ['-c', `docker exec ${c.name} /app token new ${shellQuote(d)}`], { env: toolEnv(), label: 'sh -c docker exec' });
secrets.registerAllIn(r.stdout);
expectCliSuccess(r, { stdout: TOKEN_LINE, stderr: '' });
created.set(r.stdout.trim(), d);
const unquoted = await runProcess('/bin/sh', ['-c', `docker exec ${c.name} /app token new two words`], { env: toolEnv(), label: 'sh -c docker exec' });
expectCliFailure(unquoted, { exitCode: 2, message: 'usage error: token new takes at most one argument' });
});
const tokens = [...created.keys()];
const victim = tokens[2];
await test.step('token list: padded header + one row per token, Go-quoted descriptions, all active', async () => {
const r = await c.exec(['token', 'list']);
expectCliSuccess(r, { stderr: '' });
const rows = parseTokenList(r.stdout);
expect(r.stdout.split('\n')[0].split(/ +/)).toEqual([...TOKEN_LIST_COLUMNS]);
expectCliSuccess(await c.exec(['token', 'list', 'all']), { stdout: r.stdout, stderr: '' });
expect(new Set(rows.map((x) => x.token))).toEqual(new Set(tokens));
for (const row of rows) {
expect(row.descriptionQuoted).toBe(goQuote(created.get(row.token)!));
expect(row.active).toBe(true);
expect(row.deletedAt).toBeNull();
expect(row.createdAt).toMatch(RFC3339NANO_UTC_RE);
}
});
let deleteWindow: [bigint, bigint] = [0n, 0n];
await test.step('token delete: silent success, then already-inactive / unknown failures', async () => {
const before = msToNanos(Date.now() - 1);
const r = await c.exec(['token', 'delete', victim]);
deleteWindow = [before, msToNanos(Date.now() + 1)];
expectCliSuccess(r, { stdout: '', stderr: '' });
const again = await execApp(c, ['token', 'delete', victim]);
expectCliFailure(again, { exitCode: 1, message: 'token already inactive' });
const unknown = await execApp(c, ['token', 'delete', 'A'.repeat(43)]);
expectCliFailure(unknown, { exitCode: 1, message: 'token not found' });
expect(unknown.stderr.includes('A'.repeat(43)), 'supplied token never echoed').toBe(false);
const rows = parseTokenList((await c.exec(['token', 'list', 'all'])).stdout);
const v = rows.find((x) => x.token === victim)!;
expect(v.active).toBe(false);
expect(v.deletedAt).toMatch(RFC3339NANO_UTC_RE);
expect(rows.filter((x) => !x.active).map((x) => x.token)).toEqual([victim]);
const active = parseTokenList((await c.exec(['token', 'list'])).stdout);
expect(active.map((x) => x.token), 'token list hides the deleted token').toEqual(rows.filter((x) => x.token !== victim).map((x) => x.token));
});
await test.step('database rows (snapshot of the mounted DB)', async () => {
const snap = await c.snapshotDb();
const rows = readTokens(snap);
expect(rows.map((r) => r.token).sort()).toEqual([...tokens].sort());
for (const r of rows) {
expect(r.descriptionHex, `description of ${JSON.stringify(created.get(r.token))} stored byte-for-byte`).toBe(hex(created.get(r.token)!));
expect(r.active).toBe(r.token === victim ? 0 : 1);
const w = windows.get(r.token);
if (w) expect(rfc3339ToNanos(r.created_at) >= w[0] && rfc3339ToNanos(r.created_at) <= w[1], 'created_at within the docker exec window').toBe(true);
}
const del = rows.find((r) => r.token === victim)!.deleted_at!;
expect(rfc3339ToNanos(del) >= deleteWindow[0] && rfc3339ToNanos(del) <= deleteWindow[1], 'deleted_at within the delete window').toBe(true);
expect(readOperations(snap), 'CLI commands never write audit rows').toEqual([]);
assertDbInvariants(snap, { tokenCount: tokens.length, operationCount: 0 });
// exact padded tables (rune-counted widths: the descriptions include multi-byte text)
for (const scope of ['active', 'all'] as const) {
expectCliSuccess(await c.exec(scope === 'all' ? ['token', 'list', 'all'] : ['token', 'list']), { stdout: expectedTokenListStdout(snap, scope), stderr: '' });
}
});
expect((await c.stop()).exitCode).toBe(0);
});
test('usage and configuration failures through docker exec: exact exit code 2/1 and stderr; nothing changes', async ({ image, http }) => {
const c = await image.start({ suffix: 'execfail' });
const keep = (await c.exec(['token', 'new', 'keep'])).stdout.trim();
const listBefore = (await c.exec(['token', 'list'])).stdout;
const levelMsg = (v: string) => `configuration error: invalid LOG_LEVEL ${JSON.stringify(v)}: must be one of ERROR, WARN, INFO, DEBUG, TRACE`;
const cases: Array<{ name: string; args: string[]; env?: Record<string, string>; exitCode: 1 | 2; message: string }> = [
{ name: 'unknown command', args: ['bogus'], exitCode: 2, message: 'usage error: unknown command' },
{ name: '--help is an unknown command', args: ['--help'], exitCode: 2, message: 'usage error: unknown command' },
{ name: '-h is an unknown command', args: ['-h'], exitCode: 2, message: 'usage error: unknown command' },
{ name: 'help is case-sensitive', args: ['Help'], exitCode: 2, message: 'usage error: unknown command' },
{ name: 'help with an argument', args: ['help', 'token'], exitCode: 2, message: 'usage error: help takes no arguments' },
{ name: 'case-sensitive command', args: ['Token', 'list'], exitCode: 2, message: 'usage error: unknown command' },
{ name: 'missing subcommand', args: ['token'], exitCode: 2, message: 'usage error: missing token subcommand' },
{ name: 'unknown subcommand', args: ['token', 'remove', keep], exitCode: 2, message: 'usage error: unknown token subcommand' },
{ name: 'list with an unknown argument', args: ['token', 'list', 'ALL'], exitCode: 2, message: 'usage error: unknown token list argument' },
{ name: 'list --all', args: ['token', 'list', '--all'], exitCode: 2, message: 'usage error: unknown token list argument' },
{ name: 'list with two arguments', args: ['token', 'list', 'all', 'all'], exitCode: 2, message: 'usage error: token list takes at most one argument' },
{ name: 'audit with two arguments', args: ['audit', '1', 'h'], exitCode: 2, message: 'usage error: audit takes at most one argument' },
{ name: 'invalid audit argument', args: ['audit', '0'], exitCode: 2, message: 'usage error: invalid audit argument' },
{ name: 'configuration error wins over help', args: ['help'], env: { HTTP_PORT: '0' }, exitCode: 2, message: 'configuration error: invalid HTTP_PORT "0": must be an integer between 1 and 65535' },
{ name: 'new with two arguments', args: ['token', 'new', 'a', 'b'], exitCode: 2, message: 'usage error: token new takes at most one argument' },
{ name: 'delete without argument', args: ['token', 'delete'], exitCode: 2, message: 'usage error: token delete requires exactly one argument' },
{ name: 'delete with two arguments', args: ['token', 'delete', keep, keep], exitCode: 2, message: 'usage error: token delete requires exactly one argument' },
{ name: 'delete unknown token', args: ['token', 'delete', 'B'.repeat(43)], exitCode: 1, message: 'token not found' },
{ name: 'invalid LOG_LEVEL', args: ['token', 'list'], env: { LOG_LEVEL: 'verbose' }, exitCode: 2, message: levelMsg('verbose') },
{ name: 'empty DB_PATH', args: ['token', 'list'], env: { DB_PATH: '' }, exitCode: 2, message: 'configuration error: DB_PATH must not be empty' },
{ name: 'invalid HTTP_PORT', args: ['token', 'list'], env: { HTTP_PORT: '0' }, exitCode: 2, message: 'configuration error: invalid HTTP_PORT "0": must be an integer between 1 and 65535' },
{ name: 'invalid HTTP_ADDR', args: ['token', 'list'], env: { HTTP_ADDR: 'localhost' }, exitCode: 2, message: 'configuration error: invalid HTTP_ADDR "localhost": must be an IPv4 or IPv6 address' },
{ name: 'configuration error wins over usage error', args: ['bogus'], env: { LOG_LEVEL: 'x' }, exitCode: 2, message: levelMsg('x') },
// scratch has no /tmp: a DB_PATH in a missing directory is a runtime (exit 1) failure
{ name: 'DB_PATH in a missing directory', args: ['token', 'list'], env: { DB_PATH: '/tmp/dpsapi.db' }, exitCode: 1, message: 'initialize database schema: unable to open database file (14)' },
];
for (const k of cases) {
await test.step(k.name, async () => {
const r = await execApp(c, k.args, { env: k.env });
expectCliFailure(r, { exitCode: k.exitCode, message: k.message });
if (k.message.startsWith('usage error:')) expect(r.stderr, 'exact stderr: error line + usage text').toBe(`dpsapi: ${k.message}\n${USAGE_TEXT}`);
expect(r.stderr.includes(keep), 'token never echoed').toBe(false);
});
}
expect((await c.exec(['token', 'list'])).stdout, 'token state unchanged').toBe(listBefore);
// the server process is unaffected by failing exec'd commands
expect(await c.isRunning()).toBe(true);
expectContainersResponse(await http.get(c.url(), { token: keep }));
expect((await c.stop()).exitCode).toBe(0);
assertDbInvariants(await c.snapshotDb(), { tokenCount: 1, operationCount: 1 });
});
test('usage, configuration and startup failures through docker run: exact exit codes, stderr, no database created', async ({ image, ws }) => {
const levelMsg = 'configuration error: invalid LOG_LEVEL "verbose": must be one of ERROR, WARN, INFO, DEBUG, TRACE';
const cases: Array<{ name: string; args: string[]; env?: Record<string, string | undefined>; exitCode: 0 | 1 | 2; message?: string; stdout?: string; createsDb: boolean }> = [
{ name: 'server: DB_PATH unset', args: [], env: { DB_PATH: undefined }, exitCode: 2, message: 'configuration error: DB_PATH is required', createsDb: false },
{ name: 'server: HTTP_PORT out of range', args: [], env: { HTTP_PORT: '70000' }, exitCode: 2, message: 'configuration error: invalid HTTP_PORT "70000": must be an integer between 1 and 65535', createsDb: false },
{ name: 'server: HTTP_ADDR with a port', args: [], env: { HTTP_ADDR: '0.0.0.0:80' }, exitCode: 2, message: 'configuration error: invalid HTTP_ADDR "0.0.0.0:80": must be an IPv4 or IPv6 address', createsDb: false },
{ name: 'server: invalid LOG_LEVEL', args: [], env: { LOG_LEVEL: 'verbose' }, exitCode: 2, message: levelMsg, createsDb: false },
{ name: 'server: empty HTTP_PORT', args: [], env: { HTTP_PORT: '' }, exitCode: 2, message: 'configuration error: HTTP_PORT must not be empty', createsDb: false },
{ name: 'unknown command', args: ['serve'], exitCode: 2, message: 'usage error: unknown command', createsDb: false },
{ name: 'missing token subcommand', args: ['token'], exitCode: 2, message: 'usage error: missing token subcommand', createsDb: false },
{ name: 'CLI: DB_PATH unset', args: ['token', 'list'], env: { DB_PATH: undefined }, exitCode: 2, message: 'configuration error: DB_PATH is required', createsDb: false },
// runtime (exit 1) failures: the server opens the DB first, then fails to listen
{ name: 'server: address not available in the container', args: [], env: { HTTP_ADDR: '192.0.2.1' }, exitCode: 1, message: 'listen tcp4 192.0.2.1:80: bind: cannot assign requested address', createsDb: true },
{ name: 'CLI: DB_PATH directory missing', args: ['token', 'list'], env: { DB_PATH: '/data/missing/dpsapi.db' }, exitCode: 1, message: 'initialize database schema: unable to open database file (14)', createsDb: false },
{ name: 'CLI success: token list on a fresh DB', args: ['token', 'list'], exitCode: 0, stdout: EMPTY_TOKEN_LIST, createsDb: true },
{ name: 'CLI success: token list all on a fresh DB', args: ['token', 'list', 'all'], exitCode: 0, stdout: EMPTY_TOKEN_LIST, createsDb: true },
{ name: 'CLI success: audit on a fresh DB', args: ['audit', '1w'], exitCode: 0, stdout: EMPTY_AUDIT, createsDb: true },
{ name: 'CLI success: audit all on a fresh DB', args: ['audit', 'all'], exitCode: 0, stdout: EMPTY_AUDIT, createsDb: true },
{ name: 'audit ALL is case-sensitive', args: ['audit', 'ALL'], exitCode: 2, message: 'usage error: invalid audit argument', createsDb: false },
{ name: 'audit all with an extra argument', args: ['audit', 'all', 'x'], exitCode: 2, message: 'usage error: audit takes at most one argument', createsDb: false },
{ name: 'help: usage text on stdout, no database', args: ['help'], exitCode: 0, stdout: USAGE_TEXT, createsDb: false },
{ name: 'help with an argument', args: ['help', 'me'], exitCode: 2, message: 'usage error: help takes no arguments', createsDb: false },
{ name: 'unknown token list argument', args: ['token', 'list', 'everything'], exitCode: 2, message: 'usage error: unknown token list argument', createsDb: false },
{ name: 'CLI: configuration error wins over help', args: ['help'], env: { DB_PATH: undefined }, exitCode: 2, message: 'configuration error: DB_PATH is required', createsDb: false },
];
const results = await mapLimit(cases, 4, async (k, i) => {
const dataDir = ws.mkdir(`run-data-${i}`);
return { k, dataDir, r: await image.run(k.args, { env: k.env, dataDir, suffix: `run${i}`, timeoutMs: 30_000 }) };
});
for (const { k, dataDir, r } of results) {
await test.step(k.name, async () => {
if (k.exitCode === 0) expectCliSuccess(r, { stdout: k.stdout, stderr: '' });
else expectCliFailure(r, { exitCode: k.exitCode, message: k.message });
if (k.message?.startsWith('usage error:')) expect(r.stderr.endsWith(USAGE_TEXT)).toBe(true);
// usage/configuration errors never open the database; the mounted dir is host-listable
expect(fs.readdirSync(dataDir).includes('dpsapi.db'), 'database file created').toBe(k.createsDb);
});
}
});
test('no shell and no docker CLI in the image: docker exec of them fails at the runtime level (127), not as an app exit code', async ({ image }) => {
const c = await image.start({ suffix: 'noshell' });
const probes: Array<[string[], RegExp]> = [
[['/bin/sh'], /exec: "\/bin\/sh": stat \/bin\/sh: no such file or directory/],
[['sh', '-c', 'true'], /exec: "sh": executable file not found in \$PATH/],
[['/bin/busybox'], /no such file or directory/],
[['docker', 'ps'], /exec: "docker": executable file not found in \$PATH/],
[['/usr/bin/docker'], /no such file or directory/],
[['ls', '/'], /executable file not found in \$PATH/],
];
for (const [argv, msg] of probes) {
await test.step(argv.join(' '), async () => {
const r = await c.execRaw(argv);
// docker exec reports "command not found" from the OCI runtime as 127; such results are
// infrastructure-level (>= 125) and c.exec() would raise a HarnessError for them.
expect(r.signal).toBeNull();
expect(r.timedOut).toBe(false);
expect(r.exitCode).toBe(127);
// the docker CLI (29.x, no TTY) prints the OCI runtime error on its stdout, so check both streams
const out = r.stdout + r.stderr;
expect(out).toMatch(/OCI runtime exec failed/);
expect(out).toMatch(msg);
});
}
// As observed (docker 29.x): a daemon-side exec failure before the process starts (unknown user —
// scratch has no /etc/passwd) makes `docker exec` exit 1, NOT >= 125. The harness must still
// classify it as infrastructure, never as an application failure.
const daemon = await c.execRaw(['/app', 'token', 'list'], { user: 'nobody' });
expect(daemon.exitCode).toBe(1);
expect(daemon.stderr).toMatch(/^Error response from daemon: unable to find user nobody/);
await expect(c.exec(['token', 'list'], { user: 'nobody' })).rejects.toThrow(/\[harness:infrastructure\]/);
await expect(execApp(c, ['token', 'list'], { user: 'nobody' })).rejects.toThrow(/\[harness:infrastructure\]/);
expect(await c.isRunning()).toBe(true);
expect((await c.stop()).exitCode).toBe(0);
});
});
test.describe('docker exec /app audit and /app help', () => {
test('help: usage text on stdout only, exit 0 (also at DEBUG: stdout identical, stderr only log records)', async ({ image }) => {
const c = await image.start({ suffix: 'help' });
const r = await c.exec(['help']);
expectCliSuccess(r, { stdout: USAGE_TEXT, stderr: '' });
const debug = await c.exec(['help'], { env: { LOG_LEVEL: 'DEBUG' } });
expectCliSuccess(debug, { stdout: USAGE_TEXT });
const logs = parseLogs(debug.stderr);
expect(logs.unparsed.map((u) => u.text), 'DEBUG stderr consists of log records only').toEqual([]);
expect(logs.find({ msg: 'dispatch' })?.attrs).toEqual({ mode: 'cli' });
expect(logs.filter({ msg: 'cli dispatch' }).map((x) => x.attrs)).toEqual([{ command: 'help' }]);
expect(logs.count({ msg: 'database opened' }), 'help never opens the database').toBe(0);
expect(await c.isRunning(), 'the server is unaffected').toBe(true);
expect((await c.stop()).exitCode).toBe(0);
});
test('audit: every selection form (incl. audit all) equals the operations rows of the DB snapshot (padded table, oldest first, full tokens registered as secrets); read-only; usage errors', async ({ image, http, secrets }) => {
const c = await image.start({ suffix: 'audit' });
expectCliSuccess(await c.exec(['audit']), { stdout: EMPTY_AUDIT, stderr: '' });
expectCliSuccess(await c.exec(['audit', 'all']), { stdout: EMPTY_AUDIT, stderr: '' });
const created: string[] = [];
for (const d of ['audit one', 'audit two', 'audit three']) {
const r = await c.exec(['token', 'new', d]);
expectCliSuccess(r, { stdout: TOKEN_LINE, stderr: '' });
created.push(r.stdout.trim());
}
const [a, b, gone] = created;
c.fakeDocker!.many(1);
const plan = [a, b, a, gone, a, b, a, a, b, a, gone, a, b];
for (const t of plan) expectContainersResponse(await http.get(c.url(), { token: t }));
expectCliSuccess(await c.exec(['token', 'delete', gone]), { stdout: '', stderr: '' });
const snap = await c.snapshotDb();
const ops = readOperations(snap);
expect(ops.map((o) => o.token), 'one row per authenticated request, in order').toEqual(plan);
const state = stateDigest(snap);
for (const [args, sel] of [
[[], { limit: 10n }],
[['1'], { limit: 1n }],
[['5'], { limit: 5n }],
[['13'], { limit: 13n }],
[['1000'], { limit: 1000n }],
[['0010'], { limit: 10n }],
] as Array<[string[], { limit: bigint }]>) {
const r = await c.exec(['audit', ...args]);
expectCliSuccess(r, { stdout: expectedAuditStdout(ops, sel), stderr: '' });
}
// windows: every row is minutes old at most, so 1h / 1d / 1w and the largest window print all of them
for (const w of ['1h', '1d', '1w', '15250w']) {
const r = await c.exec(['audit', w]);
expectCliSuccess(r, { stdout: expectedAuditStdout(ops, { limit: 1000n }), stderr: '' });
}
const all = parseAuditOutput((await c.exec(['audit', '100'])).stdout);
expect(all.map((x) => x.token), 'oldest first, the deleted token printed in full').toEqual(plan);
for (const t of created) expect(secrets.has(t), 'tokens printed by audit are registered secrets').toBe(true);
// audit all: every row (13 > the default 10), oldest first, exact padded table
const everything = await c.exec(['audit', 'all']);
expectCliSuccess(everything, { stdout: expectedAuditStdout(ops, { all: true }), stderr: '' });
expect(everything.stdout).toBe(padTable([[...AUDIT_COLUMNS], ...chronological(ops).map((o) => [o.time, o.token, o.origin, o.operation])]));
expect(parseAuditOutput(everything.stdout).map((x) => x.token), 'audit all: every row, oldest first').toEqual(plan);
// at DEBUG: identical stdout, the dispatch record says all=true, no token in the log records
const debug = await c.exec(['audit', 'all'], { env: { LOG_LEVEL: 'DEBUG' } });
expectCliSuccess(debug, { stdout: everything.stdout });
const logs = parseLogs(debug.stderr);
expect(logs.unparsed.map((u) => u.text), 'DEBUG stderr consists of log records only').toEqual([]);
expect(logs.filter({ msg: 'cli dispatch' }).map((x) => x.attrs)).toEqual([{ command: 'audit', all: 'true' }]);
secrets.assertNoSecretsIn(debug.stderr, 'DEBUG stderr of docker exec /app audit all');
for (const [args, msg] of [
[['0'], 'invalid audit argument'],
[['5H'], 'invalid audit argument'],
[['-1'], 'invalid audit argument'],
[['1', 'h'], 'audit takes at most one argument'],
[['ALL'], 'invalid audit argument'],
[['All'], 'invalid audit argument'],
[['--all'], 'invalid audit argument'],
[['all1'], 'invalid audit argument'],
[['all', 'x'], 'audit takes at most one argument'],
] as Array<[string[], string]>) {
const r = await c.exec(['audit', ...args]);
expectCliFailure(r, { exitCode: 2, message: `usage error: ${msg}` });
expect(r.stderr, 'exact stderr, the argument is not echoed').toBe(`dpsapi: usage error: ${msg}\n${USAGE_TEXT}`);
}
expect((await c.stop()).exitCode).toBe(0);
const after = await c.snapshotDb();
expect(stateDigest(after), 'audit never writes').toBe(state);
assertDbInvariants(after, { tokenCount: 3, operationCount: plan.length, auditBeforeDeletion: true });
});
});
test('curl from outside the container reaches /api/v1/containers on the real Engine (suite containers and dpsapi itself listed; 401s)', async ({ image, docker, http, testKey }) => {
test.setTimeout(120_000);
const [a, b] = await Promise.all([
docker.run({ suffix: 'peer-a', labels: { 'com.example.role': 'a' } }),
docker.run({ suffix: 'peer.b_2', command: ['tail', '-f', '/dev/null'] }),
]);
const c = await image.start({ docker: 'real', suffix: 'real' });
const token = (await c.exec(['token', 'new', 'curl outside'])).stdout.trim();
const inactive = (await c.exec(['token', 'new', 'inactive'])).stdout.trim();
expectCliSuccess(await c.exec(['token', 'delete', inactive]), { stdout: '', stderr: '' });
const res = await http.curl({ url: c.url(), token });
expect(res.exitCode).toBe(0);
const list = expectContainersResponse<{ Id: string; Names: string[]; Labels: Record<string, string>; State: string; Image: string }>(res);
const own = list.filter((x) => x.Labels?.['dpsapi.e2e.test'] === testKey);
expect(own.map((x) => x.Id).sort(), 'exactly our running containers, including the dpsapi container itself').toEqual([a.id, b.id, c.id].sort());
expect(own.map((x) => x.Id).sort()).toEqual((await docker.runningIds()).sort());
const self = own.find((x) => x.Id === c.id)!;
expect(self.Names).toEqual([`/${c.name}`]);
expect(self.Image).toBe(image.image);
expect(own.find((x) => x.Id === a.id)!.Labels['com.example.role']).toBe('a');
expect(list.every((x) => x.State === 'running'), 'only running containers').toBe(true);
// the same through Playwright's client
expect(expectContainersResponse<{ Id: string; Labels: Record<string, string> }>(await http.get(c.url(), { token })).filter((x) => x.Labels?.['dpsapi.e2e.test'] === testKey).map((x) => x.Id).sort()).toEqual([a.id, b.id, c.id].sort());
const unauth: Array<[string, string[]]> = [
['no Authorization header', []],
['unknown token', ['Authorization: Bearer ' + 'Z'.repeat(43)]],
['inactive token', [`Authorization: Bearer ${inactive}`]],
['wrong scheme', [`Authorization: Basic ${token}`]],
['empty bearer', ['Authorization: Bearer ']],
['two Authorization headers', [`Authorization: Bearer ${token}`, `Authorization: Bearer ${token}`]],
];
for (const [name, headers] of unauth) {
await test.step(name, async () => {
const r = await http.curl({ url: c.url(), headers });
expect(r.exitCode).toBe(0);
expectErrorResponse(r, 401);
});
}
const origins = await expectedOrigins(c); // network info is only available while running
const exit = await c.stop();
expect(exit.exitCode).toBe(0);
const snap = await c.snapshotDb();
const ops = readOperations(snap);
expect(ops, 'one audit row per authenticated request, none for the 401s').toHaveLength(2);
for (const o of ops) {
expect(o.token).toBe(token);
expect(origins).toContain(o.origin);
}
assertDbInvariants(snap, { tokenCount: 2, operationCount: 2 });
});