Files
cfw-autumn/server/tests/unit/usage-windows/getUsageWindowBounds.test.ts

151 lines
4.8 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { EntInterval, getUsageWindowBounds } from "@autumn/shared";
// 2026-06-15T12:34:56Z (a Monday; month index 5 = June)
const NOW = Date.UTC(2026, 5, 15, 12, 34, 56);
describe("getUsageWindowBounds", () => {
test("day window floors to UTC midnight and spans one day", () => {
expect(
getUsageWindowBounds({ interval: EntInterval.Day, now: NOW }),
).toEqual({
windowStartAt: Date.UTC(2026, 5, 15),
windowEndAt: Date.UTC(2026, 5, 16),
});
});
test("week window floors to Monday 00:00 UTC and spans 7 days", () => {
// Wednesday -> floors back to Monday June 15.
const wednesday = Date.UTC(2026, 5, 17, 8, 0, 0);
expect(
getUsageWindowBounds({ interval: EntInterval.Week, now: wednesday }),
).toEqual({
windowStartAt: Date.UTC(2026, 5, 15),
windowEndAt: Date.UTC(2026, 5, 22),
});
});
test("month window floors to the 1st and spans one month", () => {
expect(
getUsageWindowBounds({ interval: EntInterval.Month, now: NOW }),
).toEqual({
windowStartAt: Date.UTC(2026, 5, 1),
windowEndAt: Date.UTC(2026, 6, 1),
});
});
test("quarter window floors to the start of the quarter and spans 3 months", () => {
expect(
getUsageWindowBounds({ interval: EntInterval.Quarter, now: NOW }),
).toEqual({
windowStartAt: Date.UTC(2026, 3, 1),
windowEndAt: Date.UTC(2026, 6, 1),
});
});
test("semi_annual floors to Jan 1 in the first half and spans 6 months", () => {
const may = Date.UTC(2026, 4, 15);
expect(
getUsageWindowBounds({ interval: EntInterval.SemiAnnual, now: may }),
).toEqual({
windowStartAt: Date.UTC(2026, 0, 1),
windowEndAt: Date.UTC(2026, 6, 1),
});
});
test("semi_annual floors to Jul 1 in the second half", () => {
const august = Date.UTC(2026, 7, 15);
expect(
getUsageWindowBounds({ interval: EntInterval.SemiAnnual, now: august }),
).toEqual({
windowStartAt: Date.UTC(2026, 6, 1),
windowEndAt: Date.UTC(2027, 0, 1),
});
});
test("year window floors to Jan 1 and spans one year", () => {
expect(
getUsageWindowBounds({ interval: EntInterval.Year, now: NOW }),
).toEqual({
windowStartAt: Date.UTC(2026, 0, 1),
windowEndAt: Date.UTC(2027, 0, 1),
});
});
test("lifetime never resets", () => {
expect(
getUsageWindowBounds({ interval: EntInterval.Lifetime, now: NOW }),
).toEqual({ windowStartAt: 0, windowEndAt: Number.MAX_SAFE_INTEGER });
});
test("is deterministic across the same window", () => {
expect(
getUsageWindowBounds({ interval: EntInterval.Month, now: NOW }),
).toEqual(
getUsageWindowBounds({ interval: EntInterval.Month, now: NOW + 1000 }),
);
});
test("aligns a day window to the anchor's time-of-day, not UTC midnight", () => {
const anchor = Date.UTC(2026, 0, 9, 15, 30, 0); // 15:30, not midnight
const { windowStartAt, windowEndAt } = getUsageWindowBounds({
interval: EntInterval.Day,
now: NOW,
anchor,
});
const calendar = getUsageWindowBounds({
interval: EntInterval.Day,
now: NOW,
});
// Spans one day, contains now, and rolls at the anchor's 15:30.
expect(windowEndAt - windowStartAt).toBe(24 * 60 * 60 * 1000);
expect(windowStartAt).toBeLessThanOrEqual(NOW);
expect(NOW).toBeLessThan(windowEndAt);
expect(new Date(windowStartAt).getUTCHours()).toBe(15);
expect(new Date(windowStartAt).getUTCMinutes()).toBe(30);
expect(windowStartAt).not.toBe(calendar.windowStartAt);
});
test("aligns a month window to the anchor's day-of-month, not the 1st", () => {
const anchor = Date.UTC(2026, 0, 9); // the 9th
const { windowStartAt } = getUsageWindowBounds({
interval: EntInterval.Month,
now: NOW, // June 15
anchor,
});
expect(new Date(windowStartAt).getUTCDate()).toBe(9);
expect(windowStartAt).toBeLessThanOrEqual(NOW);
});
test("lifetime ignores the anchor", () => {
expect(
getUsageWindowBounds({
interval: EntInterval.Lifetime,
now: NOW,
anchor: Date.UTC(2026, 0, 9),
}),
).toEqual({ windowStartAt: 0, windowEndAt: Number.MAX_SAFE_INTEGER });
});
// Day-31 anchor on the 30th of a 30-day month must stay cycle-aligned, not drop to
// the UTC-calendar fallback (which would key a fresh counter mid-cycle).
test("day-31 anchor stays cycle-aligned on the 30th of a 30-day month (no calendar fallback)", () => {
const anchor = Date.UTC(2026, 0, 31, 8, 30, 0); // Jan 31 08:30 UTC
const now = Date.UTC(2026, 3, 30, 9, 0, 0); // Apr 30 09:00 UTC (after the anchor time)
const { windowStartAt, windowEndAt } = getUsageWindowBounds({
interval: EntInterval.Month,
now,
anchor,
});
expect(windowStartAt).toBe(Date.UTC(2026, 3, 30, 8, 30, 0)); // Apr 30 08:30
expect(windowEndAt).toBe(Date.UTC(2026, 4, 31, 8, 30, 0)); // May 31 08:30
expect(windowStartAt).toBeLessThanOrEqual(now);
expect(now).toBeLessThan(windowEndAt);
expect(windowStartAt).not.toBe(Date.UTC(2026, 3, 1)); // not the calendar fallback
});
});