Skip to content

Commit a85890e

Browse files
authored
test: add unit tests for background-refresh service (JhaSourav07#3548)
## Description This PR introduces unit tests for the BackgroundRefresh service class. It covers singleton structure, cache staleness checks, username sanitization, concurrent job lock prevention, and failure state resolution. It also fixes a bug in isStale where invalid date strings caused the cache to never refresh. Fixes JhaSourav07#3345 ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview N/A (Utility/Unit Tests only) ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally. - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors. - [x] My commits follow the Conventional Commits format. - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [ ] The SVG output matches the CommitPulse "premium quality" aesthetic standard. - [ ] (Recommended) I joined the CommitPulse Discord community.
2 parents 7da058e + c8cb5e3 commit a85890e

3 files changed

Lines changed: 95 additions & 1 deletion

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { BackgroundRefresh } from './background-refresh';
3+
import { getFullDashboardData } from '../../lib/github';
4+
5+
vi.mock('../../lib/github', () => ({
6+
getFullDashboardData: vi.fn(),
7+
}));
8+
9+
describe('BackgroundRefresh Unit Tests', () => {
10+
let service: BackgroundRefresh;
11+
12+
beforeEach(() => {
13+
service = BackgroundRefresh.getInstance();
14+
service.reset();
15+
vi.clearAllMocks();
16+
});
17+
18+
it('behaves as a singleton and returns the same instance', () => {
19+
const anotherInstance = BackgroundRefresh.getInstance();
20+
expect(service).toBe(anotherInstance);
21+
});
22+
23+
describe('isStale', () => {
24+
it('returns true when lastSyncedAt is undefined', () => {
25+
expect(service.isStale(undefined)).toBe(true);
26+
});
27+
28+
it('returns true when lastSyncedAt is an invalid date string', () => {
29+
expect(service.isStale('invalid-date')).toBe(true);
30+
});
31+
32+
it('returns true when lastSyncedAt is older than 10 minutes', () => {
33+
const elevenMinutesAgo = new Date(Date.now() - 11 * 60 * 1000).toISOString();
34+
expect(service.isStale(elevenMinutesAgo)).toBe(true);
35+
});
36+
37+
it('returns false when lastSyncedAt is within 10 minutes', () => {
38+
const nineMinutesAgo = new Date(Date.now() - 9 * 60 * 1000).toISOString();
39+
expect(service.isStale(nineMinutesAgo)).toBe(false);
40+
});
41+
});
42+
43+
describe('triggerRefresh', () => {
44+
it('sanitizes username (trims and converts to lowercase) and sets job active', async () => {
45+
vi.mocked(getFullDashboardData).mockResolvedValue({} as never);
46+
47+
service.triggerRefresh(' TestUser ');
48+
49+
expect(service.isJobActive('testuser')).toBe(true);
50+
expect(getFullDashboardData).toHaveBeenCalledWith(' TestUser ', { bypassCache: true });
51+
52+
// Allow promise microtask to resolve
53+
await new Promise((resolve) => setTimeout(resolve, 0));
54+
expect(service.isJobActive('testuser')).toBe(false);
55+
});
56+
57+
it('prevents concurrent duplicate jobs for the same user', () => {
58+
vi.mocked(getFullDashboardData).mockReturnValue(new Promise(() => {})); // Never resolves
59+
60+
service.triggerRefresh('user1');
61+
service.triggerRefresh('user1');
62+
63+
expect(service.isJobActive('user1')).toBe(true);
64+
expect(getFullDashboardData).toHaveBeenCalledTimes(1);
65+
});
66+
67+
it('removes the user from active jobs on failure', async () => {
68+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
69+
vi.mocked(getFullDashboardData).mockRejectedValue(new Error('Network error'));
70+
71+
service.triggerRefresh('user-fail');
72+
expect(service.isJobActive('user-fail')).toBe(true);
73+
74+
// Allow promise microtask to reject and run finally block
75+
await new Promise((resolve) => setTimeout(resolve, 0));
76+
77+
expect(service.isJobActive('user-fail')).toBe(false);
78+
expect(consoleErrorSpy).toHaveBeenCalled();
79+
consoleErrorSpy.mockRestore();
80+
});
81+
});
82+
83+
describe('reset', () => {
84+
it('clears all currently active jobs', () => {
85+
vi.mocked(getFullDashboardData).mockReturnValue(new Promise(() => {})); // Never resolves
86+
service.triggerRefresh('active-user');
87+
expect(service.isJobActive('active-user')).toBe(true);
88+
89+
service.reset();
90+
expect(service.isJobActive('active-user')).toBe(false);
91+
});
92+
});
93+
});

services/github/background-refresh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export class BackgroundRefresh {
2525

2626
try {
2727
const lastSyncTime = new Date(lastSyncedAt).getTime();
28+
if (isNaN(lastSyncTime)) return true;
2829
return Date.now() - lastSyncTime > STALE_THRESHOLD_MS;
2930
} catch {
3031
return true;

services/github/background-refresh.type-compiler.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ describe('BackgroundRefresh type compiler validation', () => {
4141

4242
expect(backgroundRefresh.isStale(staleDate)).toBe(true);
4343
expect(backgroundRefresh.isStale(freshDate)).toBe(false);
44-
expect(backgroundRefresh.isStale('invalid-date')).toBe(false);
44+
expect(backgroundRefresh.isStale('invalid-date')).toBe(true);
4545
});
4646
});

0 commit comments

Comments
 (0)