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:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user