Skip to content

Commit cbd449b

Browse files
authored
Merge branch 'main' into issue2416/typed-optimization-run-state
2 parents 52dfa12 + 7307d94 commit cbd449b

4 files changed

Lines changed: 34 additions & 17 deletions

File tree

frontend/src/auth/AuthProvider.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ describe("AuthProvider", () => {
161161
await waitFor(() => {
162162
expect(screen.getByText("Authentication Error")).toBeVisible();
163163
expect(screen.getByText("Config fetch failed")).toBeVisible();
164+
expect(screen.getByText("Reload the page to try again.")).toBeVisible();
164165
});
165166
});
166167

frontend/src/auth/AuthProvider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
119119
<div style={{ padding: '2rem', textAlign: 'center', color: 'red' }}>
120120
<h2>Authentication Error</h2>
121121
<p>{error}</p>
122+
<p>Reload the page to try again.</p>
122123
</div>
123124
)
124125
}

frontend/src/auth/msalConfig.test.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,28 @@ describe("msalConfig", () => {
7373
expect(global.fetch).toHaveBeenCalledWith("/api/auth/config");
7474
});
7575

76-
it("returns empty config when response is not ok", async () => {
77-
(global.fetch as jest.Mock).mockResolvedValue({ ok: false });
76+
it("throws when response is not ok (transient failure is not auth-disabled)", async () => {
77+
(global.fetch as jest.Mock).mockResolvedValue({
78+
ok: false,
79+
status: 503,
80+
statusText: "Service Unavailable",
81+
});
7882

7983
const { fetchAuthConfig } = await import("./msalConfig");
80-
const result = await fetchAuthConfig();
8184

82-
expect(result).toEqual({ clientId: "", tenantId: "", allowedGroupIds: "" });
85+
await expect(fetchAuthConfig()).rejects.toThrow("/api/auth/config returned 503 Service Unavailable");
8386
});
8487

85-
it("returns empty config on network error", async () => {
86-
(global.fetch as jest.Mock).mockRejectedValue(new Error("Network error"));
88+
it("throws on network error (transient failure is not auth-disabled)", async () => {
89+
const networkError = new Error("Network error");
90+
(global.fetch as jest.Mock).mockRejectedValue(networkError);
8791

8892
const { fetchAuthConfig } = await import("./msalConfig");
89-
const result = await fetchAuthConfig();
9093

91-
expect(result).toEqual({ clientId: "", tenantId: "", allowedGroupIds: "" });
94+
await expect(fetchAuthConfig()).rejects.toMatchObject({
95+
message: "Failed to reach /api/auth/config: Network error",
96+
cause: networkError,
97+
});
9298
});
9399
});
94100
});

frontend/src/auth/msalConfig.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,26 @@ export interface AuthConfig {
2424
}
2525

2626
export async function fetchAuthConfig(): Promise<AuthConfig> {
27+
let response: Response
2728
try {
28-
const response = await fetch('/api/auth/config')
29-
if (!response.ok) {
30-
// Auth endpoint not available — treat as auth disabled
31-
return { clientId: '', tenantId: '', allowedGroupIds: '' }
32-
}
33-
return (await response.json()) as AuthConfig
34-
} catch {
35-
// Network error (e.g., backend not running yet) — treat as auth disabled
36-
return { clientId: '', tenantId: '', allowedGroupIds: '' }
29+
response = await fetch('/api/auth/config')
30+
} catch (error) {
31+
// A network error (e.g., backend not running yet) is a transient
32+
// infrastructure failure, not proof that auth is disabled. Surface it so
33+
// AuthProvider can show its error state instead of rendering the shell
34+
// while protected APIs return 401.
35+
const fetchError = new Error(
36+
`Failed to reach /api/auth/config: ${error instanceof Error ? error.message : String(error)}`,
37+
)
38+
// ErrorOptions requires ES2022, while this project targets ES2020.
39+
Object.defineProperty(fetchError, 'cause', { value: error })
40+
throw fetchError
3741
}
42+
if (!response.ok) {
43+
// HTTP-level failures on the config endpoint are equally inconclusive.
44+
throw new Error(`/api/auth/config returned ${response.status} ${response.statusText}`)
45+
}
46+
return (await response.json()) as AuthConfig
3847
}
3948

4049
export function buildMsalConfig(authConfig: AuthConfig): Configuration {

0 commit comments

Comments
 (0)