fix(api): correlation ID survives HMR; fix endpoint template literal typos

- client.ts: store correlation ID in sessionStorage so HMR (module re-eval)
  does not generate a new ID mid-session; add clearSessionCorrelationId()
- endpoints.ts: fix 3 template literal trailing-quote bugs (missing ')' chars);
  replace template literals with string concat for encodeURIComponent calls
- AuthProvider.tsx: call clearSessionCorrelationId() on logout
- App.tsx: reorder ThemeProvider import before AuthProvider per PROVIDER_ORDER.md;
  indent Routes inside AuthProvider to match expected tree structure
- Tasks.md: update task status
- providerTreeOrder.test.tsx: add integration tests for provider nesting order

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-03 23:35:18 +02:00
parent fc57c83f79
commit c8b48b5b65
6 changed files with 361 additions and 166 deletions

View File

@@ -56,7 +56,7 @@ import React, {
} from "react";
import { useNavigate } from "react-router-dom";
import * as authApi from "../api/auth";
import { setUnauthorizedHandler, resetLogoutState } from "../api/client";
import { setUnauthorizedHandler, resetLogoutState, clearSessionCorrelationId } from "../api/client";
import { setAuthErrorHandler, resetLogoutState as resetFetchErrorLogoutState } from "../utils/fetchError";
import { STORAGE_KEY_AUTHENTICATED } from "../utils/constants";
import { SessionValidationLoading } from "../components/SessionValidationLoading";
@@ -215,6 +215,7 @@ export function AuthProvider({
// Always clear local state even if the API call fails (e.g. expired session).
sessionStorage.removeItem(STORAGE_KEY_AUTHENTICATED);
setIsAuthenticated(false);
clearSessionCorrelationId();
// Broadcast logout event to other tabs for immediate sync
try {

View File

@@ -0,0 +1,219 @@
/**
* Integration Tests for Provider Tree Order (Issue #54)
*
* Verifies that the provider tree is correctly nested per PROVIDER_ORDER.md.
* These tests complement compile-time checks by validating runtime structure.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { createProviderComposition } from "../providerComposition";
import { ThemeProvider } from "../ThemeProvider";
import { AuthProvider } from "../AuthProvider";
import { TimezoneProvider } from "../TimezoneProvider";
import { NavigationCancellationProvider } from "../NavigationCancellationProvider";
import * as clientModule from "../../api/client";
import * as authModule from "../../api/auth";
// ---------------------------------------------------------------------------
// Helper: Simple component to verify each provider's context is accessible
// ---------------------------------------------------------------------------
function AuthConsumer(): React.JSX.Element {
return <div data-testid="auth-available">Auth available</div>;
}
// ---------------------------------------------------------------------------
// Test Suite: Provider Tree Order Integration
// ---------------------------------------------------------------------------
describe("Provider Tree Order Integration", () => {
beforeEach(() => {
sessionStorage.clear();
localStorage.clear();
vi.clearAllMocks();
vi.restoreAllMocks();
Object.defineProperty(window, "matchMedia", {
configurable: true,
writable: true,
value: vi.fn().mockReturnValue({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}),
});
vi.spyOn(clientModule, "setUnauthorizedHandler").mockImplementation(
(_handler: (() => void) | null) => {},
);
// Mock session validation to prevent auth errors
vi.spyOn(authModule, "validateSession").mockResolvedValue({
valid: true,
});
});
afterEach(() => {
vi.clearAllMocks();
});
// -----------------------------------------------------------------------
// Test 1: createProviderComposition renders providers without crashing
// -----------------------------------------------------------------------
it("builder creates composition that renders without errors", async () => {
const composition = createProviderComposition()
.withTheme({ children: undefined })
.withFluent({} as any)
.withNotification()
.withErrorBoundary()
.withBrowserRouter()
.withNavigationCancellation()
.withAuth()
.build(<AuthConsumer />);
render(composition);
await waitFor(() => {
expect(screen.getByTestId("auth-available")).toBeInTheDocument();
});
});
// -----------------------------------------------------------------------
// Test 2: App provider structure matches PROVIDER_ORDER.md
// -----------------------------------------------------------------------
it("App providers nested as documented in PROVIDER_ORDER.md", async () => {
function Inner(): React.JSX.Element {
return <div data-testid="inner">Inner</div>;
}
// Recreate the core provider nesting from App.tsx
const composition = (
<ThemeProvider>
<MemoryRouter initialEntries={["/"]}>
<NavigationCancellationProvider>
<AuthProvider>
<TimezoneProvider>
<Inner />
</TimezoneProvider>
</AuthProvider>
</NavigationCancellationProvider>
</MemoryRouter>
</ThemeProvider>
);
render(composition);
await waitFor(() => {
expect(screen.getByTestId("inner")).toBeInTheDocument();
});
});
// -----------------------------------------------------------------------
// Test 3: Incorrect provider nesting (BrowserRouter missing) is detected
// -----------------------------------------------------------------------
it("throws when AuthProvider is outside BrowserRouter", async () => {
// AuthProvider uses useNavigate() which requires BrowserRouter context.
// If placed outside BrowserRouter, it should throw.
function BadlyNested(): React.JSX.Element {
return (
<ThemeProvider>
<AuthProvider>
<div>Should fail</div>
</AuthProvider>
</ThemeProvider>
);
}
expect(() => render(<BadlyNested />)).toThrow();
});
// -----------------------------------------------------------------------
// Test 4: Provider order matters - FluentProvider needs theme from ThemeProvider
// -----------------------------------------------------------------------
it("verify that ThemeProvider and FluentProvider order matters", async () => {
// FluentProvider needs theme from ThemeProvider via useThemeMode().
// This test verifies they work together when correctly nested.
function FluentContent(): React.JSX.Element {
return <div data-testid="fluent-content">Fluent content</div>;
}
const composition = createProviderComposition()
.withTheme({ children: undefined })
.withFluent({} as any)
.withNotification()
.withErrorBoundary()
.withBrowserRouter()
.withNavigationCancellation()
.withAuth()
.build(<FluentContent />);
render(composition);
await waitFor(() => {
expect(screen.getByTestId("fluent-content")).toBeInTheDocument();
});
});
// -----------------------------------------------------------------------
// Test 5: buildWithTimezone adds TimezoneProvider after AuthProvider
// -----------------------------------------------------------------------
it("buildWithTimezone() includes TimezoneProvider for protected routes", async () => {
function TimezoneContent(): React.JSX.Element {
return <div data-testid="timezone-content">Timezone content</div>;
}
const composition = createProviderComposition()
.withTheme({ children: undefined })
.withFluent({} as any)
.withNotification()
.withErrorBoundary()
.withBrowserRouter()
.withNavigationCancellation()
.withAuth()
.withTimezone({ children: <TimezoneContent /> })
.buildWithTimezone();
// Just verify it renders without throwing - ordering is enforced by builder
render(composition);
await waitFor(() => {
expect(screen.getByTestId("timezone-content")).toBeInTheDocument();
});
});
// -----------------------------------------------------------------------
// Test 6: All providers render without errors (nesting depth check)
// -----------------------------------------------------------------------
it("composition with all providers renders correctly", async () => {
function AllProvidersContent(): React.JSX.Element {
return <div data-testid="all-providers">All providers rendered</div>;
}
const composition = createProviderComposition()
.withTheme({ children: undefined })
.withFluent({} as any)
.withNotification()
.withErrorBoundary()
.withBrowserRouter()
.withNavigationCancellation()
.withAuth()
.withTimezone({ children: <AllProvidersContent /> })
.buildWithTimezone();
render(composition);
await waitFor(() => {
expect(screen.getByTestId("all-providers")).toBeInTheDocument();
});
});
});