diff --git a/src/features/flights-map/buyTicketUrl.test.ts b/src/features/flights-map/buyTicketUrl.test.ts new file mode 100644 index 00000000..2b5a6b7d --- /dev/null +++ b/src/features/flights-map/buyTicketUrl.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { buildBuyTicketUrl, escapeHtml } from "./buyTicketUrl.js"; + +describe("buildBuyTicketUrl", () => { + it("builds a URL with the correct base", () => { + const url = buildBuyTicketUrl("MOW", "LED", "20260501"); + expect(url.startsWith("https://www.aeroflot.ru/sb/app/ru-ru#/search?")).toBe(true); + }); + + it("includes the routes triple dep.date.arr", () => { + const url = buildBuyTicketUrl("MOW", "LED", "20260501"); + expect(url).toContain("routes=MOW.20260501.LED"); + }); + + it("includes the stable UTM and adults/cabin params", () => { + const url = buildBuyTicketUrl("MOW", "LED", "20260501"); + expect(url).toContain("adults=1"); + expect(url).toContain("cabin=economy"); + expect(url).toContain("autosearch=Y"); + expect(url).toContain("utm_source=aflwebbot"); + }); +}); + +describe("escapeHtml", () => { + it("escapes &, <, >, and double-quote", () => { + expect(escapeHtml("A & B < C > D \"E\"")).toBe( + "A & B < C > D "E"", + ); + }); + + it("returns the input unchanged when no special chars", () => { + expect(escapeHtml("Москва")).toBe("Москва"); + }); +}); diff --git a/src/features/flights-map/buyTicketUrl.ts b/src/features/flights-map/buyTicketUrl.ts new file mode 100644 index 00000000..f96bdec7 --- /dev/null +++ b/src/features/flights-map/buyTicketUrl.ts @@ -0,0 +1,38 @@ +/** + * Buy-ticket URL for the arrival popup (Angular parity: `getLink`). + * + * Base path + fixed params are hardcoded to the Russian-locale Aeroflot + * search page. The routes triple `{dep}.{date}.{arr}` is the only variable + * part of the URL. + */ + +const BASE = "https://www.aeroflot.ru/sb/app/ru-ru#/search"; +const FIXED_PARAMS = + "adults=1&cabin=economy&children=0&infants=0&autosearch=Y" + + "&utm_source=aflwebbot&utm_medium=referral" + + "&utm_campaign=ref_3015_general_rf_button.index__all_flight.map"; + +/** + * @param departure 3-letter city code (e.g. "MOW"). + * @param arrival 3-letter city code (e.g. "LED"). + * @param date YYYYMMDD — typically filter.date or today. + */ +export function buildBuyTicketUrl( + departure: string, + arrival: string, + date: string, +): string { + return `${BASE}?${FIXED_PARAMS}&routes=${departure}.${date}.${arrival}`; +} + +/** + * Minimal HTML escape for popup content — prevents injection via + * city names containing `& < > "`. + */ +export function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +}