mirror of
https://github.com/aculix/ticket-monitoring.git
synced 2026-10-27 07:15:51 +00:00
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.
166 lines
7.7 KiB
JavaScript
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);
|
|
}
|
|
});
|