import { MantineProvider } from "@mantine/core"; import { type QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { HttpResponse, http } from "msw"; import { describe, expect, it, vi } from "vitest"; import { apiQueryClient, configureAuthClient } from "@/shared/api"; import { createTestQueryClient } from "@/test/query"; import { server } from "@/test/server"; import { TbankConnectionSection } from "./TbankConnectionSection"; 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) { configureAuthClient({ accessToken: () => "test-token", refresh: async () => true, clear: vi.fn(), }); return render( , ); } function seededClient() { const client = createTestQueryClient(); client.setQueryData(brokerageAccountsQueryKey, [ { id: "account-1", name: "Основной счет", type: "BROKERAGE", openedOn: null, }, ]); client.setQueryData(portfolioQueryKey, { source: "previous-connection" }); client.setQueryData(unrelatedQueryKey, { id: "user-1" }); return client; } describe("TbankConnectionSection", () => { it("commits connection status and resets the complete brokerage query subtree after connecting", async () => { 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 waitFor(() => expect( screen.getByRole("button", { name: "Заменить token" }), ).toBeInTheDocument(), ); expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined(); expect(client.getQueryData(portfolioQueryKey)).toBeUndefined(); expect(client.getQueryData(unrelatedQueryKey)).toEqual({ id: "user-1" }); }); 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({ connected: true, connectedAt: "2026-08-14T00:00:00Z", }), ), ); const client = seededClient(); renderSection(client); fireEvent.click( await screen.findByRole("button", { name: "Удалить подключение" }), ); fireEvent.click(await screen.findByRole("button", { name: /^Удалить$/ })); await waitFor(() => expect( screen.getByRole("button", { name: "Подключить" }), ).toBeInTheDocument(), ); 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", }); }); });