144 lines
3.8 KiB
TypeScript
144 lines
3.8 KiB
TypeScript
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
import type { NextRequest } from 'next/server';
|
|
|
|
const mocks = vi.hoisted(() => ({ validateUrlForSSRF: vi.fn() }));
|
|
|
|
vi.mock('@/lib/server/ssrf-guard', () => ({
|
|
validateUrlForSSRF: mocks.validateUrlForSSRF,
|
|
}));
|
|
|
|
vi.mock('@/lib/logger', () => ({
|
|
createLogger: () => ({
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
async function postProbeModels(body: Record<string, unknown>) {
|
|
const { POST } = await import('@/app/api/provider/probe-models/route');
|
|
const request = new Request('http://localhost/api/provider/probe-models', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
return POST(request as unknown as NextRequest);
|
|
}
|
|
|
|
describe('POST /api/provider/probe-models', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
mocks.validateUrlForSSRF.mockReset();
|
|
mocks.validateUrlForSSRF.mockResolvedValue(null);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('maps an upstream redirect to the exact redirect-not-allowed contract without reading it', async () => {
|
|
const text = vi.fn().mockResolvedValue('redirect response body');
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 302,
|
|
text,
|
|
} as unknown as Response);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const res = await postProbeModels({
|
|
baseUrl: 'https://api.example.com',
|
|
apiKey: 'test-key',
|
|
});
|
|
const json = await res.json();
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(json).toEqual({
|
|
success: false,
|
|
errorCode: 'REDIRECT_NOT_ALLOWED',
|
|
error: 'Redirects are not allowed',
|
|
});
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(text).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('preserves successful model filtering and response metadata', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
data: [
|
|
{ id: 'chat-model', owned_by: 'provider' },
|
|
{ id: 'text-embedding-3-small', owned_by: 'provider' },
|
|
],
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
),
|
|
),
|
|
);
|
|
|
|
const res = await postProbeModels({
|
|
baseUrl: 'https://api.example.com',
|
|
apiKey: 'test-key',
|
|
});
|
|
const json = await res.json();
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(json).toEqual({
|
|
success: true,
|
|
models: [{ id: 'chat-model', ownedBy: 'provider' }],
|
|
total: 2,
|
|
filtered: 1,
|
|
});
|
|
});
|
|
|
|
it.each([401, 403])('preserves the API-key error contract for upstream %i', async (status) => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status,
|
|
text: vi.fn().mockResolvedValue('invalid key'),
|
|
} as unknown as Response),
|
|
);
|
|
|
|
const res = await postProbeModels({
|
|
baseUrl: 'https://api.example.com',
|
|
apiKey: 'bad-key',
|
|
});
|
|
const json = await res.json();
|
|
|
|
expect(res.status).toBe(401);
|
|
expect(json).toEqual({
|
|
success: false,
|
|
errorCode: 'INVALID_REQUEST',
|
|
error: 'API key is invalid or expired',
|
|
});
|
|
});
|
|
|
|
it('preserves the manual-entry response when no model endpoint exists', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 404,
|
|
text: vi.fn(),
|
|
} as unknown as Response),
|
|
);
|
|
|
|
const res = await postProbeModels({
|
|
baseUrl: 'https://api.example.com',
|
|
apiKey: 'test-key',
|
|
});
|
|
const json = await res.json();
|
|
|
|
expect(res.status).toBe(404);
|
|
expect(json).toEqual({
|
|
success: false,
|
|
errorCode: 'INVALID_REQUEST',
|
|
error: 'This provider does not expose a model list',
|
|
});
|
|
});
|
|
});
|