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.
This commit is contained in:
1 parent
a02eef13a7
commit
fe0cd4bf6d
33 files changed
+6370
No files matched your search
@@ -0,0 +1,62 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user