@flusys/nestjs-event-manager 6.0.2 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/adapters/event-manager.adapter.spec.js +104 -0
- package/cjs/controllers/event.controller.spec.js +156 -0
- package/cjs/docs/event-manager-swagger.config.spec.js +65 -0
- package/cjs/entities/index.spec.js +18 -0
- package/cjs/modules/event-manager.module.spec.js +146 -0
- package/cjs/services/event-manager-config.service.spec.js +111 -0
- package/cjs/services/event-manager-datasource.provider.spec.js +198 -0
- package/cjs/services/event-manager-helper.service.spec.js +327 -0
- package/cjs/services/event.service.js +9 -4
- package/cjs/services/event.service.spec.js +855 -0
- package/fesm/adapters/event-manager.adapter.spec.js +100 -0
- package/fesm/controllers/event.controller.spec.js +152 -0
- package/fesm/docs/event-manager-swagger.config.spec.js +61 -0
- package/fesm/entities/index.spec.js +14 -0
- package/fesm/modules/event-manager.module.spec.js +142 -0
- package/fesm/services/event-manager-config.service.spec.js +107 -0
- package/fesm/services/event-manager-datasource.provider.spec.js +194 -0
- package/fesm/services/event-manager-helper.service.spec.js +323 -0
- package/fesm/services/event.service.js +9 -4
- package/fesm/services/event.service.spec.js +851 -0
- package/package.json +3 -3
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import { NotFoundException } from '@nestjs/common';
|
|
2
|
+
import { ParticipantStatus, RecurrenceType } from '@flusys/nestjs-shared/enums';
|
|
3
|
+
import { createMockRepository } from '@test-utils/mocks/repository.mock';
|
|
4
|
+
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
5
|
+
import { EVENT_MESSAGES, EVENT_PARTICIPANT_MESSAGES } from '../config';
|
|
6
|
+
import { Event, EventParticipant, EventWithCompany } from '../entities';
|
|
7
|
+
import { EventService } from './event.service';
|
|
8
|
+
function buildEvent(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
id: 'event-uuid-1',
|
|
11
|
+
title: 'Sprint Planning',
|
|
12
|
+
description: null,
|
|
13
|
+
eventDate: '2026-04-03',
|
|
14
|
+
startTime: '09:00',
|
|
15
|
+
endTime: '10:00',
|
|
16
|
+
isAllDay: false,
|
|
17
|
+
recurrenceType: RecurrenceType.NONE,
|
|
18
|
+
recurrenceEndDate: null,
|
|
19
|
+
weekDays: null,
|
|
20
|
+
color: '#3B82F6',
|
|
21
|
+
meetingLink: null,
|
|
22
|
+
isActive: true,
|
|
23
|
+
createdAt: new Date('2026-01-01T00:00:00Z'),
|
|
24
|
+
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
25
|
+
deletedAt: null,
|
|
26
|
+
createdById: null,
|
|
27
|
+
updatedById: null,
|
|
28
|
+
deletedById: null,
|
|
29
|
+
...overrides
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function buildParticipant(overrides = {}) {
|
|
33
|
+
return {
|
|
34
|
+
id: 'participant-uuid-1',
|
|
35
|
+
eventId: 'event-uuid-1',
|
|
36
|
+
userId: 'user-uuid-1',
|
|
37
|
+
status: ParticipantStatus.PENDING,
|
|
38
|
+
isOrganizer: false,
|
|
39
|
+
createdAt: new Date('2026-01-01T00:00:00Z'),
|
|
40
|
+
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
41
|
+
deletedAt: null,
|
|
42
|
+
createdById: null,
|
|
43
|
+
updatedById: null,
|
|
44
|
+
deletedById: null,
|
|
45
|
+
...overrides
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
describe('EventService', ()=>{
|
|
49
|
+
let service;
|
|
50
|
+
let mockEventRepo;
|
|
51
|
+
let mockParticipantRepo;
|
|
52
|
+
let mockConfigService;
|
|
53
|
+
let mockDataSourceProvider;
|
|
54
|
+
let mockEventQb;
|
|
55
|
+
const user = buildMockUser();
|
|
56
|
+
beforeEach(()=>{
|
|
57
|
+
mockEventRepo = createMockRepository();
|
|
58
|
+
mockParticipantRepo = createMockRepository();
|
|
59
|
+
mockEventQb = mockEventRepo.createQueryBuilder();
|
|
60
|
+
// `leftJoinAndMapMany` isn't part of the shared MockQueryBuilder.
|
|
61
|
+
mockEventQb.leftJoinAndMapMany = jest.fn().mockReturnThis();
|
|
62
|
+
mockConfigService = {
|
|
63
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false),
|
|
64
|
+
getDefaultColor: jest.fn().mockReturnValue('#3B82F6'),
|
|
65
|
+
getMaxRecurrenceOccurrences: jest.fn().mockReturnValue(365)
|
|
66
|
+
};
|
|
67
|
+
mockDataSourceProvider = {
|
|
68
|
+
getRepository: jest.fn((entity)=>Promise.resolve(entity === EventParticipant ? mockParticipantRepo : mockEventRepo)),
|
|
69
|
+
getDataSource: jest.fn()
|
|
70
|
+
};
|
|
71
|
+
service = new EventService({}, {}, mockConfigService, mockDataSourceProvider);
|
|
72
|
+
});
|
|
73
|
+
describe('resolveEntity', ()=>{
|
|
74
|
+
it('resolves to the company-scoped entity when the company feature is enabled', ()=>{
|
|
75
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
76
|
+
expect(service.resolveEntity()).toBe(EventWithCompany);
|
|
77
|
+
});
|
|
78
|
+
it('resolves to the core entity when the company feature is disabled', ()=>{
|
|
79
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
80
|
+
expect(service.resolveEntity()).toBe(Event);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('convertSingleDtoToEntity', ()=>{
|
|
84
|
+
it('builds a new entity with sensible defaults on create', async ()=>{
|
|
85
|
+
const dto = {
|
|
86
|
+
title: 'Sprint Planning',
|
|
87
|
+
eventDate: '2026-04-03',
|
|
88
|
+
startTime: '09:00',
|
|
89
|
+
endTime: '10:00'
|
|
90
|
+
};
|
|
91
|
+
const entity = await service.convertSingleDtoToEntity(dto, user);
|
|
92
|
+
expect(entity).toMatchObject({
|
|
93
|
+
title: 'Sprint Planning',
|
|
94
|
+
startTime: '09:00',
|
|
95
|
+
endTime: '10:00',
|
|
96
|
+
isAllDay: false,
|
|
97
|
+
recurrenceType: RecurrenceType.NONE,
|
|
98
|
+
recurrenceEndDate: null,
|
|
99
|
+
color: '#3B82F6',
|
|
100
|
+
isActive: true
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
it('forces 00:00 / 23:59 markers on create for all-day events', async ()=>{
|
|
104
|
+
const dto = {
|
|
105
|
+
title: 'Holiday',
|
|
106
|
+
eventDate: '2026-12-25',
|
|
107
|
+
startTime: '09:00',
|
|
108
|
+
endTime: '17:00',
|
|
109
|
+
isAllDay: true
|
|
110
|
+
};
|
|
111
|
+
const entity = await service.convertSingleDtoToEntity(dto, user);
|
|
112
|
+
expect(entity.startTime).toBe('00:00');
|
|
113
|
+
expect(entity.endTime).toBe('23:59');
|
|
114
|
+
});
|
|
115
|
+
it('attaches companyId from the current user when the company feature is enabled', async ()=>{
|
|
116
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
117
|
+
const dto = {
|
|
118
|
+
title: 'Sprint Planning',
|
|
119
|
+
eventDate: '2026-04-03',
|
|
120
|
+
startTime: '09:00',
|
|
121
|
+
endTime: '10:00'
|
|
122
|
+
};
|
|
123
|
+
const entity = await service.convertSingleDtoToEntity(dto, user);
|
|
124
|
+
expect(entity.companyId).toBe(user.companyId);
|
|
125
|
+
});
|
|
126
|
+
it('queues participantIds for the after-insert hook when supplied', async ()=>{
|
|
127
|
+
const dto = {
|
|
128
|
+
title: 'Sprint Planning',
|
|
129
|
+
eventDate: '2026-04-03',
|
|
130
|
+
startTime: '09:00',
|
|
131
|
+
endTime: '10:00',
|
|
132
|
+
participantIds: [
|
|
133
|
+
'participant-uuid-1'
|
|
134
|
+
]
|
|
135
|
+
};
|
|
136
|
+
await service.convertSingleDtoToEntity(dto, user);
|
|
137
|
+
expect(service.pendingParticipantsList).toEqual([
|
|
138
|
+
[
|
|
139
|
+
'participant-uuid-1'
|
|
140
|
+
]
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
it('merges an update dto onto the existing entity, preserving untouched fields', async ()=>{
|
|
144
|
+
service.repository = mockEventRepo;
|
|
145
|
+
const existing = buildEvent({
|
|
146
|
+
title: 'Original Title',
|
|
147
|
+
color: '#111111'
|
|
148
|
+
});
|
|
149
|
+
mockEventRepo.findOne.mockResolvedValue(existing);
|
|
150
|
+
const dto = {
|
|
151
|
+
id: 'event-uuid-1',
|
|
152
|
+
title: 'Updated Title'
|
|
153
|
+
};
|
|
154
|
+
const entity = await service.convertSingleDtoToEntity(dto, user);
|
|
155
|
+
expect(entity.title).toBe('Updated Title');
|
|
156
|
+
expect(entity.color).toBe('#111111');
|
|
157
|
+
});
|
|
158
|
+
it('throws NotFoundException with the correct messageKey when updating a missing event', async ()=>{
|
|
159
|
+
service.repository = mockEventRepo;
|
|
160
|
+
mockEventRepo.findOne.mockResolvedValue(null);
|
|
161
|
+
const dto = {
|
|
162
|
+
id: 'missing-uuid',
|
|
163
|
+
title: 'Updated Title'
|
|
164
|
+
};
|
|
165
|
+
await expect(service.convertSingleDtoToEntity(dto, user)).rejects.toMatchObject({
|
|
166
|
+
response: expect.objectContaining({
|
|
167
|
+
messageKey: EVENT_MESSAGES.NOT_FOUND
|
|
168
|
+
})
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
it('clears an explicitly-null meetingLink but preserves the existing one when omitted', async ()=>{
|
|
172
|
+
service.repository = mockEventRepo;
|
|
173
|
+
const existing = buildEvent({
|
|
174
|
+
meetingLink: 'https://meet.example.com/original'
|
|
175
|
+
});
|
|
176
|
+
mockEventRepo.findOne.mockResolvedValue(existing);
|
|
177
|
+
const clearedDto = {
|
|
178
|
+
id: 'event-uuid-1',
|
|
179
|
+
meetingLink: null
|
|
180
|
+
};
|
|
181
|
+
const clearedEntity = await service.convertSingleDtoToEntity(clearedDto, user);
|
|
182
|
+
expect(clearedEntity.meetingLink).toBeNull();
|
|
183
|
+
const omittedDto = {
|
|
184
|
+
id: 'event-uuid-1'
|
|
185
|
+
};
|
|
186
|
+
const preservedEntity = await service.convertSingleDtoToEntity(omittedDto, user);
|
|
187
|
+
expect(preservedEntity.meetingLink).toBe('https://meet.example.com/original');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('getFilterQuery', ()=>{
|
|
191
|
+
it('filters by isActive when provided as a boolean', async ()=>{
|
|
192
|
+
const query = mockEventQb;
|
|
193
|
+
await service.getFilterQuery(query, {
|
|
194
|
+
isActive: true
|
|
195
|
+
}, user);
|
|
196
|
+
expect(query.andWhere).toHaveBeenCalledWith('event.isActive = :isActive', {
|
|
197
|
+
isActive: true
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
it('filters by recurrenceType', async ()=>{
|
|
201
|
+
const query = mockEventQb;
|
|
202
|
+
await service.getFilterQuery(query, {
|
|
203
|
+
recurrenceType: RecurrenceType.WEEKLY
|
|
204
|
+
}, user);
|
|
205
|
+
expect(query.andWhere).toHaveBeenCalledWith('event.recurrenceType = :recurrenceType', {
|
|
206
|
+
recurrenceType: RecurrenceType.WEEKLY
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
it('scopes to participant event ids when participantUserId is provided and matches exist', async ()=>{
|
|
210
|
+
const participantQb = mockParticipantRepo.createQueryBuilder();
|
|
211
|
+
participantQb.getRawMany = jest.fn().mockResolvedValue([
|
|
212
|
+
{
|
|
213
|
+
ep_event_id: 'event-uuid-1'
|
|
214
|
+
}
|
|
215
|
+
]);
|
|
216
|
+
const query = mockEventQb;
|
|
217
|
+
await service.getFilterQuery(query, {
|
|
218
|
+
participantUserId: 'user-uuid-1'
|
|
219
|
+
}, user);
|
|
220
|
+
expect(query.andWhere).toHaveBeenCalledWith('event.id IN (:...participantEventIds)', {
|
|
221
|
+
participantEventIds: [
|
|
222
|
+
'event-uuid-1'
|
|
223
|
+
]
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
it('short-circuits to an always-false predicate when the user participates in no events', async ()=>{
|
|
227
|
+
const participantQb = mockParticipantRepo.createQueryBuilder();
|
|
228
|
+
participantQb.getRawMany = jest.fn().mockResolvedValue([]);
|
|
229
|
+
const query = mockEventQb;
|
|
230
|
+
await service.getFilterQuery(query, {
|
|
231
|
+
participantUserId: 'user-uuid-1'
|
|
232
|
+
}, user);
|
|
233
|
+
expect(query.andWhere).toHaveBeenCalledWith('1 = 0');
|
|
234
|
+
});
|
|
235
|
+
it('falls back to an equality filter for unrecognized keys', async ()=>{
|
|
236
|
+
const query = mockEventQb;
|
|
237
|
+
await service.getFilterQuery(query, {
|
|
238
|
+
color: '#000000'
|
|
239
|
+
}, user);
|
|
240
|
+
expect(query.andWhere).toHaveBeenCalledWith('event.color = :value', {
|
|
241
|
+
value: '#000000'
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
describe('getExtraManipulateQuery', ()=>{
|
|
246
|
+
it('applies the company filter and left-joins participants', async ()=>{
|
|
247
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
248
|
+
const query = mockEventQb;
|
|
249
|
+
await service.getExtraManipulateQuery(query, {}, user);
|
|
250
|
+
expect(query.andWhere).toHaveBeenCalledWith('event.companyId = :companyId', {
|
|
251
|
+
companyId: user.companyId
|
|
252
|
+
});
|
|
253
|
+
expect(query.leftJoinAndMapMany).toHaveBeenCalledWith('event.participants', EventParticipant, 'ep', 'ep.event_id = event.id AND ep.deleted_at IS NULL');
|
|
254
|
+
});
|
|
255
|
+
it('does not apply a company filter when the company feature is disabled', async ()=>{
|
|
256
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
257
|
+
const query = mockEventQb;
|
|
258
|
+
await service.getExtraManipulateQuery(query, {}, user);
|
|
259
|
+
const companyCalls = query.andWhere.mock.calls.filter((call)=>call[0] === 'event.companyId = :companyId');
|
|
260
|
+
expect(companyCalls).toHaveLength(0);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
describe('afterInsertOperation / afterUpdateOperation', ()=>{
|
|
264
|
+
it('adds the current user and requested participants as organizer + attendees on insert', async ()=>{
|
|
265
|
+
mockParticipantRepo.find.mockResolvedValue([]);
|
|
266
|
+
mockParticipantRepo.save.mockResolvedValue([]);
|
|
267
|
+
service.pendingParticipantsList = [
|
|
268
|
+
[
|
|
269
|
+
'participant-uuid-2'
|
|
270
|
+
]
|
|
271
|
+
];
|
|
272
|
+
await service.afterInsertOperation([
|
|
273
|
+
buildEvent()
|
|
274
|
+
], {}, user, {});
|
|
275
|
+
const savedParticipants = mockParticipantRepo.save.mock.calls[0][0];
|
|
276
|
+
expect(savedParticipants).toEqual(expect.arrayContaining([
|
|
277
|
+
expect.objectContaining({
|
|
278
|
+
userId: user.id,
|
|
279
|
+
isOrganizer: true
|
|
280
|
+
}),
|
|
281
|
+
expect.objectContaining({
|
|
282
|
+
userId: 'participant-uuid-2',
|
|
283
|
+
isOrganizer: false
|
|
284
|
+
})
|
|
285
|
+
]));
|
|
286
|
+
expect(service.pendingParticipantsList).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
it('restores a previously soft-deleted participant instead of duplicating it', async ()=>{
|
|
289
|
+
const softDeleted = buildParticipant({
|
|
290
|
+
userId: user.id,
|
|
291
|
+
deletedAt: new Date()
|
|
292
|
+
});
|
|
293
|
+
mockParticipantRepo.find.mockResolvedValue([
|
|
294
|
+
softDeleted
|
|
295
|
+
]);
|
|
296
|
+
mockParticipantRepo.save.mockResolvedValue([]);
|
|
297
|
+
service.pendingParticipantsList = [
|
|
298
|
+
[]
|
|
299
|
+
];
|
|
300
|
+
await service.afterInsertOperation([
|
|
301
|
+
buildEvent()
|
|
302
|
+
], {}, user, {});
|
|
303
|
+
const savedParticipants = mockParticipantRepo.save.mock.calls[0][0];
|
|
304
|
+
expect(savedParticipants[0]).toMatchObject({
|
|
305
|
+
userId: user.id,
|
|
306
|
+
deletedAt: null,
|
|
307
|
+
isOrganizer: true
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
it('does nothing when there is no authenticated user', async ()=>{
|
|
311
|
+
service.pendingParticipantsList = [
|
|
312
|
+
[
|
|
313
|
+
'participant-uuid-2'
|
|
314
|
+
]
|
|
315
|
+
];
|
|
316
|
+
await service.afterInsertOperation([
|
|
317
|
+
buildEvent()
|
|
318
|
+
], {}, null, {});
|
|
319
|
+
expect(mockParticipantRepo.save).not.toHaveBeenCalled();
|
|
320
|
+
});
|
|
321
|
+
it('soft-deletes existing participants before re-adding the current set on update', async ()=>{
|
|
322
|
+
mockParticipantRepo.find.mockResolvedValue([]);
|
|
323
|
+
mockParticipantRepo.save.mockResolvedValue([]);
|
|
324
|
+
mockParticipantRepo.update.mockResolvedValue({});
|
|
325
|
+
service.pendingParticipantsList = [
|
|
326
|
+
[
|
|
327
|
+
'participant-uuid-2'
|
|
328
|
+
]
|
|
329
|
+
];
|
|
330
|
+
await service.afterUpdateOperation([
|
|
331
|
+
buildEvent()
|
|
332
|
+
], {}, user, {});
|
|
333
|
+
expect(mockParticipantRepo.update).toHaveBeenCalledWith({
|
|
334
|
+
eventId: 'event-uuid-1',
|
|
335
|
+
deletedAt: expect.anything()
|
|
336
|
+
}, {
|
|
337
|
+
deletedAt: expect.any(Date)
|
|
338
|
+
});
|
|
339
|
+
expect(mockParticipantRepo.save).toHaveBeenCalled();
|
|
340
|
+
expect(service.pendingParticipantsList).toEqual([]);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
describe('updateParticipantStatus', ()=>{
|
|
344
|
+
it('updates the participant status and returns the saved entity', async ()=>{
|
|
345
|
+
const participant = buildParticipant({
|
|
346
|
+
status: ParticipantStatus.PENDING
|
|
347
|
+
});
|
|
348
|
+
mockParticipantRepo.findOne.mockResolvedValue(participant);
|
|
349
|
+
mockParticipantRepo.save.mockImplementation(async (p)=>p);
|
|
350
|
+
const dto = {
|
|
351
|
+
id: 'participant-uuid-1',
|
|
352
|
+
status: ParticipantStatus.TENTATIVE
|
|
353
|
+
};
|
|
354
|
+
const result = await service.updateParticipantStatus(dto, user);
|
|
355
|
+
expect(result.status).toBe(ParticipantStatus.TENTATIVE);
|
|
356
|
+
expect(mockParticipantRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
357
|
+
status: ParticipantStatus.TENTATIVE
|
|
358
|
+
}));
|
|
359
|
+
});
|
|
360
|
+
it('throws NotFoundException with the correct messageKey when the participant is missing', async ()=>{
|
|
361
|
+
mockParticipantRepo.findOne.mockResolvedValue(null);
|
|
362
|
+
const dto = {
|
|
363
|
+
id: 'missing-uuid',
|
|
364
|
+
status: ParticipantStatus.ACCEPTED
|
|
365
|
+
};
|
|
366
|
+
await expect(service.updateParticipantStatus(dto, user)).rejects.toMatchObject({
|
|
367
|
+
response: expect.objectContaining({
|
|
368
|
+
messageKey: EVENT_PARTICIPANT_MESSAGES.NOT_FOUND
|
|
369
|
+
})
|
|
370
|
+
});
|
|
371
|
+
await expect(service.updateParticipantStatus(dto, user)).rejects.toBeInstanceOf(NotFoundException);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
describe('getEventsForCalendarRange', ()=>{
|
|
375
|
+
function queryDto(overrides = {}) {
|
|
376
|
+
return {
|
|
377
|
+
startDate: '2026-04-01',
|
|
378
|
+
endDate: '2026-04-30',
|
|
379
|
+
...overrides
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
it('returns a single non-recurring event unchanged when it falls in range', async ()=>{
|
|
383
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
384
|
+
buildEvent({
|
|
385
|
+
eventDate: '2026-04-10'
|
|
386
|
+
})
|
|
387
|
+
]);
|
|
388
|
+
const result = await service.getEventsForCalendarRange(queryDto(), user);
|
|
389
|
+
expect(result).toHaveLength(1);
|
|
390
|
+
expect(result[0].id).toBe('event-uuid-1');
|
|
391
|
+
expect(result[0].isRecurrenceInstance).toBeUndefined();
|
|
392
|
+
});
|
|
393
|
+
it('defaults activeOnly to true when not specified', async ()=>{
|
|
394
|
+
mockEventQb.getMany.mockResolvedValue([]);
|
|
395
|
+
await service.getEventsForCalendarRange(queryDto(), user);
|
|
396
|
+
expect(mockEventQb.andWhere).toHaveBeenCalledWith('event.isActive = :isActive', {
|
|
397
|
+
isActive: true
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
it('respects activeOnly = false', async ()=>{
|
|
401
|
+
mockEventQb.getMany.mockResolvedValue([]);
|
|
402
|
+
await service.getEventsForCalendarRange(queryDto({
|
|
403
|
+
activeOnly: false
|
|
404
|
+
}), user);
|
|
405
|
+
expect(mockEventQb.andWhere).toHaveBeenCalledWith('event.isActive = :isActive', {
|
|
406
|
+
isActive: false
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
it('handles a zero-length range (startDate === endDate)', async ()=>{
|
|
410
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
411
|
+
buildEvent({
|
|
412
|
+
eventDate: '2026-04-10'
|
|
413
|
+
})
|
|
414
|
+
]);
|
|
415
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
416
|
+
startDate: '2026-04-10',
|
|
417
|
+
endDate: '2026-04-10'
|
|
418
|
+
}), user);
|
|
419
|
+
expect(mockEventQb.andWhere).toHaveBeenCalledWith(expect.stringContaining('event.eventDate >= :startDate'), {
|
|
420
|
+
startDate: '2026-04-10',
|
|
421
|
+
endDate: '2026-04-10',
|
|
422
|
+
none: RecurrenceType.NONE
|
|
423
|
+
});
|
|
424
|
+
expect(result).toHaveLength(1);
|
|
425
|
+
});
|
|
426
|
+
it('short-circuits to an empty array when filtering by a userId with no participant events', async ()=>{
|
|
427
|
+
const participantQb = mockParticipantRepo.createQueryBuilder();
|
|
428
|
+
participantQb.getRawMany = jest.fn().mockResolvedValue([]);
|
|
429
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
430
|
+
userId: 'user-uuid-1'
|
|
431
|
+
}), user);
|
|
432
|
+
expect(result).toEqual([]);
|
|
433
|
+
expect(mockEventQb.getMany).not.toHaveBeenCalled();
|
|
434
|
+
});
|
|
435
|
+
it('filters events to the participant event ids when userId matches events', async ()=>{
|
|
436
|
+
const participantQb = mockParticipantRepo.createQueryBuilder();
|
|
437
|
+
participantQb.getRawMany = jest.fn().mockResolvedValue([
|
|
438
|
+
{
|
|
439
|
+
ep_event_id: 'event-uuid-1'
|
|
440
|
+
}
|
|
441
|
+
]);
|
|
442
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
443
|
+
buildEvent()
|
|
444
|
+
]);
|
|
445
|
+
await service.getEventsForCalendarRange(queryDto({
|
|
446
|
+
userId: 'user-uuid-1'
|
|
447
|
+
}), user);
|
|
448
|
+
expect(mockEventQb.andWhere).toHaveBeenCalledWith('event.id IN (:...eventIds)', {
|
|
449
|
+
eventIds: [
|
|
450
|
+
'event-uuid-1'
|
|
451
|
+
]
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
it('loads and attaches participants when includeParticipants is true', async ()=>{
|
|
455
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
456
|
+
buildEvent()
|
|
457
|
+
]);
|
|
458
|
+
mockParticipantRepo.find.mockResolvedValue([
|
|
459
|
+
buildParticipant({
|
|
460
|
+
eventId: 'event-uuid-1',
|
|
461
|
+
userId: 'user-uuid-1'
|
|
462
|
+
}),
|
|
463
|
+
buildParticipant({
|
|
464
|
+
id: 'participant-uuid-2',
|
|
465
|
+
eventId: 'event-uuid-1',
|
|
466
|
+
userId: 'user-uuid-2'
|
|
467
|
+
})
|
|
468
|
+
]);
|
|
469
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
470
|
+
includeParticipants: true
|
|
471
|
+
}), user);
|
|
472
|
+
expect(result[0].participants).toHaveLength(2);
|
|
473
|
+
expect(result[0].participants?.[0].userId).toBe('user-uuid-1');
|
|
474
|
+
});
|
|
475
|
+
it('does not query participants when includeParticipants is falsy', async ()=>{
|
|
476
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
477
|
+
buildEvent()
|
|
478
|
+
]);
|
|
479
|
+
await service.getEventsForCalendarRange(queryDto(), user);
|
|
480
|
+
expect(mockParticipantRepo.find).not.toHaveBeenCalled();
|
|
481
|
+
});
|
|
482
|
+
it('does not query participants when there are no events to attach them to', async ()=>{
|
|
483
|
+
mockEventQb.getMany.mockResolvedValue([]);
|
|
484
|
+
await service.getEventsForCalendarRange(queryDto({
|
|
485
|
+
includeParticipants: true
|
|
486
|
+
}), user);
|
|
487
|
+
expect(mockParticipantRepo.find).not.toHaveBeenCalled();
|
|
488
|
+
});
|
|
489
|
+
// ---------------------------------------------------------------------
|
|
490
|
+
// Recurrence expansion — daily
|
|
491
|
+
// ---------------------------------------------------------------------
|
|
492
|
+
it('expands an infinite daily recurrence within the range, marking only the first occurrence as non-instance', async ()=>{
|
|
493
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
494
|
+
buildEvent({
|
|
495
|
+
eventDate: '2026-01-01',
|
|
496
|
+
recurrenceType: RecurrenceType.DAILY
|
|
497
|
+
})
|
|
498
|
+
]);
|
|
499
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
500
|
+
startDate: '2026-01-01',
|
|
501
|
+
endDate: '2026-01-05'
|
|
502
|
+
}), user);
|
|
503
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
504
|
+
'2026-01-01',
|
|
505
|
+
'2026-01-02',
|
|
506
|
+
'2026-01-03',
|
|
507
|
+
'2026-01-04',
|
|
508
|
+
'2026-01-05'
|
|
509
|
+
]);
|
|
510
|
+
expect(result[0].isRecurrenceInstance).toBe(false);
|
|
511
|
+
expect(result.slice(1).every((e)=>e.isRecurrenceInstance === true)).toBe(true);
|
|
512
|
+
expect(result.every((e)=>e.originalEventId === 'event-uuid-1')).toBe(true);
|
|
513
|
+
expect(result[1].id).toBe('event-uuid-1_2026-01-02');
|
|
514
|
+
});
|
|
515
|
+
it('stops a daily recurrence at the until (recurrenceEndDate) bound, before the range end', async ()=>{
|
|
516
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
517
|
+
buildEvent({
|
|
518
|
+
eventDate: '2026-01-01',
|
|
519
|
+
recurrenceType: RecurrenceType.DAILY,
|
|
520
|
+
recurrenceEndDate: '2026-01-03'
|
|
521
|
+
})
|
|
522
|
+
]);
|
|
523
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
524
|
+
startDate: '2026-01-01',
|
|
525
|
+
endDate: '2026-01-10'
|
|
526
|
+
}), user);
|
|
527
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
528
|
+
'2026-01-01',
|
|
529
|
+
'2026-01-02',
|
|
530
|
+
'2026-01-03'
|
|
531
|
+
]);
|
|
532
|
+
});
|
|
533
|
+
it('caps a count-based (no end date) recurrence at the configured max occurrences', async ()=>{
|
|
534
|
+
mockConfigService.getMaxRecurrenceOccurrences.mockReturnValue(3);
|
|
535
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
536
|
+
buildEvent({
|
|
537
|
+
eventDate: '2026-01-01',
|
|
538
|
+
recurrenceType: RecurrenceType.DAILY
|
|
539
|
+
})
|
|
540
|
+
]);
|
|
541
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
542
|
+
startDate: '2026-01-01',
|
|
543
|
+
endDate: '2026-12-31'
|
|
544
|
+
}), user);
|
|
545
|
+
expect(result).toHaveLength(3);
|
|
546
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
547
|
+
'2026-01-01',
|
|
548
|
+
'2026-01-02',
|
|
549
|
+
'2026-01-03'
|
|
550
|
+
]);
|
|
551
|
+
});
|
|
552
|
+
it('returns a single occurrence when the recurrence range only covers the start date', async ()=>{
|
|
553
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
554
|
+
buildEvent({
|
|
555
|
+
eventDate: '2026-01-01',
|
|
556
|
+
recurrenceType: RecurrenceType.DAILY
|
|
557
|
+
})
|
|
558
|
+
]);
|
|
559
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
560
|
+
startDate: '2026-01-01',
|
|
561
|
+
endDate: '2026-01-01'
|
|
562
|
+
}), user);
|
|
563
|
+
expect(result).toHaveLength(1);
|
|
564
|
+
expect(result[0].eventDate).toBe('2026-01-01');
|
|
565
|
+
});
|
|
566
|
+
it('produces no occurrences when the query range is entirely before the series start', async ()=>{
|
|
567
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
568
|
+
buildEvent({
|
|
569
|
+
eventDate: '2026-06-01',
|
|
570
|
+
recurrenceType: RecurrenceType.DAILY,
|
|
571
|
+
recurrenceEndDate: '2026-06-10'
|
|
572
|
+
})
|
|
573
|
+
]);
|
|
574
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
575
|
+
startDate: '2026-01-01',
|
|
576
|
+
endDate: '2026-01-31'
|
|
577
|
+
}), user);
|
|
578
|
+
expect(result).toEqual([]);
|
|
579
|
+
});
|
|
580
|
+
it('does not shift dates across a US daylight-saving boundary (spring-forward, 2026-03-08)', async ()=>{
|
|
581
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
582
|
+
buildEvent({
|
|
583
|
+
eventDate: '2026-03-06',
|
|
584
|
+
recurrenceType: RecurrenceType.DAILY
|
|
585
|
+
})
|
|
586
|
+
]);
|
|
587
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
588
|
+
startDate: '2026-03-06',
|
|
589
|
+
endDate: '2026-03-10'
|
|
590
|
+
}), user);
|
|
591
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
592
|
+
'2026-03-06',
|
|
593
|
+
'2026-03-07',
|
|
594
|
+
'2026-03-08',
|
|
595
|
+
'2026-03-09',
|
|
596
|
+
'2026-03-10'
|
|
597
|
+
]);
|
|
598
|
+
});
|
|
599
|
+
it('does not shift dates across a US daylight-saving boundary (fall-back, 2026-11-01)', async ()=>{
|
|
600
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
601
|
+
buildEvent({
|
|
602
|
+
eventDate: '2026-10-30',
|
|
603
|
+
recurrenceType: RecurrenceType.DAILY
|
|
604
|
+
})
|
|
605
|
+
]);
|
|
606
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
607
|
+
startDate: '2026-10-30',
|
|
608
|
+
endDate: '2026-11-03'
|
|
609
|
+
}), user);
|
|
610
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
611
|
+
'2026-10-30',
|
|
612
|
+
'2026-10-31',
|
|
613
|
+
'2026-11-01',
|
|
614
|
+
'2026-11-02',
|
|
615
|
+
'2026-11-03'
|
|
616
|
+
]);
|
|
617
|
+
});
|
|
618
|
+
// ---------------------------------------------------------------------
|
|
619
|
+
// Recurrence expansion — weekly / biweekly
|
|
620
|
+
// ---------------------------------------------------------------------
|
|
621
|
+
it('expands a weekly recurrence using the event own weekday when weekDays is not set', async ()=>{
|
|
622
|
+
// 2026-01-05 is a Monday
|
|
623
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
624
|
+
buildEvent({
|
|
625
|
+
eventDate: '2026-01-05',
|
|
626
|
+
recurrenceType: RecurrenceType.WEEKLY,
|
|
627
|
+
weekDays: null
|
|
628
|
+
})
|
|
629
|
+
]);
|
|
630
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
631
|
+
startDate: '2026-01-05',
|
|
632
|
+
endDate: '2026-01-26'
|
|
633
|
+
}), user);
|
|
634
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
635
|
+
'2026-01-05',
|
|
636
|
+
'2026-01-12',
|
|
637
|
+
'2026-01-19',
|
|
638
|
+
'2026-01-26'
|
|
639
|
+
]);
|
|
640
|
+
});
|
|
641
|
+
it('expands a weekly recurrence across multiple weekDays', async ()=>{
|
|
642
|
+
// 2026-01-05 is a Monday; weekDays MON + WED
|
|
643
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
644
|
+
buildEvent({
|
|
645
|
+
eventDate: '2026-01-05',
|
|
646
|
+
recurrenceType: RecurrenceType.WEEKLY,
|
|
647
|
+
weekDays: [
|
|
648
|
+
'MON',
|
|
649
|
+
'WED'
|
|
650
|
+
]
|
|
651
|
+
})
|
|
652
|
+
]);
|
|
653
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
654
|
+
startDate: '2026-01-01',
|
|
655
|
+
endDate: '2026-01-14'
|
|
656
|
+
}), user);
|
|
657
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
658
|
+
'2026-01-05',
|
|
659
|
+
'2026-01-07',
|
|
660
|
+
'2026-01-12',
|
|
661
|
+
'2026-01-14'
|
|
662
|
+
]);
|
|
663
|
+
expect(result[0].isRecurrenceInstance).toBe(false);
|
|
664
|
+
expect(result.slice(1).every((e)=>e.isRecurrenceInstance === true)).toBe(true);
|
|
665
|
+
});
|
|
666
|
+
it('skips a weekday occurrence that falls before the series start date in its first week', async ()=>{
|
|
667
|
+
// 2026-01-07 is a Wednesday; the same week's Monday (Jan 5) is before the series
|
|
668
|
+
// start and must not produce an occurrence, but later weeks' Mondays should.
|
|
669
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
670
|
+
buildEvent({
|
|
671
|
+
eventDate: '2026-01-07',
|
|
672
|
+
recurrenceType: RecurrenceType.WEEKLY,
|
|
673
|
+
weekDays: [
|
|
674
|
+
'MON',
|
|
675
|
+
'WED'
|
|
676
|
+
]
|
|
677
|
+
})
|
|
678
|
+
]);
|
|
679
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
680
|
+
startDate: '2026-01-01',
|
|
681
|
+
endDate: '2026-01-21'
|
|
682
|
+
}), user);
|
|
683
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
684
|
+
'2026-01-07',
|
|
685
|
+
'2026-01-12',
|
|
686
|
+
'2026-01-14',
|
|
687
|
+
'2026-01-19',
|
|
688
|
+
'2026-01-21'
|
|
689
|
+
]);
|
|
690
|
+
});
|
|
691
|
+
it('expands a biweekly recurrence with two-week spacing', async ()=>{
|
|
692
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
693
|
+
buildEvent({
|
|
694
|
+
eventDate: '2026-01-05',
|
|
695
|
+
recurrenceType: RecurrenceType.BIWEEKLY,
|
|
696
|
+
weekDays: null
|
|
697
|
+
})
|
|
698
|
+
]);
|
|
699
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
700
|
+
startDate: '2026-01-05',
|
|
701
|
+
endDate: '2026-02-28'
|
|
702
|
+
}), user);
|
|
703
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
704
|
+
'2026-01-05',
|
|
705
|
+
'2026-01-19',
|
|
706
|
+
'2026-02-02',
|
|
707
|
+
'2026-02-16'
|
|
708
|
+
]);
|
|
709
|
+
});
|
|
710
|
+
it('stops a weekly recurrence at the until bound mid-week', async ()=>{
|
|
711
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
712
|
+
buildEvent({
|
|
713
|
+
eventDate: '2026-01-05',
|
|
714
|
+
recurrenceType: RecurrenceType.WEEKLY,
|
|
715
|
+
weekDays: null,
|
|
716
|
+
recurrenceEndDate: '2026-01-12'
|
|
717
|
+
})
|
|
718
|
+
]);
|
|
719
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
720
|
+
startDate: '2026-01-01',
|
|
721
|
+
endDate: '2026-01-31'
|
|
722
|
+
}), user);
|
|
723
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
724
|
+
'2026-01-05',
|
|
725
|
+
'2026-01-12'
|
|
726
|
+
]);
|
|
727
|
+
});
|
|
728
|
+
// ---------------------------------------------------------------------
|
|
729
|
+
// Recurrence expansion — monthly
|
|
730
|
+
// ---------------------------------------------------------------------
|
|
731
|
+
it('expands a monthly recurrence, clamping to the last day of shorter months', async ()=>{
|
|
732
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
733
|
+
buildEvent({
|
|
734
|
+
eventDate: '2026-01-31',
|
|
735
|
+
recurrenceType: RecurrenceType.MONTHLY
|
|
736
|
+
})
|
|
737
|
+
]);
|
|
738
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
739
|
+
startDate: '2026-01-01',
|
|
740
|
+
endDate: '2026-04-30'
|
|
741
|
+
}), user);
|
|
742
|
+
// 2026 is not a leap year: Feb has 28 days; Mar has 31 (back to day 31); Apr has 30.
|
|
743
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
744
|
+
'2026-01-31',
|
|
745
|
+
'2026-02-28',
|
|
746
|
+
'2026-03-31',
|
|
747
|
+
'2026-04-30'
|
|
748
|
+
]);
|
|
749
|
+
});
|
|
750
|
+
it('restores the original day-of-month once a longer month is reached again', async ()=>{
|
|
751
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
752
|
+
buildEvent({
|
|
753
|
+
eventDate: '2026-01-31',
|
|
754
|
+
recurrenceType: RecurrenceType.MONTHLY
|
|
755
|
+
})
|
|
756
|
+
]);
|
|
757
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
758
|
+
startDate: '2026-02-01',
|
|
759
|
+
endDate: '2026-03-31'
|
|
760
|
+
}), user);
|
|
761
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
762
|
+
'2026-02-28',
|
|
763
|
+
'2026-03-31'
|
|
764
|
+
]);
|
|
765
|
+
});
|
|
766
|
+
it('clamps to Feb 29 on a leap year for a monthly recurrence starting Jan 31', async ()=>{
|
|
767
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
768
|
+
buildEvent({
|
|
769
|
+
eventDate: '2028-01-31',
|
|
770
|
+
recurrenceType: RecurrenceType.MONTHLY
|
|
771
|
+
})
|
|
772
|
+
]);
|
|
773
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
774
|
+
startDate: '2028-01-01',
|
|
775
|
+
endDate: '2028-02-29'
|
|
776
|
+
}), user);
|
|
777
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
778
|
+
'2028-01-31',
|
|
779
|
+
'2028-02-29'
|
|
780
|
+
]);
|
|
781
|
+
});
|
|
782
|
+
// ---------------------------------------------------------------------
|
|
783
|
+
// All-day events
|
|
784
|
+
// ---------------------------------------------------------------------
|
|
785
|
+
it('preserves isAllDay through non-recurring expansion', async ()=>{
|
|
786
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
787
|
+
buildEvent({
|
|
788
|
+
eventDate: '2026-04-10',
|
|
789
|
+
isAllDay: true,
|
|
790
|
+
startTime: '00:00',
|
|
791
|
+
endTime: '23:59'
|
|
792
|
+
})
|
|
793
|
+
]);
|
|
794
|
+
const result = await service.getEventsForCalendarRange(queryDto(), user);
|
|
795
|
+
expect(result[0].isAllDay).toBe(true);
|
|
796
|
+
expect(result[0].startTime).toBe('00:00');
|
|
797
|
+
expect(result[0].endTime).toBe('23:59');
|
|
798
|
+
});
|
|
799
|
+
it('preserves isAllDay through recurring expansion', async ()=>{
|
|
800
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
801
|
+
buildEvent({
|
|
802
|
+
eventDate: '2026-01-01',
|
|
803
|
+
recurrenceType: RecurrenceType.DAILY,
|
|
804
|
+
isAllDay: true,
|
|
805
|
+
startTime: '00:00',
|
|
806
|
+
endTime: '23:59'
|
|
807
|
+
})
|
|
808
|
+
]);
|
|
809
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
810
|
+
startDate: '2026-01-01',
|
|
811
|
+
endDate: '2026-01-02'
|
|
812
|
+
}), user);
|
|
813
|
+
expect(result.every((e)=>e.isAllDay === true)).toBe(true);
|
|
814
|
+
});
|
|
815
|
+
// ---------------------------------------------------------------------
|
|
816
|
+
// Sorting and multi-event ordering
|
|
817
|
+
// ---------------------------------------------------------------------
|
|
818
|
+
it('returns events sorted chronologically by eventDate, including expanded recurrence instances', async ()=>{
|
|
819
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
820
|
+
buildEvent({
|
|
821
|
+
id: 'event-b',
|
|
822
|
+
eventDate: '2026-01-03',
|
|
823
|
+
recurrenceType: RecurrenceType.NONE
|
|
824
|
+
}),
|
|
825
|
+
buildEvent({
|
|
826
|
+
id: 'event-a',
|
|
827
|
+
eventDate: '2026-01-01',
|
|
828
|
+
recurrenceType: RecurrenceType.DAILY,
|
|
829
|
+
recurrenceEndDate: '2026-01-02'
|
|
830
|
+
})
|
|
831
|
+
]);
|
|
832
|
+
const result = await service.getEventsForCalendarRange(queryDto({
|
|
833
|
+
startDate: '2026-01-01',
|
|
834
|
+
endDate: '2026-01-05'
|
|
835
|
+
}), user);
|
|
836
|
+
expect(result.map((e)=>e.eventDate)).toEqual([
|
|
837
|
+
'2026-01-01',
|
|
838
|
+
'2026-01-02',
|
|
839
|
+
'2026-01-03'
|
|
840
|
+
]);
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
describe('ensureParticipantRepositoryInitialized', ()=>{
|
|
844
|
+
it('caches the participant repository after the first resolution', async ()=>{
|
|
845
|
+
await service.ensureParticipantRepositoryInitialized();
|
|
846
|
+
await service.ensureParticipantRepositoryInitialized();
|
|
847
|
+
expect(mockDataSourceProvider.getRepository).toHaveBeenCalledTimes(1);
|
|
848
|
+
expect(mockDataSourceProvider.getRepository).toHaveBeenCalledWith(EventParticipant);
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
});
|