| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; |
|
|
| |
| const originalFetch = global.fetch; |
|
|
| describe("Provider Validation API", () => { |
| beforeEach(() => { |
| vi.clearAllMocks(); |
| }); |
|
|
| afterEach(() => { |
| global.fetch = originalFetch; |
| }); |
|
|
| describe("OpenAI Compatible", () => { |
| it("should return valid:true when /models succeeds", async () => { |
| global.fetch = vi.fn().mockResolvedValue({ |
| ok: true, |
| json: () => Promise.resolve({ data: [] }), |
| }); |
|
|
| |
| const baseUrl = "https://api.openai.com/v1"; |
| const apiKey = "test-key"; |
| const modelsUrl = `${baseUrl}/models`; |
|
|
| const res = await fetch(modelsUrl, { |
| headers: { Authorization: `Bearer ${apiKey}` }, |
| }); |
|
|
| expect(res.ok).toBe(true); |
| expect(fetch).toHaveBeenCalledWith(modelsUrl, expect.objectContaining({ |
| headers: { Authorization: `Bearer ${apiKey}` }, |
| })); |
| }); |
|
|
| it("should fallback to chat/completions when /models fails and modelId provided", async () => { |
| const modelsCall = vi.fn().mockResolvedValue({ ok: false, status: 404 }); |
| const chatCall = vi.fn().mockResolvedValue({ ok: true }); |
|
|
| global.fetch = vi.fn().mockImplementation((url) => { |
| if (url.includes("/models")) return modelsCall(); |
| if (url.includes("/chat/completions")) return chatCall(); |
| return Promise.reject(new Error("Unknown URL")); |
| }); |
|
|
| |
| const baseUrl = "https://custom-provider.com/v1"; |
| const modelsRes = await fetch(`${baseUrl}/models`); |
| expect(modelsRes.ok).toBe(false); |
|
|
| |
| const chatRes = await fetch(`${baseUrl}/chat/completions`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| model: "gpt-4", |
| messages: [{ role: "user", content: "ping" }], |
| max_tokens: 1, |
| }), |
| }); |
| expect(chatRes.ok).toBe(true); |
| }); |
|
|
| it("should return error when /models fails and no modelId", async () => { |
| global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404 }); |
|
|
| const baseUrl = "https://custom-provider.com/v1"; |
| const res = await fetch(`${baseUrl}/models`); |
|
|
| expect(res.ok).toBe(false); |
| |
| }); |
| }); |
|
|
| describe("Anthropic Compatible", () => { |
| it("should normalize URL by removing /messages suffix", () => { |
| const normalizeUrl = (url) => { |
| let normalized = url.trim().replace(/\/$/, ""); |
| if (normalized.endsWith("/messages")) { |
| normalized = normalized.slice(0, -9); |
| } |
| return normalized; |
| }; |
|
|
| expect(normalizeUrl("https://api.anthropic.com/v1/messages")).toBe("https://api.anthropic.com/v1"); |
| expect(normalizeUrl("https://api.anthropic.com/v1/messages/")).toBe("https://api.anthropic.com/v1"); |
| expect(normalizeUrl("https://api.anthropic.com/v1")).toBe("https://api.anthropic.com/v1"); |
| }); |
|
|
| it("should send correct headers for Anthropic API", async () => { |
| global.fetch = vi.fn().mockResolvedValue({ ok: true }); |
|
|
| const baseUrl = "https://api.anthropic.com/v1"; |
| const apiKey = "test-key"; |
|
|
| await fetch(`${baseUrl}/models`, { |
| method: "GET", |
| headers: { |
| "x-api-key": apiKey, |
| "anthropic-version": "2023-06-01", |
| Authorization: `Bearer ${apiKey}`, |
| }, |
| }); |
|
|
| expect(fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ |
| headers: expect.objectContaining({ |
| "x-api-key": apiKey, |
| "anthropic-version": "2023-06-01", |
| }), |
| })); |
| }); |
| }); |
|
|
| describe("Error Messages - Network", () => { |
| it("should map ECONNREFUSED to user-friendly message", () => { |
| const getErrorMessage = (error) => { |
| if (error.cause?.code === "ECONNREFUSED") return "Connection refused - provider node offline or unreachable"; |
| return "Unknown error"; |
| }; |
|
|
| const error = { cause: { code: "ECONNREFUSED" } }; |
| expect(getErrorMessage(error)).toBe("Connection refused - provider node offline or unreachable"); |
| }); |
|
|
| it("should map ENOTFOUND to user-friendly message", () => { |
| const getErrorMessage = (error) => { |
| if (error.cause?.code === "ENOTFOUND") return "DNS lookup failed - invalid domain or network issue"; |
| return "Unknown error"; |
| }; |
|
|
| const error = { cause: { code: "ENOTFOUND" } }; |
| expect(getErrorMessage(error)).toBe("DNS lookup failed - invalid domain or network issue"); |
| }); |
|
|
| it("should map timeout to user-friendly message", () => { |
| const getErrorMessage = (error) => { |
| if (error.message.includes("timeout")) return "Request timeout (>10s) - provider node not responding"; |
| return "Unknown error"; |
| }; |
|
|
| const error = { message: "Request timeout" }; |
| expect(getErrorMessage(error)).toBe("Request timeout (>10s) - provider node not responding"); |
| }); |
|
|
| it("should map CERT_HAS_EXPIRED to user-friendly message", () => { |
| const getErrorMessage = (error) => { |
| if (error.cause?.code === "CERT_HAS_EXPIRED") return "SSL certificate expired"; |
| return "Unknown error"; |
| }; |
|
|
| const error = { cause: { code: "CERT_HAS_EXPIRED" } }; |
| expect(getErrorMessage(error)).toBe("SSL certificate expired"); |
| }); |
| }); |
|
|
| describe("URL Validation", () => { |
| it("should validate correct URL format", () => { |
| const isValidUrl = (url) => { |
| try { |
| new URL(url); |
| return true; |
| } catch { |
| return false; |
| } |
| }; |
|
|
| expect(isValidUrl("https://api.openai.com/v1")).toBe(true); |
| expect(isValidUrl("http://localhost:8080")).toBe(true); |
| expect(isValidUrl("not-a-url")).toBe(false); |
| expect(isValidUrl("")).toBe(false); |
| }); |
| }); |
|
|
| describe("Error Messages - /models Status Codes", () => { |
| const getModelsErrorMessage = (status) => { |
| if (status === 401 || status === 403) return "API key unauthorized"; |
| if (status === 404) return "/models endpoint not found - try chat validation with model ID"; |
| if (status >= 500) return "Server error - try again later"; |
| return `Unexpected response (${status})`; |
| }; |
|
|
| it("should return auth error for 401", () => { |
| expect(getModelsErrorMessage(401)).toBe("API key unauthorized"); |
| }); |
|
|
| it("should return auth error for 403", () => { |
| expect(getModelsErrorMessage(403)).toBe("API key unauthorized"); |
| }); |
|
|
| it("should return not found for 404", () => { |
| expect(getModelsErrorMessage(404)).toBe("/models endpoint not found - try chat validation with model ID"); |
| }); |
|
|
| it("should return server error for 500", () => { |
| expect(getModelsErrorMessage(500)).toBe("Server error - try again later"); |
| }); |
|
|
| it("should return server error for 502", () => { |
| expect(getModelsErrorMessage(502)).toBe("Server error - try again later"); |
| }); |
|
|
| it("should return unexpected for other codes", () => { |
| expect(getModelsErrorMessage(418)).toBe("Unexpected response (418)"); |
| }); |
| }); |
|
|
| describe("Error Messages - /chat/completions Status Codes", () => { |
| const getChatErrorMessage = (status) => { |
| if (status === 401 || status === 403) return "API key unauthorized"; |
| if (status === 400) return "Invalid model or bad request"; |
| if (status === 404) return "Chat endpoint not found"; |
| if (status >= 500) return "Server error - try again later"; |
| return `Chat request failed (${status})`; |
| }; |
|
|
| it("should return auth error for 401", () => { |
| expect(getChatErrorMessage(401)).toBe("API key unauthorized"); |
| }); |
|
|
| it("should return invalid model for 400", () => { |
| expect(getChatErrorMessage(400)).toBe("Invalid model or bad request"); |
| }); |
|
|
| it("should return not found for 404", () => { |
| expect(getChatErrorMessage(404)).toBe("Chat endpoint not found"); |
| }); |
|
|
| it("should return server error for 503", () => { |
| expect(getChatErrorMessage(503)).toBe("Server error - try again later"); |
| }); |
|
|
| it("should return failed for other codes", () => { |
| expect(getChatErrorMessage(429)).toBe("Chat request failed (429)"); |
| }); |
| }); |
|
|
| describe("Response Format", () => { |
| it("should return correct format for success via /models", () => { |
| const response = { valid: true }; |
| expect(response).toEqual({ valid: true }); |
| }); |
|
|
| it("should return correct format for success via chat", () => { |
| const response = { valid: true, error: null, method: "chat" }; |
| expect(response.valid).toBe(true); |
| expect(response.method).toBe("chat"); |
| expect(response.error).toBeNull(); |
| }); |
|
|
| it("should return correct format for failure with error", () => { |
| const response = { |
| valid: false, |
| error: "API key unauthorized or model unavailable", |
| method: "chat" |
| }; |
| expect(response.valid).toBe(false); |
| expect(response.error).toBeDefined(); |
| }); |
| }); |
| }); |
|
|