Skip to content

Commit c45603c

Browse files
authored
test(background-refresh-timezone-boundaries): timezone normalization and calendar boundary alignment for BackgroundRefresh (JhaSourav07#3297)
## Description Fixes JhaSourav07#2920 - Created `services/github/background-refresh.timezone-boundaries.test.ts` as an isolated unit test file for `isStale()` under timezone and calendar boundary conditions - Fixes `Date.now()` to `2024-03-10T12:00:00.000Z` (US DST spring-forward day) via `vi.setSystemTime()` so all assertions are deterministic - Resets the private singleton slot in `beforeEach` to prevent state leakage across tests - Mocks `lib/github` to keep tests fully isolated from network calls - Test 1: UTC (`Z`) suffix — 9-minute-old entry correctly identified as not stale - Test 2: EST (`-05:00`) offset — same UTC moment re-expressed in Eastern time resolves identically, confirming negative offset normalisation - Test 3: IST (`+05:30`) half-hour offset — confirms non-whole-hour offsets don't corrupt minute arithmetic - Test 4: JST (`+09:00`) offset — 11-minute-old entry crosses the 10-minute threshold and is correctly marked stale - Test 5: Leap year (`2024-02-29`) parses without error and is stale; DST spring-forward moment (`2024-03-10T02:00:00-05:00`) also parses correctly and is stale — each sub-assertion includes a `Number.isFinite` guard to distinguish elapsed-time staleness from catch-block staleness ## Pillar - 🛠️ Other (Bug fix, refactoring, docs) ## Checklist before requesting a review: - I have read the `CONTRIBUTING.md` file. - I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - I have run `npm run format` and `npm run lint` locally and resolved all errors. - My commits follow the Conventional Commits format. - I have starred the repo. - I have made sure that i have only one commit to merge in this PR.
2 parents ed389f8 + 51d6460 commit c45603c

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { BackgroundRefresh } from './background-refresh';
3+
4+
vi.mock('../../lib/github', () => ({
5+
getFullDashboardData: vi.fn().mockResolvedValue({}),
6+
}));
7+
8+
function freshInstance(): BackgroundRefresh {
9+
// Reset the private static so getInstance() constructs a new object
10+
(BackgroundRefresh as unknown as { instance: BackgroundRefresh | undefined }).instance =
11+
undefined;
12+
return BackgroundRefresh.getInstance();
13+
}
14+
15+
// Fixed "now" anchored to 2024-03-10T12:00:00.000Z — this is the US DST
16+
// spring-forward day (clocks moved forward at 02:00 EST → 03:00 EDT), which makes it the ideal anchor for DST boundary assertions.
17+
const NOW_ISO = '2024-03-10T12:00:00.000Z';
18+
const NOW_MS = new Date(NOW_ISO).getTime(); // 1710072000000
19+
20+
// STALE_THRESHOLD_MS = 10 * 60 * 1000 = 600_000 ms (from source)
21+
const THRESHOLD_MS = 10 * 60 * 1000;
22+
23+
describe('BackgroundRefresh — Timezone Normalization & Calendar Data Boundary Alignment', () => {
24+
let instance: BackgroundRefresh;
25+
26+
beforeEach(() => {
27+
vi.useFakeTimers();
28+
vi.setSystemTime(new Date(NOW_ISO));
29+
instance = freshInstance();
30+
vi.spyOn(console, 'info').mockImplementation(() => {});
31+
vi.spyOn(console, 'error').mockImplementation(() => {});
32+
});
33+
34+
afterEach(() => {
35+
instance.reset();
36+
vi.useRealTimers();
37+
vi.restoreAllMocks();
38+
});
39+
40+
// ── Test 1 ────────────────────────────────────────────────────────────────
41+
// UTC suffix (Z): a timestamp 9 minutes ago must NOT be stale.
42+
// 9 min = 540_000 ms < 600_000 ms threshold.
43+
it('treats a UTC-suffixed timestamp 9 minutes ago as not stale', () => {
44+
// 2024-03-10T11:51:00.000Z is exactly 9 minutes before NOW
45+
const nineMinutesAgoUTC = new Date(NOW_MS - 9 * 60 * 1000).toISOString();
46+
47+
expect(instance.isStale(nineMinutesAgoUTC)).toBe(false);
48+
});
49+
50+
// ── Test 2 ────────────────────────────────────────────────────────────────
51+
// EST offset (-05:00): the same UTC moment re-expressed in Eastern Standard
52+
// Time must resolve to an identical staleness result, confirming that the parser correctly normalises the offset before comparing.
53+
it('correctly normalises an EST (-05:00) offset and marks a 9-minute-old entry as not stale', () => {
54+
// 2024-03-10T11:51:00Z == 2024-03-10T06:51:00-05:00
55+
const nineMinutesAgoEST = '2024-03-10T06:51:00-05:00';
56+
57+
// Sanity-check: both representations decode to the same epoch ms
58+
expect(new Date(nineMinutesAgoEST).getTime()).toBe(NOW_MS - 9 * 60 * 1000);
59+
60+
expect(instance.isStale(nineMinutesAgoEST)).toBe(false);
61+
});
62+
63+
// ── Test 3 ────────────────────────────────────────────────────────────────
64+
// IST offset (+05:30): the same UTC moment expressed in Indian Standard Time.
65+
// Half-hour offsets are less common; this guards against off-by-one in minute arithmetic when the offset is not a whole hour.
66+
it('correctly normalises an IST (+05:30) offset and marks a 9-minute-old entry as not stale', () => {
67+
// 2024-03-10T11:51:00Z == 2024-03-10T17:21:00+05:30
68+
const nineMinutesAgoIST = '2024-03-10T17:21:00+05:30';
69+
70+
expect(new Date(nineMinutesAgoIST).getTime()).toBe(NOW_MS - 9 * 60 * 1000);
71+
72+
expect(instance.isStale(nineMinutesAgoIST)).toBe(false);
73+
});
74+
75+
// ── Test 4 ────────────────────────────────────────────────────────────────
76+
// JST offset (+09:00): a timestamp 11 minutes ago expressed in Japan Standard
77+
// Time must be considered stale (11 min = 660_000 ms > 600_000 ms threshold).
78+
it('correctly normalises a JST (+09:00) offset and marks an 11-minute-old entry as stale', () => {
79+
// 2024-03-10T11:49:00Z == 2024-03-10T20:49:00+09:00 (11 min before NOW)
80+
const elevenMinutesAgoJST = '2024-03-10T20:49:00+09:00';
81+
82+
expect(new Date(elevenMinutesAgoJST).getTime()).toBe(NOW_MS - 11 * 60 * 1000);
83+
84+
expect(instance.isStale(elevenMinutesAgoJST)).toBe(true);
85+
});
86+
87+
// ── Test 5 ────────────────────────────────────────────────────────────────
88+
// Leap year boundary (Feb 29 2024) + DST transition context:
89+
// • Confirms that Feb 29 parses without throwing (no gap in the grid).
90+
// • The DST spring-forward in US/Eastern occurred at 02:00 EST on 2024-03-10 — exactly our NOW anchor — so a lastSyncedAt anchored to the spring-forward moment (2024-03-10T02:00:00-05:00, i.e. the last second before the clock jumped) is always stale relative to noon UTC on the same day (~10 hours elapsed >> 600_000 ms threshold).
91+
// • Two sub-assertions guard both boundary types in one test.
92+
it('parses a Feb 29 leap-year date without error and marks it stale; also marks the DST spring-forward moment as stale', () => {
93+
// ── Leap year sub-assertion ──────────────────────────────────────────────
94+
// 2024-02-29T23:55:00Z is ~9.5 days before NOW — unambiguously stale.
95+
const leapDayTimestamp = '2024-02-29T23:55:00Z';
96+
const leapDayMs = new Date(leapDayTimestamp).getTime();
97+
98+
// Confirm the date parsed to a real epoch value (not NaN), meaning the leap-year date did not cause a parse error that would trigger the catch.
99+
expect(Number.isFinite(leapDayMs)).toBe(true);
100+
expect(NOW_MS - leapDayMs).toBeGreaterThan(THRESHOLD_MS);
101+
expect(instance.isStale(leapDayTimestamp)).toBe(true);
102+
103+
// ── DST transition sub-assertion ─────────────────────────────────────────
104+
// The exact spring-forward moment in US/Eastern: clocks skipped from 02:00 EST to 03:00 EDT on 2024-03-10, so 2024-03-10T02:00:00-05:00 (= 2024-03-10T07:00:00Z) is ~5 hours before our NOW anchor.
105+
const dstTransitionTimestamp = '2024-03-10T02:00:00-05:00';
106+
const dstMs = new Date(dstTransitionTimestamp).getTime();
107+
108+
expect(Number.isFinite(dstMs)).toBe(true);
109+
expect(NOW_MS - dstMs).toBeGreaterThan(THRESHOLD_MS);
110+
expect(instance.isStale(dstTransitionTimestamp)).toBe(true);
111+
});
112+
});

0 commit comments

Comments
 (0)