diff --git a/client/src/shared/auth/AuthProvider.tsx b/client/src/shared/auth/AuthProvider.tsx index 07c1343..71274da 100644 --- a/client/src/shared/auth/AuthProvider.tsx +++ b/client/src/shared/auth/AuthProvider.tsx @@ -6,6 +6,10 @@ import { getStoredToken, clearStoredToken } from "./token-storage.ts"; export type AuthUser = ApiCurrentUserDto; +const isAbortError = (e: unknown): boolean => { + return e instanceof DOMException && e.name === "AbortError"; +}; + const AuthContext = createContext< | { user: AuthUser | null; @@ -33,8 +37,12 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => setUser(current); if (!current) clearStoredToken(); }) - .catch(() => setUser(null)) - .finally(() => setLoading(false)); + .catch((e) => { + if (!isAbortError(e)) setUser(null); + }) + .finally(() => { + if (!abortController.signal.aborted) setLoading(false); + }); return () => abortController.abort(); }, []); diff --git a/client/src/shared/auth/__tests__/AuthProvider.test.tsx b/client/src/shared/auth/__tests__/AuthProvider.test.tsx new file mode 100644 index 0000000..be227ec --- /dev/null +++ b/client/src/shared/auth/__tests__/AuthProvider.test.tsx @@ -0,0 +1,73 @@ +/** @jest-environment jsdom */ + +const getCurrentUserMock = jest.fn(); + +jest.mock("@features/auth/apis", () => ({ + getCurrentUser: (...args: unknown[]) => getCurrentUserMock(...args), +})); + +import { StrictMode } from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import { AuthProvider } from "../AuthProvider"; +import { useAuth } from "../AuthHooks"; +import type { ApiCurrentUserDto } from "@features/auth/apis/auth-api.ts"; + +const user: ApiCurrentUserDto = { + id: 1, + first_name: "Taro", + last_name: "Yamada", + email: "taro@example.com", +}; + +type LogEntry = { isLoading: boolean; user: unknown }; + +function Probe({ log }: { log: LogEntry[] }) { + const { isLoading, user } = useAuth(); + log.push({ isLoading, user }); + return
{isLoading ? "loading" : user ? "authed" : "guest"}
; +} + +describe("AuthProvider", () => { + beforeEach(() => { + localStorage.setItem("auth_token", "tok-123"); + getCurrentUserMock.mockReset(); + }); + + afterEach(() => { + localStorage.clear(); + }); + + it("does not report logged-out while the request from a StrictMode-duplicated effect is still in flight", async () => { + getCurrentUserMock.mockImplementation( + (_token: string, init?: { signal?: AbortSignal }) => + new Promise((resolve, reject) => { + const signal = init?.signal; + if (signal?.aborted) { + reject(new DOMException("Aborted", "AbortError")); + return; + } + const onAbort = () => reject(new DOMException("Aborted", "AbortError")); + signal?.addEventListener("abort", onAbort); + setTimeout(() => { + signal?.removeEventListener("abort", onAbort); + resolve(user); + }, 20); + }), + ); + + const log: LogEntry[] = []; + + render( + + + + + , + ); + + await waitFor(() => expect(screen.getByTestId("state").textContent).toBe("authed")); + + const wronglyReportedLoggedOut = log.some((entry) => !entry.isLoading && entry.user === null); + expect(wronglyReportedLoggedOut).toBe(false); + }); +});