328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const catalog =
|
|
await import("../../src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts");
|
|
const api =
|
|
await import("../../src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingApi.ts");
|
|
|
|
test("provider onboarding catalog exposes API-key and OAuth providers for the wizard", () => {
|
|
const apiKeyOptions = catalog.getWizardApiKeyProviderOptions();
|
|
const oauthOptions = catalog.getWizardOAuthProviderOptions();
|
|
|
|
assert.ok(apiKeyOptions.some((option) => option.id === "openai"));
|
|
assert.ok(apiKeyOptions.some((option) => option.id === "openrouter"));
|
|
assert.ok(oauthOptions.some((option) => option.id === "claude"));
|
|
assert.ok(oauthOptions.some((option) => option.id === "kiro"));
|
|
assert.ok(oauthOptions.some((option) => option.id === "cursor"));
|
|
assert.ok(!oauthOptions.some((option) => option.id === "zed"));
|
|
assert.ok(!oauthOptions.some((option) => option.id === "windsurf"));
|
|
assert.ok(!oauthOptions.some((option) => option.id === "devin-cli"));
|
|
assert.ok(!oauthOptions.some((option) => option.id === "qoder"));
|
|
|
|
assert.ok(apiKeyOptions.every((option) => option.authKind === "apikey"));
|
|
assert.ok(oauthOptions.every((option) => option.authKind === "oauth"));
|
|
});
|
|
|
|
test("provider onboarding option filter matches id, name, alias, and description", () => {
|
|
const options = [
|
|
{
|
|
id: "openrouter",
|
|
name: "OpenRouter",
|
|
icon: "router",
|
|
alias: "or",
|
|
description: "multi-model routing gateway",
|
|
authKind: "apikey",
|
|
apiKeyOptional: false,
|
|
deprecated: false,
|
|
},
|
|
{
|
|
id: "claude",
|
|
name: "Claude Code",
|
|
icon: "smart_toy",
|
|
alias: "cc",
|
|
description: "OAuth coding provider",
|
|
authKind: "oauth",
|
|
apiKeyOptional: false,
|
|
deprecated: false,
|
|
},
|
|
];
|
|
|
|
assert.deepEqual(
|
|
catalog.filterWizardProviderOptions(options, "router").map((option) => option.id),
|
|
["openrouter"]
|
|
);
|
|
assert.deepEqual(
|
|
catalog.filterWizardProviderOptions(options, "coding").map((option) => option.id),
|
|
["claude"]
|
|
);
|
|
assert.equal(catalog.filterWizardProviderOptions(options, " ").length, 2);
|
|
});
|
|
|
|
test("provider onboarding builds providerSpecificData without empty fields", () => {
|
|
assert.deepEqual(
|
|
catalog.buildProviderSpecificData({
|
|
baseUrl: " https://gateway.example/v1 ",
|
|
region: "",
|
|
cx: " cx-123 ",
|
|
customUserAgent: " ",
|
|
}),
|
|
{
|
|
baseUrl: "https://gateway.example/v1",
|
|
cx: "cx-123",
|
|
}
|
|
);
|
|
assert.equal(catalog.buildProviderSpecificData({}), null);
|
|
});
|
|
|
|
test("provider onboarding API sends validate, create, and test requests with expected payloads", async () => {
|
|
const calls = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
if (String(url) === "/api/providers/validate") {
|
|
return Response.json({ valid: true });
|
|
}
|
|
if (String(url) === "/api/providers") {
|
|
return Response.json(
|
|
{ connection: { id: "conn-1", provider: "openai", name: "OpenAI Primary" } },
|
|
{ status: 201 }
|
|
);
|
|
}
|
|
if (String(url) === "/api/providers/conn-1/test") {
|
|
return Response.json({ valid: true, latencyMs: 42 });
|
|
}
|
|
return Response.json({ error: "unexpected" }, { status: 500 });
|
|
};
|
|
|
|
try {
|
|
await api.validateOnboardingApiKey({
|
|
provider: "openai",
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://api.example/v1",
|
|
});
|
|
const connection = await api.createOnboardingConnection({
|
|
provider: "openai",
|
|
name: "OpenAI Primary",
|
|
apiKey: "sk-test",
|
|
providerSpecificData: { baseUrl: "https://api.example/v1" },
|
|
});
|
|
const result = await api.testOnboardingConnection(connection.id);
|
|
|
|
assert.equal(connection.id, "conn-1");
|
|
assert.equal(result.valid, true);
|
|
assert.equal(calls.length, 3);
|
|
assert.equal(calls[0].url, "/api/providers/validate");
|
|
assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
|
|
provider: "openai",
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://api.example/v1",
|
|
});
|
|
assert.equal(calls[1].url, "/api/providers");
|
|
assert.deepEqual(JSON.parse(String(calls[1].init.body)), {
|
|
provider: "openai",
|
|
name: "OpenAI Primary",
|
|
apiKey: "sk-test",
|
|
priority: 1,
|
|
testStatus: "unknown",
|
|
providerSpecificData: { baseUrl: "https://api.example/v1" },
|
|
});
|
|
assert.equal(calls[2].url, "/api/providers/conn-1/test");
|
|
assert.equal(calls[2].init.method, "POST");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding validation rejects HTTP 200 responses with valid false", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let createCalled = false;
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url) === "/api/providers/validate") {
|
|
return Response.json({ valid: false, error: "Invalid API key" });
|
|
}
|
|
if (String(url) === "/api/providers") {
|
|
createCalled = true;
|
|
return Response.json({ connection: { id: "conn-1", provider: "openai" } });
|
|
}
|
|
return Response.json({ error: "unexpected" }, { status: 500 });
|
|
};
|
|
|
|
try {
|
|
await assert.rejects(
|
|
() => api.validateOnboardingApiKey({ provider: "openai", apiKey: "bad-key" }),
|
|
/Invalid API key/
|
|
);
|
|
assert.equal(createCalled, false);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("#5692 onboarding validation treats unsupported providers as non-blocking (save proceeds)", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url) === "/api/providers/validate") {
|
|
// #5565/#5567: providers with no live validator (lmarena, piapi, …) return
|
|
// HTTP 400 + { unsupported: true }. The wizard must NOT treat this as a hard
|
|
// failure — otherwise the connection is never created (#5692).
|
|
return Response.json(
|
|
{ error: "Provider validation not supported", unsupported: true },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
return Response.json({ error: "unexpected" }, { status: 500 });
|
|
};
|
|
|
|
try {
|
|
const data = await api.validateOnboardingApiKey({ provider: "lmarena", apiKey: "test-key" });
|
|
assert.equal(data.unsupported, true);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding API ignores non-object error JSON", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => Response.json(null, { status: 500 });
|
|
|
|
try {
|
|
await assert.rejects(() => api.fetchOnboardingProviderNodes(), /Failed to load provider nodes/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding validates API payloads before sending requests", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let fetchCalled = false;
|
|
globalThis.fetch = async () => {
|
|
fetchCalled = true;
|
|
return Response.json({ valid: true });
|
|
};
|
|
|
|
try {
|
|
await assert.rejects(
|
|
() =>
|
|
api.validateOnboardingApiKey({
|
|
provider: "openai",
|
|
apiKey: "sk-test",
|
|
baseUrl: "not-a-url",
|
|
}),
|
|
/Provider credentials are not valid/
|
|
);
|
|
await assert.rejects(
|
|
() =>
|
|
api.createOnboardingConnection({
|
|
provider: "openai",
|
|
name: "OpenAI Primary",
|
|
}),
|
|
/Provider connection data is invalid/
|
|
);
|
|
assert.equal(fetchCalled, false);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding loads provider node feature flags through the API helper", async () => {
|
|
const calls = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
return Response.json({ ccCompatibleProviderEnabled: true });
|
|
};
|
|
|
|
try {
|
|
const result = await api.fetchOnboardingProviderNodes();
|
|
|
|
assert.deepEqual(result, { ccCompatibleProviderEnabled: true });
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].url, "/api/provider-nodes");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding can create compatible connections without an API key", async () => {
|
|
const calls = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
return Response.json(
|
|
{ connection: { id: "conn-custom", provider: "openai-compatible-local" } },
|
|
{ status: 201 }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const connection = await api.createOnboardingConnection({
|
|
provider: "openai-compatible-local",
|
|
name: "Local Gateway",
|
|
});
|
|
|
|
assert.equal(connection.id, "conn-custom");
|
|
assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
|
|
provider: "openai-compatible-local",
|
|
name: "Local Gateway",
|
|
priority: 1,
|
|
testStatus: "unknown",
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding API builds compatible provider node request bodies", () => {
|
|
assert.deepEqual(
|
|
api.buildCompatibleNodeRequest({
|
|
mode: "openai",
|
|
name: "Local Gateway",
|
|
prefix: "local-gw",
|
|
baseUrl: "https://gateway.example/v1",
|
|
apiType: "responses",
|
|
modelsPath: "/models",
|
|
}),
|
|
{
|
|
name: "Local Gateway",
|
|
prefix: "local-gw",
|
|
baseUrl: "https://gateway.example/v1",
|
|
type: "openai-compatible",
|
|
chatPath: "",
|
|
apiType: "responses",
|
|
modelsPath: "/models",
|
|
}
|
|
);
|
|
|
|
assert.deepEqual(
|
|
api.buildCompatibleNodeRequest({
|
|
mode: "cc",
|
|
name: "CC Gateway",
|
|
prefix: "cc-gw",
|
|
baseUrl: "https://cc.example",
|
|
}),
|
|
{
|
|
name: "CC Gateway",
|
|
prefix: "cc-gw",
|
|
baseUrl: "https://cc.example",
|
|
type: "anthropic-compatible",
|
|
chatPath: "/v1/messages?beta=true",
|
|
compatMode: "cc",
|
|
}
|
|
);
|
|
});
|
|
|
|
test("provider onboarding validates compatible provider node request bodies", () => {
|
|
assert.throws(
|
|
() =>
|
|
api.buildCompatibleNodeRequest({
|
|
mode: "openai",
|
|
name: "Local Gateway",
|
|
prefix: "local-gw",
|
|
baseUrl: "https://gateway.example/v1",
|
|
chatPath: "chat/completions",
|
|
}),
|
|
/Compatible provider data is invalid/
|
|
);
|
|
});
|