forked from rinafcode/teachLink_backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathevent-batching.service.spec.ts
More file actions
160 lines (128 loc) · 5 KB
/
Copy pathevent-batching.service.spec.ts
File metadata and controls
160 lines (128 loc) · 5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EventBatchingService } from './event-batching.service';
import { AnalyticsEvent, EventType } from '../entities/event.entity';
function makeEvent(overrides: Partial<AnalyticsEvent> = {}): AnalyticsEvent {
return {
eventType: EventType.CUSTOM,
category: 'c',
action: 'a',
...overrides,
} as AnalyticsEvent;
}
describe('EventBatchingService', () => {
let service: EventBatchingService;
let repo: jest.Mocked<Repository<AnalyticsEvent>>;
const originalBatchSize = process.env.EVENT_BATCH_SIZE;
const originalFlushInterval = process.env.EVENT_FLUSH_INTERVAL_MS;
async function buildService(): Promise<void> {
const module: TestingModule = await Test.createTestingModule({
providers: [
EventBatchingService,
{
provide: getRepositoryToken(AnalyticsEvent),
useValue: { insert: jest.fn().mockResolvedValue(undefined) },
},
],
}).compile();
service = module.get<EventBatchingService>(EventBatchingService);
repo = module.get(getRepositoryToken(AnalyticsEvent));
}
afterEach(() => {
jest.useRealTimers();
process.env.EVENT_BATCH_SIZE = originalBatchSize;
process.env.EVENT_FLUSH_INTERVAL_MS = originalFlushInterval;
});
describe('addEvent', () => {
beforeEach(async () => {
process.env.EVENT_BATCH_SIZE = '3';
await buildService();
});
it('adds an event to the batch without flushing below the batch size', () => {
service.addEvent(makeEvent());
expect(service.getBatchSize()).toBe(1);
expect(repo.insert).not.toHaveBeenCalled();
});
it('flushes automatically once the batch reaches BATCH_SIZE', async () => {
service.addEvent(makeEvent());
service.addEvent(makeEvent());
service.addEvent(makeEvent());
// flushBatch() is fire-and-forget from addEvent — allow its microtask to settle.
await Promise.resolve();
await Promise.resolve();
expect(repo.insert).toHaveBeenCalledTimes(1);
expect(repo.insert).toHaveBeenCalledWith(expect.arrayContaining([expect.any(Object)]));
expect(service.getBatchSize()).toBe(0);
});
it('discards events received after shutdown has begun', () => {
service.onModuleDestroy();
service.addEvent(makeEvent());
expect(service.getBatchSize()).toBe(0);
});
});
describe('forceFlush', () => {
beforeEach(async () => {
process.env.EVENT_BATCH_SIZE = '100';
await buildService();
});
it('persists all pending events and clears the batch', async () => {
service.addEvent(makeEvent({ category: 'a' }));
service.addEvent(makeEvent({ category: 'b' }));
await service.forceFlush();
expect(repo.insert).toHaveBeenCalledTimes(1);
expect(repo.insert).toHaveBeenCalledWith([
expect.objectContaining({ category: 'a' }),
expect.objectContaining({ category: 'b' }),
]);
expect(service.getBatchSize()).toBe(0);
});
it('is a no-op when the batch is empty', async () => {
await service.forceFlush();
expect(repo.insert).not.toHaveBeenCalled();
});
it('re-queues events (up to the retry limit) and rethrows on a failed flush', async () => {
const error = new Error('insert failed');
repo.insert.mockRejectedValueOnce(error);
service.addEvent(makeEvent());
await expect(service.forceFlush()).rejects.toThrow(error);
expect(service.getBatchSize()).toBe(1);
});
});
describe('onModuleInit / onModuleDestroy', () => {
beforeEach(async () => {
jest.useFakeTimers();
process.env.EVENT_BATCH_SIZE = '100';
process.env.EVENT_FLUSH_INTERVAL_MS = '1000';
await buildService();
});
it('periodically flushes any pending events on the configured interval', async () => {
service.onModuleInit();
service.addEvent(makeEvent());
jest.advanceTimersByTime(1000);
await Promise.resolve();
await Promise.resolve();
expect(repo.insert).toHaveBeenCalledTimes(1);
});
it('does not flush on the interval when the batch is empty', () => {
service.onModuleInit();
jest.advanceTimersByTime(1000);
expect(repo.insert).not.toHaveBeenCalled();
});
it('stops the interval and performs a final flush of pending events', async () => {
service.onModuleInit();
service.addEvent(makeEvent());
await service.onModuleDestroy();
expect(repo.insert).toHaveBeenCalledTimes(1);
// Interval must be cleared — advancing time should not trigger another flush.
service.addEvent(makeEvent());
jest.advanceTimersByTime(5000);
expect(repo.insert).toHaveBeenCalledTimes(1);
});
it('returns undefined synchronously on destroy when there is nothing to flush', () => {
service.onModuleInit();
expect(service.onModuleDestroy()).toBeUndefined();
expect(repo.insert).not.toHaveBeenCalled();
});
});
});