Files
stack/packages/bus/tests/proxy-page-state.test.mjs

94 lines
6.5 KiB
JavaScript

// P4: callback must match an unfinished, unexpired login owned by this human/business.
// Named mutant: skip pending-state check. Canary errors must never reflect upstream input.
import test from 'node:test';
import assert from 'node:assert/strict';
const moduleURL = new URL('../src/proxy-page.mjs', import.meta.url);
const owner = { business: 'demo', human: 'owner' };
async function setup(options = {}) {
const { ProxyPage } = await import(moduleURL);
let now = 1000;
const calls = [];
const client = Object.fromEntries(['getCredentials', 'getKeys', 'oauthStart', 'oauthCallback', 'oauthStatus', 'oauthCancel',
'getUpstreamGroups', 'putUpstreamGroups', 'patchCredential'].map(name => [name, async (...args) => {
calls.push([name, ...args]);
if (name === 'oauthStart') return { status: 'ok', state: 'p4-state', url: 'https://claude.ai/oauth/authorize?state=p4-state' };
if (name === 'oauthStatus') return { status: 'wait' };
if (name === 'getCredentials') return { files: [] };
if (name === 'getUpstreamGroups') return [];
if (name === 'getKeys') return [];
return { status: 'ok' };
}]));
const p = new ProxyPage({ client, instanceTag: 'fixture', now: () => now, ...options });
return { client, calls, p, time: value => { now = value; }, call: (verb, args = {}, who = owner) => p.request(verb, args, who) };
}
for (const [verb, args] of [
['proxy.login.start', { provider: 'other' }], ['proxy.apikey.add', { provider: 'other', key: 'fixture-key' }],
]) test(`${verb}: provider-not-allowed`, async () => {
const s = await setup(); await assert.rejects(s.call(verb, args), { code: 'provider-not-allowed' }); assert.equal(s.calls.length, 0);
});
for (const [name, key] of [['empty', ''], ['too long', 'x'.repeat(4097)], ['charset', 'bad\nkey']])
test(`apikey-invalid ${name}`, async () => {
const s = await setup(); await assert.rejects(s.call('proxy.apikey.add', { provider: 'z.ai', key }), { code: 'apikey-invalid' }); assert.equal(s.calls.length, 0);
});
for (const [name, url] of [['not URL', 'not a url'], ['oversize', 'http://localhost/?' + 'a'.repeat(8193)],
['repeated state', 'http://localhost/?state=p4-state&state=p4-state']])
test(`callback-url-invalid ${name}`, async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
await assert.rejects(s.call('proxy.login.submit', { redirect_url: url }), { code: 'callback-url-invalid' });
assert.equal(s.calls.filter(c => c[0] === 'oauthCallback').length, 0);
});
test('login-state-unknown callback never reaches proxy', async () => {
const s = await setup();
await assert.rejects(s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=unknown&code=p4-code' }), { code: 'login-state-unknown' });
assert.equal(s.calls.length, 0);
});
test('login state is scoped to the human and business', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
for (const who of [{ business: 'other', human: 'owner' }, { business: 'demo', human: 'other' }])
await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }, who), { code: 'login-state-unknown' });
assert.equal(s.calls.length, 1);
});
test('accepted callback wait is pending; only a status ok completes and retires state', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
assert.deepEqual(await s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=p4-state&code=p4-code' }), { status: 'wait' });
assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'wait' });
s.client.oauthStatus = async () => ({ status: 'ok', token: 'never-return' });
assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'ok' });
await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' });
await assert.rejects(s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=p4-state&code=p4-code' }), { code: 'login-state-unknown' });
});
test('expiry reports login-timeout once with no management call and retires state', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' }); s.time(301000);
assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'error', reason: 'login-timeout' });
assert.equal(s.calls.length, 1);
await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' });
});
test('cancel calls DELETE and retires state', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
assert.deepEqual(await s.call('proxy.login.cancel', { state: 'p4-state' }), { status: 'cancelled' });
assert.deepEqual(s.calls.at(-1), ['oauthCancel', 'p4-state']);
await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' });
});
test('upstream OAuth error body never crosses bus response', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
s.client.oauthStatus = async () => ({ status: 'error', error: 'p4-private-code', state: 'p4-state' });
assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'error', reason: 'login-failed' });
});
for (const args of [{ accountId: 'bad', priority: 1 }, { accountId: 'a'.repeat(12), priority: 1.5 },
{ accountId: 'a'.repeat(12), priority: 1001 }]) test('priority-invalid ' + JSON.stringify(args), async () => {
const s = await setup(); await assert.rejects(s.call('proxy.priority.set', args), { code: 'priority-invalid' });
});
test('successful account configuration and login invalidate the catalogue', async () => {
const { ProxyPage } = await import(moduleURL);
let changed = 0;
const client = { getCredentials: async () => ({ files: [] }), getUpstreamGroups: async () => [], putUpstreamGroups: async () => ({}),
oauthStart: async () => ({ state: 'state', url: 'https://claude.ai/oauth/authorize?state=state' }), oauthStatus: async () => ({ status: 'ok' }) };
const page = new ProxyPage({ client, instanceTag: 'fixture', onChange: () => { changed++; } });
const scope = { business: 'fixture', human: 'owner' };
await page.request('proxy.apikey.add', { provider: 'z.ai', key: 'fixture-key' }, scope);
assert.equal(changed, 2);
await page.request('proxy.login.start', { provider: 'claude' }, scope);
await page.request('proxy.login.status', { state: 'state' }, scope);
assert.equal(changed, 3);
});