skillguard/artifacts/api-server/src/routes/providers.listModels.test.ts
amertensreplit 4a7607d3a5 Merged changes from qt0ebghx/main
Replit-Task-Id: e786be21-972b-4d23-bbe7-9eb4ae617f7b
2026-06-11 05:23:53 +00:00

303 lines
9.2 KiB
TypeScript

import {
describe,
it,
expect,
beforeAll,
afterAll,
afterEach,
vi,
} from "vitest";
import type { AddressInfo } from "node:net";
import type { Server } from "node:http";
// The /providers routes are admin-gated. These tests exercise the route logic
// itself, not the Clerk allowlist, so we stub the auth middleware to grant
// admin access. Auth enforcement is covered separately.
vi.mock("../middlewares/auth", () => ({
getAdminAllowlist: () => ["admin@test.local"],
resolveAuth: async () => ({
userId: "test-admin",
email: "admin@test.local",
isAdmin: true,
}),
requireAdmin: (
_req: unknown,
_res: unknown,
next: () => void,
) => next(),
}));
import app from "../app";
import { db, pool, aiProvidersTable } from "@workspace/db";
import { inArray } from "drizzle-orm";
const createdProviderIds: number[] = [];
let server: Server;
let baseUrl: string;
// The test client itself uses fetch to reach the in-process server, so we must
// only intercept upstream provider calls and let localhost requests pass through
// to the real implementation.
const realFetch = globalThis.fetch.bind(globalThis);
beforeAll(async () => {
await new Promise<void>((resolve) => {
server = app.listen(0, () => resolve());
});
const { port } = server.address() as AddressInfo;
baseUrl = `http://127.0.0.1:${port}`;
});
afterEach(async () => {
vi.restoreAllMocks();
if (createdProviderIds.length > 0) {
await db
.delete(aiProvidersTable)
.where(inArray(aiProvidersTable.id, createdProviderIds.splice(0)));
}
});
afterAll(async () => {
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
await pool.end();
});
type FetchResponseInit = {
ok: boolean;
status?: number;
jsonBody?: unknown;
textBody?: string;
};
function mockFetch(resp: FetchResponseInit): ReturnType<typeof vi.fn> {
const fn = vi.fn(
async (input: unknown, init?: RequestInit): Promise<Response> => {
const url = typeof input === "string" ? input : String(input);
// Let the test client's own request to the in-process server through.
if (url.startsWith(baseUrl)) {
return realFetch(input as never, init as never);
}
return {
ok: resp.ok,
status: resp.status ?? (resp.ok ? 200 : 500),
json: async () => resp.jsonBody ?? {},
text: async () => resp.textBody ?? "",
} as unknown as Response;
},
);
vi.spyOn(globalThis, "fetch").mockImplementation(
fn as unknown as typeof fetch,
);
return fn;
}
// The spy records both the test client's localhost request and the upstream
// provider request. Pick the upstream one (anything not aimed at our server).
function upstreamCall(
fn: ReturnType<typeof vi.fn>,
): [string, RequestInit] {
const call = fn.mock.calls.find(
(c) => !String(c[0]).startsWith(baseUrl),
);
if (!call) throw new Error("no upstream fetch call was recorded");
return [String(call[0]), call[1] as RequestInit];
}
async function listModels(body: Record<string, unknown>): Promise<{
status: number;
json: { ok: boolean; models: string[]; message?: string | null };
}> {
const res = await fetch(`${baseUrl}/api/providers/list-models`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const json = (await res.json()) as {
ok: boolean;
models: string[];
message?: string | null;
};
return { status: res.status, json };
}
async function insertProvider(apiToken: string | null): Promise<number> {
const [row] = await db
.insert(aiProvidersTable)
.values({
name: `list-models-test ${Math.random().toString(36).slice(2)}`,
apiType: "openai",
baseUrl: "https://api.example.test/v1",
model: "",
apiToken,
enabled: true,
})
.returning();
createdProviderIds.push(row.id);
return row.id;
}
describe("POST /api/providers/list-models", () => {
it("returns ok=false with a clear message when no token is supplied", async () => {
const upstream = mockFetch({ ok: true, jsonBody: { data: [] } });
const { status, json } = await listModels({
apiType: "openai",
baseUrl: "https://api.example.test/v1",
apiToken: "",
});
expect(status).toBe(200);
expect(json.ok).toBe(false);
expect(json.models).toEqual([]);
expect(json.message).toBe("Kein API-Token angegeben.");
// It must short-circuit before ever calling the upstream provider.
const upstreamCalls = upstream.mock.calls.filter(
(c) => !String(c[0]).startsWith(baseUrl),
);
expect(upstreamCalls).toHaveLength(0);
});
it("falls back to the stored token when providerId is given and apiToken is empty", async () => {
const storedToken = "stored-secret-token-1234567890";
const providerId = await insertProvider(storedToken);
const upstream = mockFetch({
ok: true,
jsonBody: { data: [{ id: "gpt-4o" }] },
});
const { json } = await listModels({
apiType: "openai",
baseUrl: "https://api.example.test/v1",
apiToken: "",
providerId,
});
expect(json.ok).toBe(true);
expect(json.models).toEqual(["gpt-4o"]);
// The stored token was used for the Bearer header.
const [, init] = upstreamCall(upstream);
const headers = init.headers as Record<string, string>;
expect(headers.Authorization).toBe(`Bearer ${storedToken}`);
});
it("normalizes the OpenAI-compatible response into a deduped, sorted list of model IDs", async () => {
const upstream = mockFetch({
ok: true,
jsonBody: {
data: [
{ id: "gpt-4o" },
{ id: "gpt-3.5-turbo" },
{ id: "gpt-4o" }, // duplicate
{ id: "claude-ignored-by-shape" }, // still a string id
{ id: 12345 }, // non-string id is dropped
],
},
});
const { json } = await listModels({
apiType: "openai",
baseUrl: "https://api.example.test/v1",
apiToken: "tok-abc",
});
expect(json.ok).toBe(true);
expect(json.models).toEqual([
"claude-ignored-by-shape",
"gpt-3.5-turbo",
"gpt-4o",
]);
// OpenAI-compatible path: GET /models with a Bearer header.
const [url, init] = upstreamCall(upstream);
expect(url).toBe("https://api.example.test/v1/models");
expect(init.method).toBe("GET");
const headers = init.headers as Record<string, string>;
expect(headers.Authorization).toBe("Bearer tok-abc");
expect(headers["x-api-key"]).toBeUndefined();
});
it("uses Anthropic headers (x-api-key + anthropic-version) and reads the models array", async () => {
const upstream = mockFetch({
ok: true,
jsonBody: {
models: [
{ id: "claude-3-5-sonnet" },
{ name: "claude-3-haiku" }, // name fallback when id is absent
{ id: "claude-3-5-sonnet" }, // duplicate
],
},
});
const { json } = await listModels({
apiType: "anthropic",
baseUrl: "https://api.anthropic.test/v1",
apiToken: "anthropic-key-xyz",
});
expect(json.ok).toBe(true);
expect(json.models).toEqual(["claude-3-5-sonnet", "claude-3-haiku"]);
const [url, init] = upstreamCall(upstream);
expect(url).toBe("https://api.anthropic.test/v1/models");
expect(init.method).toBe("GET");
const headers = init.headers as Record<string, string>;
expect(headers["x-api-key"]).toBe("anthropic-key-xyz");
expect(headers["anthropic-version"]).toBe("2023-06-01");
expect(headers.Authorization).toBeUndefined();
});
it("returns ok=false (not a 500) and never leaks the token when the upstream call fails", async () => {
const token = "leaky-secret-token-abcdef123456";
mockFetch({
ok: false,
status: 401,
// The upstream error body echoes the token back to us.
textBody: `{"error":"invalid api key ${token}, Bearer ${token}"}`,
});
const { status, json } = await listModels({
apiType: "openai",
baseUrl: "https://api.example.test/v1",
apiToken: token,
});
// Errors are surfaced gracefully, not as a 500.
expect(status).toBe(200);
expect(json.ok).toBe(false);
expect(json.models).toEqual([]);
expect(typeof json.message).toBe("string");
// The token must be redacted from the error message.
expect(json.message).not.toContain(token);
expect(json.message).toContain("[REDACTED]");
expect(json.message).toContain("HTTP 401");
});
it("returns ok=false without leaking the token when fetch itself throws", async () => {
const token = "throwy-secret-token-zzz999";
vi.spyOn(globalThis, "fetch").mockImplementation((async (
input: unknown,
init?: RequestInit,
) => {
const url = typeof input === "string" ? input : String(input);
if (url.startsWith(baseUrl)) {
return realFetch(input as never, init as never);
}
throw new Error("network down");
}) as unknown as typeof fetch);
const { status, json } = await listModels({
apiType: "openai",
baseUrl: "https://api.example.test/v1",
apiToken: token,
});
expect(status).toBe(200);
expect(json.ok).toBe(false);
expect(json.models).toEqual([]);
expect(json.message).not.toContain(token);
});
});