Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Failing after 37m50s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Failing after 42m10s
Conventional commits / commits (pull_request) Successful in 29s
Enable every plugin recommended preset and replace Biome with ESLint formatting. Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
import { act, renderHook, waitFor } from "@testing-library/react";
|
||
import { http, HttpResponse } 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({
|
||
accessToken: () => "synthetic",
|
||
clear: vi.fn(),
|
||
generation: () => generation,
|
||
refresh: () => Promise.resolve(true),
|
||
});
|
||
|
||
const { promise: blocked, resolve: release } = Promise.withResolvers<void>();
|
||
const accepted = {
|
||
id: "work",
|
||
now: "2026-09-25T12:00:00Z",
|
||
scope: { screen: "accounts" },
|
||
};
|
||
let started = 0;
|
||
let isChanged = 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 = isChanged ? "2026-09-25T12:01:00Z" : accepted.now;
|
||
started += 1;
|
||
await blocked;
|
||
return HttpResponse.json((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||
{
|
||
...accepted,
|
||
errorCode: null,
|
||
now,
|
||
progress: { completed: 1, discovering: false, total: 1 },
|
||
resources: [],
|
||
state: "succeeded",
|
||
}
|
||
/* eslint-enable unicorn/no-null */));
|
||
}),
|
||
);
|
||
const { queryClient, wrapper } = createQueryWrapper();
|
||
const published: string[] = [];
|
||
const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
|
||
if ((event.query.queryKey as readonly unknown[])[1]
|
||
!== "/api/v1/tbank-sync/{id}") {
|
||
return;
|
||
}
|
||
|
||
const data = event.query.state.data as undefined | { now?: string };
|
||
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 });
|
||
});
|
||
// eslint-disable-next-line vitest/valid-expect -- Attach rejection handling before releasing the request; the assertion is awaited below.
|
||
const discarded = expect(mutation).rejects.toMatchObject({
|
||
name: "AbortError",
|
||
});
|
||
await waitFor(() => expect(started).toBe(2));
|
||
await act(async () => {
|
||
isChanged = true;
|
||
if (boundary === "connection")
|
||
commitTbankConnectionBoundary(queryClient, (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||
{
|
||
connected: false,
|
||
connectedAt: null,
|
||
}
|
||
/* eslint-enable unicorn/no-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")
|
||
// eslint-disable-next-line vitest/no-conditional-expect -- The fixed table case selects the expected outcome; every case asserts its own branch.
|
||
expect(queryClient.getQueryData(key)).toBeUndefined();
|
||
else
|
||
await waitFor(() =>
|
||
// eslint-disable-next-line vitest/no-conditional-expect -- The fixed table case selects the expected outcome; every case asserts its own branch.
|
||
expect(queryClient.getQueryData(key)).toMatchObject({
|
||
now: "2026-09-25T12:01:00Z",
|
||
}),
|
||
);
|
||
expect(published).not.toContain(accepted.now);
|
||
unsubscribe();
|
||
},
|
||
);
|
||
});
|