Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/auth/AuthProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand Down
1 change: 1 addition & 0 deletions frontend/src/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
<div style={{ padding: '2rem', textAlign: 'center', color: 'red' }}>
<h2>Authentication Error</h2>
<p>{error}</p>
<p>Reload the page to try again.</p>
</div>
)
}
Expand Down
22 changes: 14 additions & 8 deletions frontend/src/auth/msalConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
});
27 changes: 18 additions & 9 deletions frontend/src/auth/msalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,26 @@ export interface AuthConfig {
}

export async function fetchAuthConfig(): Promise<AuthConfig> {
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 {
Expand Down