diff --git a/frontend/src/auth/AuthProvider.test.tsx b/frontend/src/auth/AuthProvider.test.tsx index e3023b1e28..8ddae4ca70 100644 --- a/frontend/src/auth/AuthProvider.test.tsx +++ b/frontend/src/auth/AuthProvider.test.tsx @@ -161,6 +161,7 @@ describe("AuthProvider", () => { await waitFor(() => { expect(screen.getByText("Authentication Error")).toBeVisible(); expect(screen.getByText("Config fetch failed")).toBeVisible(); + expect(screen.getByText("Reload the page to try again.")).toBeVisible(); }); }); diff --git a/frontend/src/auth/AuthProvider.tsx b/frontend/src/auth/AuthProvider.tsx index 34577c7b9b..f707ffa159 100644 --- a/frontend/src/auth/AuthProvider.tsx +++ b/frontend/src/auth/AuthProvider.tsx @@ -119,6 +119,7 @@ export function AuthProvider({ children }: AuthProviderProps) {

Authentication Error

{error}

+

Reload the page to try again.

) } diff --git a/frontend/src/auth/msalConfig.test.ts b/frontend/src/auth/msalConfig.test.ts index bda21bf5fa..c9eb612b3b 100644 --- a/frontend/src/auth/msalConfig.test.ts +++ b/frontend/src/auth/msalConfig.test.ts @@ -73,22 +73,28 @@ describe("msalConfig", () => { expect(global.fetch).toHaveBeenCalledWith("/api/auth/config"); }); - it("returns empty config when response is not ok", async () => { - (global.fetch as jest.Mock).mockResolvedValue({ ok: false }); + it("throws when response is not ok (transient failure is not auth-disabled)", async () => { + (global.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 503, + statusText: "Service Unavailable", + }); const { fetchAuthConfig } = await import("./msalConfig"); - const result = await fetchAuthConfig(); - expect(result).toEqual({ clientId: "", tenantId: "", allowedGroupIds: "" }); + await expect(fetchAuthConfig()).rejects.toThrow("/api/auth/config returned 503 Service Unavailable"); }); - it("returns empty config on network error", async () => { - (global.fetch as jest.Mock).mockRejectedValue(new Error("Network error")); + it("throws on network error (transient failure is not auth-disabled)", async () => { + const networkError = new Error("Network error"); + (global.fetch as jest.Mock).mockRejectedValue(networkError); const { fetchAuthConfig } = await import("./msalConfig"); - const result = await fetchAuthConfig(); - expect(result).toEqual({ clientId: "", tenantId: "", allowedGroupIds: "" }); + await expect(fetchAuthConfig()).rejects.toMatchObject({ + message: "Failed to reach /api/auth/config: Network error", + cause: networkError, + }); }); }); }); diff --git a/frontend/src/auth/msalConfig.ts b/frontend/src/auth/msalConfig.ts index c63e3a68cf..a99aebc3fd 100644 --- a/frontend/src/auth/msalConfig.ts +++ b/frontend/src/auth/msalConfig.ts @@ -24,17 +24,26 @@ export interface AuthConfig { } export async function fetchAuthConfig(): Promise { + let response: Response try { - const response = await fetch('/api/auth/config') - if (!response.ok) { - // Auth endpoint not available — treat as auth disabled - return { clientId: '', tenantId: '', allowedGroupIds: '' } - } - return (await response.json()) as AuthConfig - } catch { - // Network error (e.g., backend not running yet) — treat as auth disabled - return { clientId: '', tenantId: '', allowedGroupIds: '' } + response = await fetch('/api/auth/config') + } catch (error) { + // A network error (e.g., backend not running yet) is a transient + // infrastructure failure, not proof that auth is disabled. Surface it so + // AuthProvider can show its error state instead of rendering the shell + // while protected APIs return 401. + const fetchError = new Error( + `Failed to reach /api/auth/config: ${error instanceof Error ? error.message : String(error)}`, + ) + // ErrorOptions requires ES2022, while this project targets ES2020. + Object.defineProperty(fetchError, 'cause', { value: error }) + throw fetchError } + if (!response.ok) { + // HTTP-level failures on the config endpoint are equally inconclusive. + throw new Error(`/api/auth/config returned ${response.status} ${response.statusText}`) + } + return (await response.json()) as AuthConfig } export function buildMsalConfig(authConfig: AuthConfig): Configuration {