mirror of
https://github.com/aculix/ticket-monitoring.git
synced 2026-10-27 07:15:51 +00:00
A film that is announced but has no showtimes yet answers 400 with code 32, which is exactly the state you are monitoring for before a release. Counting that as a check failure meant pointing District at an upcoming film produced a false BROKEN alert every six minutes until the listing appeared. Code 32 now reports closed and carries a note through to the status endpoint and the daily heartbeat, so a typo'd content id still surfaces rather than looking like a quiet 'not open yet' forever. Other 400s, including the missing-parameter error, still fail loudly.
135 lines
6.1 KiB
JavaScript
135 lines
6.1 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;
|
|
}
|
|
});
|