import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import CallbackPage from "./page"; // Mock next/navigation const mockPush = vi.fn(); const mockSearchParams = new Map(); vi.mock("next/navigation", () => ({ useRouter: (): { push: typeof mockPush } => ({ push: mockPush, }), useSearchParams: (): { get: (key: string) => string | undefined } => ({ get: (key: string): string | undefined => mockSearchParams.get(key), }), })); // Mock auth context vi.mock("@/lib/auth/auth-context", () => ({ useAuth: vi.fn(() => ({ refreshSession: vi.fn(), })), })); const { useAuth } = await import("@/lib/auth/auth-context"); describe("CallbackPage", (): void => { beforeEach((): void => { mockPush.mockClear(); mockSearchParams.clear(); vi.mocked(useAuth).mockReturnValue({ refreshSession: vi.fn(), user: null, isLoading: false, isAuthenticated: false, authError: null, sessionExpiring: false, sessionMinutesRemaining: 0, signOut: vi.fn(), }); }); it("should render processing message", (): void => { render(); expect(screen.getByText(/completing authentication/i)).toBeInTheDocument(); }); it("should redirect to tasks page on success", async (): Promise => { const mockRefreshSession = vi.fn().mockResolvedValue(undefined); vi.mocked(useAuth).mockReturnValue({ refreshSession: mockRefreshSession, user: null, isLoading: false, isAuthenticated: false, authError: null, sessionExpiring: false, sessionMinutesRemaining: 0, signOut: vi.fn(), }); render(); await waitFor(() => { expect(mockRefreshSession).toHaveBeenCalled(); expect(mockPush).toHaveBeenCalledWith("/tasks"); }); }); it("should redirect to login on error parameter", async (): Promise => { mockSearchParams.set("error", "access_denied"); mockSearchParams.set("error_description", "User cancelled"); render(); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith("/login?error=access_denied"); }); }); it("should sanitize unknown error codes to prevent open redirect", async (): Promise => { // Malicious error parameter that could be used for XSS or redirect attacks mockSearchParams.set("error", ""); render(); await waitFor(() => { // Should replace unknown error with generic authentication_error expect(mockPush).toHaveBeenCalledWith("/login?error=authentication_error"); }); }); it("should sanitize URL-like error codes to prevent open redirect", async (): Promise => { // Attacker tries to inject a URL-like value mockSearchParams.set("error", "https://evil.com/phishing"); render(); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith("/login?error=authentication_error"); }); }); it("should allow valid OAuth 2.0 error codes", async (): Promise => { const validErrors = [ "access_denied", "invalid_request", "unauthorized_client", "server_error", "login_required", "consent_required", ]; for (const errorCode of validErrors) { mockPush.mockClear(); mockSearchParams.clear(); mockSearchParams.set("error", errorCode); const { unmount } = render(); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith(`/login?error=${errorCode}`); }); unmount(); } }); it("should encode special characters in error parameter", async (): Promise => { // Even valid errors should be encoded in the URL mockSearchParams.set("error", "session_failed"); render(); await waitFor(() => { // session_failed doesn't need encoding but the function should still call encodeURIComponent expect(mockPush).toHaveBeenCalledWith("/login?error=session_failed"); }); }); it("should handle refresh session errors gracefully", async (): Promise => { const mockRefreshSession = vi.fn().mockRejectedValue(new Error("Session error")); vi.mocked(useAuth).mockReturnValue({ refreshSession: mockRefreshSession, user: null, isLoading: false, isAuthenticated: false, authError: null, sessionExpiring: false, sessionMinutesRemaining: 0, signOut: vi.fn(), }); render(); await waitFor(() => { expect(mockRefreshSession).toHaveBeenCalled(); expect(mockPush).toHaveBeenCalledWith("/login?error=session_failed"); }); }); });