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();
});
});