Consume the provided resource-envelope API snapshot. Preserve financial snapshots during refresh, observe synchronization without importing, and fence late responses across account and connection changes.
115 lines
3.3 KiB
TypeScript
115 lines
3.3 KiB
TypeScript
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<void>((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<unknown>;
|
|
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();
|
|
},
|
|
);
|
|
});
|