11import { jest } from "@jest/globals" ;
22
3+ const mockLoggerError = jest . fn ( ) ;
4+
5+ jest . unstable_mockModule ( "../utils/logger.js" , ( ) => ( {
6+ default : {
7+ error : mockLoggerError ,
8+ info : jest . fn ( ) ,
9+ warn : jest . fn ( ) ,
10+ debug : jest . fn ( ) ,
11+ } ,
12+ } ) ) ;
13+
14+ jest . unstable_mockModule ( "../services/cacheService.js" , ( ) => ( {
15+ cacheService : {
16+ setNotExists : jest . fn ( async ( ) => true ) ,
17+ delete : jest . fn ( async ( ) => undefined ) ,
18+ } ,
19+ } ) ) ;
20+
321// Use unstable_mockModule for robust ESM mocking of the connection module
422jest . unstable_mockModule ( "../db/connection.js" , ( ) => ( {
523 query : jest . fn ( ) ,
@@ -11,14 +29,26 @@ jest.unstable_mockModule("../db/connection.js", () => ({
1129
1230// Use dynamic imports TO ENSURE mocks are applied BEFORE the module is loaded
1331const { query } = await import ( "../db/connection.js" ) ;
14- const { notificationService } =
15- await import ( "../services/notificationService.js" ) ;
32+ const {
33+ notificationService,
34+ startNotificationCleanupScheduler,
35+ stopNotificationCleanupScheduler,
36+ } = await import ( "../services/notificationService.js" ) ;
1637
1738const mockedQuery = query as jest . MockedFunction < typeof query > ;
1839
1940describe ( "Notification Cleanup Strategy" , ( ) => {
2041 beforeEach ( ( ) => {
2142 jest . clearAllMocks ( ) ;
43+ jest . useRealTimers ( ) ;
44+ stopNotificationCleanupScheduler ( ) ;
45+ delete process . env . NOTIFICATION_CLEANUP_INTERVAL_MS ;
46+ } ) ;
47+
48+ afterEach ( ( ) => {
49+ stopNotificationCleanupScheduler ( ) ;
50+ jest . useRealTimers ( ) ;
51+ delete process . env . NOTIFICATION_CLEANUP_INTERVAL_MS ;
2252 } ) ;
2353
2454 describe ( "deleteOldNotifications" , ( ) => {
@@ -87,6 +117,36 @@ describe("Notification Cleanup Strategy", () => {
87117 } ) ;
88118 } ) ;
89119
120+ describe ( "startNotificationCleanupScheduler" , ( ) => {
121+ it ( "should catch and log thrown cleanup errors without stopping future runs" , async ( ) => {
122+ jest . useFakeTimers ( ) ;
123+ process . env . NOTIFICATION_CLEANUP_INTERVAL_MS = "1000" ;
124+
125+ const deleteOldSpy = jest
126+ . spyOn ( notificationService , "deleteOldNotifications" )
127+ . mockRejectedValueOnce ( new Error ( "cleanup failed" ) )
128+ . mockResolvedValue ( 0 ) ;
129+ const deleteReadSpy = jest
130+ . spyOn ( notificationService , "deleteReadAndArchived" )
131+ . mockResolvedValue ( 0 ) ;
132+
133+ startNotificationCleanupScheduler ( ) ;
134+ for ( let i = 0 ; i < 10 ; i += 1 ) {
135+ await Promise . resolve ( ) ;
136+ }
137+
138+ expect ( mockLoggerError ) . toHaveBeenCalledWith (
139+ "Notification cleanup scheduled run failed" ,
140+ { error : expect . any ( Error ) } ,
141+ ) ;
142+
143+ await jest . advanceTimersByTimeAsync ( 1000 ) ;
144+
145+ expect ( deleteOldSpy ) . toHaveBeenCalledTimes ( 2 ) ;
146+ expect ( deleteReadSpy ) . toHaveBeenCalledTimes ( 1 ) ;
147+ } ) ;
148+ } ) ;
149+
90150 describe ( "archiveNotifications" , ( ) => {
91151 it ( "should set status to archived and read to true for the given ids" , async ( ) => {
92152 mockedQuery . mockResolvedValue ( { rowCount : 2 } as any ) ;
0 commit comments