mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-17 11:45:05 +02:00
Explicitly test for console warnings & errors (#6502)
# Description of Changes Disallow warnings and errors from being thrown in the browser console during tests unless explicitly expected in the test. Also adds a Playwright test to prod around some main UI areas and checks that no warnings/errors have been thrown.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
POST_LOGIN_REDIRECT_STORAGE_KEY,
|
||||
springAuth,
|
||||
} from "@app/auth/springAuthClient";
|
||||
import { expectConsole } from "@app/tests/failOnConsole";
|
||||
|
||||
// Mock springAuth; keep the real redirect-path helpers.
|
||||
vi.mock("@app/auth/springAuthClient", async () => {
|
||||
@@ -90,6 +91,7 @@ describe("AuthCallback", () => {
|
||||
});
|
||||
|
||||
it("should redirect to login when no access token in hash", async () => {
|
||||
expectConsole.error(/\[AuthCallback\] No access_token in URL fragment/);
|
||||
// No hash or empty hash
|
||||
window.location.hash = "";
|
||||
|
||||
@@ -109,6 +111,7 @@ describe("AuthCallback", () => {
|
||||
});
|
||||
|
||||
it("should redirect to login when token validation fails", async () => {
|
||||
expectConsole.error(/\[AuthCallback\] Failed to validate token/);
|
||||
const invalidToken = "invalid-oauth-token";
|
||||
window.location.hash = `#access_token=${invalidToken}`;
|
||||
|
||||
@@ -137,6 +140,7 @@ describe("AuthCallback", () => {
|
||||
});
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
expectConsole.error(/\[AuthCallback\] Authentication failed/);
|
||||
const mockToken = "error-token";
|
||||
window.location.hash = `#access_token=${mockToken}`;
|
||||
|
||||
|
||||
@@ -18,42 +18,16 @@ export default function AuthCallback() {
|
||||
const navigate = useNavigate();
|
||||
const processingRef = useRef(false);
|
||||
|
||||
// Log component lifecycle
|
||||
useEffect(() => {
|
||||
const mountId = Math.random().toString(36).substring(7);
|
||||
console.log(`[AuthCallback:${mountId}] 🔵 Component mounted`);
|
||||
return () => {
|
||||
console.log(`[AuthCallback:${mountId}] 🔴 Component unmounting`);
|
||||
};
|
||||
}, []);
|
||||
const startedAt = performance.now();
|
||||
const elapsed = () => `${(performance.now() - startedAt).toFixed(0)}ms`;
|
||||
|
||||
useEffect(() => {
|
||||
const handleCallback = async () => {
|
||||
const startTime = performance.now();
|
||||
const executionId = Math.random().toString(36).substring(7);
|
||||
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] ════════════════════════════════════`,
|
||||
);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Starting authentication callback`,
|
||||
);
|
||||
console.log(`[AuthCallback:${executionId}] URL: ${window.location.href}`);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Hash: ${window.location.hash}`,
|
||||
);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Document readyState: ${document.readyState}`,
|
||||
);
|
||||
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.sessionStorage.getItem("stirling_sso_auto_login_logged_out") ===
|
||||
"1"
|
||||
) {
|
||||
console.warn(
|
||||
`[AuthCallback:${executionId}] ⚠️ Logout block active, skipping token processing`,
|
||||
);
|
||||
navigate("/login", {
|
||||
replace: true,
|
||||
state: { error: "You have been signed out. Please sign in again." },
|
||||
@@ -62,30 +36,16 @@ export default function AuthCallback() {
|
||||
}
|
||||
|
||||
// Prevent double execution (React 18 Strict Mode + navigate dependency)
|
||||
if (processingRef.current) {
|
||||
console.warn(
|
||||
`[AuthCallback:${executionId}] ⚠️ Already processing, skipping duplicate execution`,
|
||||
);
|
||||
console.warn(
|
||||
`[AuthCallback:${executionId}] This is expected in React Strict Mode (development)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (processingRef.current) return;
|
||||
processingRef.current = true;
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Step 1: Extracting token from URL fragment`,
|
||||
);
|
||||
|
||||
// Extract JWT from URL fragment (#access_token=...)
|
||||
const hash = window.location.hash.substring(1); // Remove '#'
|
||||
const params = new URLSearchParams(hash);
|
||||
const token = params.get("access_token");
|
||||
const hash = window.location.hash.substring(1);
|
||||
const token = new URLSearchParams(hash).get("access_token");
|
||||
|
||||
if (!token) {
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] ❌ No access_token in URL fragment`,
|
||||
`[AuthCallback] No access_token in URL fragment (${elapsed()})`,
|
||||
);
|
||||
navigate("/login", {
|
||||
replace: true,
|
||||
@@ -94,39 +54,13 @@ export default function AuthCallback() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] ✓ Token extracted (length: ${token.length})`,
|
||||
);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Step 2: Storing JWT in localStorage`,
|
||||
);
|
||||
|
||||
// Store JWT in localStorage
|
||||
localStorage.setItem("stirling_jwt", token);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] ✓ JWT stored in localStorage`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Step 3: Dispatching 'jwt-available' event`,
|
||||
);
|
||||
// Dispatch custom event for other components to react to JWT availability
|
||||
window.dispatchEvent(new CustomEvent("jwt-available"));
|
||||
console.log(`[AuthCallback:${executionId}] ✓ Event dispatched`);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Elapsed after jwt-available: ${(performance.now() - startTime).toFixed(2)}ms`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Step 4: Validating token with backend`,
|
||||
);
|
||||
// Validate the token and load user info
|
||||
// This calls /api/v1/auth/me with the JWT to get user details
|
||||
const { data, error } = await springAuth.getSession();
|
||||
|
||||
if (error || !data.session) {
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] ❌ Failed to validate token:`,
|
||||
`[AuthCallback] Failed to validate token (${elapsed()}):`,
|
||||
error,
|
||||
);
|
||||
localStorage.removeItem("stirling_jwt");
|
||||
@@ -137,68 +71,21 @@ export default function AuthCallback() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] ✓ Token validated, user: ${data.session.user.username}`,
|
||||
);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Step 5: Running platform-specific callback handlers`,
|
||||
);
|
||||
|
||||
await handleAuthCallbackSuccess(token);
|
||||
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] ✓ Callback handlers complete`,
|
||||
);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Step 6: Waiting for context stabilization`,
|
||||
);
|
||||
|
||||
// Wait for all context providers to process jwt-available event
|
||||
// This prevents infinite render loop when coming from cross-domain SAML redirect
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Elapsed after stabilization wait: ${(performance.now() - startTime).toFixed(2)}ms`,
|
||||
);
|
||||
|
||||
const target = consumePostLoginRedirectPath() ?? "/";
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] Step 7: Navigating to ${target}`,
|
||||
console.info(
|
||||
`[AuthCallback] Authenticated ${data.session.user.username} in ${elapsed()}, navigating to ${target}`,
|
||||
);
|
||||
navigate(target, { replace: true });
|
||||
|
||||
const duration = performance.now() - startTime;
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] ✓ Authentication complete (${duration.toFixed(2)}ms)`,
|
||||
);
|
||||
console.log(
|
||||
`[AuthCallback:${executionId}] ════════════════════════════════════`,
|
||||
);
|
||||
} catch (error) {
|
||||
const duration = performance.now() - startTime;
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] ════════════════════════════════════`,
|
||||
);
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] ❌ FATAL ERROR during authentication`,
|
||||
);
|
||||
console.error(`[AuthCallback:${executionId}] Error:`, error);
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] Error name:`,
|
||||
(error as Error)?.name,
|
||||
);
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] Error message:`,
|
||||
(error as Error)?.message,
|
||||
);
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] Error stack:`,
|
||||
(error as Error)?.stack,
|
||||
);
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] Duration before failure: ${duration.toFixed(2)}ms`,
|
||||
);
|
||||
console.error(
|
||||
`[AuthCallback:${executionId}] ════════════════════════════════════`,
|
||||
`[AuthCallback] Authentication failed (${elapsed()}):`,
|
||||
error,
|
||||
);
|
||||
navigate("/login", {
|
||||
replace: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { BrowserRouter, MemoryRouter } from "react-router-dom";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
@@ -176,7 +176,7 @@ describe("Login", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should show loading state while auth is loading", () => {
|
||||
it("should show loading state while auth is loading", async () => {
|
||||
vi.mocked(useAuth).mockReturnValue({
|
||||
session: null,
|
||||
user: null,
|
||||
@@ -187,13 +187,15 @@ describe("Login", () => {
|
||||
refreshSession: vi.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<BrowserRouter>
|
||||
<Login />
|
||||
</BrowserRouter>
|
||||
</TestWrapper>,
|
||||
);
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<BrowserRouter>
|
||||
<Login />
|
||||
</BrowserRouter>
|
||||
</TestWrapper>,
|
||||
);
|
||||
});
|
||||
|
||||
// Component shouldn't redirect or show form while loading
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
@@ -510,26 +512,30 @@ describe("Login", () => {
|
||||
expect(springAuth.signInWithPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display session expired message from URL param", () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MemoryRouter initialEntries={["/login?expired=true"]}>
|
||||
<Login />
|
||||
</MemoryRouter>
|
||||
</TestWrapper>,
|
||||
);
|
||||
it("should display session expired message from URL param", async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MemoryRouter initialEntries={["/login?expired=true"]}>
|
||||
<Login />
|
||||
</MemoryRouter>
|
||||
</TestWrapper>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText(/session.*expired/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display account created success message", () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MemoryRouter initialEntries={["/login?messageType=accountCreated"]}>
|
||||
<Login />
|
||||
</MemoryRouter>
|
||||
</TestWrapper>,
|
||||
);
|
||||
it("should display account created success message", async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<MemoryRouter initialEntries={["/login?messageType=accountCreated"]}>
|
||||
<Login />
|
||||
</MemoryRouter>
|
||||
</TestWrapper>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText(/account created/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -287,8 +287,6 @@ export default function Login() {
|
||||
setPostLoginRedirectPath(returnPath);
|
||||
}
|
||||
|
||||
console.log(`[Login] Signing in with provider: ${provider}`);
|
||||
|
||||
// Redirect to Spring OAuth2 endpoint using the actual provider ID from backend
|
||||
// The backend returns the correct registration ID (e.g., 'authentik', 'oidc', 'keycloak')
|
||||
const { error } = await springAuth.signInWithOAuth({
|
||||
@@ -520,8 +518,6 @@ export default function Login() {
|
||||
setError(null);
|
||||
clearLogoutBlock();
|
||||
|
||||
console.log("[Login] Signing in with email:", email);
|
||||
|
||||
const { user, session, error } = await springAuth.signInWithPassword({
|
||||
email: email.trim(),
|
||||
password: password,
|
||||
@@ -529,13 +525,11 @@ export default function Login() {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("[Login] Email sign in error:", error);
|
||||
setError(error.message);
|
||||
if (error.mfaRequired || error.code === "invalid_mfa_code") {
|
||||
setRequiresMfa(true);
|
||||
}
|
||||
} else if (user && session) {
|
||||
console.log("[Login] Email sign in successful");
|
||||
clearLogoutBlock();
|
||||
setRequiresMfa(false);
|
||||
setMfaCode("");
|
||||
|
||||
Reference in New Issue
Block a user