From f5304e200e15fc6f2acc7c2e51d655ac10da08b9 Mon Sep 17 00:00:00 2001 From: gnezim Date: Tue, 21 Apr 2026 16:50:17 +0300 Subject: [PATCH] Enforce [-1, +14] date-window guard on Online-Board flight route per TZ 4.1.2-R11 Out-of-window dates now redirect to /{locale}/onlineboard instead of rendering stale data. Parse failures (malformed URLs) continue to 404 unchanged. --- .../onlineboard/flight/[params]/page.test.tsx | 110 ++++++++++++++++++ .../onlineboard/flight/[params]/page.tsx | 6 +- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/routes/[lang]/onlineboard/flight/[params]/page.test.tsx diff --git a/src/routes/[lang]/onlineboard/flight/[params]/page.test.tsx b/src/routes/[lang]/onlineboard/flight/[params]/page.test.tsx new file mode 100644 index 00000000..de0ddec5 --- /dev/null +++ b/src/routes/[lang]/onlineboard/flight/[params]/page.test.tsx @@ -0,0 +1,110 @@ +/** + * Tests for the flight number Online-Board route page. + * + * Verifies that out-of-window dates redirect to /{locale}/onlineboard (TZ §4.1.2), + * in-window dates render normally, and malformed URLs still produce 404. + * + * @vitest-environment jsdom + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import React from "react"; + +// Freeze clock: today = 2026-05-15. Board window: 2026-05-14 … 2026-05-29. +beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2026, 4, 15, 12)); +}); +afterEach(() => { + vi.useRealTimers(); +}); + +// --- mocks --- + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ t: (k: string) => k, i18n: { language: "ru" } }), +})); + +vi.mock("@/env/index.js", () => ({ + getEnv: () => ({ PROD_ORIGIN: "https://aeroflot.ru" }), +})); + +vi.mock("@/ui/seo/SeoHead.js", () => ({ + SeoHead: () =>
, +})); + +vi.mock("@/ui/errors/ErrorPage.js", () => ({ + ErrorPage: ({ code }: { code: string }) =>
, +})); + +vi.mock("@/ui/flights/FlightListSkeleton.js", () => ({ + FlightListSkeleton: () =>
, +})); + +vi.mock( + "@/features/online-board/components/OnlineBoardSearchPage.js", + () => ({ OnlineBoardSearchPage: () =>
}), +); + +vi.mock("@/features/online-board/seo.js", () => ({ + buildFlightSearchSeo: () => ({}), +})); + +// Track Navigate calls +const mockNavigate = vi.fn(); +vi.mock("@modern-js/runtime/router", () => ({ + useParams: vi.fn(), + Navigate: ({ to }: { to: string }) => { + mockNavigate(to); + return
; + }, +})); + +import { useParams } from "@modern-js/runtime/router"; +import FlightSearchPage from "./page.js"; + +function renderWithParams(params: string, lang = "ru-ru") { + vi.mocked(useParams).mockReturnValue({ params, lang }); + return render(); +} + +describe("FlightSearchPage — date-window guard", () => { + beforeEach(() => { + mockNavigate.mockReset(); + }); + + it("renders normally for an in-window date (today)", () => { + renderWithParams("SU1-20260515"); + expect(screen.getByTestId("seo-head")).toBeTruthy(); + expect(screen.queryByTestId("navigate")).toBeNull(); + }); + + it("renders normally for the boundary date (today - 1)", () => { + renderWithParams("SU1-20260514"); + expect(screen.queryByTestId("navigate")).toBeNull(); + }); + + it("redirects to /{locale}/onlineboard when date is too late (today + 15)", () => { + renderWithParams("SU1-20260530"); + expect(screen.getByTestId("navigate")).toBeTruthy(); + expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/onlineboard"); + }); + + it("redirects to /{locale}/onlineboard when date is too early (today - 2)", () => { + renderWithParams("SU1-20260513"); + expect(screen.getByTestId("navigate")).toBeTruthy(); + expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/onlineboard"); + }); + + it("uses the route locale in the redirect path", () => { + renderWithParams("SU1-20260530", "en-us"); + expect(mockNavigate).toHaveBeenCalledWith("/en-us/onlineboard"); + }); + + it("shows 404 for a malformed URL (no date)", () => { + renderWithParams("SU1"); + expect(screen.getByTestId("error-404")).toBeTruthy(); + expect(screen.queryByTestId("navigate")).toBeNull(); + }); +}); diff --git a/src/routes/[lang]/onlineboard/flight/[params]/page.tsx b/src/routes/[lang]/onlineboard/flight/[params]/page.tsx index 762fcfcd..2126db5d 100644 --- a/src/routes/[lang]/onlineboard/flight/[params]/page.tsx +++ b/src/routes/[lang]/onlineboard/flight/[params]/page.tsx @@ -6,7 +6,7 @@ */ import { lazy, Suspense } from "react"; -import { useParams } from "@modern-js/runtime/router"; +import { useParams, Navigate } from "@modern-js/runtime/router"; import { useTranslation } from "@/i18n/provider.js"; import { parseFlightUrlParams } from "@/features/online-board/url.js"; import { buildFlightSearchSeo } from "@/features/online-board/seo.js"; @@ -14,6 +14,7 @@ import { SeoHead } from "@/ui/seo/SeoHead.js"; import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js"; import { ErrorPage } from "@/ui/errors/ErrorPage.js"; import { getEnv } from "@/env/index.js"; +import { boardDateRedirect } from "../../_guards.js"; const OnlineBoardSearchPage = lazy(() => import("@/features/online-board/components/OnlineBoardSearchPage.js").then( @@ -30,6 +31,9 @@ export default function FlightSearchPage(): JSX.Element { if (!parsed) return ; + const redirect = boardDateRedirect(locale, parsed.date); + if (redirect) return ; + const canonicalOrigin = getEnv().PROD_ORIGIN; const searchParams = parsed.suffix ? {