// 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); } } } }