Skip to content

Commit ca0e90c

Browse files
authored
Merge pull request #40 from Johnpii1/main
[Backend] notificationService cleanup interval lacks a try/catch around its async callback
2 parents fa78058 + 2c728ed commit ca0e90c

2 files changed

Lines changed: 74 additions & 4 deletions

File tree

src/services/notificationService.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,16 @@ export async function runNotificationCleanup(): Promise<void> {
509509
await runCleanup();
510510
}
511511

512+
function runScheduledCleanup(): void {
513+
void (async () => {
514+
try {
515+
await runCleanup();
516+
} catch (error) {
517+
logger.error("Notification cleanup scheduled run failed", { error });
518+
}
519+
})();
520+
}
521+
512522
/**
513523
* Starts a periodic scheduler to clean up old notifications based on retention policy.
514524
*/
@@ -529,10 +539,10 @@ export function startNotificationCleanupScheduler(): void {
529539
);
530540

531541
// Run once immediately on start to clear any backlog
532-
void runCleanup();
542+
runScheduledCleanup();
533543

534544
cleanupInterval = setInterval(() => {
535-
void runCleanup();
545+
runScheduledCleanup();
536546
}, intervalMs);
537547

538548
logger.info("Notification cleanup scheduler started", {

src/tests/notificationCleanup.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
import { 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
422
jest.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
1331
const { 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

1738
const mockedQuery = query as jest.MockedFunction<typeof query>;
1839

1940
describe("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

Comments
 (0)