import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
const { getCredentialsStatus, setCredentials, clearCredentials } = vi.hoisted(
() => ({
getCredentialsStatus: vi.fn(async () => ({ has_credentials: false })),
setCredentials: vi.fn(async () => ({ has_credentials: true })),
clearCredentials: vi.fn(async () => ({ has_credentials: false })),
}),
);
vi.mock("@/lib/api", () => ({
githubAppApi: { getCredentialsStatus, setCredentials, clearCredentials },
}));
import { GitHubAppCredentialsCard } from "../github-app-credentials-card";
function withQueryClient(ui: ReactNode) {
const client = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return {ui};
}
describe("GitHubAppCredentialsCard", () => {
beforeEach(() => {
getCredentialsStatus.mockClear();
setCredentials.mockClear();
clearCredentials.mockClear();
});
afterEach(() => {
vi.clearAllMocks();
});
it("shows 'no credentials configured' by default and no Clear button", async () => {
render(withQueryClient());
expect(
await screen.findByText("No credentials configured"),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Clear" }),
).not.toBeInTheDocument();
});
it("disables Save until both App id and private key are filled", async () => {
render(withQueryClient());
await screen.findByText("No credentials configured");
const saveButton = screen.getByRole("button", { name: "Save" });
expect(saveButton).toBeDisabled();
fireEvent.change(screen.getByLabelText("App id"), {
target: { value: "123456" },
});
expect(saveButton).toBeDisabled(); // private key still unfilled
fireEvent.change(screen.getByLabelText("Private key (PEM)"), {
target: { value: "-----BEGIN KEY-----" },
});
expect(saveButton).not.toBeDisabled();
});
it("saves both fields and clears the inputs on success", async () => {
render(withQueryClient());
await screen.findByText("No credentials configured");
fireEvent.change(screen.getByLabelText("App id"), {
target: { value: "123456" },
});
fireEvent.change(screen.getByLabelText("Private key (PEM)"), {
target: { value: "-----BEGIN KEY-----" },
});
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() =>
expect(setCredentials).toHaveBeenCalledWith({
app_id: "123456",
private_key: "-----BEGIN KEY-----",
}),
);
await waitFor(() =>
expect((screen.getByLabelText("App id") as HTMLInputElement).value).toBe(
"",
),
);
});
it("shows a Clear button once credentials are set, gated behind a confirm dialog", async () => {
getCredentialsStatus.mockResolvedValueOnce({ has_credentials: true });
render(withQueryClient());
await screen.findByText("Credentials are set");
const clearButton = screen.getByRole("button", { name: "Clear" });
fireEvent.click(clearButton);
const dialog = await screen.findByRole("alertdialog");
expect(dialog).toBeInTheDocument();
expect(clearCredentials).not.toHaveBeenCalled();
fireEvent.click(
screen.getAllByRole("button", { name: "Clear" }).slice(-1)[0],
);
await waitFor(() => expect(clearCredentials).toHaveBeenCalled());
});
it("cancelling the clear confirm dialog does NOT call clearCredentials", async () => {
getCredentialsStatus.mockResolvedValueOnce({ has_credentials: true });
render(withQueryClient());
await screen.findByText("Credentials are set");
fireEvent.click(screen.getByRole("button", { name: "Clear" }));
const dialog = await screen.findByRole("alertdialog");
expect(dialog).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
await waitFor(() =>
expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument(),
);
expect(clearCredentials).not.toHaveBeenCalled();
});
});