mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 03:32:21 +03:00
feat(radar): complete guided provider setup
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
// Issue #3501 strangler-fig decomposition — Phase 1t (final push)
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card, Button, CardSkeleton } from "@/shared/components";
|
||||
@@ -68,6 +68,7 @@ import AnonymousFallbackToggle from "./components/AnonymousFallbackToggle";
|
||||
|
||||
export default function ProviderDetailPageClient() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const providerId = params.id as string;
|
||||
|
||||
// ── UI-only modal state (not owned by hooks) ─────────────────────────────
|
||||
@@ -357,6 +358,10 @@ export default function ProviderDetailPageClient() {
|
||||
setShowAddApiKeyModal(true);
|
||||
}, [providerId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get("action") === "add-api-key") gateConnectionFlow(openApiKeyAddFlow);
|
||||
}, [searchParams, gateConnectionFlow, openApiKeyAddFlow]);
|
||||
|
||||
const openPrimaryAddFlow = useCallback(() => {
|
||||
if (providerId === "kimi-coding") return setShowKimiAuthMethodModal(true);
|
||||
if (isOAuth) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Card } from "@/shared/components";
|
||||
import {
|
||||
firstProviderConnectionId,
|
||||
providerConnectionsRequestUrl,
|
||||
providerSetupConnectionUrl,
|
||||
type RadarSetupConnection,
|
||||
} from "@/lib/radar/setupConnections";
|
||||
import type { RadarLocalizedText } from "@/lib/radar/feedSchema";
|
||||
@@ -132,13 +133,13 @@ export default function RadarSetupPage() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
if (res.ok && data?.valid === true) {
|
||||
setTestResult({ ok: true, message: t("testSuccess") });
|
||||
} else {
|
||||
const data = await res.json().catch(() => null);
|
||||
setTestResult({
|
||||
ok: false,
|
||||
message: data?.error?.message || t("testFailed"),
|
||||
message: t("testFailed"),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
@@ -236,14 +237,6 @@ export default function RadarSetupPage() {
|
||||
<Card>
|
||||
<div className="text-center py-8 text-text-muted">
|
||||
<p>{t("noGuide")}</p>
|
||||
<a
|
||||
href={`https://${provider}.com`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-violet-400 hover:underline mt-2 inline-block"
|
||||
>
|
||||
{t("visitDocs")}
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
@@ -276,7 +269,7 @@ export default function RadarSetupPage() {
|
||||
<h2 className="font-semibold">{t("addConnection")}</h2>
|
||||
<p className="text-sm text-text-muted">{t("addConnectionDescription")}</p>
|
||||
<Link
|
||||
href={`/dashboard/providers?add=${encodeURIComponent(provider)}`}
|
||||
href={providerSetupConnectionUrl(provider)}
|
||||
className="text-violet-400 hover:underline text-sm"
|
||||
>
|
||||
{t("addConnectionLink")}
|
||||
|
||||
@@ -1736,6 +1736,8 @@
|
||||
"quotaShare": "Quota Share",
|
||||
"discovery": "Discovery",
|
||||
"freeProviderRankings": "Free Provider Rankings",
|
||||
"radar": "Radar",
|
||||
"setup": "Setup",
|
||||
"freeTiers": "Free Tiers",
|
||||
"gamification": "Gamification",
|
||||
"leaderboard": "Leaderboard",
|
||||
|
||||
@@ -1753,7 +1753,9 @@
|
||||
"tools": "Ferramentas",
|
||||
"agentBridge": "Agent Bridge",
|
||||
"trafficInspector": "Inspector de Tráfego",
|
||||
"usage": "Uso"
|
||||
"usage": "Uso",
|
||||
"radar": "Radar",
|
||||
"setup": "Configuração"
|
||||
},
|
||||
"home": {
|
||||
"quickStart": "Início Rápido",
|
||||
|
||||
@@ -9,6 +9,11 @@ export function providerConnectionsRequestUrl(provider: string): string {
|
||||
return `/api/providers?provider=${encodeURIComponent(provider)}`;
|
||||
}
|
||||
|
||||
/** Link the Radar tour to the provider's existing, validated API-key form. */
|
||||
export function providerSetupConnectionUrl(provider: string): string {
|
||||
return `/dashboard/providers/${encodeURIComponent(provider)}?action=add-api-key`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a concrete connection id for the setup test endpoint. Prefer an active
|
||||
* connection, then fall back to the first valid connection for the provider.
|
||||
|
||||
@@ -115,7 +115,8 @@ const PATH_LABELS = {
|
||||
*/
|
||||
function getLabel(segment, t) {
|
||||
const key = PATH_LABELS[segment];
|
||||
return key ? t(key) : segment.charAt(0).toUpperCase() + segment.slice(1);
|
||||
const fallback = segment.charAt(0).toUpperCase() + segment.slice(1);
|
||||
return key && typeof t.has === "function" && t.has(key) ? t(key) : fallback;
|
||||
}
|
||||
|
||||
export default function Breadcrumbs() {
|
||||
|
||||
177
tests/e2e/radar-guided-setup.spec.ts
Normal file
177
tests/e2e/radar-guided-setup.spec.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
|
||||
|
||||
const NAVIGATION_TIMEOUT_MS = 75_000;
|
||||
const EVIDENCE_DIR = process.env.RADAR_G11_EVIDENCE_DIR;
|
||||
|
||||
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
async function installGuidedSetupApi(page: Page) {
|
||||
await page.route("**/api/**", async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const method = request.method();
|
||||
|
||||
if (url.pathname === "/api/radar/catalog") {
|
||||
await fulfillJson(route, {
|
||||
entries: [
|
||||
{
|
||||
provider: "groq",
|
||||
setup: {
|
||||
keyUrl: "https://console.groq.com/keys",
|
||||
steps: [
|
||||
{
|
||||
en: "Create a project-specific Groq key.",
|
||||
pt: "Crie uma chave Groq específica para o projeto.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/providers/expiration") {
|
||||
await fulfillJson(route, { summary: { expired: 0, expiringSoon: 0 }, list: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/provider-nodes") {
|
||||
await fulfillJson(route, { nodes: [], ccCompatibleProviderEnabled: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/models/alias") {
|
||||
await fulfillJson(route, method === "GET" ? { aliases: {} } : { success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/settings/proxy") {
|
||||
await fulfillJson(route, url.searchParams.has("resolve") ? { proxy: null, level: null } : {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/provider-models") {
|
||||
await fulfillJson(route, { models: [], modelCompatOverrides: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/rate-limits") {
|
||||
await fulfillJson(route, { providers: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/providers/validate" && method === "POST") {
|
||||
await fulfillJson(route, { valid: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/providers") {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^\/api\/providers\/[^/]+\/sync-models$/.test(url.pathname) && method === "POST") {
|
||||
await fulfillJson(route, { syncedModels: 0, models: [], availableModelsCount: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^\/api\/providers\/[^/]+\/test$/.test(url.pathname) && method === "POST") {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.continue();
|
||||
});
|
||||
}
|
||||
|
||||
async function captureEvidence(page: Page, name: string) {
|
||||
if (!EVIDENCE_DIR) return;
|
||||
await page.screenshot({ path: `${EVIDENCE_DIR}/${name}.png`, fullPage: true });
|
||||
}
|
||||
|
||||
test.describe("Radar guided setup", () => {
|
||||
test("uses the official URL, real provider routes, isolated persistence, and real test route", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installGuidedSetupApi(page);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/radar/setup?provider=groq", {
|
||||
timeoutMs: NAVIGATION_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
const officialKeyLink = page.locator('a[href="https://console.groq.com/keys"]');
|
||||
await expect(officialKeyLink).toBeVisible();
|
||||
await captureEvidence(page, "01-official-guide");
|
||||
|
||||
const addConnectionLink = page.locator(
|
||||
'a[href="/dashboard/providers/groq?action=add-api-key"]'
|
||||
);
|
||||
await expect(addConnectionLink).toBeVisible();
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/dashboard\/providers\/groq\?action=add-api-key$/, {
|
||||
timeout: NAVIGATION_TIMEOUT_MS,
|
||||
}),
|
||||
addConnectionLink.click(),
|
||||
]);
|
||||
const addDialog = page.getByRole("dialog");
|
||||
await expect(addDialog).toBeVisible({ timeout: NAVIGATION_TIMEOUT_MS });
|
||||
await captureEvidence(page, "02-real-api-key-form");
|
||||
await addDialog.locator('input[type="password"]').fill("test-key-not-real");
|
||||
const createResponsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith("/api/providers") &&
|
||||
response.request().method() === "POST" &&
|
||||
response.status() === 201,
|
||||
{ timeout: NAVIGATION_TIMEOUT_MS }
|
||||
);
|
||||
await addDialog.getByRole("button", { name: /^save$/i }).click();
|
||||
const createResponse = await createResponsePromise;
|
||||
const created = (await createResponse.json()) as { connection?: { id?: string } };
|
||||
const connectionId = created.connection?.id;
|
||||
expect(connectionId).toBeTruthy();
|
||||
|
||||
const importDialog = page.getByRole("dialog").last();
|
||||
const closeImportButton = importDialog.getByRole("button", { name: "Close" }).last();
|
||||
await expect(closeImportButton).toBeVisible({
|
||||
timeout: NAVIGATION_TIMEOUT_MS,
|
||||
});
|
||||
await closeImportButton.click();
|
||||
|
||||
await page.goto("/dashboard/radar/setup?provider=groq", {
|
||||
waitUntil: "commit",
|
||||
timeout: NAVIGATION_TIMEOUT_MS,
|
||||
});
|
||||
await expect(page).toHaveURL(/\/dashboard\/radar\/setup\?provider=groq$/, {
|
||||
timeout: NAVIGATION_TIMEOUT_MS,
|
||||
});
|
||||
await expect(page.getByText(/provider is configured|provedor configurado/i)).toBeVisible({
|
||||
timeout: NAVIGATION_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
const testButton = page.getByRole("button", { name: /test connection|testar conexão/i });
|
||||
await expect(testButton).toBeEnabled({ timeout: NAVIGATION_TIMEOUT_MS });
|
||||
const testResponsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith(`/api/providers/${connectionId}/test`) &&
|
||||
response.request().method() === "POST",
|
||||
{ timeout: NAVIGATION_TIMEOUT_MS }
|
||||
);
|
||||
await testButton.click();
|
||||
const testResponse = await testResponsePromise;
|
||||
expect(testResponse.status()).toBe(200);
|
||||
const testResult = (await testResponse.json()) as { valid?: boolean };
|
||||
expect(testResult.valid).toBe(false);
|
||||
await expect(page.getByText(/connection test failed|falha no teste de conexão/i)).toBeVisible({
|
||||
timeout: NAVIGATION_TIMEOUT_MS,
|
||||
});
|
||||
await captureEvidence(page, "03-real-connection-test-result");
|
||||
});
|
||||
});
|
||||
78
tests/unit/breadcrumbs-i18n-fallback.test.tsx
Normal file
78
tests/unit/breadcrumbs-i18n-fallback.test.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import Breadcrumbs from "../../src/shared/components/Breadcrumbs";
|
||||
import ptBrMessages from "../../src/i18n/messages/pt-BR.json";
|
||||
|
||||
let labels: Record<string, string> = {};
|
||||
const translate = Object.assign(
|
||||
vi.fn((key: string) => {
|
||||
if (key in labels) return labels[key];
|
||||
throw new Error(`missing translation: ${key}`);
|
||||
}),
|
||||
{ has: vi.fn((key: string) => key in labels) }
|
||||
);
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
usePathname: () => "/dashboard/radar/setup",
|
||||
}));
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => translate,
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<a href={String(href)} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("Breadcrumbs missing translation fallback", () => {
|
||||
beforeEach(() => {
|
||||
labels = { ariaLabel: "Breadcrumb", dashboard: "Dashboard" };
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
translate.mockClear();
|
||||
translate.has.mockClear();
|
||||
});
|
||||
|
||||
it("renders humanized Radar setup labels without asking next-intl for missing keys", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
await act(async () => root.render(<Breadcrumbs />));
|
||||
|
||||
expect(container.textContent).toContain("Dashboard");
|
||||
expect(container.textContent).toContain("Radar");
|
||||
expect(container.textContent).toContain("Setup");
|
||||
expect(translate).not.toHaveBeenCalledWith("radar");
|
||||
expect(translate).not.toHaveBeenCalledWith("setup");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("uses localized breadcrumb labels when Radar and setup translations exist", async () => {
|
||||
labels = ptBrMessages.breadcrumbs;
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
await act(async () => root.render(<Breadcrumbs />));
|
||||
|
||||
expect(container.textContent).toContain("Painel");
|
||||
expect(container.textContent).toContain("Radar");
|
||||
expect(container.textContent).toContain("Configuração");
|
||||
expect(translate).toHaveBeenCalledWith("radar");
|
||||
expect(translate).toHaveBeenCalledWith("setup");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
246
tests/unit/radar-guided-setup-action.test.tsx
Normal file
246
tests/unit/radar-guided-setup-action.test.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import ProviderDetailPageClient from "../../src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient";
|
||||
import RadarSetupPage from "../../src/app/(dashboard)/dashboard/radar/setup/page";
|
||||
|
||||
let providerId = "openai";
|
||||
let searchParams = new URLSearchParams("action=add-api-key");
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useParams: () => ({ id: providerId }),
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), refresh: vi.fn() }),
|
||||
usePathname: () => `/dashboard/providers/${providerId}`,
|
||||
useSearchParams: () => searchParams,
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<a href={String(href)} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
function response(body: unknown = {}) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
headers: { get: () => null },
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
async function renderComponent(element: React.ReactNode): Promise<{
|
||||
container: HTMLDivElement;
|
||||
root: Root;
|
||||
}> {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(element);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
return { container, root };
|
||||
}
|
||||
|
||||
async function renderProviderPage(): Promise<{ container: HTMLDivElement; root: Root }> {
|
||||
return renderComponent(<ProviderDetailPageClient />);
|
||||
}
|
||||
|
||||
async function settle(): Promise<void> {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function setInputValue(input: HTMLInputElement, value: string): void {
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
||||
setter?.call(input, value);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
|
||||
describe("Radar guided setup provider action", () => {
|
||||
const fetchMock = vi.fn(() => Promise.resolve(response()));
|
||||
|
||||
beforeEach(() => {
|
||||
providerId = "openai";
|
||||
searchParams = new URLSearchParams("action=add-api-key");
|
||||
fetchMock.mockClear();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: () => null,
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
});
|
||||
vi.stubGlobal(
|
||||
"matchMedia",
|
||||
vi.fn(() => ({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
}))
|
||||
);
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
vi.unstubAllGlobals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("opens the existing API key modal for a normal provider", async () => {
|
||||
const { container, root } = await renderProviderPage();
|
||||
expect(container.querySelector('input[type="password"]')).not.toBeNull();
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("keeps a subscription-risk provider behind the existing acknowledgement gate", async () => {
|
||||
providerId = "chatgpt-web";
|
||||
const { container, root } = await renderProviderPage();
|
||||
|
||||
expect(container.querySelector('input[type="password"]')).toBeNull();
|
||||
expect(container.textContent).toContain("I understand, continue");
|
||||
|
||||
const confirm = [...container.querySelectorAll("button")].find((button) =>
|
||||
button.textContent?.includes("I understand, continue")
|
||||
);
|
||||
expect(confirm).toBeDefined();
|
||||
await act(async () => confirm?.click());
|
||||
expect(container.querySelector('input[type="password"]')).not.toBeNull();
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("completes get key, paste, persist, reload, and connection test with a concrete id", async () => {
|
||||
providerId = "groq";
|
||||
searchParams = new URLSearchParams("provider=groq");
|
||||
let connectionExists = false;
|
||||
let savedBody: Record<string, unknown> | null = null;
|
||||
let testedConnectionId: string | null = null;
|
||||
|
||||
fetchMock.mockImplementation(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
if (url === "/api/radar/catalog") {
|
||||
return response({
|
||||
entries: [
|
||||
{
|
||||
provider: "groq",
|
||||
setup: {
|
||||
keyUrl: "https://console.groq.com/keys",
|
||||
steps: [{ en: "Create a project-specific Groq key.", pt: "Crie a chave Groq." }],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url.startsWith("/api/providers?") || url === "/api/providers") {
|
||||
if (init?.method === "POST") {
|
||||
savedBody = JSON.parse(String(init.body)) as Record<string, unknown>;
|
||||
connectionExists = true;
|
||||
return response({ connection: { id: "conn-groq", provider: "groq", isActive: true } });
|
||||
}
|
||||
return response({
|
||||
connections: connectionExists
|
||||
? [{ id: "conn-groq", provider: "groq", isActive: true }]
|
||||
: [],
|
||||
});
|
||||
}
|
||||
if (url === "/api/providers/validate") return response({ valid: true });
|
||||
if (url === "/api/providers/conn-groq/sync-models") {
|
||||
return response({ syncedModels: 0, models: [] });
|
||||
}
|
||||
if (url === "/api/providers/conn-groq/test") {
|
||||
testedConnectionId = "conn-groq";
|
||||
return response({ valid: true });
|
||||
}
|
||||
return response();
|
||||
});
|
||||
|
||||
const firstTour = await renderComponent(<RadarSetupPage />);
|
||||
await settle();
|
||||
expect(firstTour.container.textContent).toContain("https://console.groq.com/keys");
|
||||
expect(
|
||||
firstTour.container.querySelector('a[href="/dashboard/providers/groq?action=add-api-key"]')
|
||||
).not.toBeNull();
|
||||
act(() => firstTour.root.unmount());
|
||||
|
||||
searchParams = new URLSearchParams("action=add-api-key");
|
||||
const providerPage = await renderProviderPage();
|
||||
const credential = providerPage.container.querySelector(
|
||||
'input[type="password"]'
|
||||
) as HTMLInputElement | null;
|
||||
expect(credential).not.toBeNull();
|
||||
await act(async () => setInputValue(credential as HTMLInputElement, "test-key-not-real"));
|
||||
const save = [...providerPage.container.querySelectorAll("button")].find(
|
||||
(button) => button.textContent?.trim() === "Save"
|
||||
);
|
||||
expect(save).toBeDefined();
|
||||
await act(async () => save?.click());
|
||||
await settle();
|
||||
expect(savedBody).toMatchObject({ provider: "groq", apiKey: "test-key-not-real" });
|
||||
expect(connectionExists).toBe(true);
|
||||
act(() => providerPage.root.unmount());
|
||||
|
||||
searchParams = new URLSearchParams("provider=groq");
|
||||
const reloadedTour = await renderComponent(<RadarSetupPage />);
|
||||
await settle();
|
||||
const testButton = [...reloadedTour.container.querySelectorAll("button")].find((button) =>
|
||||
button.textContent?.toLowerCase().includes("test")
|
||||
);
|
||||
expect(testButton).toBeDefined();
|
||||
expect(testButton?.disabled).toBe(false);
|
||||
await act(async () => testButton?.click());
|
||||
await settle();
|
||||
expect(testedConnectionId).toBe("conn-groq");
|
||||
expect(reloadedTour.container.textContent).toContain("Connection successful!");
|
||||
act(() => reloadedTour.root.unmount());
|
||||
});
|
||||
|
||||
it("reports a 200 connection-test response with valid false as a failure", async () => {
|
||||
providerId = "groq";
|
||||
searchParams = new URLSearchParams("provider=groq");
|
||||
|
||||
fetchMock.mockImplementation(async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
if (url === "/api/radar/catalog") {
|
||||
return response({
|
||||
entries: [{ provider: "groq", setup: { keyUrl: null, steps: [] } }],
|
||||
});
|
||||
}
|
||||
if (url.startsWith("/api/providers?")) {
|
||||
return response({
|
||||
connections: [{ id: "conn-groq", provider: "groq", isActive: true }],
|
||||
});
|
||||
}
|
||||
if (url === "/api/providers/conn-groq/test") {
|
||||
return response({ valid: false, error: "Invalid API key" });
|
||||
}
|
||||
return response();
|
||||
});
|
||||
|
||||
const tour = await renderComponent(<RadarSetupPage />);
|
||||
await settle();
|
||||
const testButton = [...tour.container.querySelectorAll("button")].find((button) =>
|
||||
button.textContent?.toLowerCase().includes("test")
|
||||
);
|
||||
expect(testButton).toBeDefined();
|
||||
await act(async () => testButton?.click());
|
||||
await settle();
|
||||
|
||||
expect(tour.container.textContent).toContain("Connection test failed");
|
||||
expect(tour.container.textContent).not.toContain("Connection successful!");
|
||||
act(() => tour.root.unmount());
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { test } from "node:test";
|
||||
|
||||
import {
|
||||
firstProviderConnectionId,
|
||||
providerSetupConnectionUrl,
|
||||
providerConnectionsRequestUrl,
|
||||
} from "../../src/lib/radar/setupConnections.ts";
|
||||
|
||||
@@ -26,3 +27,10 @@ test("firstProviderConnectionId selects a real connection id, never the provider
|
||||
);
|
||||
assert.equal(firstProviderConnectionId([], "groq"), null);
|
||||
});
|
||||
|
||||
test("providerSetupConnectionUrl targets the real provider form with an explicit action", () => {
|
||||
assert.equal(
|
||||
providerSetupConnectionUrl("openrouter/custom"),
|
||||
"/dashboard/providers/openrouter%2Fcustom?action=add-api-key"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user