// #1873 the Proxy setup panel in a real browser: the next step is one button, init sends a // generated-key request unless the owner pastes a key on loopback, and pasted fields are cleared. import test from 'node:test'; import assert from 'node:assert/strict'; import { w15Fixture } from './w15-fixture.mjs'; const T = '2026-11-11T12:00:00.000Z'; const base = { release: { fetched: false, verified: false, version: '8.0.42', panelVersion: '1.26.7' }, init: { initialized: false, partial: false, files: [{ label: 'proxy settings', present: false }, { label: 'management key', present: false }] }, unit: { unit: 'mosaic-proxy.service', installed: false, active: 'inactive' }, fence: { set: false }, initLock: { present: false, clearable: false, blockers: [] }, fetch: { state: 'idle' }, restart: null, busy: null, organizations: [{ id: 'acme', proxy: 'shared', catalogue: null }], ownProxy: { available: false, state: 'not built', reason: 'The configuration has one proxy per instance.' }, next: ['fetch'], }; const fetched = { ...base, release: { ...base.release, fetched: true, verified: true }, next: ['init'] }; const fenced = { ...base, release: fetched.release, init: { initialized: true, partial: false, settings: { port: 18432 }, files: [] }, unit: { unit: 'mosaic-proxy.service', installed: true, active: 'active', pid: 4242, port: 18432, listening: true }, fence: { set: true, op: 'apikey.add', org: 'acme', time: T }, restart: { confirmed: true, pid: 4242, observedAt: T }, next: ['fence-clear'] }; const unconfigured = { 'GET /api/proxy/accounts': { status: 503, body: { error: 'not-configured' } }, 'GET /api/proxy/keys': { status: 503, body: { error: 'not-configured' } } }; const scenario = (status, config = {}) => ({ '/api/config': { send: true, devOwner: true, proxyAdmin: true, proxyAdminSend: true, ...config }, '/api/proxy/admin/status': status, ...unconfigured }); const writes = b => b.evaluate('window.__writes.filter(w=>w.path.startsWith("/api/proxy/admin/")).map(w=>({path:w.path,body:JSON.parse(w.body)}))'); test('a fresh instance shows the fetch step as one button, the not-built own proxy, and no terminal note', { timeout: 90000 }, async t => { const { f, b, wait } = await w15Fixture(t, { scenario: { ...scenario(base), 'POST /api/proxy/admin/fetch': { status: 202, body: { state: 'running' } } } }); await b.navigate(f.base + '/#/settings/model-proxy'); await wait('document.querySelector("#proxy-setup-go")?.dataset.next==="fetch"'); assert.equal(await b.evaluate('document.querySelector("#proxy-setup").hidden'), false); assert.equal(await b.evaluate('document.querySelector("#proxy-setup-go").textContent'), 'Fetch the pinned release'); assert.match(await b.evaluate('document.querySelector("#proxy-setup-next p").textContent'), /CLIProxyAPI 8\.0\.42/); assert.match(await b.evaluate('document.querySelector("#proxy-setup-own").textContent'), /not built/); assert.equal(await b.evaluate('document.querySelector("#proxy-setup-orgs td").textContent'), 'acme'); await wait('document.querySelector("#proxy-message")?.textContent.includes("Follow the next step under Proxy setup")'); assert.equal(await b.evaluate('!!document.querySelector("#proxy-dev-setup")'), false, 'the page replaces the terminal note'); assert.equal(await b.evaluate('document.querySelectorAll("#proxy-setup-init [data-setup-write]:not([disabled])").length'), 0, 'init waits for a verified release'); await b.evaluate('document.querySelector("#proxy-setup-go").click()'); await wait('window.__writes.some(w=>w.path==="/api/proxy/admin/fetch")'); assert.deepEqual(await writes(b), [{ path: '/api/proxy/admin/fetch', body: {} }]); assert.deepEqual(await b.evaluate('window.__unexpected'), []); }); test('without send the next-step button is disabled and nothing is posted', { timeout: 90000 }, async t => { const { f, b, wait } = await w15Fixture(t, { scenario: scenario(base, { send: false, proxyAdminSend: false }) }); await b.navigate(f.base + '/#/settings/model-proxy'); await wait('document.querySelector("#proxy-setup-go")?.dataset.next==="fetch"'); assert.equal(await b.evaluate('document.querySelector("#proxy-setup-go").disabled'), true); await b.evaluate('document.querySelector("#proxy-setup-go").click()'); assert.deepEqual(await writes(b), []); }); test('init sends generate by default; a pasted key on loopback is sent with its confirmation and the fields are cleared', { timeout: 90000 }, async t => { const { f, b, wait } = await w15Fixture(t, { scenario: { ...scenario(fetched), 'POST /api/proxy/admin/init': { body: { settings: { port: 18432 }, receipt: { op: 'init', time: T, port: 18432, generated: true } } } } }); await b.navigate(f.base + '/#/settings/model-proxy'); await wait('document.querySelector("#proxy-setup-go")?.dataset.next==="init"'); await b.evaluate('document.querySelector("#proxy-setup-go").click()'); await wait('document.querySelector("#proxy-setup-init-box").open===true'); assert.equal(await b.evaluate('document.querySelector("#proxy-setup-paste-choice").hidden'), false, 'loopback page offers the paste choice'); assert.equal(await b.evaluate('document.querySelector("#proxy-setup-paste").hidden'), true); await b.evaluate('(()=>{const f=document.querySelector("#proxy-setup-init");f.elements.port.value="18432";f.requestSubmit();})()'); await wait('window.__writes.some(w=>w.path==="/api/proxy/admin/init")'); await wait('document.querySelector("#proxy-setup-message").textContent.includes("with a generated key")'); assert.deepEqual(await writes(b), [{ path: '/api/proxy/admin/init', body: { port: 18432, generate: true } }]); assert.match(await b.evaluate('document.querySelector("#proxy-setup-receipts").textContent'), /init/); await wait('!document.querySelector("#proxy-setup-init [data-setup-write]").disabled'); await b.evaluate('(()=>{const f=document.querySelector("#proxy-setup-init");f.elements.keySource.value="paste";f.querySelector("[value=paste]").dispatchEvent(new Event("change"));f.elements.port.value="";f.elements.key.value="pa-browser-sentinel-0001";f.elements.confirm.value="pa-browser-sentinel-0002";f.requestSubmit();})()'); await wait('document.querySelector("#proxy-setup-message").textContent.includes("proxy-key-mismatch")'); assert.equal((await writes(b)).length, 1, 'a mismatch is refused in the page'); assert.equal(await b.evaluate('document.querySelector("#proxy-setup-init").elements.key.value'), '', 'the key field is cleared on submit'); await b.evaluate('(()=>{const f=document.querySelector("#proxy-setup-init");f.elements.key.value="pa-browser-sentinel-0001";f.elements.confirm.value="pa-browser-sentinel-0001";f.requestSubmit();})()'); await wait('window.__writes.filter(w=>w.path==="/api/proxy/admin/init").length===2'); assert.deepEqual((await writes(b))[1], { path: '/api/proxy/admin/init', body: { key: 'pa-browser-sentinel-0001', confirm: 'pa-browser-sentinel-0001' } }); assert.equal(await b.evaluate('[...document.querySelectorAll("#proxy-setup-init input[type=password]")].every(i=>i.value==="")'), true); assert.equal(await b.evaluate('document.body.innerText.includes("pa-browser-sentinel")'), false); }); test('after a confirmed restart the next step clears the fence and shows the receipt', { timeout: 90000 }, async t => { const { f, b, wait } = await w15Fixture(t, { scenario: { ...scenario(fenced), 'POST /api/proxy/admin/fence/clear': { body: { cleared: true, receipt: { op: 'fence.clear', time: T, pid: 4242 } } } } }); await b.navigate(f.base + '/#/settings/model-proxy'); await wait('document.querySelector("#proxy-setup-go")?.dataset.next==="fence-clear"'); assert.match(await b.evaluate('document.querySelector("#proxy-setup-state").textContent'), /new pid 4242/); await b.evaluate('document.querySelector("#proxy-setup-go").click()'); await wait('document.querySelector("#proxy-setup-message").textContent.includes("Fence cleared")'); assert.deepEqual(await writes(b), [{ path: '/api/proxy/admin/fence/clear', body: {} }]); assert.match(await b.evaluate('document.querySelector("#proxy-setup-receipts").textContent'), /fence\.clear.*pid: 4242/); }); test('a stopped proxy hides the account panels with a setup note, and starting the service brings them back', { timeout: 90000 }, async t => { const installed = { ...base, release: fetched.release, init: { initialized: true, partial: false, settings: { port: 18432 }, files: [] }, unit: { unit: 'mosaic-proxy.service', installed: true, active: 'inactive' }, next: ['unit-start'] }; const down = { 'GET /api/proxy/accounts': { status: 503, body: { error: 'proxy-down' } }, 'GET /api/proxy/keys': { status: 503, body: { error: 'proxy-down' } } }; const { f, b, wait } = await w15Fixture(t, { scenario: { ...scenario(installed), ...down, 'POST /api/proxy/admin/unit/start': { body: { receipt: { op: 'unit.start', time: T, pid: 4242 } } } } }); await b.navigate(f.base + '/#/settings/model-proxy'); await wait('document.querySelector("#proxy-setup-go")?.dataset.next==="unit-start"'); await wait('document.querySelector("#proxy-message").textContent.includes("not running yet")'); assert.equal(await b.evaluate('document.querySelector("[aria-labelledby=proxy-apikey-title]").hidden'), true); await b.evaluate(`Object.assign(window.__scenario, { 'GET /api/proxy/accounts': { body: { accounts: [] } }, 'GET /api/proxy/keys': { body: { keys: [] } }, '/api/proxy/admin/status': ${JSON.stringify({ ...base, release: fetched.release, init: { initialized: true, partial: false, settings: { port: 18432 }, files: [] }, unit: { unit: 'mosaic-proxy.service', installed: true, active: 'active', pid: 4242, port: 18432, listening: true }, next: ['accounts'] })} })`); await b.evaluate('document.querySelector("#proxy-setup-go").click()'); await wait('window.__writes.some(w=>w.path==="/api/proxy/admin/unit/start")'); await wait('document.querySelector("#proxy-message").textContent.includes("Accounts and client keys loaded")'); assert.equal(await b.evaluate('document.querySelector("[aria-labelledby=proxy-apikey-title]").hidden'), false); assert.equal(await b.evaluate('document.querySelector("[aria-labelledby=proxy-accounts-title]").hidden'), false); }); test('an initialized proxy whose accounts read fails with another code also hides the panels until it answers', { timeout: 90000 }, async t => { const installed = { ...base, release: fetched.release, init: { initialized: true, partial: false, settings: { port: 18432 }, files: [] }, unit: { unit: 'mosaic-proxy.service', installed: true, active: 'inactive' }, next: ['unit-start'] }; const failing = { 'GET /api/proxy/accounts': { status: 503, body: { error: 'outcome-unknown' } }, 'GET /api/proxy/keys': { status: 503, body: { error: 'outcome-unknown' } } }; const { f, b, wait } = await w15Fixture(t, { scenario: { ...scenario(installed), ...failing, 'POST /api/proxy/admin/unit/start': { body: { receipt: { op: 'unit.start', time: T, pid: 4242 } } } } }); await b.navigate(f.base + '/#/settings/model-proxy'); await wait('document.querySelector("#proxy-setup-go")?.dataset.next==="unit-start"'); await wait('document.querySelector("#proxy-message").textContent.includes("the account panels return once the proxy answers")'); assert.equal(await b.evaluate('document.querySelector("[aria-labelledby=proxy-accounts-title]").hidden'), true); await b.evaluate(`Object.assign(window.__scenario, { 'GET /api/proxy/accounts': { body: { accounts: [] } }, 'GET /api/proxy/keys': { body: { keys: [] } }, '/api/proxy/admin/status': ${JSON.stringify({ ...base, release: fetched.release, init: { initialized: true, partial: false, settings: { port: 18432 }, files: [] }, unit: { unit: 'mosaic-proxy.service', installed: true, active: 'active', pid: 4242, port: 18432, listening: true }, next: ['accounts'] })} })`); await b.evaluate('document.querySelector("#proxy-setup-go").click()'); await wait('document.querySelector("#proxy-message").textContent.includes("Accounts and client keys loaded")'); assert.equal(await b.evaluate('document.querySelector("[aria-labelledby=proxy-accounts-title]").hidden'), false); }); test('a fence with no readable time names the --proxy-restarted path instead of a page restart', { timeout: 90000 }, async t => { const timeless = { ...fenced, fence: { set: true, invalid: true }, restart: null, next: ['unit-restart'] }; const { f, b, wait } = await w15Fixture(t, { scenario: scenario(timeless) }); await b.navigate(f.base + '/#/settings/model-proxy'); await wait('document.querySelector("#proxy-setup-go")?.dataset.next==="unit-restart"'); assert.match(await b.evaluate('document.querySelector("#proxy-setup-next p").textContent'), /no readable time.*mosaic proxy fence clear --proxy-restarted/); }); test('after a reload the receipts come from status, and an initialized proxy folds the init form', { timeout: 90000 }, async t => { const initialized = { ...base, release: fetched.release, init: { initialized: true, partial: false, settings: { port: 18432 }, files: [] }, receipts: [{ op: 'fetch', time: T, version: '8.0.42' }, { op: 'init', time: T, generated: true, port: 18432 }], next: ['unit-install'] }; const { f, b, wait } = await w15Fixture(t, { scenario: { ...scenario(initialized), 'POST /api/proxy/admin/unit/install': { body: { receipt: { op: 'unit.install', time: T } } } } }); await b.navigate(f.base + '/#/settings/model-proxy'); await wait('document.querySelector("#proxy-setup-go")?.dataset.next==="unit-install"'); assert.deepEqual(await b.evaluate('[...document.querySelectorAll("#proxy-setup-receipts li strong")].map(s=>s.textContent)'), ['init', 'fetch'], 'newest first, from proxy-admin.jsonl'); await b.evaluate('document.querySelector("#proxy-setup-init-box").open = true'); await b.evaluate('document.querySelector("#proxy-setup-go").click()'); await wait('window.__writes.some(w=>w.path==="/api/proxy/admin/unit/install")'); await wait('document.querySelector("#proxy-setup-init-box").open===false'); });