T-HUB/packages/scheduling/src/timezone.test.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

92 lines
4.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
canonicalTimezone,
currentTzdataVersion,
isValidTimezone,
offsetMs,
toInstantMs,
wallOfInstant,
} from "./timezone.js";
import { InvalidInstantError, InvalidTimezoneError } from "./errors.js";
describe("canonicalTimezone", () => {
it("accepts and canonicalizes IANA ids", () => {
expect(canonicalTimezone("America/New_York")).toBe("America/New_York");
expect(canonicalTimezone("US/Eastern")).toBe("America/New_York");
expect(canonicalTimezone(" Europe/Paris ")).toBe("Europe/Paris");
expect(canonicalTimezone("Etc/GMT+5")).toBe("Etc/GMT+5");
});
it("accepts UTC in several spellings", () => {
expect(canonicalTimezone("UTC")).toBe("UTC");
expect(canonicalTimezone("gmt")).toBe("UTC");
expect(canonicalTimezone("Zulu")).toBe("UTC");
});
it("rejects unknown zones", () => {
expect(() => canonicalTimezone("America/Nowhere")).toThrow(InvalidTimezoneError);
expect(() => canonicalTimezone("Mars/Olympus")).toThrow(InvalidTimezoneError);
});
it("rejects offset spellings and empty input", () => {
for (const bad of ["UTC+3", "+05:30", "-08:00", "5", ""]) {
expect(() => canonicalTimezone(bad)).toThrow(InvalidTimezoneError);
}
});
it("rejects server-local idioms — a schedule zone must be explicit", () => {
for (const bad of ["local", "system", "server", "SERVER", "server-local", "machine"]) {
expect(() => canonicalTimezone(bad)).toThrow(InvalidTimezoneError);
}
});
it("isValidTimezone mirrors canonicalTimezone", () => {
expect(isValidTimezone("Europe/Berlin")).toBe(true);
expect(isValidTimezone("local")).toBe(false);
expect(isValidTimezone("America/Nowhere")).toBe(false);
});
});
describe("wallOfInstant / offsetMs", () => {
it("reads New York offsets across the 2024 transitions", () => {
expect(offsetMs(Date.parse("2024-03-10T06:59:59Z"), "America/New_York")).toBe(-5 * 3600_000);
expect(offsetMs(Date.parse("2024-03-10T07:00:00Z"), "America/New_York")).toBe(-4 * 3600_000);
expect(offsetMs(Date.parse("2024-11-03T05:59:59Z"), "America/New_York")).toBe(-4 * 3600_000);
expect(offsetMs(Date.parse("2024-11-03T06:00:00Z"), "America/New_York")).toBe(-5 * 3600_000);
});
it("reads half-hour and 45-minute zones", () => {
expect(offsetMs(Date.parse("2024-06-01T12:00:00Z"), "Asia/Kolkata")).toBe(5.5 * 3600_000);
expect(offsetMs(Date.parse("2024-06-01T12:00:00Z"), "Asia/Kathmandu")).toBe(5.75 * 3600_000);
});
it("sees Pacific/Apia's 2011 whole-day skip", () => {
const before = wallOfInstant(Date.parse("2011-12-29T09:59:59Z"), "Pacific/Apia");
const after = wallOfInstant(Date.parse("2011-12-30T10:00:00Z"), "Pacific/Apia");
expect(`${before.year}-${before.month}-${before.day}`).toBe("2011-12-28");
expect(`${after.year}-${after.month}-${after.day}`).toBe("2011-12-31");
});
it("returns UTC fields without a formatter", () => {
const wall = wallOfInstant(Date.parse("2024-03-10T02:30:00Z"), "UTC");
expect([wall.year, wall.month, wall.day, wall.hour, wall.minute]).toEqual([2024, 3, 10, 2, 30]);
});
});
describe("toInstantMs", () => {
it("accepts dates, numbers and offset-carrying ISO strings", () => {
expect(toInstantMs(new Date("2024-03-10T02:30:00Z"))).toBe(Date.parse("2024-03-10T02:30:00Z"));
expect(toInstantMs(1710038400000)).toBe(1710038400000);
expect(toInstantMs("2024-03-10T02:30:00Z")).toBe(Date.parse("2024-03-10T02:30:00Z"));
expect(toInstantMs("2024-03-10T04:30:00+02:00")).toBe(Date.parse("2024-03-10T02:30:00Z"));
});
it("rejects offset-less datetimes that would parse server-local", () => {
for (const bad of ["2024-03-10T02:30:00", "2024-03-10 02:30", "2024-03-10T02:30:00.500"]) {
expect(() => toInstantMs(bad)).toThrow(InvalidInstantError);
}
});
it("rejects NaN/Infinity/garbage", () => {
expect(() => toInstantMs(new Date(NaN))).toThrow(InvalidInstantError);
expect(() => toInstantMs(Number.NaN)).toThrow(InvalidInstantError);
expect(() => toInstantMs(Number.POSITIVE_INFINITY)).toThrow(InvalidInstantError);
expect(() => toInstantMs("not a date")).toThrow(InvalidInstantError);
});
});
describe("currentTzdataVersion", () => {
it("returns a non-empty version string", () => {
expect(currentTzdataVersion()).toMatch(/.+/);
});
});