import { describe, it, expect, beforeEach, vi } from "vitest"; import { render, waitFor } from "@testing-library/react"; import { BrowserRouter } from "react-router-dom"; import AuthCallback from "@app/routes/AuthCallback"; import { springAuth } from "@app/auth/springAuthClient"; // Mock springAuth vi.mock("@app/auth/springAuthClient", () => ({ springAuth: { getSession: vi.fn(), }, })); // Mock useNavigate const mockNavigate = vi.fn(); vi.mock("react-router-dom", async () => { const actual = await vi.importActual("react-router-dom"); return { ...actual, useNavigate: () => mockNavigate, }; }); describe("AuthCallback", () => { beforeEach(() => { localStorage.clear(); vi.clearAllMocks(); // Reset window.location.hash window.location.hash = ""; }); it("should extract JWT from URL hash and validate it", async () => { const mockToken = "oauth-jwt-token"; const mockUser = { id: "123", email: "oauth@example.com", username: "oauthuser", role: "USER", }; // Set URL hash with access token window.location.hash = `#access_token=${mockToken}`; // Mock successful session validation vi.mocked(springAuth.getSession).mockResolvedValueOnce({ data: { session: { user: mockUser, access_token: mockToken, expires_in: 3600, expires_at: Date.now() + 3600000, }, }, error: null, }); const dispatchEventSpy = vi.spyOn(window, "dispatchEvent"); render( , ); await waitFor(() => { // Verify JWT was stored expect(localStorage.getItem("stirling_jwt")).toBe(mockToken); // Verify jwt-available event was dispatched expect(dispatchEventSpy).toHaveBeenCalledWith(expect.objectContaining({ type: "jwt-available" })); // Verify getSession was called to validate token expect(springAuth.getSession).toHaveBeenCalled(); // Verify navigation to home expect(mockNavigate).toHaveBeenCalledWith("/", { replace: true }); }); }); it("should redirect to login when no access token in hash", async () => { // No hash or empty hash window.location.hash = ""; render( , ); await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith("/login", { replace: true, state: { error: "OAuth login failed - no token received." }, }); expect(localStorage.getItem("stirling_jwt")).toBeNull(); }); }); it("should redirect to login when token validation fails", async () => { const invalidToken = "invalid-oauth-token"; window.location.hash = `#access_token=${invalidToken}`; // Mock failed session validation vi.mocked(springAuth.getSession).mockResolvedValueOnce({ data: { session: null }, error: { message: "Invalid token" }, }); render( , ); await waitFor(() => { // JWT should be stored initially expect(localStorage.getItem("stirling_jwt")).toBeNull(); // Cleared after validation failure // Verify redirect to login expect(mockNavigate).toHaveBeenCalledWith("/login", { replace: true, state: { error: "OAuth login failed - invalid token." }, }); }); }); it("should handle errors gracefully", async () => { const mockToken = "error-token"; window.location.hash = `#access_token=${mockToken}`; // Mock getSession throwing error vi.mocked(springAuth.getSession).mockRejectedValueOnce(new Error("Network error")); render( , ); await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith("/login", { replace: true, state: { error: "OAuth login failed. Please try again." }, }); }); }); it("should display loading state while processing", () => { window.location.hash = "#access_token=processing-token"; vi.mocked(springAuth.getSession).mockImplementationOnce( () => new Promise((resolve) => setTimeout( () => resolve({ data: { session: null }, error: { message: "Token expired" }, }), 100, ), ), ); const { getByText } = render( , ); expect(getByText("Completing authentication")).toBeInTheDocument(); }); });