Files
dpsapi/e2e/harness/redacting-reporter.ts
bryanpedini fe0cd4bf6d test(e2e): add playwright e2e harness
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.
2026-10-06 10:52:25 +02:00

63 lines
2.6 KiB
TypeScript

// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
/**
* First reporter in the chain: scrubs token secrets from every test result before the list,
* JSON and HTML reporters see it. It redacts (a) every token registered by the workers for this
* run (exact substring, catching tokens embedded in longer strings) and (b) every token-shaped
* 43-character base64url run. Covers error messages/stacks/snippets (also of steps), captured
* stdout/stderr, inline attachment bodies, and text attachment files written under the output
* directory (e.g. Playwright's error-context.md).
*/
import fs from 'node:fs';
import path from 'node:path';
import type { FullConfig, Reporter, TestCase, TestError, TestResult, TestStep } from '@playwright/test/reporter';
import { loadPersistedSecrets, redactText } from './secrets';
const TEXTUAL = /^(text\/|application\/(json|xml|x-ndjson))|markdown/;
export default class RedactingReporter implements Reporter {
private outputDirs: string[] = [];
printsToStdio(): boolean {
return false;
}
onBegin(config: FullConfig): void {
this.outputDirs = config.projects.map((p) => path.resolve(p.outputDir));
}
onTestEnd(_test: TestCase, result: TestResult): void {
const dir = process.env.DPSAPI_E2E_RUN_DIR;
const secrets = dir ? loadPersistedSecrets(path.join(dir, 'secrets')) : [];
const r = (s: string) => redactText(s, secrets);
const fixError = (e: TestError | undefined) => {
if (!e) return;
if (e.message) e.message = r(e.message);
if (e.stack) e.stack = r(e.stack);
if (e.value) e.value = r(e.value);
if (e.snippet) e.snippet = r(e.snippet);
if (e.cause) fixError(e.cause);
};
result.errors.forEach(fixError);
fixError(result.error);
const fixStep = (s: TestStep) => {
fixError(s.error);
s.title = r(s.title);
s.steps.forEach(fixStep);
};
result.steps.forEach(fixStep);
result.stdout = result.stdout.map((c) => (typeof c === 'string' ? r(c) : Buffer.from(r(c.toString('utf8')))));
result.stderr = result.stderr.map((c) => (typeof c === 'string' ? r(c) : Buffer.from(r(c.toString('utf8')))));
for (const a of result.attachments) {
if (!TEXTUAL.test(a.contentType)) continue;
if (a.body) a.body = Buffer.from(r(a.body.toString('utf8')));
if (a.path && this.outputDirs.some((d) => path.resolve(a.path!).startsWith(d + path.sep)) && fs.existsSync(a.path)) {
const text = fs.readFileSync(a.path, 'utf8');
const red = r(text);
if (red !== text) fs.writeFileSync(a.path, red);
}
}
}
}