Files
ticket-monitoring/test/providers.test.js
T
Aculix Technologies ef23f25ee5 fix: District settings read per field, so DISTRICT_* always wins
A leftover v1 MOVIE_CODE used to switch the whole District block to the old names,
silently discarding DISTRICT_CONTENT_ID even when it was set deliberately. The
monitor then watched the wrong film and still reported 'closed', which is the
failure mode this project exists to avoid.

Each field now prefers the DISTRICT_* value and falls back to its v1 name, an
explicitly emptied value is respected, and any v1 name still in use is warned
about at startup. District is enabled by DISTRICT_CONTENT_ID rather than the
movie code, which is optional: District answers without it, and a film with no
showtimes yet has none.
2026-09-08 15:14:51 +05:30

166 lines
7.7 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { toLocalTime, localDate } from "../src/lib.js";
import * as district from "../src/providers/district.js";
import * as bms from "../src/providers/bookmyshow.js";
const load = (f) => JSON.parse(readFileSync(fileURLToPath(new URL(`./fixtures/${f}`, import.meta.url)), "utf8"));
const CFG = { tzOffsetMinutes: 330, currency: "₹" };
test("toLocalTime converts API UTC timestamps to local clock time", () => {
assert.equal(toLocalTime("2026-08-05T03:30", 330), "9:00 AM"); // verified against the site UI
assert.equal(toLocalTime("2026-08-05T06:30", 330), "12:00 PM"); // noon boundary
assert.equal(toLocalTime("2026-08-05T12:00", 330), "5:30 PM");
assert.equal(toLocalTime("2026-08-05T18:45", 330), "12:15 AM"); // past midnight, clock time only
assert.equal(toLocalTime("2026-08-05T03:30:00", 330), "9:00 AM"); // seconds already present
assert.equal(toLocalTime("2026-08-05T03:30", 0), "3:30 AM"); // other timezones
});
test("localDate rolls over at local midnight, not UTC midnight", () => {
assert.equal(localDate(new Date("2026-08-05T19:00:00Z"), 330), "2026-08-06");
assert.equal(localDate(new Date("2026-08-05T17:00:00Z"), 330), "2026-08-05");
});
// ---------------------------------------------------------------- District
const D_PARAMS = { venueId: "1032362", formatTag: "imax_2d", formatMatch: "IMAX" };
test("district: extracts the target cinema's sessions from a real capture", () => {
const ex = district.extract(load("response-open-date.json"), CFG, D_PARAMS);
assert.equal(ex.cinemaCount, 5);
assert.ok(ex.sessionCount > ex.matched.length);
assert.equal(ex.matched.length, 4);
assert.equal(ex.other.length, 0);
const s = ex.matched[0];
assert.equal(s.sid, "36874");
assert.equal(s.cinema, "PVR Palladium Mall, Thaltej, Ahmedabad");
assert.equal(s.time, "9:00 AM");
assert.equal(s.audi, "SCREEN 9");
assert.equal(s.avail, 103); // District reports exact seat counts
assert.ok(s.tiers.includes("CLASSIC ₹260"));
assert.ok(s.tiers.includes("RECLINER ₹640"));
assert.equal(ex.showDates.at(-1), "2026-08-09");
});
test("district: no venue watches every cinema, no format filter matches everything", () => {
const data = load("response-open-date.json");
const anyVenue = district.extract(data, CFG, { ...D_PARAMS, venueId: "" });
assert.equal(anyVenue.matched.length, 4, "still only IMAX");
assert.equal(anyVenue.other.length, 0);
const anyFormat = district.extract(data, CFG, { venueId: "", formatTag: "", formatMatch: "" });
assert.equal(anyFormat.matched.length, anyFormat.sessionCount);
const elsewhere = district.extract(data, CFG, { ...D_PARAMS, venueId: "125" });
assert.equal(elsewhere.matched.length, 0);
assert.equal(elsewhere.other.length, 4, "IMAX at another venue becomes a secondary line");
});
test("district: tolerates empty payloads", () => {
assert.deepEqual(district.extract({}, CFG, D_PARAMS),
{ matched: [], other: [], cinemaCount: 0, sessionCount: 0, showDates: [] });
});
// ------------------------------------------------------------- BookMyShow
const B_PARAMS = { venueId: "PPAM", formatMatch: "IMAX" };
test("bookmyshow: date codes drop the dashes", () => {
assert.equal(bms.toDateCode("2026-09-25"), "20260925");
});
test("bookmyshow: extracts venue sessions and per-tier availability from a real capture", () => {
const ex = bms.extract(load("bms-open-date.json"), CFG, B_PARAMS);
assert.equal(ex.cinemaCount, 1);
assert.equal(ex.sessionCount, 5);
assert.equal(ex.matched.length, 5);
const s = ex.matched[0];
assert.equal(s.sid, "37929");
assert.equal(s.cinema, "PVR: Palladium Mall, Ahmedabad");
assert.equal(s.time, "08:00 AM"); // already local, so it is passed through untouched
assert.equal(s.format, "IMAX");
assert.equal(s.avail, null, "BMS never exposes exact seat counts");
assert.ok(s.tiers.includes("RECLINER ₹960 (ALMOST FULL)"));
assert.ok(s.tiers.includes("CLASSIC ₹470 (AVAILABLE)"));
});
test("bookmyshow: a different venue code moves sessions to the secondary bucket", () => {
const ex = bms.extract(load("bms-open-date.json"), CFG, { ...B_PARAMS, venueId: "XXXX" });
assert.equal(ex.matched.length, 0);
assert.equal(ex.other.length, 5);
});
test("bookmyshow: a non-matching format filter yields nothing", () => {
const ex = bms.extract(load("bms-open-date.json"), CFG, { ...B_PARAMS, formatMatch: "4DX" });
assert.equal(ex.matched.length, 0);
assert.equal(ex.sessionCount, 5, "sessions are still counted, just not matched");
});
test("bookmyshow: a not-yet-on-sale response has no widgets and extracts to nothing", () => {
const closed = load("bms-closed-date.json");
assert.equal(closed.data.showtimeWidgets, undefined, "this is how 'not open yet' looks");
assert.deepEqual(bms.extract(closed, CFG, B_PARAMS),
{ matched: [], other: [], cinemaCount: 0, sessionCount: 0, showDates: [] });
});
// The 400 branch decides between "waiting for a listing" and "something is wrong",
// so it is worth pinning without a network.
test("district: 400 code 32 is a waiting state, other 400s are failures", async () => {
const realFetch = globalThis.fetch;
const stub = (status, body) => {
globalThis.fetch = async () => ({ status, json: async () => body });
};
const p = { movieCode: "X", contentId: "1", cityKey: "c", lat: "1", lng: "2" };
try {
stub(400, { errors: [], msg: "Content not found", code: 32 });
const waiting = await district.check({}, p, "2026-09-25");
assert.equal(waiting.kind, "closed");
assert.match(waiting.note, /not listed yet/, "the reason must survive for the heartbeat");
stub(400, { errors: [], msg: "Mandatory search params are missing", code: 3 });
await assert.rejects(() => district.check({}, p, "2026-09-25"), /Mandatory search params/,
"a real config error must still raise the failure streak");
stub(500, {});
await assert.rejects(() => district.check({}, p, "2026-09-25"), /HTTP 500/);
stub(204, {});
assert.equal((await district.check({}, p, "2026-09-25")).kind, "closed");
} finally {
globalThis.fetch = realFetch;
}
});
// A leftover v1 name silently overriding a DISTRICT_* value that was set on purpose is
// the worst kind of config bug: the monitor watches the wrong film and still says "closed".
test("config: DISTRICT_* wins per field over leftover v1 names", async () => {
const { loadConfig, legacyNamesInUse } = await import("../src/config.js");
const saved = { ...process.env };
try {
Object.assign(process.env, {
TARGET_DATE: "2026-09-25", NTFY_TOPIC: "t",
MOVIE_CODE: "OLDCODE", CONTENT_ID: "187151", CITY_KEY: "ahmedabad",
LAT: "23.03", LNG: "72.57", CINEMA_ID: "1032362",
DISTRICT_CONTENT_ID: "227928", DISTRICT_CINEMA_ID: "",
BMS_EVENT_CODE: "ET1", BMS_REGION_CODE: "AHD",
});
const d = loadConfig().providers.find((p) => p.key === "district");
assert.equal(d.contentId, "227928", "the explicitly set new name must win");
assert.equal(d.venueId, "", "an explicitly emptied new name must not fall back to the old one");
assert.equal(d.movieCode, "OLDCODE", "fields with no new value still inherit");
assert.ok(legacyNamesInUse().includes("MOVIE_CODE"), "and the leftovers are reported");
delete process.env.DISTRICT_CONTENT_ID;
delete process.env.CONTENT_ID;
const only = loadConfig().providers;
assert.equal(only.find((p) => p.key === "district"), undefined,
"no content id means District is simply not watched");
assert.equal(only.length, 1, "and the other site carries on alone");
} finally {
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
Object.assign(process.env, saved);
}
});