e8e5c82b86
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>
62 lines
3.2 KiB
JavaScript
62 lines
3.2 KiB
JavaScript
/**
|
||
* Тест refresh-session.js — поведение при ОТКАЗЕ запуска браузера.
|
||
*
|
||
* FN-SESSION (приёмка 22.06.2026): на проде browserType.launch падал (не было
|
||
* исполняемого файла headless-shell в кэше www-data). Из-за того, что
|
||
* chromium.launch() стоял ВНЕ try/catch, отказ становился unhandled promise
|
||
* rejection → Node выходил с кодом 1 + сырой стек в stderr — неотличимо от
|
||
* честного «login rejected» (тоже exit 1). Этот тест фиксирует контракт:
|
||
* отказ запуска браузера = exit 4 + структурированный JSON {error} в stderr.
|
||
*
|
||
* Runner: встроенный node:test (Node 18+). Запуск: `node --test refresh-session.test.js`.
|
||
*/
|
||
const { test } = require('node:test');
|
||
const assert = require('node:assert');
|
||
const { execFile } = require('node:child_process');
|
||
const path = require('node:path');
|
||
|
||
const SCRIPT = path.resolve(__dirname, 'refresh-session.js');
|
||
|
||
/** Спавнить refresh-session.js с заданным env, подать 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'));
|
||
resolve({
|
||
code: err ? err.code : 0,
|
||
stdout: stdout.toString(),
|
||
stderr: stderr.toString(),
|
||
});
|
||
},
|
||
);
|
||
child.stdin.write(JSON.stringify(input));
|
||
child.stdin.end();
|
||
});
|
||
}
|
||
|
||
test('отказ запуска браузера → чистый exit 4 + JSON {error} в stderr (не опасный exit 1)', async () => {
|
||
// Форсируем отказ chromium.launch: указываем кэш браузеров в несуществующий
|
||
// путь — Playwright не найдёт исполняемый файл (та же ошибка, что была на проде).
|
||
const result = await runScript(
|
||
{ login: 'x', password: 'y', url: 'http://127.0.0.1:1/' },
|
||
{ PLAYWRIGHT_BROWSERS_PATH: path.resolve(__dirname, '__nonexistent_browsers__') },
|
||
);
|
||
|
||
// Отказ запуска должен классифицироваться как exit 4 (другая ошибка),
|
||
// а НЕ как exit 1 (login rejected) и не как unhandled-rejection exit 1.
|
||
assert.strictEqual(result.code, 4, `Expected exit 4, got ${result.code}. stderr: ${result.stderr}`);
|
||
|
||
// stderr должен быть валидным JSON с ключом error (а не сырым стеком Node).
|
||
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}`);
|
||
});
|