Fix online board time range guard
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { boardDateRedirect } from "./_guards.js";
|
||||
import { boardDateRedirect, boardSearchRedirect } from "./_guards.js";
|
||||
|
||||
describe("boardDateRedirect (clock frozen 2026-05-15)", () => {
|
||||
beforeEach(() => {
|
||||
@@ -40,3 +40,53 @@ describe("boardDateRedirect (clock frozen 2026-05-15)", () => {
|
||||
expect(boardDateRedirect("en-us", "20260101")).toBe("/en-us/onlineboard");
|
||||
});
|
||||
});
|
||||
|
||||
describe("boardSearchRedirect time-range guard", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 4, 15, 12)); // May 15 2026 noon
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("allows a valid time-range suffix", () => {
|
||||
expect(
|
||||
boardSearchRedirect("ru-ru", {
|
||||
date: "20260515",
|
||||
timeFrom: "1400",
|
||||
timeTo: "1800",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("allows 2400 as the upper bound", () => {
|
||||
expect(
|
||||
boardSearchRedirect("ru-ru", {
|
||||
date: "20260515",
|
||||
timeFrom: "2359",
|
||||
timeTo: "2400",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("redirects when the time range is reversed", () => {
|
||||
expect(
|
||||
boardSearchRedirect("ru-ru", {
|
||||
date: "20260515",
|
||||
timeFrom: "1800",
|
||||
timeTo: "1400",
|
||||
}),
|
||||
).toBe("/ru-ru/onlineboard");
|
||||
});
|
||||
|
||||
it("redirects when a time component is outside HHmm bounds", () => {
|
||||
expect(
|
||||
boardSearchRedirect("ru-ru", {
|
||||
date: "20260515",
|
||||
timeFrom: "1460",
|
||||
timeTo: "1800",
|
||||
}),
|
||||
).toBe("/ru-ru/onlineboard");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,3 +17,46 @@ export function boardDateRedirect(locale: string, yyyymmdd: string): string | nu
|
||||
if (!isInBoardWindow(yyyymmdd)) return `/${locale}/onlineboard`;
|
||||
return null;
|
||||
}
|
||||
|
||||
function hhmmToMinutes(value: string): number | null {
|
||||
if (!/^\d{4}$/.test(value)) return null;
|
||||
const hours = Number(value.slice(0, 2));
|
||||
const minutes = Number(value.slice(2, 4));
|
||||
if (hours === 24 && minutes === 0) return 24 * 60;
|
||||
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Angular parity for Online-Board date + optional `Время рейса` URL guards.
|
||||
*
|
||||
* A missing time range is valid. When a range is present, both ends must be
|
||||
* valid HHmm values and `timeFrom` must be strictly before `timeTo`.
|
||||
* `2400` is accepted as the upper bound, matching Angular/moment behavior.
|
||||
*/
|
||||
export function boardSearchRedirect(
|
||||
locale: string,
|
||||
params: { date: string; timeFrom?: string; timeTo?: string },
|
||||
): string | null {
|
||||
const dateRedirect = boardDateRedirect(locale, params.date);
|
||||
if (dateRedirect) return dateRedirect;
|
||||
|
||||
const hasFrom = params.timeFrom !== undefined;
|
||||
const hasTo = params.timeTo !== undefined;
|
||||
if (!hasFrom && !hasTo) return null;
|
||||
if (!hasFrom || !hasTo) return `/${locale}/onlineboard`;
|
||||
|
||||
const timeFrom = params.timeFrom;
|
||||
const timeTo = params.timeTo;
|
||||
if (timeFrom === undefined || timeTo === undefined) {
|
||||
return `/${locale}/onlineboard`;
|
||||
}
|
||||
|
||||
const from = hhmmToMinutes(timeFrom);
|
||||
const to = hhmmToMinutes(timeTo);
|
||||
if (from === null || to === null || from >= to) {
|
||||
return `/${locale}/onlineboard`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import { useCityName } from "@/shared/hooks/useDictionaries.js";
|
||||
import { boardDateRedirect } from "../../_guards.js";
|
||||
import { boardSearchRedirect } from "../../_guards.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardSearchPage.js").then(
|
||||
@@ -35,7 +35,7 @@ export default function ArrivalSearchPage(): JSX.Element {
|
||||
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const redirect = boardDateRedirect(locale, parsed.date);
|
||||
const redirect = boardSearchRedirect(locale, parsed);
|
||||
if (redirect) return <Navigate to={redirect} replace />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import { useCityName } from "@/shared/hooks/useDictionaries.js";
|
||||
import { boardDateRedirect } from "../../_guards.js";
|
||||
import { boardSearchRedirect } from "../../_guards.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardSearchPage.js").then(
|
||||
@@ -35,7 +35,7 @@ export default function DepartureSearchPage(): JSX.Element {
|
||||
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const redirect = boardDateRedirect(locale, parsed.date);
|
||||
const redirect = boardSearchRedirect(locale, parsed);
|
||||
if (redirect) return <Navigate to={redirect} replace />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
|
||||
@@ -105,6 +105,18 @@ describe("RouteSearchPage — date-window guard", () => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/en-us/onlineboard");
|
||||
});
|
||||
|
||||
it("renders normally for a valid time-range suffix", () => {
|
||||
renderWithParams("SVO-LED-20260515-14001800");
|
||||
expect(screen.getByTestId("seo-head")).toBeTruthy();
|
||||
expect(screen.queryByTestId("navigate")).toBeNull();
|
||||
});
|
||||
|
||||
it("redirects when the time range is invalid", () => {
|
||||
renderWithParams("SVO-LED-20260515-18001400");
|
||||
expect(screen.getByTestId("navigate")).toBeTruthy();
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/onlineboard");
|
||||
});
|
||||
|
||||
it("shows 404 for a malformed URL (no date)", () => {
|
||||
renderWithParams("SVO-LED");
|
||||
expect(screen.getByTestId("error-404")).toBeTruthy();
|
||||
|
||||
@@ -15,7 +15,7 @@ import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import { useCityName } from "@/shared/hooks/useDictionaries.js";
|
||||
import { boardDateRedirect } from "../../_guards.js";
|
||||
import { boardSearchRedirect } from "../../_guards.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardSearchPage.js").then(
|
||||
@@ -36,7 +36,7 @@ export default function RouteSearchPage(): JSX.Element {
|
||||
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const redirect = boardDateRedirect(locale, parsed.date);
|
||||
const redirect = boardSearchRedirect(locale, parsed);
|
||||
if (redirect) return <Navigate to={redirect} replace />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
|
||||
Reference in New Issue
Block a user