Advertisement

← vibesdk

worker/services/aigateway-proxy/controller.test.ts

5.8 KB Download file
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SignJWT } from 'jose';
import { proxyToAiGateway, generateAppProxyToken } from './controller';

// Mutable holder for the app row returned by the mocked DB query.
const { state } = vi.hoisted(() => ({
	state: { appRow: undefined as undefined | Record<string, unknown> },
}));

vi.mock('drizzle-orm/d1', () => ({
	drizzle: () => ({
		select: () => ({
			from: () => ({
				where: () => ({
					get: async () => state.appRow,
				}),
			}),
		}),
	}),
}));

vi.mock('../../agents/inferutils/core', () => ({
	getConfigurationForModel: vi.fn(async () => ({
		baseURL: 'https://gateway.example.com/v1',
		apiKey: 'upstream-key',
		defaultHeaders: {},
	})),
}));

vi.mock('../rate-limit/rateLimits', () => ({
	RateLimitService: { enforceLLMCallsRateLimit: vi.fn(async () => undefined) },
}));

vi.mock('worker/config', () => ({
	getUserConfigurableSettings: vi.fn(async () => ({ security: { rateLimit: {} } })),
}));

const SECRET = 'test-ai-proxy-secret-0123456789abcdef';
const AUDIENCE = 'vibesdk-ai-proxy';
const ISSUER = 'vibesdk';

const testEnv = {
	AI_PROXY_JWT_SECRET: SECRET,
	DB: {},
} as unknown as Env;

const ctx = {} as ExecutionContext;
const URL_BASE = 'https://app.local/api/proxy/openai';

function makeRequest(path: string, opts: { token?: string; body?: unknown; method?: string; rawBody?: string } = {}): Request {
	const headers: Record<string, string> = { 'Content-Type': 'application/json' };
	if (opts.token) headers['Authorization'] = `Bearer ${opts.token}`;
	const body = opts.rawBody ?? (opts.body !== undefined ? JSON.stringify(opts.body) : undefined);
	return new Request(`${URL_BASE}${path}`, {
		method: opts.method ?? 'POST',
		headers,
		body,
	});
}

async function signToken(payload: Record<string, unknown>, opts: { audience?: string; issuer?: string } = {}): Promise<string> {
	let jwt = new SignJWT(payload).setProtectedHeader({ alg: 'HS256' }).setIssuedAt().setExpirationTime('1h');
	if (opts.audience) jwt = jwt.setAudience(opts.audience);
	if (opts.issuer) jwt = jwt.setIssuer(opts.issuer);
	return jwt.sign(new TextEncoder().encode(SECRET));
}

const VALID_APP = { id: 'app-1', userId: 'user-1', title: 'My App', status: 'active' };

describe('proxyToAiGateway', () => {
	beforeEach(() => {
		vi.clearAllMocks();
		state.appRow = { ...VALID_APP };
	});

	it('rejects non-POST methods with 405', async () => {
		const res = await proxyToAiGateway(makeRequest('/chat/completions', { method: 'GET' }), testEnv, ctx);
		expect(res.status).toBe(405);
	});

	it('rejects requests without an Authorization header with 401', async () => {
		const res = await proxyToAiGateway(makeRequest('/chat/completions', { body: { model: 'm' } }), testEnv, ctx);
		expect(res.status).toBe(401);
	});

	it('rejects a token missing the required audience with 401', async () => {
		const token = await signToken({ appId: 'app-1', userId: 'user-1', type: 'app-proxy' }, { issuer: ISSUER });
		const res = await proxyToAiGateway(makeRequest('/chat/completions', { token, body: { model: 'm' } }), testEnv, ctx);
		expect(res.status).toBe(401);
	});

	it('rejects a token with the wrong type with 401', async () => {
		const token = await signToken({ appId: 'app-1', userId: 'user-1', type: 'other' }, { audience: AUDIENCE, issuer: ISSUER });
		const res = await proxyToAiGateway(makeRequest('/chat/completions', { token, body: { model: 'm' } }), testEnv, ctx);
		expect(res.status).toBe(401);
	});

	it('rejects a disallowed proxy path with 404', async () => {
		const token = await generateAppProxyToken('app-1', 'user-1', testEnv);
		const res = await proxyToAiGateway(makeRequest('/admin/keys', { token, body: { model: 'm' } }), testEnv, ctx);
		expect(res.status).toBe(404);
	});

	it('rejects an oversized body with 413', async () => {
		const token = await generateAppProxyToken('app-1', 'user-1', testEnv);
		const rawBody = `{"model":"m","pad":"${'x'.repeat(5 * 1024 * 1024 + 100)}"}`;
		const res = await proxyToAiGateway(makeRequest('/chat/completions', { token, rawBody }), testEnv, ctx);
		expect(res.status).toBe(413);
	});

	it('rejects too many messages with 400', async () => {
		const token = await generateAppProxyToken('app-1', 'user-1', testEnv);
		const messages = Array.from({ length: 201 }, () => ({ role: 'user', content: 'hi' }));
		const res = await proxyToAiGateway(makeRequest('/chat/completions', { token, body: { model: 'm', messages } }), testEnv, ctx);
		expect(res.status).toBe(400);
	});

	it('rejects a request with no model with 400', async () => {
		const token = await generateAppProxyToken('app-1', 'user-1', testEnv);
		const res = await proxyToAiGateway(makeRequest('/chat/completions', { token, body: { messages: [] } }), testEnv, ctx);
		expect(res.status).toBe(400);
	});

	it('clamps max_tokens and proxies the request', async () => {
		const fetchMock = vi.fn(async () => new Response('{"ok":true}', { status: 200, headers: { 'Content-Type': 'application/json' } }));
		vi.stubGlobal('fetch', fetchMock);

		const token = await generateAppProxyToken('app-1', 'user-1', testEnv);
		const res = await proxyToAiGateway(
			makeRequest('/chat/completions', { token, body: { model: 'm', max_tokens: 1_000_000, messages: [] } }),
			testEnv,
			ctx,
		);

		expect(res.status).toBe(200);
		expect(fetchMock).toHaveBeenCalledTimes(1);
		const forwarded = JSON.parse(fetchMock.mock.calls[0][1].body as string);
		expect(forwarded.max_tokens).toBe(16384);

		vi.unstubAllGlobals();
	});

	it('returns 404 when the app is not found', async () => {
		state.appRow = undefined;
		const token = await generateAppProxyToken('app-1', 'user-1', testEnv);
		const res = await proxyToAiGateway(makeRequest('/chat/completions', { token, body: { model: 'm' } }), testEnv, ctx);
		expect(res.status).toBe(404);
	});
});