/** * Validates that the API layer can parse real responses captured from the * test environment on 2026-04-17. Each test wires a real fixture into a * mock fetch, then runs the production API function and asserts the * parsed value has the structure the UI relies on. * * These tests catch drift between the declared TS types and the actual * API contract. To refresh fixtures: `AEROFLOT_API_AUTH=user:pass * ./scripts/fetch-api-fixtures.sh`. */ import { describe, it, expect, vi } from "vitest"; import { ApiClient } from "@/shared/api/client"; import { searchFlights, getFlightDetails, getCalendarDays } from "@/features/online-board/api"; import { searchSchedule, getScheduleDetails, getScheduleCalendarDays } from "@/features/schedule/api"; import { searchDestinations, getFlightsMapCalendar } from "@/features/flights-map/api"; import { getPopularRequests } from "@/features/popular-requests/api"; import { fetchDictionaries } from "@/shared/dictionaries/api"; import { fixtures } from "../fixtures/api"; function clientWith(body: unknown, status = 200): { client: ApiClient; fetchMock: ReturnType } { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" }, }), ); const client = new ApiClient({ baseUrl: "https://api.test.com", locale: "ru", fetchImpl: fetchMock, retry: { maxRetries: 0 }, }); return { client, fetchMock }; } describe("online-board API against real fixtures", () => { it("searchFlights parses a by-route response with multiple flights", async () => { const { client } = clientWith(fixtures.boardByRoute); const result = await searchFlights(client, { dateFrom: "2026-04-17", dateTo: "2026-04-18", departure: "SVO", arrival: "LED", }); expect(result.data.routes.length).toBeGreaterThan(0); expect(result.data.partners).toBeInstanceOf(Array); const first = result.data.routes[0]!; expect(first.routeType).toMatch(/Direct|MultiLeg/); expect(first.flightId.carrier).toBe("SU"); }); it("searchFlights parses a by-flight-number response", async () => { const { client } = clientWith(fixtures.boardByFlight); const result = await searchFlights(client, { dateFrom: "2026-04-17", dateTo: "2026-04-18", flightNumber: "SU6951", }); expect(result.data.routes[0]!.flightId.flightNumber).toBe("6951"); }); it("getFlightDetails parses the onlineboard/details response", async () => { const { client } = clientWith(fixtures.onlineboardDetails); const result = await getFlightDetails(client, { flights: "SU6951", dates: "2026-04-17", }); expect(result.data.routes.length).toBeGreaterThan(0); }); it("getCalendarDays parses bitmask into date list", async () => { const { client } = clientWith(fixtures.boardDaysFlight); const days = await getCalendarDays(client, { date: "2026-04-17", searchType: "flight", flightNumber: "SU6951", }); expect(days.length).toBeGreaterThan(0); expect(days[0]).toMatch(/^\d{8}$/); }); }); describe("schedule API against real fixtures", () => { it("searchSchedule parses an array response", async () => { const { client } = clientWith(fixtures.scheduleSearch); const result = await searchSchedule(client, { departure: "SVO", arrival: "LED", dateFrom: "2026-04-18", dateTo: "2026-04-22", }); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeGreaterThan(0); }); it("getScheduleDetails parses a details response", async () => { const { client } = clientWith(fixtures.scheduleDetails); const result = await getScheduleDetails(client, { flights: ["SU6951"], dates: ["2026-04-17"], departure: "SVO", arrival: "LED", }); expect(result.data).toBeDefined(); }); it("getScheduleCalendarDays parses the days response", async () => { const { client } = clientWith(fixtures.scheduleDaysRoute); const days = await getScheduleCalendarDays(client, { date: "20260417", departure: "SVO", arrival: "LED", connections: false, }); expect(days).toBeInstanceOf(Array); }); }); describe("flights-map API against real fixtures", () => { it("searchDestinations parses the destinations response", async () => { const { client } = clientWith(fixtures.destinationsFrom); const result = await searchDestinations(client, { departure: "SVO", dateFrom: "2026-04-18", dateTo: "2026-04-22", connections: 0, }); expect(result.data.routes).toBeInstanceOf(Array); expect(result.data.routes.length).toBeGreaterThan(0); const first = result.data.routes[0]!; expect(first.route).toBeInstanceOf(Array); expect(typeof first.isDirect).toBe("boolean"); }); it("getFlightsMapCalendar parses the days bitmap", async () => { const { client } = clientWith(fixtures.mapDaysRoute); const days = await getFlightsMapCalendar(client, { date: "20260417", departure: "SVO", arrival: "LED", connections: false, }); expect(days).toBeInstanceOf(Array); }); }); describe("popular-requests API against real fixture", () => { it("getPopularRequests returns a list of discriminated requests", async () => { const { client } = clientWith(fixtures.popularRequests); const result = await getPopularRequests(client); expect(result.length).toBeGreaterThan(0); for (const req of result) { expect(["FlightNumber", "Route", "RouteWithBack", "Departure", "Arrival"]).toContain(req.mode); expect(["Schedule", "Onlineboard"]).toContain(req.type); } }); }); describe("dictionaries API against real fixtures", () => { it("fetchDictionaries parses all four endpoints", async () => { const calls = [ fixtures.dictionaryRegions, fixtures.dictionaryCountries, fixtures.dictionaryCities, fixtures.dictionaryAirports, ]; let i = 0; const fetchMock = vi.fn().mockImplementation(() => Promise.resolve( new Response(JSON.stringify(calls[i++]!), { status: 200, headers: { "Content-Type": "application/json" }, }), ), ); const client = new ApiClient({ baseUrl: "https://api.test.com", locale: "ru", fetchImpl: fetchMock, retry: { maxRetries: 0 }, }); const result = await fetchDictionaries(client); expect(result.regions.length).toBeGreaterThan(0); expect(result.countries.length).toBeGreaterThan(0); expect(result.cities.length).toBeGreaterThan(0); expect(result.airports.length).toBeGreaterThan(0); expect(result.regions[0]!.title).toBeTypeOf("object"); expect(result.airports[0]!.code).toBeTypeOf("string"); }); });