// @vitest-environment jsdom import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { afterEach, expect, it, vi } from "vitest"; import { api } from "../api"; // The session stop is a keepalive beacon: nothing answers it on screen, so a URL that no longer // matches the server's route fails silently — every transcode the player leaves runs on until the // idle reaper. The route is read from the server's source, as the other contract tests do. afterEach(() => vi.restoreAllMocks()); function serverRoute(): RegExp { // Anchored to this file, not the working directory, so the test holds wherever vitest starts. const source = readFileSync( resolve(__dirname, "../../../../backend/app/routes/plex/playback.py"), "utf8", ); const prefix = /APIRouter\(prefix="([^"]+)"/.exec(source)?.[1]; const path = /@router\.post\("(\/stream\/[^"]*\/stop)"\)/.exec(source)?.[1]; // If either is missing the route was renamed or moved — re-point this test, the rule stands. expect(prefix, "router prefix").toBeDefined(); expect(path, "the session stop route").toBeDefined(); // Each path parameter becomes a group of its own NAME: two ids that swapped places still match a // pattern of anonymous wildcards, and the server then finds no session to stop. const pattern = (prefix! + path!).replace(/\{(\w+)\}/g, "(?<$1>[^/]+)"); return new RegExp(`^${pattern}$`); } it("sends the session stop to the route the server serves it on", () => { const fetch = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(null)); api.plexStopSession("12345", "a1b2c3"); const url = String(fetch.mock.calls[0]![0]); expect(url.match(serverRoute())?.groups).toEqual({ rating_key: "12345", session_id: "a1b2c3" }); expect(fetch.mock.calls[0]![1]).toMatchObject({ method: "POST", keepalive: true }); });