Process and API-only Playwright harness: binary and image setup, server/CLI runners, fake Docker Engine socket, read-only database inspection, secret redaction in reports, seeded fuzzing, leak detection and harness self-checks.
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
/**
|
|
* Shared assertions on the HTTP contract of dpsapi.
|
|
*/
|
|
import { expect } from '@playwright/test';
|
|
import type { CurlResult, HttpResult, RawHttpResponse } from './http';
|
|
|
|
/** Exact error bodies of the application. */
|
|
export const ERROR_BODIES = {
|
|
401: '{"error":"unauthorized"}',
|
|
404: '{"error":"not found"}',
|
|
405: '{"error":"method not allowed"}',
|
|
500: '{"error":"internal server error"}',
|
|
} as const;
|
|
|
|
export type AppErrorStatus = keyof typeof ERROR_BODIES;
|
|
|
|
/** Client-independent response view. */
|
|
export interface ResponseView {
|
|
status: number;
|
|
/** Lower-cased names; repeated headers joined with ', '. */
|
|
headers: Record<string, string>;
|
|
body: string;
|
|
}
|
|
|
|
/** Normalizes Playwright / curl / raw responses into a ResponseView. */
|
|
export function view(r: HttpResult | CurlResult | RawHttpResponse | ResponseView): ResponseView {
|
|
if ('bodyText' in r) {
|
|
const headers: Record<string, string> = {};
|
|
for (const [k, v] of r.headers) {
|
|
const l = k.toLowerCase();
|
|
headers[l] = l in headers ? `${headers[l]}, ${v}` : v;
|
|
}
|
|
return { status: r.status, headers, body: r.bodyText };
|
|
}
|
|
return { status: r.status, headers: r.headers, body: r.body };
|
|
}
|
|
|
|
/** Substrings that must never appear in an HTTP response body. */
|
|
export const INTERNAL_DETAIL_MARKERS = [
|
|
'sql',
|
|
'sqlite',
|
|
'docker.sock',
|
|
'/var/run',
|
|
'unix',
|
|
'dial',
|
|
'goroutine',
|
|
'panic',
|
|
'runtime error',
|
|
'fake engine',
|
|
'connection refused',
|
|
'permission denied',
|
|
'no such file',
|
|
'context deadline',
|
|
'list containers',
|
|
'token_id',
|
|
];
|
|
|
|
/** Fails if the body contains any internal-detail marker (case-insensitive). */
|
|
export function expectNoInternalDetails(body: string): void {
|
|
const low = body.toLowerCase();
|
|
expect(INTERNAL_DETAIL_MARKERS.filter((m) => low.includes(m)), `internal details leaked in body ${JSON.stringify(body.slice(0, 300))}`).toEqual([]);
|
|
}
|
|
|
|
/**
|
|
* Asserts one of the application's JSON error responses exactly: status, body, Content-Type,
|
|
* Content-Length, WWW-Authenticate (only on 401: `Bearer`) and Allow (only on 405: `GET`).
|
|
*/
|
|
export function expectErrorResponse(res: HttpResult | CurlResult | RawHttpResponse | ResponseView, status: AppErrorStatus, { head = false }: { head?: boolean } = {}): void {
|
|
const r = view(res);
|
|
const body = ERROR_BODIES[status];
|
|
expect(r.status, `status (body ${JSON.stringify(r.body.slice(0, 200))})`).toBe(status);
|
|
expect(r.body, 'error body').toBe(head ? '' : body);
|
|
expect(r.headers['content-type'], 'content-type').toBe('application/json');
|
|
expect(r.headers['content-length'], 'content-length').toBe(String(body.length));
|
|
if (status === 401) expect(r.headers['www-authenticate'], 'www-authenticate').toBe('Bearer');
|
|
else expect(r.headers['www-authenticate'], 'www-authenticate').toBeUndefined();
|
|
if (status === 405) expect(r.headers['allow'], 'allow').toBe('GET');
|
|
else expect(r.headers['allow'], 'allow').toBeUndefined();
|
|
expect(r.headers['location'], 'no redirect').toBeUndefined();
|
|
}
|
|
|
|
/**
|
|
* Asserts a successful container listing: 200, application/json, exact Content-Length, a JSON
|
|
* array without trailing newline. Returns the parsed array.
|
|
*/
|
|
export function expectContainersResponse<T = Record<string, unknown>>(res: HttpResult | CurlResult | RawHttpResponse | ResponseView): T[] {
|
|
const r = view(res);
|
|
expect(r.status, `status (body ${JSON.stringify(r.body.slice(0, 200))})`).toBe(200);
|
|
expect(r.headers['content-type'], 'content-type').toBe('application/json');
|
|
expect(r.headers['content-length'], 'content-length').toBe(String(Buffer.byteLength(r.body)));
|
|
expect(r.body.endsWith('\n'), 'no trailing newline').toBe(false);
|
|
const parsed = JSON.parse(r.body) as unknown;
|
|
expect(Array.isArray(parsed), 'body is a JSON array').toBe(true);
|
|
return parsed as T[];
|
|
}
|