import { act, renderHook, waitFor } from "@testing-library/react"; import { HttpResponse, http } from "msw"; import { describe, expect, it, vi } from "vitest"; import { apiQueryClient, configureAuthClient } from "@/shared/api"; import { createQueryWrapper } from "@/test/query"; import { server } from "@/test/server"; import { type InvestmentScreen, useTbankSync, useTbankSyncStatus, } from "../index"; import { commitTbankConnectionBoundary } from "./connection-boundary"; describe("sync connection and session boundaries", () => { it.each(["connection", "session"])( "discards pending status and mutation after %s changes", async (boundary) => { let generation = 0; configureAuthClient({ generation: () => generation, accessToken: () => "synthetic", refresh: async () => true, clear: vi.fn(), }); let release!: () => void; const blocked = new Promise((resolve) => { release = resolve; }); const accepted = { id: "work", scope: { screen: "accounts" }, now: "2026-09-25T12:00:00Z", }; let started = 0; let changed = false; server.use( http.post("*/api/v1/tbank-sync", async () => { started += 1; await blocked; return HttpResponse.json(accepted, { status: 202 }); }), http.get("*/api/v1/tbank-sync/:id", async () => { const now = changed ? "2026-09-25T12:01:00Z" : accepted.now; started += 1; await blocked; return HttpResponse.json({ ...accepted, now, state: "succeeded", errorCode: null, progress: { total: 1, completed: 1, discovering: false }, resources: [], }); }), ); const { wrapper, queryClient } = createQueryWrapper(); const published: string[] = []; const unsubscribe = queryClient.getQueryCache().subscribe((event) => { if (event.query.queryKey[1] === "/api/v1/tbank-sync/{id}") { const data = event.query.state.data as { now?: string } | undefined; if (data?.now) published.push(data.now); } }); const { result } = renderHook( () => ({ mutation: useTbankSync(), status: useTbankSyncStatus("work"), }), { wrapper }, ); const screen: InvestmentScreen = { screen: "accounts" }; let mutation!: Promise; act(() => { mutation = result.current.mutation.mutateAsync({ body: screen }); }); const discarded = expect(mutation).rejects.toMatchObject({ name: "AbortError", }); await waitFor(() => expect(started).toBe(2)); await act(async () => { changed = true; if (boundary === "connection") commitTbankConnectionBoundary(queryClient, { connected: false, connectedAt: null, }); else { generation += 1; queryClient.clear(); } release(); await blocked; }); await discarded; expect(result.current.mutation.data).toBeUndefined(); const key = apiQueryClient.queryOptions( "get", "/api/v1/tbank-sync/{id}", { params: { path: { id: "work" } } }, ).queryKey; // A new request may start after a successful connection boundary; an old result cannot win it. if (boundary === "session") expect(queryClient.getQueryData(key)).toBeUndefined(); else await waitFor(() => expect(queryClient.getQueryData(key)).toMatchObject({ now: "2026-09-25T12:01:00Z", }), ); expect(published).not.toContain(accepted.now); unsubscribe(); }, ); });