206 lines
5.7 KiB
TypeScript
206 lines
5.7 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
|
|
|
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
children: ReactNode;
|
|
}
|
|
|
|
interface MockBadgeProps extends HTMLAttributes<HTMLElement> {
|
|
children: ReactNode;
|
|
}
|
|
|
|
interface AuditLogEntry {
|
|
id: string;
|
|
userId: string;
|
|
sessionId: string;
|
|
provider: string;
|
|
action: string;
|
|
content: string | null;
|
|
metadata: unknown;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface AuditLogResponse {
|
|
items: AuditLogEntry[];
|
|
total: number;
|
|
page: number;
|
|
pages: number;
|
|
}
|
|
|
|
const mockApiGet = vi.fn<(endpoint: string) => Promise<AuditLogResponse>>();
|
|
|
|
vi.mock("@/lib/api/client", () => ({
|
|
apiGet: (endpoint: string): Promise<AuditLogResponse> => mockApiGet(endpoint),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/button", () => ({
|
|
Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
|
|
<button {...props}>{children}</button>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/badge", () => ({
|
|
Badge: ({ children, ...props }: MockBadgeProps): React.JSX.Element => (
|
|
<span {...props}>{children}</span>
|
|
),
|
|
}));
|
|
|
|
import { AuditLogDrawer } from "./AuditLogDrawer";
|
|
|
|
function renderWithQueryClient(ui: React.JSX.Element): ReturnType<typeof render> {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
|
|
}
|
|
|
|
function responseWith(items: AuditLogEntry[], page: number, pages: number): AuditLogResponse {
|
|
return {
|
|
items,
|
|
total: items.length,
|
|
page,
|
|
pages,
|
|
};
|
|
}
|
|
|
|
describe("AuditLogDrawer", (): void => {
|
|
beforeEach((): void => {
|
|
vi.clearAllMocks();
|
|
mockApiGet.mockResolvedValue(responseWith([], 1, 0));
|
|
});
|
|
|
|
it("opens from trigger text and renders empty state", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
|
|
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Audit" }));
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByText("Audit Log")).toBeInTheDocument();
|
|
expect(screen.getByText("No audit entries found.")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("renders audit entries with action, session id, and payload", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
|
|
mockApiGet.mockResolvedValue(
|
|
responseWith(
|
|
[
|
|
{
|
|
id: "entry-1",
|
|
userId: "operator-1",
|
|
sessionId: "1234567890abcdef",
|
|
provider: "internal",
|
|
action: "inject",
|
|
content: "Run diagnostics",
|
|
metadata: { payload: { ignored: true } },
|
|
createdAt: "2026-03-07T19:00:00.000Z",
|
|
},
|
|
],
|
|
1,
|
|
1
|
|
)
|
|
);
|
|
|
|
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Audit" }));
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByText("inject")).toBeInTheDocument();
|
|
expect(screen.getByText("12345678")).toBeInTheDocument();
|
|
expect(screen.getByText("Run diagnostics")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("supports pagination and metadata payload summary", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
|
|
mockApiGet.mockImplementation((endpoint: string): Promise<AuditLogResponse> => {
|
|
const query = endpoint.split("?")[1] ?? "";
|
|
const params = new URLSearchParams(query);
|
|
const page = Number(params.get("page") ?? "1");
|
|
|
|
if (page === 1) {
|
|
return Promise.resolve({
|
|
items: [
|
|
{
|
|
id: "entry-page-1",
|
|
userId: "operator-2",
|
|
sessionId: "abcdefgh12345678",
|
|
provider: "internal",
|
|
action: "pause",
|
|
content: "",
|
|
metadata: { payload: { reason: "hold" } },
|
|
createdAt: "2026-03-07T19:01:00.000Z",
|
|
},
|
|
],
|
|
total: 2,
|
|
page: 1,
|
|
pages: 2,
|
|
});
|
|
}
|
|
|
|
return Promise.resolve({
|
|
items: [
|
|
{
|
|
id: "entry-page-2",
|
|
userId: "operator-3",
|
|
sessionId: "zzzz111122223333",
|
|
provider: "internal",
|
|
action: "kill",
|
|
content: null,
|
|
metadata: { payload: { force: true } },
|
|
createdAt: "2026-03-07T19:02:00.000Z",
|
|
},
|
|
],
|
|
total: 2,
|
|
page: 2,
|
|
pages: 2,
|
|
});
|
|
});
|
|
|
|
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Audit" }));
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByText("Page 1 of 2")).toBeInTheDocument();
|
|
expect(screen.getByText("reason=hold")).toBeInTheDocument();
|
|
});
|
|
|
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByText("Page 2 of 2")).toBeInTheDocument();
|
|
expect(screen.getByText("force=true")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("includes sessionId filter in query string", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
|
|
renderWithQueryClient(<AuditLogDrawer trigger="Audit" sessionId="session 7" />);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Audit" }));
|
|
|
|
await waitFor((): void => {
|
|
expect(mockApiGet).toHaveBeenCalled();
|
|
});
|
|
|
|
const firstCall = mockApiGet.mock.calls[0];
|
|
const endpoint = firstCall?.[0] ?? "";
|
|
|
|
expect(endpoint).toContain("sessionId=session+7");
|
|
});
|
|
});
|