Files
portal/app/playwright/manage-project.test.js
T
Дмитрий e8e5c82b86
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
fix(приёмка): FN-RESET + FN-LOGIN-ROUTE + диагностируемость FN-SESSION
FN-RESET: письмо сброса строило именованный роут password.reset которого нет в SPA.
ResetPassword::createUrlUsing → /reset/{token}?email= в AppServiceProvider boot.

FN-LOGIN-ROUTE: гость без Accept json на auth:sanctum уводил в именованный роут
login которого нет → 500. redirectGuestsTo /login + render AuthenticationException
→ 401 JSON для api/*.

FN-SESSION: chromium.launch стоял вне try/catch — отказ запуска браузера маскировался
unhandled-rejection в opaque exit 1 двойник login-rejected. launch в try + top-level
catch → чистый exit 4 + JSON stderr в refresh-session.js и manage-project.js.

Тесты: PasswordResetUrlTest, UnauthenticatedApiResponseTest, node:test launch-failure
в обоих playwright-скриптах. Разбор FN-SESSION + ops-долг playwright install под
www-data + поправки отчёта приёмки + новая находка FN-INN-LOOKUP.

Прод не трогался. Накат — позже вместе с остальным.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:49:19 +03:00

160 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Фикстурный тест manage-project.js — против локального HTTP-сервера с Element UI фикстурой.
*
* Почему HTTP, не file://: manage-project.js перехватывает ответ page.waitForResponse()
* с URL endsWith('/admin/visit/rt-project-save'). Браузер не шлёт network-запросы при
* file://-origin fetch из-за CORS/same-origin ограничений в Chromium.
*
* Runner: встроенный node:test (Node 18+). Запуск: `node --test manage-project.test.js`.
*/
const { test } = require('node:test');
const assert = require('node:assert');
const { execFile } = require('node:child_process');
const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const SCRIPT = path.resolve(__dirname, 'manage-project.js');
const FIXTURE_PATH = path.resolve(__dirname, 'fixtures', 'rt-form-element-ui.html');
/** Запустить ephemeral HTTP-сервер, отдающий фикстуру и обрабатывающий mock-эндпоинты. */
function startFixtureServer() {
return new Promise((resolve) => {
const html = fs.readFileSync(FIXTURE_PATH, 'utf8');
const server = http.createServer((req, res) => {
// Mock rt-project-save — Playwright перехватывает реальный сетевой запрос
if (req.url && req.url.includes('rt-project-save') && req.method === 'POST') {
// Consume request body (important — don't hang connection)
let body = '';
req.on('data', (c) => { body += c; });
req.on('end', () => {
const payload = JSON.stringify({ status: 'OK', message: '', result: null, id: '99001' });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(payload);
});
return;
}
// Default: serve fixture HTML
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
});
server.listen(0, '127.0.0.1', () => resolve(server));
});
}
/** Спавнить manage-project.js, подать JSON на stdin, вернуть {code, stdout, stderr}. */
function runScript(input, extraEnv) {
return new Promise((resolve, reject) => {
const child = execFile(
'node',
[SCRIPT],
{ timeout: 90_000, env: { ...process.env, ...extraEnv } },
(err, stdout, stderr) => {
if (err && err.killed) return reject(new Error('Process killed / timed out'));
// err.code — exit code; treat as expected (tests assert on code)
resolve({
code: err ? err.code : 0,
stdout: stdout.toString(),
stderr: stderr.toString(),
});
},
);
child.stdin.write(JSON.stringify(input));
child.stdin.end();
});
}
// ---------------------------------------------------------------------------
// Test 1 — createProject через Element UI фикстуру → external_id из mock-response
// ---------------------------------------------------------------------------
test('createProject fills Element UI form and returns external_id from intercept response', async () => {
const server = await startFixtureServer();
try {
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const result = await runScript({
operation: 'create',
url,
skipLogin: true,
dto: {
tag: '_lidpotok',
name: 'example.com',
platforms: ['B1'],
signal_type: 'site',
limit: 5,
workdays: [1, 2, 3, 4, 5],
domains: ['example.com'],
region_mode: 'include',
regions: [],
active: true,
},
});
assert.strictEqual(result.code, 0, `Expected exit 0, got ${result.code}. stderr: ${result.stderr}`);
let out;
try {
out = JSON.parse(result.stdout);
} catch (e) {
assert.fail(`stdout is not valid JSON: ${result.stdout}\nstderr: ${result.stderr}`);
}
assert.strictEqual(out.external_id, '99001', `expected external_id "99001", got ${JSON.stringify(out)}`);
} finally {
server.close();
}
});
// ---------------------------------------------------------------------------
// Test 2 — listProjects в skipLogin-режиме возвращает массив projects
// ---------------------------------------------------------------------------
test('listProjects returns array (skipLogin mode, fixture page)', async () => {
const server = await startFixtureServer();
try {
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const result = await runScript({
operation: 'list',
url,
skipLogin: true,
});
// listOp в skipLogin-режиме не навигирует на /admin/visit/rt — просто открывает url.
// Фикстура не содержит Vuex и таблицы с проектами → возвращает {projects: []}.
assert.strictEqual(result.code, 0, `Expected exit 0. stderr: ${result.stderr}`);
let out;
try {
out = JSON.parse(result.stdout);
} catch (e) {
assert.fail(`stdout is not valid JSON: ${result.stdout}`);
}
assert.ok(Array.isArray(out.projects), `expected projects array, got: ${JSON.stringify(out)}`);
} finally {
server.close();
}
});
// ---------------------------------------------------------------------------
// Test 3 — отказ запуска браузера → чистый exit 4 + JSON {error}, не опасный exit 1
// FN-SESSION (приёмка 22.06.2026): launch вне try/catch уводил отказ запуска в
// unhandled-rejection exit 1 — двойник «login rejected». Фикс: launch в try → exit 4.
// ---------------------------------------------------------------------------
test('browser launch failure → exit 4 + JSON {error} (not unhandled-rejection exit 1)', async () => {
const result = await runScript(
{ operation: 'list', url: 'http://127.0.0.1:1/', skipLogin: true },
{ PLAYWRIGHT_BROWSERS_PATH: path.resolve(__dirname, '__nonexistent_browsers__') },
);
assert.strictEqual(result.code, 4, `Expected exit 4, got ${result.code}. stderr: ${result.stderr}`);
let parsed;
try {
parsed = JSON.parse(result.stderr.trim());
} catch (e) {
assert.fail(`stderr не валидный JSON (сырой стек?): ${result.stderr}`);
}
assert.ok(typeof parsed.error === 'string' && parsed.error.length > 0, `expected {error}, got ${result.stderr}`);
});