import { act, renderHook, waitFor } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { describe, expect, it, vi } from "vitest"; import { configureAuthClient } from "@/shared/api"; import { createQueryWrapper } from "@/test/query"; import { resource } from "@/test/resource"; import { server } from "@/test/server"; import { apiQueryClient, type paths, useResourceQuery } from "./index"; type Portfolio = NonNullable; function resourceOptions(accountId: string) { return apiQueryClient.queryOptions("get", "/api/v1/brokerage-accounts/{accountId}/portfolio", { params: { path: { accountId } } }); } function usePrivateResource(accountId: string) { return useResourceQuery({ ...resourceOptions(accountId), meta: { accessGroup: "brokerage" } }); } const snapshot: Portfolio = { cashBalances: [], isStale: false, lastSuccessfulAt: "2026-08-14T00:00:00Z", positions: [], structure: { currencies: [], unvaluedCategories: [] }, totalsByCurrency: [{ currency: "RUB", value: "123.000000001" }], }; function auth() { configureAuthClient({ accessToken: () => "synthetic", clear: vi.fn(), refresh: () => Promise.resolve(true), }); } describe("persistent resource query lifecycle", () => { it("observes once into the canonical cache without another import", async () => { auth(); const modes: (null | string)[] = []; let observations = 0; server.use( http.get( "*/api/v1/brokerage-accounts/:accountId/portfolio", ({ request }) => { const mode = new URL(request.url).searchParams.get("sync"); modes.push(mode); if (mode) observations += 1; return HttpResponse.json( resource(snapshot, { dataRevision: observations > 0 ? "new" : "old", }), ); }, ), ); const { queryClient, wrapper } = createQueryWrapper(); const { result } = renderHook(() => usePrivateResource("a"), { wrapper }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); await act(() => result.current.observe()); await waitFor(() => expect(result.current.meta?.dataRevision).toBe("new")); expect(modes).toEqual([null, "observe"]); expect(queryClient.getQueryCache().getAll()).toHaveLength(1); expect( queryClient.getQueryData(resourceOptions("a").queryKey), ).toMatchObject({ data: snapshot, meta: { dataRevision: "new" } }); }); it("keeps successful HTTP state distinct from missing or failed source data", async () => { auth(); server.use( http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => HttpResponse.json( resource(null, { lastAttempt: { finishedAt: "2026-08-14T00:01:00Z", problems: [], startedAt: "2026-08-14T00:00:00Z", state: "failed", }, }), ), ), ); const { result } = renderHook(() => usePrivateResource("a"), { wrapper: createQueryWrapper().wrapper, }); await waitFor(() => expect(result.current.meta?.lastAttempt.state).toBe("failed"), ); expect(result.current.data).toBeUndefined(); expect(result.current.isSuccess).toBe(true); expect(result.current.status).toBe("success"); expect(result.current.isPending).toBe(false); expect(result.current.isLoading).toBe(false); expect(result.current.meta?.availability).toBe("initial-loading"); }); it("keeps the refetch envelope while exposing the refreshed resource payload", async () => { auth(); let isReady = false; const refreshed = resource(snapshot, { dataRevision: "ready" }); server.use( http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => HttpResponse.json(isReady ? refreshed : resource(null)), ), ); const { result } = renderHook(() => usePrivateResource("a"), { wrapper: createQueryWrapper().wrapper, }); await waitFor(() => expect(result.current.status).toBe("success"), ); await act(async () => { const missing = await result.current.refetch(); expect(missing.status).toBe("success"); expect(missing.data).toEqual(resource(null)); expect(missing.data?.data).toBeNull(); isReady = true; const loaded = await result.current.refetch(); expect(loaded.data).toEqual(refreshed); expect(loaded.data?.data?.totalsByCurrency).toEqual( snapshot.totalsByCurrency, ); }); await waitFor(() => expect(result.current.data).toEqual(snapshot)); expect(result.current.meta).toEqual(refreshed.meta); expect(result.current.status).toBe("success"); expect(result.current.isSuccess).toBe(true); expect(result.current.isPending).toBe(false); }); it("keeps the observed snapshot when an older query finishes later", async () => { auth(); const { promise: blocked, resolve: release } = Promise.withResolvers(); let isStarted = false; server.use( http.get( "*/api/v1/brokerage-accounts/:accountId/portfolio", async ({ request }) => { if (!new URL(request.url).searchParams.has("sync")) { isStarted = true; await blocked; return HttpResponse.json(resource({ ...snapshot, isStale: true })); } return HttpResponse.json(resource(snapshot)); }, ), ); const { result } = renderHook(() => usePrivateResource("a"), { wrapper: createQueryWrapper().wrapper, }); await waitFor(() => expect(isStarted).toBe(true)); await act(() => result.current.observe()); await act(async () => { release(); await blocked; }); expect(result.current.data).toEqual(snapshot); }); it("clears private snapshots on confirmed access loss and fences another pending success", async () => { auth(); let isDenied = false; const { promise: blocked, resolve: release } = Promise.withResolvers(); server.use( http.get( "*/api/v1/brokerage-accounts/:accountId/portfolio", async ({ params }) => { if (params.accountId === "b") await blocked; return isDenied && params.accountId === "a" ? HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 }) : HttpResponse.json(resource(snapshot)); }, ), ); const { wrapper } = createQueryWrapper(); const { result } = renderHook( () => ({ a: usePrivateResource("a"), b: usePrivateResource("b") }), { wrapper }, ); await waitFor(() => expect(result.current.a.data).toEqual(snapshot)); isDenied = true; await act(() => result.current.a.refetch()); await waitFor(() => expect(result.current.a.data).toBeUndefined()); await act(async () => { release(); await blocked; }); expect(result.current.b.data).toBeUndefined(); expect(result.current.a.isError).toBe(true); }); });