Skip to content

Commit 2ee8a1f

Browse files
committed
add test case
1 parent fa8f211 commit 2ee8a1f

File tree

1 file changed

+135
-0
lines changed

1 file changed

+135
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import GrouperWorker from '../src';
2+
import type { GroupWorkerTask } from '../types/group-worker-task';
3+
import type { ErrorsCatcherType, EventAddons, EventData } from '@hawk.so/types';
4+
5+
jest.mock('amqplib');
6+
7+
/**
8+
* Ensure DatabaseController constructor sees some connection string
9+
* so it does not throw before we stub database dependencies on worker instance.
10+
*/
11+
process.env.MONGO_EVENTS_DATABASE_URI = process.env.MONGO_EVENTS_DATABASE_URI || 'mongodb://127.0.0.1:27017/hawk-test';
12+
process.env.MONGO_ACCOUNTS_DATABASE_URI = process.env.MONGO_ACCOUNTS_DATABASE_URI || 'mongodb://127.0.0.1:27017/hawk-test';
13+
14+
/**
15+
* Generates minimal task for testing
16+
*
17+
* @param event - allows to override some event properties in generated task
18+
*/
19+
function generateTask(event: Partial<EventData<EventAddons>> = undefined): GroupWorkerTask<ErrorsCatcherType> {
20+
return {
21+
projectId: '5d206f7f9aaf7c0071d64596',
22+
catcherType: 'errors/javascript',
23+
timestamp: Math.floor(Date.now() / 1000),
24+
payload: Object.assign({
25+
title: 'Duplicate key recovery test',
26+
backtrace: [],
27+
user: {
28+
id: 'user-1',
29+
},
30+
context: {},
31+
addons: {},
32+
}, event),
33+
};
34+
}
35+
36+
describe('GrouperWorker duplicate key error recovery (cache interaction)', () => {
37+
let worker: GrouperWorker;
38+
39+
beforeEach(() => {
40+
worker = new GrouperWorker();
41+
42+
/**
43+
* Prepare real in-memory cache controller
44+
*/
45+
(worker as unknown as { prepareCache: () => void }).prepareCache();
46+
47+
/**
48+
* Stub external dependencies that are not relevant for this unit test
49+
*/
50+
(worker as any).eventsDb = {};
51+
(worker as any).accountsDb = {};
52+
(worker as any).redis = {
53+
checkOrSetlockEventForAffectedUsersIncrement: jest.fn().mockResolvedValue(false),
54+
checkOrSetlockDailyEventForAffectedUsersIncrement: jest.fn().mockResolvedValue(false),
55+
safeTsAdd: jest.fn().mockResolvedValue(undefined),
56+
};
57+
});
58+
59+
test('Should invalidate stale event cache and process duplicate-key as repetition instead of recursing infinitely', async () => {
60+
const task = generateTask();
61+
const cache = (worker as any).cache;
62+
63+
const uniqueEventHash = 'test-hash';
64+
65+
/**
66+
* Always use the same hash and force grouping by hash (no patterns)
67+
*/
68+
jest.spyOn(worker as any, 'getUniqueEventHash').mockResolvedValue(uniqueEventHash);
69+
jest.spyOn(worker as any, 'findSimilarEvent').mockResolvedValue(undefined);
70+
71+
const eventCacheKey = await (worker as any).getEventCacheKey(task.projectId, uniqueEventHash);
72+
73+
/**
74+
* Simulate stale cache entry created before another worker inserted the event
75+
* Cached value is null, but the "database" already contains the event
76+
*/
77+
cache.set(eventCacheKey, null);
78+
79+
const dbEvent = {
80+
groupHash: uniqueEventHash,
81+
payload: task.payload,
82+
timestamp: task.timestamp,
83+
totalCount: 1,
84+
};
85+
86+
/**
87+
* Use real CacheController semantics for getEvent: first call returns cached null,
88+
* subsequent calls should see the real event once the cache key is deleted.
89+
*/
90+
(worker as any).getEvent = jest.fn(async () => {
91+
return cache.get(eventCacheKey, async () => dbEvent);
92+
});
93+
94+
/**
95+
* saveEvent always throws duplicate-key error to trigger the recursive path.
96+
* With the fix, this branch is executed only once; without the fix it will
97+
* recurse indefinitely because isFirstOccurrence stays true.
98+
*/
99+
const duplicateError: NodeJS.ErrnoException = new Error('E11000 duplicate key error') as NodeJS.ErrnoException;
100+
101+
duplicateError.code = '11000';
102+
103+
const saveEventMock = jest.fn(async () => {
104+
throw duplicateError;
105+
});
106+
107+
(worker as any).saveEvent = saveEventMock;
108+
109+
const incrementMock = jest.fn().mockResolvedValue(1);
110+
const saveRepetitionMock = jest.fn().mockResolvedValue('rep-1');
111+
const saveDailyEventsMock = jest.fn().mockResolvedValue(undefined);
112+
const recordMetricsMock = jest.fn().mockResolvedValue(undefined);
113+
114+
(worker as any).incrementEventCounterAndAffectedUsers = incrementMock;
115+
(worker as any).saveRepetition = saveRepetitionMock;
116+
(worker as any).saveDailyEvents = saveDailyEventsMock;
117+
(worker as any).recordProjectMetrics = recordMetricsMock;
118+
119+
await worker.handle(task);
120+
121+
/**
122+
* Without the cache invalidation fix, this call above would never resolve
123+
* because handle() would recurse indefinitely on duplicate-key error.
124+
* The assertions below verify that we only tried to insert once and then
125+
* proceeded as a repetition with a cached original event.
126+
*/
127+
expect(saveEventMock).toHaveBeenCalledTimes(1);
128+
expect((worker as any).getEvent).toHaveBeenCalledTimes(2);
129+
expect(incrementMock).toHaveBeenCalledTimes(1);
130+
expect(saveRepetitionMock).toHaveBeenCalledTimes(1);
131+
expect(saveDailyEventsMock).toHaveBeenCalledTimes(1);
132+
expect(recordMetricsMock).toHaveBeenCalledTimes(1);
133+
}, 10000);
134+
});
135+

0 commit comments

Comments
 (0)