import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; const originalDataDir = process.env.DATA_DIR; async function setupTestContext(nodeData) { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-compatible-provider-")); process.env.DATA_DIR = tempDir; vi.resetModules(); vi.doMock("next/server", () => ({ NextResponse: { json(body, init = {}) { return new Response(JSON.stringify(body), { status: init.status || 200, headers: { "Content-Type": "application/json" }, }); }, }, })); const { POST } = await import("@/app/api/providers/route.js"); const { createProviderNode, getProviderConnections, } = await import("@/models/index.js"); const node = await createProviderNode(nodeData); return { node, POST, getProviderConnections, cleanup() { fs.rmSync(tempDir, { recursive: true, force: true }); }, }; } function makeRequest(provider) { return new Request("https://9router.local/api/providers", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider, apiKey: "test-key", name: "Test Connection", defaultModel: "test-model", }), }); } function expectCompatibleConnection(connection, node, { apiType } = {}) { expect(connection.provider).toBe(node.id); expect(connection.authType).toBe("apikey"); expect(connection.defaultModel).toBe("test-model"); expect(connection.providerSpecificData).toMatchObject({ prefix: node.prefix, baseUrl: node.baseUrl, nodeName: node.name, }); if (apiType !== undefined) { expect(connection.providerSpecificData.apiType).toBe(apiType); } } describe("compatible provider connections API", () => { let cleanup = () => {}; beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.doUnmock("next/server"); vi.resetModules(); vi.clearAllMocks(); cleanup(); cleanup = () => {}; if (originalDataDir === undefined) delete process.env.DATA_DIR; else process.env.DATA_DIR = originalDataDir; }); it("creates one API-key connection for an OpenAI-compatible node", async () => { const ctx = await setupTestContext({ id: "openai-compatible-test", type: "openai-compatible", name: "OpenAI Compatible Test Node", prefix: "oct", apiType: "chat", baseUrl: "https://openai-compatible.test/v1", }); cleanup = ctx.cleanup; const response = await ctx.POST(makeRequest(ctx.node.id)); const body = await response.json(); const connection = body.connection; const storedConnections = await ctx.getProviderConnections({ provider: ctx.node.id }); expect(response.status).toBe(201); expect(storedConnections).toHaveLength(1); expectCompatibleConnection(connection, ctx.node, { apiType: "chat" }); expect(storedConnections[0]).toMatchObject({ provider: ctx.node.id, authType: "apikey", defaultModel: "test-model", providerSpecificData: { prefix: ctx.node.prefix, apiType: "chat", baseUrl: ctx.node.baseUrl, nodeName: ctx.node.name, }, }); }); it("creates one API-key connection for an Anthropic-compatible node", async () => { const ctx = await setupTestContext({ id: "anthropic-compatible-test", type: "anthropic-compatible", name: "Anthropic Compatible Test Node", prefix: "act", baseUrl: "https://anthropic-compatible.test/v1", }); cleanup = ctx.cleanup; const response = await ctx.POST(makeRequest(ctx.node.id)); const body = await response.json(); const connection = body.connection; const storedConnections = await ctx.getProviderConnections({ provider: ctx.node.id }); expect(response.status).toBe(201); expect(storedConnections).toHaveLength(1); expectCompatibleConnection(connection, ctx.node); expect(storedConnections[0]).toMatchObject({ provider: ctx.node.id, authType: "apikey", defaultModel: "test-model", providerSpecificData: { prefix: ctx.node.prefix, baseUrl: ctx.node.baseUrl, nodeName: ctx.node.name, }, }); }); it("returns 400 for a duplicate connection on the same compatible node", async () => { const ctx = await setupTestContext({ id: "openai-compatible-duplicate-test", type: "openai-compatible", name: "Duplicate Guard Node", prefix: "dup", apiType: "chat", baseUrl: "https://duplicate-guard.test/v1", }); cleanup = ctx.cleanup; const firstResponse = await ctx.POST(makeRequest(ctx.node.id)); const secondResponse = await ctx.POST(makeRequest(ctx.node.id)); const secondBody = await secondResponse.json(); const storedConnections = await ctx.getProviderConnections({ provider: ctx.node.id }); expect(firstResponse.status).toBe(201); expect(secondResponse.status).toBe(400); expect(secondBody.error).toContain("Only one connection is allowed"); expect(storedConnections).toHaveLength(1); expectCompatibleConnection(storedConnections[0], ctx.node, { apiType: "chat" }); }); });