coulomb-social/scripts/native_recovery/reset_browser.mjs
tegwick 7b4b75089c
Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Verify native reset SMTP issuer and app recovery through Chromium
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
2026-10-31 00:15:24 +01:00

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