import { test, expect } from '@playwright/test'; import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { attachNetlog, sanitizePost } from '../lib/netlog'; import { WEB } from '../lib/auth'; test('sanitizePost: /auth/ - тело не пишется', () => { expect(sanitizePost('/api/v1/auth/login', JSON.stringify({ username: 'perf_rf', password: 'perf' }))).toBeNull(); expect(sanitizePost('/api/v1/auth/refresh', JSON.stringify({ refresh_token: 'abc' }))).toBeNull(); }); test('sanitizePost: поля password/token вырезаются из JSON на любой глубине', () => { const out = sanitizePost('/api/v1/users/', JSON.stringify({ username: 'u', password: 'p', nested: { access_token: 't', Token: 'x', keep: 1 }, list: [{ secret: 's', v: 2 }], })); expect(out).not.toBeNull(); const v = JSON.parse(out!); expect(v).toEqual({ username: 'u', nested: { keep: 1 }, list: [{ v: 2 }] }); expect(out).not.toMatch(/"p"|"t"|"x"|"s"/); }); test('sanitizePost: form-urlencoded - поля password/token вырезаются', () => { expect(sanitizePost('/api/v1/x', 'username=u&password=p&token=t&a=1')).toBe('username=u&a=1'); }); test('sanitizePost: тело без секретов не меняется, пустое - null', () => { const body = JSON.stringify({ value: 1000, column: 'plan' }); expect(sanitizePost('/api/v1/form/7/sheet/AHR/cell', body)).toBe(body); expect(sanitizePost('/api/v1/x', null)).toBeNull(); }); test('attachNetlog: логин не попадает в network.jsonl, остальные POST - без секретов', async ({ browser }) => { const tmp = mkdtempSync(path.join(tmpdir(), 'perf-netlog-')); process.env.PERF_RESULTS_DIR = tmp; const ctx = await browser.newContext(); try { // стенд не нужен: все запросы к WEB отвечает route await ctx.route(WEB + '/**', (r) => r.fulfill({ status: 200, contentType: r.request().url().includes('/api/') ? 'application/json' : 'text/html', body: r.request().url().includes('/api/') ? '{}' : '' })); const nl = attachNetlog(ctx, { scenario: 'netlog_test', target: 't', role: 'rf' }); const page = await ctx.newPage(); await page.goto(WEB + '/'); await page.evaluate(async () => { const post = (u: string, b: unknown) => fetch(u, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) }); await post('/api/v1/auth/login', { username: 'perf_rf', password: 'SECRET-PASS' }); await post('/api/v1/form/7/rows', { value: 1, token: 'SECRET-TOKEN' }); }); await page.waitForTimeout(300); // событие requestfinished приходит после завершения fetch на странице await nl.flush(); const lines = readFileSync(path.join(tmp, 'network.jsonl'), 'utf-8').trim().split('\n').map((l) => JSON.parse(l)); const login = lines.find((l) => l.path === '/api/v1/auth/login'); const rows = lines.find((l) => l.path === '/api/v1/form/7/rows'); expect(login).toBeTruthy(); expect(login.post_data).toBeNull(); expect(JSON.parse(rows.post_data)).toEqual({ value: 1 }); expect(readFileSync(path.join(tmp, 'network.jsonl'), 'utf-8')).not.toMatch(/SECRET/); } finally { await ctx.close(); delete process.env.PERF_RESULTS_DIR; rmSync(tmp, { recursive: true, force: true }); } });