Initial commit: CI Agent competitive-intelligence monitoring app

FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
This commit is contained in:
2026-08-05 10:48:20 -04:00
commit 1a4c80958f
365 changed files with 43541 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import AddCompanyWizardPage from "@/app/(app)/companies/new/page";
import { renderWithQueryClient } from "./test-utils";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
useSearchParams: () => new URLSearchParams(),
}));
const DISCOVERED_PROFILE = {
name: "Acme Widgets",
official_website: "https://acmewidgets.com",
description: "Acme Widgets designs and sells handcrafted mechanical widgets.",
monitoring_focus: null,
industry: null,
country: null,
region: null,
headquarters: "Austin, Texas",
aliases: ["Acme Industries"],
competitors: [],
public_identifiers: {},
potential_sources: [
{ source_type: "website", name: "Acme Widgets - Website", base_url: "https://acmewidgets.com" },
],
sources_consulted: ["https://acmewidgets.com"],
};
const EXISTING_COMPANIES = [
{
id: "existing-1",
name: "Acme Widgets",
slug: "acme-widgets",
official_website: null,
description: null,
monitoring_focus: null,
industry: null,
country: null,
region: null,
headquarters: null,
public_identifiers: {},
status: "active",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
aliases: [],
competitors: [],
monitor_configuration: null,
report_count: 0,
unresolved_alert_count: 0,
},
];
function mockFetchImplementation(existingCompanies: unknown[] = []) {
return vi.fn().mockImplementation((url: string, init?: RequestInit) => {
const path = url.replace("http://localhost:8000", "");
if (path === "/api/v1/companies/discover" && init?.method === "POST") {
return Promise.resolve({ ok: true, status: 200, json: async () => DISCOVERED_PROFILE });
}
if (path === "/api/v1/companies" && (!init?.method || init.method === "GET")) {
return Promise.resolve({ ok: true, status: 200, json: async () => existingCompanies });
}
return Promise.resolve({ ok: false, status: 404, json: async () => ({ detail: "not found" }) });
});
}
beforeEach(() => {
vi.stubGlobal("fetch", mockFetchImplementation());
});
describe("AddCompanyWizardPage", () => {
it("only requires a company name on the Discover step", async () => {
const user = userEvent.setup();
renderWithQueryClient(<AddCompanyWizardPage />);
await user.click(screen.getByRole("button", { name: /discover company/i }));
expect(await screen.findByText(/company name is required/i)).toBeInTheDocument();
});
it("discovers a profile and pre-fills the Review step, editable", async () => {
const user = userEvent.setup();
renderWithQueryClient(<AddCompanyWizardPage />);
await user.type(screen.getByLabelText("Company name"), "Acme Widgets");
await user.click(screen.getByRole("button", { name: /discover company/i }));
await waitFor(() => {
expect(screen.getByText(/here's what we found for/i)).toBeInTheDocument();
});
const website = screen.getByLabelText("Official website") as HTMLInputElement;
expect(website.value).toBe("https://acmewidgets.com");
const headquarters = screen.getByLabelText("Headquarters") as HTMLInputElement;
expect(headquarters.value).toBe("Austin, Texas");
const description = screen.getByLabelText("Description") as HTMLTextAreaElement;
expect(description.value).toBe(
"Acme Widgets designs and sells handcrafted mechanical widgets.",
);
const aliases = screen.getByLabelText(/known aliases/i) as HTMLInputElement;
expect(aliases.value).toBe("Acme Industries");
expect(screen.getByText(/we'll start monitoring 1 source/i)).toBeInTheDocument();
// Still editable - the whole point of the review step.
await user.clear(headquarters);
await user.type(headquarters, "Denver, Colorado");
expect(headquarters.value).toBe("Denver, Colorado");
});
it("warns about a likely duplicate before discovering, and proceeds after Continue anyway", async () => {
vi.stubGlobal("fetch", mockFetchImplementation(EXISTING_COMPANIES));
const user = userEvent.setup();
renderWithQueryClient(<AddCompanyWizardPage />);
await user.type(screen.getByLabelText("Company name"), "Acme Widgets");
await user.click(screen.getByRole("button", { name: /discover company/i }));
expect(await screen.findByText(/you might already be monitoring/i)).toBeInTheDocument();
// Discovery must not have fired yet - still on the Discover step.
expect(screen.queryByText(/here's what we found for/i)).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /continue anyway/i }));
await user.click(screen.getByRole("button", { name: /discover company/i }));
await waitFor(() => {
expect(screen.getByText(/here's what we found for/i)).toBeInTheDocument();
});
expect(screen.queryByText(/you might already be monitoring/i)).not.toBeInTheDocument();
});
});
+71
View File
@@ -0,0 +1,71 @@
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import AlertsPage from "@/app/(app)/alerts/page";
import { renderWithQueryClient } from "./test-utils";
const COMPANIES = [
{ id: "company-1", name: "Acme Mobility Systems" },
{ id: "company-2", name: "Globex Corp" },
];
const ALERTS = [
{
id: "alert-1",
company_id: "company-1",
detected_change_id: "change-1",
title: "Base plan price increased",
summary: "Pricing changed.",
why_it_matters: "Signals margin pressure.",
severity: "high",
confidence: 0.82,
read: false,
resolved: false,
created_at: new Date().toISOString(),
},
];
function mockFetchImplementation() {
return vi.fn().mockImplementation((url: string) => {
const path = url.replace("http://localhost:8000", "");
if (path.startsWith("/api/v1/companies")) {
return Promise.resolve({ ok: true, status: 200, json: async () => COMPANIES });
}
if (path.startsWith("/api/v1/alerts")) {
return Promise.resolve({ ok: true, status: 200, json: async () => ALERTS });
}
return Promise.resolve({ ok: false, status: 404, json: async () => ({ detail: "not found" }) });
});
}
beforeEach(() => {
vi.stubGlobal("fetch", mockFetchImplementation());
});
describe("AlertsPage", () => {
it("renders alerts with the resolved company name", async () => {
renderWithQueryClient(<AlertsPage />);
expect(await screen.findByText("Base plan price increased")).toBeInTheDocument();
const list = screen.getByRole("list");
expect(within(list).getByText(/Acme Mobility Systems/)).toBeInTheDocument();
expect(within(list).getByText("high")).toBeInTheDocument();
});
it("re-fetches with a severity filter when changed", async () => {
const fetchMock = mockFetchImplementation();
vi.stubGlobal("fetch", fetchMock);
const user = userEvent.setup();
renderWithQueryClient(<AlertsPage />);
await screen.findByText("Base plan price increased");
await user.click(screen.getByRole("combobox", { name: "Filter by severity" }));
await user.click(await screen.findByRole("option", { name: "Critical" }));
await waitFor(() => {
const calledUrls = fetchMock.mock.calls.map((call) => call[0] as string);
expect(calledUrls.some((u) => u.includes("severity=critical"))).toBe(true);
});
});
});
+64
View File
@@ -0,0 +1,64 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { DashboardAnalyticsSection } from "@/components/dashboard/analytics-section";
import type { DashboardAnalytics } from "@/lib/types";
const EMPTY_ANALYTICS: DashboardAnalytics = {
changes_by_type: {
new_document: 0,
removed_document: 0,
content_modified: 0,
price_change: 0,
leadership_change: 0,
filing_new: 0,
},
alerts_by_severity: { critical: 0, high: 0, medium: 0, low: 0 },
sources_by_status: {
active: 0,
disabled: 0,
rate_limited: 0,
auth_required: 0,
blocked_by_policy: 0,
failed: 0,
},
runs_by_day: [],
recent_signals: [],
};
describe("DashboardAnalyticsSection", () => {
it("shows empty states when there is no activity yet", () => {
render(<DashboardAnalyticsSection analytics={EMPTY_ANALYTICS} />);
expect(screen.getByText(/no changes detected in the last 30 days/i)).toBeInTheDocument();
expect(screen.getByText(/no alerts in the last 30 days/i)).toBeInTheDocument();
expect(screen.getByText(/no monitoring runs in the last 30 days/i)).toBeInTheDocument();
expect(screen.getByText(/no signals detected yet/i)).toBeInTheDocument();
});
it("renders a recent signal with company link and severity", () => {
const analytics: DashboardAnalytics = {
...EMPTY_ANALYTICS,
recent_signals: [
{
id: "change-1",
company_id: "company-1",
company_name: "Acme Mobility",
change_type: "leadership_change",
severity: "high",
confidence_score: 0.8,
summary: "New CEO announced",
created_at: new Date().toISOString(),
},
],
};
render(<DashboardAnalyticsSection analytics={analytics} />);
expect(screen.getByText("New CEO announced")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Acme Mobility" })).toHaveAttribute(
"href",
"/companies/company-1",
);
expect(screen.getByText("high")).toBeInTheDocument();
});
});
+96
View File
@@ -0,0 +1,96 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
beforeEach(() => {
window.localStorage.clear();
vi.unstubAllGlobals();
vi.resetModules();
});
// Fresh module import per test so the internal `refreshPromise` module-level
// state (shared across concurrent 401s) never leaks between tests.
async function loadApiClient() {
return import("@/lib/api-client");
}
describe("api client automatic token refresh", () => {
it("transparently retries once after a 401 when the refresh succeeds", async () => {
const { api, setTokens, getAccessToken } = await loadApiClient();
setTokens("expired-access", "valid-refresh");
const fetchMock = vi
.fn()
// The actual request, with the now-expired access token -> 401.
.mockResolvedValueOnce({
ok: false,
status: 401,
json: async () => ({ detail: "Invalid or expired token" }),
})
// POST /auth/refresh -> a fresh token pair.
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
access_token: "new-access",
refresh_token: "new-refresh",
token_type: "bearer",
expires_in_minutes: 15,
}),
})
// The retried original request, now with the new access token.
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ id: "1", email: "[email protected]" }),
});
vi.stubGlobal("fetch", fetchMock);
const result = await api.me();
expect(result).toEqual({ id: "1", email: "[email protected]" });
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(getAccessToken()).toBe("new-access");
const retryCall = fetchMock.mock.calls[2];
const retryHeaders = retryCall?.[1]?.headers as Headers;
expect(retryHeaders.get("Authorization")).toBe("Bearer new-access");
});
it("clears tokens and reports the original 401 when the refresh itself also fails", async () => {
const { api, setTokens, getAccessToken, getRefreshToken, ApiError } = await loadApiClient();
setTokens("expired-access", "expired-refresh");
const fetchMock = vi
.fn()
.mockResolvedValueOnce({
ok: false,
status: 401,
json: async () => ({ detail: "Invalid or expired token" }),
})
.mockResolvedValueOnce({
ok: false,
status: 401,
json: async () => ({ detail: "Invalid or expired refresh token" }),
});
vi.stubGlobal("fetch", fetchMock);
await expect(api.me()).rejects.toBeInstanceOf(ApiError);
expect(fetchMock).toHaveBeenCalledTimes(2); // original request + failed refresh, no retry
expect(getAccessToken()).toBeNull();
expect(getRefreshToken()).toBeNull();
});
it("does not attempt a refresh when there is no refresh token at all", async () => {
const { api, clearTokens } = await loadApiClient();
clearTokens();
const fetchMock = vi.fn().mockResolvedValueOnce({
ok: false,
status: 401,
json: async () => ({ detail: "Not authenticated" }),
});
vi.stubGlobal("fetch", fetchMock);
await expect(api.me()).rejects.toThrow();
expect(fetchMock).toHaveBeenCalledTimes(1); // no refresh attempt made
});
});
+65
View File
@@ -0,0 +1,65 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import LoginPage from "@/app/login/page";
import RegisterPage from "@/app/register/page";
import { renderWithQueryClient } from "./test-utils";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
useSearchParams: () => new URLSearchParams(),
}));
beforeEach(() => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: false,
status: 401,
json: async () => ({ detail: "Not authenticated" }),
}),
);
});
describe("LoginPage", () => {
it("shows validation errors for an empty submission", async () => {
const user = userEvent.setup();
renderWithQueryClient(<LoginPage />);
await user.click(await screen.findByRole("button", { name: /sign in/i }));
expect(await screen.findByText(/enter a valid email address/i)).toBeInTheDocument();
expect(await screen.findByText(/enter your password/i)).toBeInTheDocument();
});
it("links to the register page", async () => {
renderWithQueryClient(<LoginPage />);
expect(await screen.findByRole("link", { name: /create one/i })).toHaveAttribute(
"href",
"/register",
);
});
});
describe("RegisterPage", () => {
it("rejects a weak password before submitting", async () => {
const user = userEvent.setup();
renderWithQueryClient(<RegisterPage />);
await user.type(screen.getByLabelText(/full name/i), "Ada Lovelace");
await user.type(screen.getByLabelText(/^email$/i), "[email protected]");
await user.type(screen.getByLabelText(/^password$/i), "allletters");
await user.click(screen.getByRole("button", { name: /create account/i }));
await waitFor(() => {
expect(
screen.getByText(/must contain at least one letter and one digit/i),
).toBeInTheDocument();
});
});
it("links to the login page", async () => {
renderWithQueryClient(<RegisterPage />);
expect(await screen.findByRole("link", { name: /sign in/i })).toHaveAttribute("href", "/login");
});
});
+23
View File
@@ -0,0 +1,23 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { CompanyStatusBadge, SeverityBadge } from "@/components/ui/badge";
import { SEVERITY_LEVELS } from "@/lib/types";
describe("SeverityBadge", () => {
it.each(SEVERITY_LEVELS)("renders the %s severity label", (severity) => {
render(<SeverityBadge severity={severity} />);
expect(screen.getByText(severity)).toBeInTheDocument();
});
});
describe("CompanyStatusBadge", () => {
it("renders active status", () => {
render(<CompanyStatusBadge status="active" />);
expect(screen.getByText("active")).toBeInTheDocument();
});
it("renders paused status", () => {
render(<CompanyStatusBadge status="paused" />);
expect(screen.getByText("paused")).toBeInTheDocument();
});
});
+70
View File
@@ -0,0 +1,70 @@
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import LandingPage from "@/app/page";
import { renderWithQueryClient } from "./test-utils";
function mockFetchImplementation(isLocalhost: boolean) {
return vi.fn().mockImplementation((url: string) => {
const path = url.replace("http://localhost:8000", "");
if (path.startsWith("/api/v1/system/status")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
app_env: "development",
auth_mode: "local",
llm_provider: "mock",
search_provider: "mock",
sms_enabled: false,
sms_provider: "twilio",
ninjapear_configured: false,
ninjapear_credit_balance: null,
ninjapear_estimated_credits_per_company: null,
is_localhost: isLocalhost,
components: [],
}),
});
}
return Promise.resolve({ ok: false, status: 404, json: async () => ({ detail: "not found" }) });
});
}
describe("LandingPage", () => {
beforeEach(() => {
vi.stubGlobal("fetch", mockFetchImplementation(true));
});
it("renders the value proposition and primary calls to action", async () => {
renderWithQueryClient(<LandingPage />);
expect(
screen.getByRole("heading", { name: /know what your competitors are doing/i }),
).toBeInTheDocument();
expect(
await screen.findByRole("link", { name: /open dashboard \(local mode\)/i }),
).toHaveAttribute("href", "/dashboard");
expect(screen.getByRole("link", { name: /sign in/i })).toHaveAttribute("href", "/login");
expect(screen.getByRole("link", { name: /create account/i })).toHaveAttribute(
"href",
"/register",
);
});
it("discloses the public-information-only collection policy", () => {
renderWithQueryClient(<LandingPage />);
expect(
screen.getByRole("heading", { name: /only publicly available information/i }),
).toBeInTheDocument();
});
it("drops the local-mode wording when the request isn't from localhost", async () => {
vi.stubGlobal("fetch", mockFetchImplementation(false));
renderWithQueryClient(<LandingPage />);
expect(await screen.findByRole("link", { name: /^open dashboard$/i })).toHaveAttribute(
"href",
"/dashboard",
);
expect(screen.queryByText(/local mode/i)).not.toBeInTheDocument();
});
});
+112
View File
@@ -0,0 +1,112 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ForgotPasswordPage from "@/app/forgot-password/page";
import ResetPasswordPage from "@/app/reset-password/page";
import UnbanRequestPage from "@/app/unban-request/page";
import VerifyEmailPage from "@/app/verify-email/page";
import { renderWithQueryClient } from "./test-utils";
let searchParams = new URLSearchParams();
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
useSearchParams: () => searchParams,
}));
beforeEach(() => {
searchParams = new URLSearchParams();
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: false,
status: 401,
json: async () => ({ detail: "Not authenticated" }),
}),
);
});
describe("VerifyEmailPage", () => {
it("shows a validation error for an empty code", async () => {
const user = userEvent.setup();
renderWithQueryClient(<VerifyEmailPage />);
await user.click(await screen.findByRole("button", { name: /verify email/i }));
expect(await screen.findByText(/^enter the 6-digit code$/i)).toBeInTheDocument();
});
it("shows the email address pulled from the query string", async () => {
searchParams = new URLSearchParams({ email: "[email protected]" });
renderWithQueryClient(<VerifyEmailPage />);
expect(await screen.findByText(/ada@example\.com/i)).toBeInTheDocument();
});
});
describe("ForgotPasswordPage", () => {
it("rejects an invalid email", async () => {
const user = userEvent.setup();
renderWithQueryClient(<ForgotPasswordPage />);
await user.type(screen.getByLabelText(/email/i), "not-an-email");
await user.click(screen.getByRole("button", { name: /send reset code/i }));
await waitFor(() => {
expect(screen.getByText(/enter a valid email address/i)).toBeInTheDocument();
});
});
it("links back to sign in", async () => {
renderWithQueryClient(<ForgotPasswordPage />);
expect(await screen.findByRole("link", { name: /back to sign in/i })).toHaveAttribute(
"href",
"/login",
);
});
});
describe("ResetPasswordPage", () => {
it("prefills email from the query string", async () => {
searchParams = new URLSearchParams({ email: "[email protected]" });
renderWithQueryClient(<ResetPasswordPage />);
expect(await screen.findByDisplayValue("[email protected]")).toBeInTheDocument();
});
it("rejects a weak new password", async () => {
const user = userEvent.setup();
renderWithQueryClient(<ResetPasswordPage />);
await user.type(screen.getByLabelText(/^email$/i), "[email protected]");
await user.type(screen.getByLabelText(/reset code/i), "123456");
await user.type(screen.getByLabelText(/new password/i), "allletters");
await user.click(screen.getByRole("button", { name: /reset password/i }));
await waitFor(() => {
expect(
screen.getByText(/must contain at least one letter and one digit/i),
).toBeInTheDocument();
});
});
});
describe("UnbanRequestPage", () => {
it("submits and shows a confirmation", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
status: 204,
json: async () => ({}),
}),
);
const user = userEvent.setup();
renderWithQueryClient(<UnbanRequestPage />);
await user.type(screen.getByLabelText(/message/i), "This looks like a mistake.");
await user.click(screen.getByRole("button", { name: /send request/i }));
expect(await screen.findByText(/request received/i)).toBeInTheDocument();
});
it("links back to sign in", async () => {
renderWithQueryClient(<UnbanRequestPage />);
expect(await screen.findByRole("link", { name: /back to sign in/i })).toHaveAttribute(
"href",
"/login",
);
});
});
+17
View File
@@ -0,0 +1,17 @@
import "@testing-library/jest-dom/vitest";
// jsdom doesn't implement these - Radix UI's Select (and other primitives
// built on pointer events) call them unconditionally, so tests that open
// one throw without these no-op polyfills.
if (!Element.prototype.hasPointerCapture) {
Element.prototype.hasPointerCapture = () => false;
}
if (!Element.prototype.setPointerCapture) {
Element.prototype.setPointerCapture = () => {};
}
if (!Element.prototype.releasePointerCapture) {
Element.prototype.releasePointerCapture = () => {};
}
if (!Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = () => {};
}
+87
View File
@@ -0,0 +1,87 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemSecretRow } from "@/components/ui/system-secret-row";
import type { SystemSecretStatus } from "@/lib/types";
import { renderWithQueryClient } from "./test-utils";
const unconfiguredSecret: SystemSecretStatus = {
key: "turnstile_secret",
label: "Turnstile Secret Key",
configured: false,
value: null,
};
const configuredSecret: SystemSecretStatus = {
key: "turnstile_site_key",
label: "Turnstile Site Key",
configured: true,
value: "0x-my-site-key",
};
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
});
describe("SystemSecretRow", () => {
it("shows the label and a masked input for a configured secret", () => {
renderWithQueryClient(<SystemSecretRow secret={configuredSecret} />);
expect(screen.getByText("Turnstile Site Key")).toBeInTheDocument();
const textbox = screen.getByDisplayValue("0x-my-site-key") as HTMLInputElement;
expect(textbox.type).toBe("password");
});
it("toggles the input between masked and revealed", async () => {
const user = userEvent.setup();
renderWithQueryClient(<SystemSecretRow secret={configuredSecret} />);
const textbox = screen.getByDisplayValue("0x-my-site-key") as HTMLInputElement;
expect(textbox.type).toBe("password");
await user.click(screen.getByTitle(/show key/i));
expect(textbox.type).toBe("text");
await user.click(screen.getByTitle(/hide key/i));
expect(textbox.type).toBe("password");
});
it("disables Update until the value actually changes", async () => {
const user = userEvent.setup();
renderWithQueryClient(<SystemSecretRow secret={unconfiguredSecret} />);
const updateButton = screen.getByRole("button", { name: /update/i });
expect(updateButton).toBeDisabled();
const textbox = screen.getByPlaceholderText(/not set/i);
await user.type(textbox, "sk-new-secret");
expect(updateButton).toBeEnabled();
});
it("submits the new value via PUT to the keyed endpoint and reflects the saved value", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ ...unconfiguredSecret, configured: true, value: "sk-new-secret" }),
});
vi.stubGlobal("fetch", fetchMock);
const user = userEvent.setup();
renderWithQueryClient(<SystemSecretRow secret={unconfiguredSecret} />);
const textbox = screen.getByPlaceholderText(/not set/i);
await user.type(textbox, "sk-new-secret");
await user.click(screen.getByRole("button", { name: /update/i }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/system/secrets/turnstile_secret"),
expect.objectContaining({
method: "PUT",
body: JSON.stringify({ value: "sk-new-secret" }),
}),
);
});
await waitFor(() => {
expect(screen.getByDisplayValue("sk-new-secret")).toBeInTheDocument();
});
});
});
+10
View File
@@ -0,0 +1,10 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render } from "@testing-library/react";
import type { ReactElement } from "react";
export function renderWithQueryClient(ui: ReactElement) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
}
+55
View File
@@ -0,0 +1,55 @@
import { act, render } from "@testing-library/react";
import { createRef } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { TurnstileWidget, type TurnstileWidgetHandle } from "@/components/ui/turnstile-widget";
afterEach(() => {
delete (window as unknown as { turnstile?: unknown }).turnstile;
});
describe("TurnstileWidget", () => {
it("renders nothing when no site key is configured", () => {
const { container } = render(<TurnstileWidget siteKey={null} onToken={() => {}} />);
expect(container).toBeEmptyDOMElement();
});
it("renders via window.turnstile once the script loads, reports tokens, and resets", () => {
let capturedCallback: ((token: string) => void) | undefined;
const renderMock = vi.fn(
(_container: HTMLElement, options: { callback: (t: string) => void }) => {
capturedCallback = options.callback;
return "widget-1";
},
);
const resetMock = vi.fn();
const removeMock = vi.fn();
window.turnstile = { render: renderMock, reset: resetMock, remove: removeMock };
const onToken = vi.fn();
const ref = createRef<TurnstileWidgetHandle>();
render(<TurnstileWidget ref={ref} siteKey="test-site-key" onToken={onToken} />);
// jsdom never actually executes the remote Cloudflare script - simulate
// next/script's load detection by firing "load" on the tag it inserted.
const scriptEl = document.querySelector('script[src*="turnstile"]');
expect(scriptEl).not.toBeNull();
act(() => {
scriptEl?.dispatchEvent(new Event("load"));
});
expect(renderMock).toHaveBeenCalledTimes(1);
expect(renderMock.mock.calls[0]?.[1]).toMatchObject({
sitekey: "test-site-key",
action: "turnstile-spin-v2",
});
capturedCallback?.("fake-token");
expect(onToken).toHaveBeenCalledWith("fake-token");
act(() => {
ref.current?.reset();
});
expect(resetMock).toHaveBeenCalledWith("widget-1");
});
});
+137
View File
@@ -0,0 +1,137 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { UserApiKeyRow } from "@/components/ui/user-api-key-row";
import type { UserApiKeyStatus } from "@/lib/types";
import { renderWithQueryClient } from "./test-utils";
const anthropicKey: UserApiKeyStatus = {
provider: "anthropic",
label: "Anthropic",
configured: false,
value: null,
credits: null,
credits_note: "Anthropic doesn't expose a credit/usage-balance API.",
free: false,
requires_government_id: false,
};
const usptoKey: UserApiKeyStatus = {
provider: "uspto",
label: "USPTO",
configured: true,
value: "my-uspto-key",
credits: null,
credits_note: "Free - USPTO Open Data Portal has no usage limit or credit cost.",
free: true,
requires_government_id: true,
};
const ninjapearKey: UserApiKeyStatus = {
provider: "ninjapear",
label: "NinjaPear",
configured: true,
value: "my-ninjapear-key",
credits: null,
credits_note: "Credit balance shown in System configuration below.",
free: false,
requires_government_id: false,
};
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
});
describe("UserApiKeyRow", () => {
it("shows the provider label and a masked input for a configured key", () => {
renderWithQueryClient(<UserApiKeyRow apiKey={usptoKey} />);
expect(screen.getByText("USPTO")).toBeInTheDocument();
const textbox = screen.getByDisplayValue("my-uspto-key") as HTMLInputElement;
expect(textbox.type).toBe("password");
});
it("shows the free + government-ID badge for USPTO", () => {
renderWithQueryClient(<UserApiKeyRow apiKey={usptoKey} />);
expect(screen.getByText(/free · requires government id approval/i)).toBeInTheDocument();
});
it("does not show the free badge for a paid provider", () => {
renderWithQueryClient(<UserApiKeyRow apiKey={anthropicKey} />);
expect(screen.queryByText(/requires government id approval/i)).not.toBeInTheDocument();
});
it("toggles the input between masked and revealed", async () => {
const user = userEvent.setup();
renderWithQueryClient(<UserApiKeyRow apiKey={usptoKey} />);
const textbox = screen.getByDisplayValue("my-uspto-key") as HTMLInputElement;
expect(textbox.type).toBe("password");
await user.click(screen.getByTitle(/show key/i));
expect(textbox.type).toBe("text");
await user.click(screen.getByTitle(/hide key/i));
expect(textbox.type).toBe("password");
});
it("disables Update until the value actually changes", async () => {
const user = userEvent.setup();
renderWithQueryClient(<UserApiKeyRow apiKey={anthropicKey} />);
const updateButton = screen.getByRole("button", { name: /update/i });
expect(updateButton).toBeDisabled();
const textbox = screen.getByPlaceholderText(/^not set$/i);
await user.type(textbox, "sk-new-key");
expect(updateButton).toBeEnabled();
});
it("submits the new key via PUT and reflects the saved value", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ ...anthropicKey, configured: true, value: "sk-new-key" }),
});
vi.stubGlobal("fetch", fetchMock);
const user = userEvent.setup();
renderWithQueryClient(<UserApiKeyRow apiKey={anthropicKey} />);
const textbox = screen.getByPlaceholderText(/^not set$/i);
await user.type(textbox, "sk-new-key");
await user.click(screen.getByRole("button", { name: /update/i }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/user-api-keys/anthropic"),
expect.objectContaining({
method: "PUT",
body: JSON.stringify({ key: "sk-new-key" }),
}),
);
});
await waitFor(() => {
expect(screen.getByDisplayValue("sk-new-key")).toBeInTheDocument();
});
});
it("ignores the backend's own credits field and shows nothing extra when liveCredits is not passed", () => {
renderWithQueryClient(<UserApiKeyRow apiKey={ninjapearKey} />);
expect(
screen.getByText(/credit balance shown in system configuration below/i),
).toBeInTheDocument();
expect(screen.queryByText(/credits remaining/i)).not.toBeInTheDocument();
});
it("shows the liveCredits number when passed, sourced from system status rather than this row's own fetch", () => {
renderWithQueryClient(<UserApiKeyRow apiKey={ninjapearKey} liveCredits={891} />);
expect(screen.getByText(/891 credits remaining/i)).toBeInTheDocument();
});
it("falls back to the note-only text when liveCredits is explicitly null", () => {
renderWithQueryClient(<UserApiKeyRow apiKey={ninjapearKey} liveCredits={null} />);
expect(screen.queryByText(/credits remaining/i)).not.toBeInTheDocument();
expect(
screen.getByText(/credit balance shown in system configuration below/i),
).toBeInTheDocument();
});
});