3b8345dfe0
REQ-006: configure BYOM endpoint (URL in DB, key in secret manager) REQ-007: validate-on-save test inference call (OpenAI-compatible) REQ-008: route all inference to BYOM (G-001: test-inference proxy endpoint) REQ-009: reject when unconfigured/unreachable (ByomUnconfiguredError/ByomUnreachableError) ---ci--- phase: 3 milestone: v0.1 status: execute ---/ci---
122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
/**
|
|
* validator.test.ts — validateEndpoint (REQ-007).
|
|
*
|
|
* Mocks the global `fetch` to assert:
|
|
* - 200 with a valid chat-completion body → { ok: true }
|
|
* - 401 → { ok: false, error: "auth_failed" }
|
|
* - fetch throws (network/timeout) → { ok: false, error: "connection_failed" }
|
|
*/
|
|
|
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { validateEndpoint } from "../src/validator.js";
|
|
|
|
const URL = "https://byom.example.com";
|
|
const KEY = "sk-test-key";
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("validateEndpoint (REQ-007)", () => {
|
|
it("returns ok:true on a 200 with a valid chat completion body", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue(
|
|
jsonResponse({
|
|
id: "chatcmpl-1",
|
|
choices: [{ index: 0, message: { role: "assistant", content: "pong" }, finish_reason: "stop" }],
|
|
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
|
}),
|
|
),
|
|
);
|
|
const res = await validateEndpoint(URL, KEY);
|
|
expect(res.ok).toBe(true);
|
|
expect(res.error).toBeUndefined();
|
|
|
|
// Verify it POSTed to /v1/chat/completions with the bearer key.
|
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
const [calledUrl, init] = (fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0] as [
|
|
string,
|
|
RequestInit,
|
|
];
|
|
expect(calledUrl).toBe(`${URL}/v1/chat/completions`);
|
|
expect(init.method).toBe("POST");
|
|
expect((init.headers as Record<string, string>).Authorization).toBe(`Bearer ${KEY}`);
|
|
});
|
|
|
|
it("returns auth_failed on 401", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue(new Response("unauthorized", { status: 401 })),
|
|
);
|
|
const res = await validateEndpoint(URL, KEY);
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error).toBe("auth_failed");
|
|
expect(res.detail).toBeTruthy();
|
|
});
|
|
|
|
it("returns auth_failed on 403", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue(new Response("forbidden", { status: 403 })),
|
|
);
|
|
const res = await validateEndpoint(URL, KEY);
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error).toBe("auth_failed");
|
|
});
|
|
|
|
it("returns connection_failed when fetch throws", async () => {
|
|
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ENOTFOUND")));
|
|
const res = await validateEndpoint(URL, KEY);
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error).toBe("connection_failed");
|
|
expect(res.detail).toContain("ENOTFOUND");
|
|
});
|
|
|
|
it("returns connection_failed on 5xx", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue(new Response("bad gateway", { status: 502 })),
|
|
);
|
|
const res = await validateEndpoint(URL, KEY);
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error).toBe("connection_failed");
|
|
});
|
|
|
|
it("returns invalid_response on 200 with a non-chat-completion body", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue(jsonResponse({ hello: "world" })),
|
|
);
|
|
const res = await validateEndpoint(URL, KEY);
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error).toBe("invalid_response");
|
|
});
|
|
|
|
it("returns invalid_response on 200 with non-JSON body", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue(new Response("not json", { status: 200 })),
|
|
);
|
|
const res = await validateEndpoint(URL, KEY);
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error).toBe("invalid_response");
|
|
});
|
|
|
|
it("tolerates a configured URL that already ends in /v1", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue(jsonResponse({ choices: [] })),
|
|
);
|
|
await validateEndpoint(`${URL}/v1`, KEY);
|
|
const [calledUrl] = (fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0] as [string];
|
|
expect(calledUrl).toBe(`${URL}/v1/chat/completions`);
|
|
});
|
|
}); |