9router / tests /unit /embeddings.cloud.test.js
2api
feat: configurable stream stall timeout + per-provider UI
88c4c60
Raw
History Blame Contribute Delete
20.1 kB
/**
* Unit tests for cloud/src/handlers/embeddings.js
*
* Tests cover:
* - CORS OPTIONS β†’ 200 with CORS headers
* - Auth: missing Bearer β†’ 401
* - Auth: invalid key format β†’ 401
* - Auth: valid new-format key but wrong key value β†’ 401
* - Body validation: missing model β†’ 400, missing input β†’ 400
* - Invalid model format β†’ 400
* - Happy path β†’ delegates to handleEmbeddingsCore and returns response
* - Rate-limited provider β†’ 503 with Retry-After
* - No credentials β†’ 400
*
* Strategy: mock all external dependencies (D1 storage, handleEmbeddingsCore, apiKey utils)
* so tests run without Cloudflare Workers runtime.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// ─── Module mocks (hoisted before imports) ───────────────────────────────────
vi.mock("../../open-sse/services/model.js", () => ({
getModelInfoCore: vi.fn(),
}));
vi.mock("../../open-sse/handlers/embeddingsCore.js", () => ({
handleEmbeddingsCore: vi.fn(),
}));
vi.mock("../../open-sse/utils/error.js", async (importOriginal) => {
// Use real errorResponse implementation so response bodies are realistic
const actual = await importOriginal();
return actual;
});
vi.mock("../../open-sse/services/accountFallback.js", async (importOriginal) => {
const actual = await importOriginal();
return actual;
});
vi.mock("../../cloud/src/utils/logger.js", () => ({
info: vi.fn(),
debug: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}));
vi.mock("../../cloud/src/utils/apiKey.js", () => ({
parseApiKey: vi.fn(),
extractBearerToken: vi.fn(),
}));
vi.mock("../../cloud/src/services/storage.js", () => ({
getMachineData: vi.fn(),
saveMachineData: vi.fn(),
}));
// ─── Imports (after mocks) ────────────────────────────────────────────────────
import { handleEmbeddings } from "../../cloud/src/handlers/embeddings.js";
import { getModelInfoCore } from "../../open-sse/services/model.js";
import { handleEmbeddingsCore } from "../../open-sse/handlers/embeddingsCore.js";
import { parseApiKey, extractBearerToken } from "../../cloud/src/utils/apiKey.js";
import { getMachineData, saveMachineData } from "../../cloud/src/services/storage.js";
// ─── Fixtures ─────────────────────────────────────────────────────────────────
const MACHINE_ID = "mach01";
const VALID_API_KEY = "sk-mach01-key01-ab12cd34"; // new format shape
const VALID_EMBEDDING_RESPONSE_BODY = {
object: "list",
data: [{ object: "embedding", index: 0, embedding: [0.1, 0.2, 0.3] }],
model: "text-embedding-ada-002",
usage: { prompt_tokens: 3, total_tokens: 3 },
};
/** Build a minimal mock env (Cloudflare Worker env bindings) */
function makeEnv() {
return { DB: {}, KV: {} };
}
/** Build a mock machine data record stored in D1 */
function makeMachineData(overrides = {}) {
return {
machineId: MACHINE_ID,
apiKeys: [{ key: VALID_API_KEY, label: "test" }],
providers: {
"conn-001": {
provider: "openai",
apiKey: "sk-openai-provider-key",
isActive: true,
priority: 1,
status: "active",
rateLimitedUntil: null,
lastError: null,
},
},
modelAliases: {},
...overrides,
};
}
/** Make a Request object */
function makeRequest(method = "POST", body = null, authHeader = `Bearer ${VALID_API_KEY}`) {
const headers = { "Content-Type": "application/json" };
if (authHeader) headers["Authorization"] = authHeader;
return new Request("https://9cli.hxd.app/v1/embeddings", {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
}
// ─── Tests: CORS OPTIONS ──────────────────────────────────────────────────────
describe("handleEmbeddings β€” CORS OPTIONS", () => {
it("OPTIONS request β†’ 200 with Access-Control-Allow-Origin: *", async () => {
const req = makeRequest("OPTIONS", null, null);
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(200);
expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
expect(res.headers.get("Access-Control-Allow-Methods")).toMatch(/POST/);
});
it("OPTIONS request β†’ body is empty/null", async () => {
const req = makeRequest("OPTIONS", null, null);
const res = await handleEmbeddings(req, makeEnv(), {});
const text = await res.text();
expect(text).toBe("");
});
});
// ─── Tests: Authentication ────────────────────────────────────────────────────
describe("handleEmbeddings β€” authentication", () => {
beforeEach(() => {
vi.mocked(extractBearerToken).mockReturnValue(null);
vi.mocked(parseApiKey).mockResolvedValue(null);
vi.mocked(getMachineData).mockResolvedValue(makeMachineData());
vi.mocked(getModelInfoCore).mockResolvedValue({ provider: "openai", model: "text-embedding-ada-002" });
});
afterEach(() => {
vi.clearAllMocks();
});
it("missing Authorization header β†’ 401", async () => {
vi.mocked(extractBearerToken).mockReturnValue(null);
const req = makeRequest("POST", { model: "ag/gemini-embedding-001", input: "hello" }, null);
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error.message).toMatch(/missing api key/i);
});
it("Authorization header without Bearer scheme β†’ 401", async () => {
vi.mocked(extractBearerToken).mockReturnValue(null);
const req = makeRequest("POST", { model: "ag/gemini-embedding-001", input: "hello" }, "Token abc123");
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(401);
});
it("Bearer key that fails parseApiKey β†’ 401", async () => {
vi.mocked(extractBearerToken).mockReturnValue("sk-invalidkey");
vi.mocked(parseApiKey).mockResolvedValue(null);
const req = makeRequest("POST", { model: "ag/gemini-embedding-001", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error.message).toMatch(/invalid api key format/i);
});
it("old-format key (no machineId) β†’ 400 asking to use machineId endpoint", async () => {
vi.mocked(extractBearerToken).mockReturnValue("sk-oldfmt8");
vi.mocked(parseApiKey).mockResolvedValue({ machineId: null, keyId: "oldfmt8", isNewFormat: false });
const req = makeRequest("POST", { model: "ag/gemini-embedding-001", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error.message).toMatch(/machineId/i);
});
it("valid key format but key value not in machine apiKeys β†’ 401", async () => {
vi.mocked(extractBearerToken).mockReturnValue("sk-mach01-key01-ab12cd34");
vi.mocked(parseApiKey).mockResolvedValue({ machineId: MACHINE_ID, keyId: "key01", isNewFormat: true });
vi.mocked(getMachineData).mockResolvedValue(makeMachineData({
apiKeys: [{ key: "sk-different-key" }], // key doesn't match
}));
const req = makeRequest("POST", { model: "ag/gemini-embedding-001", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error.message).toMatch(/invalid api key/i);
});
it("valid key β†’ passes auth (proceeds to body parsing)", async () => {
vi.mocked(extractBearerToken).mockReturnValue(VALID_API_KEY);
vi.mocked(parseApiKey).mockResolvedValue({ machineId: MACHINE_ID, keyId: "key01", isNewFormat: true });
vi.mocked(getMachineData).mockResolvedValue(makeMachineData());
vi.mocked(getModelInfoCore).mockResolvedValue({ provider: "openai", model: "text-embedding-ada-002" });
vi.mocked(handleEmbeddingsCore).mockResolvedValue({
success: true,
response: new Response(JSON.stringify(VALID_EMBEDDING_RESPONSE_BODY), {
status: 200,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
}),
});
const req = makeRequest("POST", { model: "openai/text-embedding-ada-002", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
// Should not be 401
expect(res.status).not.toBe(401);
expect(res.status).not.toBe(403);
});
});
// ─── Tests: Body validation ───────────────────────────────────────────────────
describe("handleEmbeddings β€” body validation", () => {
beforeEach(() => {
vi.mocked(extractBearerToken).mockReturnValue(VALID_API_KEY);
vi.mocked(parseApiKey).mockResolvedValue({ machineId: MACHINE_ID, keyId: "key01", isNewFormat: true });
vi.mocked(getMachineData).mockResolvedValue(makeMachineData());
});
afterEach(() => {
vi.clearAllMocks();
});
it("invalid JSON body β†’ 400", async () => {
const req = new Request("https://9cli.hxd.app/v1/embeddings", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${VALID_API_KEY}`,
},
body: "{ bad json",
});
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error.message).toMatch(/invalid json/i);
});
it("missing model field β†’ 400", async () => {
const req = makeRequest("POST", { input: "hello world" });
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error.message).toMatch(/missing model/i);
});
it("missing input field β†’ 400", async () => {
const req = makeRequest("POST", { model: "ag/gemini-embedding-001" });
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error.message).toMatch(/missing required field: input/i);
});
it("model with no provider mapping β†’ 400", async () => {
vi.mocked(getModelInfoCore).mockResolvedValue({ provider: null, model: null });
const req = makeRequest("POST", { model: "nonexistent/model", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error.message).toMatch(/invalid model format/i);
});
});
// ─── Tests: Happy path β€” valid request ────────────────────────────────────────
describe("handleEmbeddings β€” valid request (happy path)", () => {
beforeEach(() => {
vi.mocked(extractBearerToken).mockReturnValue(VALID_API_KEY);
vi.mocked(parseApiKey).mockResolvedValue({ machineId: MACHINE_ID, keyId: "key01", isNewFormat: true });
vi.mocked(getMachineData).mockResolvedValue(makeMachineData());
vi.mocked(getModelInfoCore).mockResolvedValue({ provider: "openai", model: "text-embedding-ada-002" });
vi.mocked(handleEmbeddingsCore).mockResolvedValue({
success: true,
response: new Response(JSON.stringify(VALID_EMBEDDING_RESPONSE_BODY), {
status: 200,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
}),
});
vi.mocked(saveMachineData).mockResolvedValue(undefined);
});
afterEach(() => {
vi.clearAllMocks();
});
it("single string input β†’ 200 with embeddings data", async () => {
const req = makeRequest("POST", {
model: "openai/text-embedding-ada-002",
input: "Hello world test embedding",
});
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.object).toBe("list");
expect(Array.isArray(body.data)).toBe(true);
});
it("array input β†’ 200 with embeddings data", async () => {
const req = makeRequest("POST", {
model: "openai/text-embedding-ada-002",
input: ["Hello", "World"],
});
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.object).toBe("list");
});
it("delegates to handleEmbeddingsCore with correct args", async () => {
const req = makeRequest("POST", {
model: "openai/text-embedding-ada-002",
input: "Test",
});
await handleEmbeddings(req, makeEnv(), {});
expect(handleEmbeddingsCore).toHaveBeenCalledOnce();
const callArgs = vi.mocked(handleEmbeddingsCore).mock.calls[0][0];
expect(callArgs.body.input).toBe("Test");
expect(callArgs.modelInfo.provider).toBe("openai");
expect(callArgs.modelInfo.model).toBe("text-embedding-ada-002");
expect(callArgs.credentials).toBeDefined();
});
it("response has CORS header from addCorsHeaders wrapper", async () => {
const req = makeRequest("POST", {
model: "openai/text-embedding-ada-002",
input: "Hello",
});
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
});
it("machineId-override path: /{machineId}/v1/embeddings works", async () => {
// Direct call with machineId override (old format URL path)
const req = new Request(`https://9cli.hxd.app/${MACHINE_ID}/v1/embeddings`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${VALID_API_KEY}`,
},
body: JSON.stringify({ model: "openai/text-embedding-ada-002", input: "Hello" }),
});
const res = await handleEmbeddings(req, makeEnv(), {}, MACHINE_ID);
expect(res.status).toBe(200);
});
});
// ─── Tests: Rate limiting ──────────────────────────────────────────────────────
describe("handleEmbeddings β€” rate limit fallback", () => {
beforeEach(() => {
vi.mocked(extractBearerToken).mockReturnValue(VALID_API_KEY);
vi.mocked(parseApiKey).mockResolvedValue({ machineId: MACHINE_ID, keyId: "key01", isNewFormat: true });
vi.mocked(getModelInfoCore).mockResolvedValue({ provider: "openai", model: "text-embedding-ada-002" });
vi.mocked(saveMachineData).mockResolvedValue(undefined);
});
afterEach(() => {
vi.clearAllMocks();
});
it("all provider accounts rate-limited β†’ 503 with Retry-After header", async () => {
const rateLimitedUntil = new Date(Date.now() + 60000).toISOString(); // 60s from now
vi.mocked(getMachineData).mockResolvedValue(makeMachineData({
providers: {
"conn-001": {
provider: "openai",
apiKey: "sk-key",
isActive: true,
priority: 1,
status: "unavailable",
rateLimitedUntil, // rate-limited
lastError: "Rate limit exceeded",
errorCode: 429,
backoffLevel: 1,
},
},
}));
const req = makeRequest("POST", { model: "openai/text-embedding-ada-002", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(429);
expect(res.headers.get("Retry-After")).toBeDefined();
const retryAfter = parseInt(res.headers.get("Retry-After"));
expect(retryAfter).toBeGreaterThan(0);
});
it("provider account not found β†’ 400 No credentials", async () => {
vi.mocked(getMachineData).mockResolvedValue(makeMachineData({
providers: {}, // no providers
}));
const req = makeRequest("POST", { model: "openai/text-embedding-ada-002", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error.message).toMatch(/no credentials/i);
});
it("core returns non-fallback error β†’ propagates error response directly", async () => {
vi.mocked(getMachineData).mockResolvedValue(makeMachineData());
vi.mocked(handleEmbeddingsCore).mockResolvedValue({
success: false,
status: 400,
error: "input must be a string or array",
response: new Response(
JSON.stringify({ error: { message: "input must be a string or array" } }),
{ status: 400, headers: { "Content-Type": "application/json" } }
),
});
const req = makeRequest("POST", { model: "openai/text-embedding-ada-002", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
// Non-fallback error (400) should not trigger account cycle; returns error directly
expect(res.status).toBe(400);
});
it("core returns 429 β†’ marks account unavailable, then no more accounts β†’ 503", async () => {
vi.mocked(getMachineData).mockResolvedValue(makeMachineData());
vi.mocked(handleEmbeddingsCore).mockResolvedValue({
success: false,
status: 429,
error: "Rate limit exceeded",
response: new Response(
JSON.stringify({ error: { message: "Rate limit exceeded" } }),
{ status: 429, headers: { "Content-Type": "application/json" } }
),
});
const req = makeRequest("POST", { model: "openai/text-embedding-ada-002", input: "hello" });
const res = await handleEmbeddings(req, makeEnv(), {});
// After fallback loop exhausts accounts
expect([429, 503]).toContain(res.status);
});
});
// ─── Tests: machineId-override (old-format URL path) ─────────────────────────
describe("handleEmbeddings β€” machineId override path", () => {
beforeEach(() => {
// When machineId is provided via URL, no apiKey parsing needed for machineId
vi.mocked(getMachineData).mockResolvedValue(makeMachineData());
vi.mocked(getModelInfoCore).mockResolvedValue({ provider: "openai", model: "text-embedding-ada-002" });
vi.mocked(handleEmbeddingsCore).mockResolvedValue({
success: true,
response: new Response(JSON.stringify(VALID_EMBEDDING_RESPONSE_BODY), {
status: 200,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
}),
});
vi.mocked(saveMachineData).mockResolvedValue(undefined);
});
afterEach(() => {
vi.clearAllMocks();
});
it("with machineIdOverride, still validates API key via Authorization header", async () => {
// Key IS in the machine's apiKeys β†’ should succeed
const req = new Request(`https://9cli.hxd.app/${MACHINE_ID}/v1/embeddings`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${VALID_API_KEY}`,
},
body: JSON.stringify({ model: "openai/text-embedding-ada-002", input: "test" }),
});
const res = await handleEmbeddings(req, makeEnv(), {}, MACHINE_ID);
expect(res.status).toBe(200);
});
it("with machineIdOverride, wrong API key β†’ 401", async () => {
vi.mocked(getMachineData).mockResolvedValue(makeMachineData({
apiKeys: [{ key: "sk-correct-key" }],
}));
const req = new Request(`https://9cli.hxd.app/${MACHINE_ID}/v1/embeddings`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer sk-wrong-key",
},
body: JSON.stringify({ model: "openai/text-embedding-ada-002", input: "test" }),
});
const res = await handleEmbeddings(req, makeEnv(), {}, MACHINE_ID);
expect(res.status).toBe(401);
});
});