100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { Hono } from 'hono';
|
|
import type { Principal } from '../src/lib/auth';
|
|
import type { ApiEnv } from '../src/lib/mutation';
|
|
import { createPiggyChatRoutes } from '../src/routes/piggy-chat';
|
|
|
|
const principal: Principal = {
|
|
userId: '10000000-0000-4000-8000-000000000001',
|
|
email: 'member@example.com',
|
|
name: 'Member',
|
|
isPlatformAdmin: false,
|
|
teams: [{ team: 'demand', role: 'member' }],
|
|
via: 'jwt',
|
|
scopes: ['read', 'write'],
|
|
};
|
|
|
|
function appFor(fetchImpl: typeof fetch, identity: Principal = principal) {
|
|
const app = new Hono<ApiEnv>();
|
|
app.use('*', async (context, next) => {
|
|
context.set('principal', identity);
|
|
await next();
|
|
});
|
|
app.route(
|
|
'/',
|
|
createPiggyChatRoutes({
|
|
enabled: true,
|
|
internalUrl: 'http://127.0.0.1:8931',
|
|
internalToken: 'internal-token-with-at-least-32-characters',
|
|
fetchImpl,
|
|
}),
|
|
);
|
|
return app;
|
|
}
|
|
|
|
test('the authenticated proxy forwards bounded identity and relays NDJSON unchanged', async () => {
|
|
let forwarded: Record<string, unknown> | undefined;
|
|
const fetchImpl: typeof fetch = async (input, init) => {
|
|
assert.equal(String(input), 'http://127.0.0.1:8931/internal/chat');
|
|
assert.equal(
|
|
new Headers(init?.headers).get('authorization'),
|
|
'Bearer internal-token-with-at-least-32-characters',
|
|
);
|
|
forwarded = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
|
return new Response(
|
|
`${JSON.stringify({ type: 'content_delta', delta: 'Scoped answer' })}\n` +
|
|
`${JSON.stringify({ type: 'done', inputTokens: 2, outputTokens: 3 })}\n`,
|
|
{ status: 200, headers: { 'content-type': 'application/x-ndjson' } },
|
|
);
|
|
};
|
|
const app = appFor(fetchImpl);
|
|
const response = await app.request('/api/piggy/chat', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
message: 'Summarise this contract.',
|
|
context: {
|
|
type: 'contract',
|
|
id: '20000000-0000-4000-8000-000000000002',
|
|
label: 'Order form',
|
|
},
|
|
}),
|
|
});
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.match(response.headers.get('content-type') ?? '', /application\/x-ndjson/);
|
|
assert.deepEqual(forwarded, {
|
|
principalUserId: principal.userId,
|
|
message: 'Summarise this contract.',
|
|
context: {
|
|
type: 'contract',
|
|
id: '20000000-0000-4000-8000-000000000002',
|
|
label: 'Order form',
|
|
},
|
|
});
|
|
assert.equal(
|
|
await response.text(),
|
|
`${JSON.stringify({ type: 'content_delta', delta: 'Scoped answer' })}\n` +
|
|
`${JSON.stringify({ type: 'done', inputTokens: 2, outputTokens: 3 })}\n`,
|
|
);
|
|
});
|
|
|
|
test('a credential without read scope never reaches the internal service', async () => {
|
|
let fetched = false;
|
|
const app = appFor(
|
|
async () => {
|
|
fetched = true;
|
|
return new Response();
|
|
},
|
|
{ ...principal, scopes: ['write'] },
|
|
);
|
|
const response = await app.request('/api/piggy/chat', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ message: 'Read the book.' }),
|
|
});
|
|
assert.equal(response.status, 403);
|
|
assert.equal(fetched, false);
|
|
});
|