261 lines
7 KiB
TypeScript
261 lines
7 KiB
TypeScript
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
|
|
import { expect, test, vi, beforeEach, afterEach, describe } from "vitest";
|
|
import Home from "./page";
|
|
|
|
// Mock next/navigation
|
|
const mockPush = vi.fn();
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({
|
|
push: mockPush,
|
|
}),
|
|
}));
|
|
|
|
// Default mock values
|
|
let mockUser: { id: number; email: string; roles: string[]; permissions: string[] } | null = {
|
|
id: 1,
|
|
email: "test@example.com",
|
|
roles: ["regular"],
|
|
permissions: ["view_counter", "increment_counter", "use_sum"],
|
|
};
|
|
let mockIsLoading = false;
|
|
const mockLogout = vi.fn();
|
|
const mockHasPermission = vi.fn((permission: string) =>
|
|
mockUser?.permissions.includes(permission) ?? false
|
|
);
|
|
const mockHasRole = vi.fn((role: string) =>
|
|
mockUser?.roles.includes(role) ?? false
|
|
);
|
|
|
|
vi.mock("./auth-context", () => ({
|
|
useAuth: () => ({
|
|
user: mockUser,
|
|
isLoading: mockIsLoading,
|
|
logout: mockLogout,
|
|
hasPermission: mockHasPermission,
|
|
hasRole: mockHasRole,
|
|
}),
|
|
Permission: {
|
|
VIEW_COUNTER: "view_counter",
|
|
INCREMENT_COUNTER: "increment_counter",
|
|
USE_SUM: "use_sum",
|
|
VIEW_AUDIT: "view_audit",
|
|
},
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Reset to authenticated state
|
|
mockUser = {
|
|
id: 1,
|
|
email: "test@example.com",
|
|
roles: ["regular"],
|
|
permissions: ["view_counter", "increment_counter", "use_sum"],
|
|
};
|
|
mockIsLoading = false;
|
|
mockHasPermission.mockImplementation((permission: string) =>
|
|
mockUser?.permissions.includes(permission) ?? false
|
|
);
|
|
mockHasRole.mockImplementation((role: string) =>
|
|
mockUser?.roles.includes(role) ?? false
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
describe("Home - Authenticated", () => {
|
|
test("renders loading state when isLoading is true", () => {
|
|
mockIsLoading = true;
|
|
vi.spyOn(global, "fetch").mockImplementation(() => new Promise(() => {}));
|
|
|
|
render(<Home />);
|
|
expect(screen.getByText("Loading...")).toBeDefined();
|
|
});
|
|
|
|
test("renders user email in header", async () => {
|
|
vi.spyOn(global, "fetch").mockResolvedValue({
|
|
json: () => Promise.resolve({ value: 42 }),
|
|
} as Response);
|
|
|
|
render(<Home />);
|
|
expect(screen.getByText("test@example.com")).toBeDefined();
|
|
});
|
|
|
|
test("renders sign out button", async () => {
|
|
vi.spyOn(global, "fetch").mockResolvedValue({
|
|
json: () => Promise.resolve({ value: 42 }),
|
|
} as Response);
|
|
|
|
render(<Home />);
|
|
expect(screen.getByText("Sign out")).toBeDefined();
|
|
});
|
|
|
|
test("clicking sign out calls logout and redirects", async () => {
|
|
vi.spyOn(global, "fetch").mockResolvedValue({
|
|
json: () => Promise.resolve({ value: 42 }),
|
|
} as Response);
|
|
|
|
render(<Home />);
|
|
fireEvent.click(screen.getByText("Sign out"));
|
|
|
|
await waitFor(() => {
|
|
expect(mockLogout).toHaveBeenCalled();
|
|
expect(mockPush).toHaveBeenCalledWith("/login");
|
|
});
|
|
});
|
|
|
|
test("renders counter value after fetch", async () => {
|
|
vi.spyOn(global, "fetch").mockResolvedValue({
|
|
json: () => Promise.resolve({ value: 42 }),
|
|
} as Response);
|
|
|
|
render(<Home />);
|
|
await waitFor(() => {
|
|
expect(screen.getByText("42")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
test("fetches counter with credentials", async () => {
|
|
const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue({
|
|
json: () => Promise.resolve({ value: 0 }),
|
|
} as Response);
|
|
|
|
render(<Home />);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchSpy).toHaveBeenCalledWith(
|
|
"http://localhost:8000/api/counter",
|
|
expect.objectContaining({
|
|
credentials: "include",
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
test("renders increment button", async () => {
|
|
vi.spyOn(global, "fetch").mockResolvedValue({
|
|
json: () => Promise.resolve({ value: 0 }),
|
|
} as Response);
|
|
|
|
render(<Home />);
|
|
expect(screen.getByText("Increment")).toBeDefined();
|
|
});
|
|
|
|
test("clicking increment button calls API with credentials", async () => {
|
|
const fetchSpy = vi
|
|
.spyOn(global, "fetch")
|
|
.mockResolvedValueOnce({ json: () => Promise.resolve({ value: 0 }) } as Response)
|
|
.mockResolvedValueOnce({ json: () => Promise.resolve({ value: 1 }) } as Response);
|
|
|
|
render(<Home />);
|
|
await waitFor(() => expect(screen.getByText("0")).toBeDefined());
|
|
|
|
fireEvent.click(screen.getByText("Increment"));
|
|
|
|
await waitFor(() => {
|
|
expect(fetchSpy).toHaveBeenCalledWith(
|
|
"http://localhost:8000/api/counter/increment",
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
credentials: "include",
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
test("clicking increment updates displayed count", async () => {
|
|
vi.spyOn(global, "fetch")
|
|
.mockResolvedValueOnce({ json: () => Promise.resolve({ value: 0 }) } as Response)
|
|
.mockResolvedValueOnce({ json: () => Promise.resolve({ value: 1 }) } as Response);
|
|
|
|
render(<Home />);
|
|
await waitFor(() => expect(screen.getByText("0")).toBeDefined());
|
|
|
|
fireEvent.click(screen.getByText("Increment"));
|
|
await waitFor(() => expect(screen.getByText("1")).toBeDefined());
|
|
});
|
|
});
|
|
|
|
describe("Home - Unauthenticated", () => {
|
|
test("redirects to login when not authenticated", async () => {
|
|
mockUser = null;
|
|
|
|
render(<Home />);
|
|
|
|
await waitFor(() => {
|
|
expect(mockPush).toHaveBeenCalledWith("/login");
|
|
});
|
|
});
|
|
|
|
test("returns null when not authenticated", () => {
|
|
mockUser = null;
|
|
|
|
const { container } = render(<Home />);
|
|
// Should render nothing (just redirects)
|
|
expect(container.querySelector("main")).toBeNull();
|
|
});
|
|
|
|
test("does not fetch counter when no user", () => {
|
|
mockUser = null;
|
|
const fetchSpy = vi.spyOn(global, "fetch");
|
|
|
|
render(<Home />);
|
|
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Home - Loading State", () => {
|
|
test("does not redirect while loading", () => {
|
|
mockIsLoading = true;
|
|
mockUser = null;
|
|
|
|
render(<Home />);
|
|
|
|
expect(mockPush).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("shows loading indicator while loading", () => {
|
|
mockIsLoading = true;
|
|
|
|
render(<Home />);
|
|
|
|
expect(screen.getByText("Loading...")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("Home - Navigation", () => {
|
|
test("shows My Profile link for regular user", async () => {
|
|
vi.spyOn(global, "fetch").mockResolvedValue({
|
|
json: () => Promise.resolve({ value: 0 }),
|
|
} as Response);
|
|
|
|
render(<Home />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("My Profile")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
test("does not show My Profile link for admin user", async () => {
|
|
mockUser = {
|
|
id: 1,
|
|
email: "admin@example.com",
|
|
roles: ["admin"],
|
|
permissions: ["view_counter", "view_audit"],
|
|
};
|
|
|
|
vi.spyOn(global, "fetch").mockResolvedValue({
|
|
json: () => Promise.resolve({ value: 0 }),
|
|
} as Response);
|
|
|
|
render(<Home />);
|
|
|
|
// Wait for render, then check profile link is not present
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Counter")).toBeDefined();
|
|
});
|
|
expect(screen.queryByText("My Profile")).toBeNull();
|
|
});
|
|
});
|