Files
stack/packages/webui/tests/proxy-browser.test.mjs

113 lines
9.2 KiB
JavaScript

// Native page behavior. HTTP authorization is covered by proxy-page.test.mjs;
// this stand-in serves the real page/script and records only route names/counts.
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { readFileSync, existsSync } from 'node:fs';
import { resolve, extname } from 'node:path';
import { browser } from './browser.mjs';
async function setup(t, readonly = false, refusal = null) {
const state = { routes: [], loginStatus: 'wait', receivedKey: false, release: null, priority: 0, models: ['fixture-old'] };
const server = http.createServer(async (req, res) => {
const send = value => { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(value)); };
if (req.url === '/api/config') return send({ send: !readonly, devOwner: true });
if (req.url.startsWith('/api/proxy/')) {
state.routes.push(req.url);
if (refusal && req.url.endsWith(refusal)) { res.statusCode = 503; return send({ error: 'not-configured' }); }
const chunks = []; for await (const c of req) chunks.push(c);
const args = chunks.length ? JSON.parse(Buffer.concat(chunks)) : {};
if (req.url.endsWith('/accounts')) return send({ accounts: [{ accountId: 'a'.repeat(12), provider: 'claude', displayName: '<img src=x onerror="window.injected=true">', status: 'ready', priority: state.priority }, { accountId: 'c'.repeat(12), provider: 'z.ai', displayName: 'Fixture z.ai account', status: 'active', priority: 0, models: state.models }] });
if (req.url.endsWith('/keys')) return send({ keys: [{ keyId: 'b'.repeat(12), management: 'sentinel', agent: null, issuedAt: '2026-11-07T02:24:02.021Z', state: null }] });
if (req.url.endsWith('/apikey/add')) {
state.receivedKey = args.key === 'p4-browser-key';
await new Promise(r => { state.release = r; }); return send({ added: true });
}
if (req.url.endsWith('/login/start')) return send({ url: 'https://claude.ai/oauth/authorize?state=p4-state', state: 'p4-state' });
if (req.url.endsWith('/login/status')) return send({ status: state.loginStatus });
if (req.url.endsWith('/login/submit')) return send({ status: 'wait' });
if (req.url.endsWith('/login/cancel')) return send({ status: 'cancelled' });
if (req.url.endsWith('/group/models/set')) { state.models = args.models; return send({ accountId: args.accountId, models: state.models }); }
if (req.url.endsWith('/priority/set')) { state.priority = args.priority; return send({ priority: args.priority }); }
}
if (req.url === '/') { res.setHeader('content-type', 'text/html'); return res.end(readFileSync(new URL('../src/public/index.html', import.meta.url))); }
if (req.url.startsWith('/api/')) return send({rows:[],unread:0,sessions:[]});
const root=resolve(import.meta.dirname,'../src/public'), file=resolve(root,'.'+req.url.split('?')[0]);
if(file.startsWith(root+'/') && existsSync(file)) { const type={'.js':'text/javascript','.css':'text/css','.svg':'image/svg+xml','.woff2':'font/woff2'}[extname(file)] || 'application/octet-stream';res.setHeader('content-type',type);return res.end(readFileSync(file)); }
res.statusCode = 404; res.end();
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
t.after(() => { state.release?.(); return new Promise(r => server.close(r)); });
const b = await browser({ machine: false }); t.after(() => b.close());
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: "window.__stubReady=true;window.openFeatures=null;window.open=(url,target,features)=>{window.openFeatures=features;return null;};" });
await b.navigate(`http://127.0.0.1:${server.address().port}/#/settings/model-proxy`);
const wait = expression => b.evaluate(`(async()=>{for(let n=0;n<600;n++){if(${expression})return true;await new Promise(r=>setTimeout(r,25));}throw Error('proxy UI condition timed out');})()`);
await wait('window.__stubReady === true && document.readyState === "complete" && document.querySelector("#proxy-message")');
if (!refusal) await wait('document.querySelector("#proxy-message").textContent.includes("loaded")');
return { b, state, wait };
}
test('proxy browser clears write-only key immediately, sends once and renders data as text', { timeout: 60000 }, async t => {
const { b, state, wait } = await setup(t);
assert.equal(await b.evaluate('window.injected===true'), false);
assert.equal(await b.evaluate('document.querySelectorAll("#proxy-accounts img").length'), 0);
assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").type'), 'password');
assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").autocomplete'), 'off');
await b.evaluate('document.querySelector("#proxy-apikey input").value="p4-browser-key";document.querySelector("#proxy-apikey").requestSubmit();true');
await wait('document.querySelector("#proxy-apikey input").value===""');
for (let n = 0; n < 100 && !state.release; n++) await new Promise(r => setTimeout(r, 20));
assert.equal(state.receivedKey, true);
state.release(); await wait('document.querySelector("#proxy-message").textContent.includes("added")');
assert.equal(state.routes.filter(r => r.endsWith('/apikey/add')).length, 1);
assert.equal(await b.evaluate('JSON.stringify(localStorage).includes("p4-browser-key") || JSON.stringify(sessionStorage).includes("p4-code")'), false);
assert.equal(await b.evaluate('document.body.textContent.includes("p4-browser-key")'), false);
});
test('proxy browser OAuth opens safely, callback wait stays pending, status ok completes', { timeout: 60000 }, async t => {
const { b, state, wait } = await setup(t);
await b.evaluate('document.querySelector("#proxy-login-start").requestSubmit();true');
await wait('!document.querySelector("#proxy-authorize").hidden');
assert.equal(await b.evaluate('window.openFeatures'), 'noopener,noreferrer');
assert.equal(await b.evaluate('document.querySelector("#proxy-authorize").rel'), 'noopener noreferrer');
await b.evaluate('document.querySelector("#proxy-login-submit input").value="http://localhost/callback?state=p4-state&code=p4-code";document.querySelector("#proxy-login-submit").requestSubmit();true');
await wait('document.querySelector("#proxy-login-message").textContent.includes("Waiting")');
assert.equal(await b.evaluate('document.querySelector("#proxy-login-message").textContent.includes("signed in")'), false);
assert.equal(await b.evaluate('document.querySelector("#proxy-login-submit input").value===""'), true);
state.loginStatus = 'ok';
await wait('document.querySelector("#proxy-login-message").textContent==="Account signed in."');
assert.equal(await b.evaluate('document.querySelector("#proxy-authorize").hasAttribute("href")'), false);
assert.equal(await b.evaluate('JSON.stringify(localStorage).includes("p4-browser-key") || JSON.stringify(sessionStorage).includes("p4-code")'), false);
});
test('proxy browser read-only disables account changes', { timeout: 60000 }, async t => {
const { b, state } = await setup(t, true);
assert.equal(await b.evaluate('[...document.querySelectorAll("[data-write]")].every(e=>e.disabled)'), true);
assert.equal(await b.evaluate('document.querySelector("#proxy-accounts input").disabled'), true);
assert.equal(state.routes.some(r => r.endsWith('/apikey/add') || r.endsWith('/login/start')), false);
});
test('proxy read refusal is visible and never reported as loaded', { timeout: 60000 }, async t => {
const { b, wait } = await setup(t, false, '/accounts');
await wait('document.querySelector("#proxy-message").textContent.includes("not configured")');
assert.equal(await b.evaluate('document.querySelector("#proxy-message").textContent.includes("loaded")'), false);
});
test('proxy write refusal never reports success or exposes the submitted key', { timeout: 60000 }, async t => {
const { b, wait } = await setup(t, false, '/apikey/add');
await b.evaluate('document.querySelector("#proxy-apikey input").value="refused-secret-canary";document.querySelector("#proxy-apikey").requestSubmit();true');
await wait('document.querySelector("#proxy-message").textContent.includes("not configured")');
assert.equal(await b.evaluate('document.querySelector("#proxy-message").textContent.includes("added")'), false);
assert.equal(await b.evaluate('document.documentElement.outerHTML.includes("refused-secret-canary")'), false);
assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").value'), '');
});
test('proxy browser saves the configured model names for a z.ai group', { timeout: 60000 }, async t => {
const { b, state, wait } = await setup(t);
await b.evaluate(`const f=document.querySelector('[data-proxy-models]'); f.elements.models.value='fixture-one, fixture-two\\nfixture-three'; f.requestSubmit(); true`);
await wait('document.querySelector("#proxy-message").textContent.includes("Models saved")');
assert.deepEqual(state.models, ['fixture-one', 'fixture-two', 'fixture-three']);
assert.equal(state.routes.filter(r => r.endsWith('/group/models/set')).length, 1);
});
test('client key issued time is displayed in local time', { timeout: 60000 }, async t => {
const {b}=await setup(t);
assert.equal(await b.evaluate(`document.querySelector('#proxy-keys tr td:nth-child(4)').textContent`),await b.evaluate(`new Date('2026-11-07T02:24:02.021Z').toLocaleString()`));
});