Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
119 lines
5.1 KiB
JavaScript
119 lines
5.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
import { chromium } from './node_modules/playwright/index.mjs';
|
|
|
|
const input = JSON.parse(await readFile('/run/fixture/browser-input.json', 'utf8'));
|
|
const auth = 'https://auth.recovery.test:9091';
|
|
const mailbox = 'http://127.0.0.1:8088';
|
|
const allowed = new Set([auth, mailbox]);
|
|
let stage = 'browser_startup';
|
|
let outside = 0;
|
|
let browser;
|
|
try {
|
|
browser = await chromium.launch({
|
|
executablePath: '/run/fixture/browser/chrome-headless-shell-linux64/chrome-headless-shell',
|
|
headless: true,
|
|
args: ['--host-resolver-rules=MAP auth.recovery.test 127.0.0.1',
|
|
'--no-proxy-server', '--disable-background-networking'],
|
|
});
|
|
const context = await browser.newContext({
|
|
ignoreHTTPSErrors: true, serviceWorkers: 'block', locale: 'en-US',
|
|
});
|
|
context.on('request', request => {
|
|
const u = new URL(request.url());
|
|
if (['http:', 'https:'].includes(u.protocol) && !allowed.has(u.origin)) outside += 1;
|
|
});
|
|
await context.route('**/*', route => {
|
|
if (!allowed.has(new URL(route.request().url()).origin)) {
|
|
return route.abort();
|
|
}
|
|
return route.continue();
|
|
});
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(15000);
|
|
async function messages() {
|
|
const response = await context.request.get(mailbox + '/messages', { maxRedirects: 0 });
|
|
assert.equal(response.status(), 200);
|
|
return (await response.json()).messages;
|
|
}
|
|
stage = 'native_login_page';
|
|
for (let n = 0; n < 120; n++) {
|
|
try {
|
|
await page.goto(auth, { timeout: 2000 });
|
|
break;
|
|
} catch {
|
|
if (n === 119) throw new Error('Native startup');
|
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
}
|
|
}
|
|
await page.locator('#reset-password-button').waitFor();
|
|
const before = (await messages()).length;
|
|
stage = 'request_reset_through_native_form';
|
|
await page.locator('#reset-password-button').click();
|
|
await page.getByLabel('Username', { exact: true }).fill('alice');
|
|
const started = page.waitForResponse(r => r.url() === auth + '/api/reset-password/identity/start');
|
|
await page.locator('#reset-button').click();
|
|
assert.equal((await started).status(), 200);
|
|
let delivered;
|
|
for (let n = 0; n < 60; n++) {
|
|
const newer = (await messages()).slice(before).filter(m => m.reset_links.length > 0);
|
|
if (newer.length) { delivered = newer[0]; break; }
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
assert(delivered);
|
|
assert.equal(delivered.recipient, 'alice@example.test');
|
|
const link = delivered.reset_links[0];
|
|
assert.equal(new URL(link).origin, auth);
|
|
assert.equal(new URL(link).pathname, '/reset-password/step2');
|
|
if (input.expect_expired) {
|
|
stage = 'wait_for_native_handle_expiry';
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
}
|
|
stage = 'consume_native_reset_link';
|
|
const verified = page.waitForResponse(r => r.url() === auth + '/api/reset-password/identity/finish');
|
|
await page.goto(link);
|
|
const verifiedBody = await (await verified).json();
|
|
if (input.expect_expired) {
|
|
assert.notEqual(verifiedBody.status, 'OK');
|
|
assert.equal(await page.locator('#reset-button').isEnabled(), false);
|
|
assert.equal(outside, 0);
|
|
console.log(JSON.stringify({status: 'browser_checks_passed', native_form_reset_requested: true,
|
|
native_smtp_sink_delivery: true, expired_native_handle_refused: true,
|
|
password_change_accepted: false, requests_outside_fixture_origins: outside,
|
|
browser_version: browser.version()}));
|
|
await context.close();
|
|
await browser.close();
|
|
browser = undefined;
|
|
process.exit(0);
|
|
}
|
|
await page.getByLabel('New password', { exact: true }).fill(input.new_password);
|
|
await page.getByLabel('Repeat new password', { exact: true }).fill(input.new_password);
|
|
stage = 'submit_native_replacement_password';
|
|
const changed = page.waitForResponse(r => r.url() === auth + '/api/reset-password'
|
|
&& r.request().method() === 'POST');
|
|
await page.locator('#reset-button').click();
|
|
const result = await changed;
|
|
const responseBody = await result.text();
|
|
const changeAccepted = result.status() === 200
|
|
&& (!responseBody || JSON.parse(responseBody).status !== 'KO');
|
|
assert.equal(changeAccepted, input.expected_success);
|
|
stage = 'replayed_reset_link';
|
|
const replay = page.waitForResponse(r => r.url() === auth + '/api/reset-password/identity/finish');
|
|
await page.goto(link);
|
|
const replayBody = await (await replay).json();
|
|
assert.notEqual(replayBody.status, 'OK');
|
|
assert.equal(outside, 0);
|
|
console.log(JSON.stringify({
|
|
status: 'browser_checks_passed', native_form_reset_requested: true,
|
|
native_smtp_sink_delivery: true, native_link_verified: true,
|
|
password_change_accepted: changeAccepted, consumed_link_replay_refused: true,
|
|
requests_outside_fixture_origins: outside, browser_version: browser.version(),
|
|
}));
|
|
await context.close();
|
|
} catch (error) {
|
|
await writeFile('/run/fixture/browser-diagnostic.txt', String(error.message), { mode: 0o600 });
|
|
console.log(JSON.stringify({ status: 'browser_check_failed', stage, failure_type: error.name }));
|
|
process.exitCode = 1;
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
}
|