import { describe, expect, it } from "vitest"; import { formatDateTimeLocal, parseLocalDateTime, resolveZonedDateTime, shiftTimestampByPeriods, zonedDateTimeCandidates, } from "../../src/otp/time"; describe("TOTP time travel", () => { it("parses exact UTC wall-clock values", () => { const resolved = resolveZonedDateTime("2025-08-19T12:34:56", "UTC"); expect(new Date(resolved.timestampMs).toISOString()).toBe( "2025-08-19T12:34:56.000Z", ); expect(formatDateTimeLocal(resolved.timestampMs, "UTC")).toBe( "2025-08-19T12:34:56", ); expect( resolveZonedDateTime("2025-08-19T12:34:56.125", "UTC").timestampMs, ).toBe(Date.UTC(2025, 7, 19, 12, 34, 56, 125)); }); it("respects seasonal IANA timezone offsets", () => { expect( new Date( resolveZonedDateTime("2024-01-15T12:00:00", "America/New_York") .timestampMs, ).toISOString(), ).toBe("2024-01-15T17:00:00.000Z"); expect( new Date( resolveZonedDateTime("2024-07-15T12:00:00", "America/New_York") .timestampMs, ).toISOString(), ).toBe("2024-07-15T16:00:00.000Z"); }); it("rejects nonexistent DST times and exposes both repeated times", () => { expect(() => resolveZonedDateTime("2024-03-10T02:30:00", "America/New_York"), ).toThrow(/does not exist/iu); const candidates = zonedDateTimeCandidates( "2024-11-03T01:30:00", "America/New_York", ); expect(candidates.map((value) => new Date(value).toISOString())).toEqual([ "2024-11-03T05:30:00.000Z", "2024-11-03T06:30:00.000Z", ]); expect( new Date( resolveZonedDateTime("2024-11-03T01:30:00", "America/New_York", "later") .timestampMs, ).toISOString(), ).toBe("2024-11-03T06:30:00.000Z"); }); it("handles non-hour timezone offsets", () => { expect( new Date( resolveZonedDateTime("2025-08-19T12:00:00", "Asia/Kathmandu") .timestampMs, ).toISOString(), ).toBe("2025-08-19T06:15:00.000Z"); }); it("validates calendar values, timezone names and bounded period shifts", () => { expect(() => parseLocalDateTime("2025-02-29T12:00:00")).toThrow( /valid calendar/iu, ); expect(() => resolveZonedDateTime("2025-01-01T00:00:00", "Mars/Base"), ).toThrow(/timezone/iu); expect(shiftTimestampByPeriods(90_000, 30, -2)).toBe(30_000); expect(() => shiftTimestampByPeriods(1_000, 30, -1)).toThrow( /outside supported time/iu, ); }); });