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/.
92 lines
4.3 KiB
TypeScript
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(/.+/);
|
|
});
|
|
});
|