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.
63 lines
2.6 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|
|
}
|