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