// 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([ ['.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(); // token -> description const windows = new Map(); 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 ""', 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; 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; 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; 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 }>(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 }); });