94 lines
6.5 KiB
JavaScript
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);
|
|
});
|