Browse Source

test(18-02): add LLM route tests for OpenAI provider

Tests OpenAI provider path covering success, vision input, unknown provider,
missing API key, rate limits, and error handling with mocked fetch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
8ba3a7184b
  1. 278
      src/app/api/llm/__tests__/route.test.ts

278
src/app/api/llm/__tests__/route.test.ts

@ -46,8 +46,12 @@ vi.mock("@/utils/logger", () => ({
import { POST } from "../route";
// Store original env
// Store original env and fetch
const originalEnv = { ...process.env };
const originalFetch = global.fetch;
// Mock fetch for OpenAI API
const mockFetch = vi.fn();
// Helper to create mock NextRequest for POST
function createMockPostRequest(
@ -70,6 +74,7 @@ describe("/api/llm route", () => {
afterEach(() => {
process.env = originalEnv;
global.fetch = originalFetch;
});
describe("Google provider", () => {
@ -307,4 +312,275 @@ describe("/api/llm route", () => {
});
});
});
describe("OpenAI provider", () => {
beforeEach(() => {
global.fetch = mockFetch;
});
it("should generate text successfully with OpenAI", async () => {
process.env.OPENAI_API_KEY = "test-openai-key";
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
choices: [{ message: { content: "OpenAI response text" } }],
}),
});
const request = createMockPostRequest({
prompt: "Test prompt",
provider: "openai",
model: "gpt-4.1-mini",
temperature: 0.7,
maxTokens: 1024,
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.text).toBe("OpenAI response text");
// Verify fetch was called with correct parameters
expect(mockFetch).toHaveBeenCalledWith(
"https://api.openai.com/v1/chat/completions",
expect.objectContaining({
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer test-openai-key",
},
body: JSON.stringify({
model: "gpt-4.1-mini",
messages: [{ role: "user", content: "Test prompt" }],
temperature: 0.7,
max_tokens: 1024,
}),
})
);
});
it("should handle vision input (images + prompt)", async () => {
process.env.OPENAI_API_KEY = "test-openai-key";
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
choices: [{ message: { content: "Image description from OpenAI" } }],
}),
});
const request = createMockPostRequest({
prompt: "Describe this image",
images: ["data:image/png;base64,iVBORw0KGgo="],
provider: "openai",
model: "gpt-4.1-mini",
temperature: 0.7,
maxTokens: 1024,
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.text).toBe("Image description from OpenAI");
// Verify fetch was called with vision content structure
expect(mockFetch).toHaveBeenCalledWith(
"https://api.openai.com/v1/chat/completions",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
model: "gpt-4.1-mini",
messages: [
{
role: "user",
content: [
{ type: "text", text: "Describe this image" },
{ type: "image_url", image_url: { url: "data:image/png;base64,iVBORw0KGgo=" } },
],
},
],
temperature: 0.7,
max_tokens: 1024,
}),
})
);
});
it("should reject unknown provider", async () => {
const request = createMockPostRequest({
prompt: "Test prompt",
provider: "unknown-provider",
model: "some-model",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toBe("Unknown provider: unknown-provider");
});
it("should reject missing OpenAI API key", async () => {
delete process.env.OPENAI_API_KEY;
const request = createMockPostRequest({
prompt: "Test prompt",
provider: "openai",
model: "gpt-4.1-mini",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.success).toBe(false);
expect(data.error).toContain("OPENAI_API_KEY not configured");
});
it("should use X-OpenAI-API-Key header over env var", async () => {
process.env.OPENAI_API_KEY = "env-openai-key";
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
choices: [{ message: { content: "Response with header key" } }],
}),
});
const request = createMockPostRequest(
{
prompt: "Test prompt",
provider: "openai",
model: "gpt-4.1-mini",
},
{ "X-OpenAI-API-Key": "header-openai-key" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
// Verify fetch was called with header key (takes precedence)
expect(mockFetch).toHaveBeenCalledWith(
"https://api.openai.com/v1/chat/completions",
expect.objectContaining({
headers: {
"Content-Type": "application/json",
Authorization: "Bearer header-openai-key",
},
})
);
});
it("should return 429 on rate limit errors", async () => {
process.env.OPENAI_API_KEY = "test-openai-key";
mockFetch.mockResolvedValueOnce({
ok: false,
status: 429,
json: () =>
Promise.resolve({
error: { message: "429 Rate limit exceeded" },
}),
});
const request = createMockPostRequest({
prompt: "Test prompt",
provider: "openai",
model: "gpt-4.1-mini",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(429);
expect(data.success).toBe(false);
expect(data.error).toBe("Rate limit reached. Please wait and try again.");
});
it("should handle OpenAI API error responses", async () => {
process.env.OPENAI_API_KEY = "test-openai-key";
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
json: () =>
Promise.resolve({
error: { message: "Invalid API key" },
}),
});
const request = createMockPostRequest({
prompt: "Test prompt",
provider: "openai",
model: "gpt-4.1-mini",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.success).toBe(false);
expect(data.error).toBe("Invalid API key");
});
it("should handle OpenAI API error without message", async () => {
process.env.OPENAI_API_KEY = "test-openai-key";
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
json: () => Promise.resolve({}),
});
const request = createMockPostRequest({
prompt: "Test prompt",
provider: "openai",
model: "gpt-4.1-mini",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.success).toBe(false);
expect(data.error).toBe("OpenAI API error: 500");
});
it("should handle no text in OpenAI response", async () => {
process.env.OPENAI_API_KEY = "test-openai-key";
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
choices: [{ message: { content: null } }],
}),
});
const request = createMockPostRequest({
prompt: "Test prompt",
provider: "openai",
model: "gpt-4.1-mini",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.success).toBe(false);
expect(data.error).toBe("No text in OpenAI response");
});
});
});

Loading…
Cancel
Save