fix(tbank): reset connection-derived query cache

This commit is contained in:
2026-09-01 05:54:50 +03:00
parent 7ab043dce6
commit 175727bc93
7 changed files with 305 additions and 29 deletions
@@ -23,6 +23,20 @@ const brokerageAccountsQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts",
).queryKey;
const portfolioQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/portfolio",
{ params: { path: { accountId: "account-1" } } },
).queryKey;
const operationsQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/operations",
{ params: { path: { accountId: "account-1" } } },
).queryKey;
const unrelatedQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/auth/me",
).queryKey;
function renderSection(client: QueryClient) {
return render(
@@ -46,11 +60,13 @@ function seededClient() {
openedOn: null,
},
]);
client.setQueryData(portfolioQueryKey, { source: "previous-connection" });
client.setQueryData(unrelatedQueryKey, { id: "user-1" });
return client;
}
describe("TbankConnectionSection", () => {
it("updates the generated status cache and invalidates accounts after connecting", async () => {
it("commits connection status and resets the complete brokerage query subtree after connecting", async () => {
const client = seededClient();
renderSection(client);
@@ -65,12 +81,36 @@ describe("TbankConnectionSection", () => {
screen.getByRole("button", { name: "Заменить token" }),
).toBeInTheDocument(),
);
expect(client.getQueryState(brokerageAccountsQueryKey)?.isInvalidated).toBe(
true,
);
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
expect(client.getQueryData(portfolioQueryKey)).toBeUndefined();
expect(client.getQueryData(unrelatedQueryKey)).toEqual({ id: "user-1" });
});
it("updates the generated status cache and invalidates accounts after deleting", async () => {
it("creates the same financial boundary when an existing connection is replaced", async () => {
server.use(
http.get("*/api/v1/tbank-connection", () =>
HttpResponse.json({
connected: true,
connectedAt: "2026-08-13T00:00:00Z",
}),
),
);
const client = seededClient();
renderSection(client);
await screen.findByRole("button", { name: "Заменить token" });
fireEvent.change(screen.getByLabelText("Token T-Bank"), {
target: { value: "replacement-token" },
});
fireEvent.click(screen.getByRole("button", { name: "Заменить token" }));
await waitFor(() =>
expect(client.getQueryData(portfolioQueryKey)).toBeUndefined(),
);
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
});
it("commits disconnected status and resets financial queries after deleting", async () => {
server.use(
http.get("*/api/v1/tbank-connection", () =>
HttpResponse.json({
@@ -92,8 +132,98 @@ describe("TbankConnectionSection", () => {
screen.getByRole("button", { name: "Подключить" }),
).toBeInTheDocument(),
);
expect(client.getQueryState(brokerageAccountsQueryKey)?.isInvalidated).toBe(
true,
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
expect(client.getQueryData(portfolioQueryKey)).toBeUndefined();
expect(client.getQueryData(unrelatedQueryKey)).toEqual({ id: "user-1" });
});
it("cancels an in-flight previous-boundary query and ignores its late response", async () => {
const client = seededClient();
let finishPrevious: ((value: { source: string }) => void) | undefined;
let aborted = false;
const previousResponse = new Promise<{ source: string }>((resolve) => {
finishPrevious = resolve;
});
const previousRequest = client
.fetchQuery({
queryKey: operationsQueryKey,
queryFn: ({ signal }) => {
signal.addEventListener("abort", () => {
aborted = true;
});
return previousResponse;
},
})
.catch(() => undefined);
renderSection(client);
await screen.findByText("Не подключено");
fireEvent.change(screen.getByLabelText("Token T-Bank"), {
target: { value: "replacement-token" },
});
fireEvent.click(screen.getByRole("button", { name: "Подключить" }));
await waitFor(() => expect(aborted).toBe(true));
await previousRequest;
finishPrevious?.({ source: "previous-connection" });
await Promise.resolve();
expect(client.getQueryData(operationsQueryKey)).toBeUndefined();
});
it("preserves status and financial cache when a connection update fails", async () => {
server.use(
http.put("*/api/v1/tbank-connection", () =>
HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }),
),
);
const client = seededClient();
renderSection(client);
await screen.findByText("Не подключено");
fireEvent.change(screen.getByLabelText("Token T-Bank"), {
target: { value: "replacement-token" },
});
fireEvent.click(screen.getByRole("button", { name: "Подключить" }));
await screen.findByText(
"T-Bank временно недоступен. Повторите попытку позже.",
);
expect(
screen.getByRole("button", { name: "Подключить" }),
).toBeInTheDocument();
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeDefined();
expect(client.getQueryData(portfolioQueryKey)).toEqual({
source: "previous-connection",
});
});
it("preserves connected status and financial cache when deletion fails", async () => {
server.use(
http.get("*/api/v1/tbank-connection", () =>
HttpResponse.json({
connected: true,
connectedAt: "2026-08-14T00:00:00Z",
}),
),
http.delete("*/api/v1/tbank-connection", () =>
HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }),
),
);
const client = seededClient();
renderSection(client);
fireEvent.click(
await screen.findByRole("button", { name: "Удалить подключение" }),
);
fireEvent.click(await screen.findByRole("button", { name: /^Удалить$/ }));
await screen.findByText("Не удалось выполнить запрос. Попробуйте ещё раз.");
expect(
screen.getByRole("button", { name: "Заменить token" }),
).toBeInTheDocument();
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeDefined();
expect(client.getQueryData(portfolioQueryKey)).toEqual({
source: "previous-connection",
});
});
});