@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,194 @@
|
|
|
1
|
+
import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
|
|
2
|
+
import { Event, EventParticipant, EventWithCompany } from '../entities';
|
|
3
|
+
import { EventManagerDataSourceProvider } from './event-manager-datasource.provider';
|
|
4
|
+
function buildConfigService(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(overrides.isCompanyFeatureEnabled ?? false),
|
|
7
|
+
getDatabaseMode: jest.fn().mockReturnValue('single'),
|
|
8
|
+
isMultiTenant: jest.fn().mockReturnValue(false),
|
|
9
|
+
getDefaultColor: jest.fn(),
|
|
10
|
+
getMaxRecurrenceOccurrences: jest.fn(),
|
|
11
|
+
getOptions: jest.fn().mockReturnValue(overrides.options ?? {
|
|
12
|
+
bootstrapAppConfig: {
|
|
13
|
+
enableCompanyFeature: false,
|
|
14
|
+
databaseMode: 'single'
|
|
15
|
+
},
|
|
16
|
+
config: {
|
|
17
|
+
defaultDatabaseConfig: {
|
|
18
|
+
type: 'mysql',
|
|
19
|
+
host: 'localhost'
|
|
20
|
+
},
|
|
21
|
+
tenantDefaultDatabaseConfig: undefined,
|
|
22
|
+
tenants: []
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
describe('EventManagerDataSourceProvider', ()=>{
|
|
28
|
+
afterEach(()=>{
|
|
29
|
+
jest.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
describe('constructor / buildParentOptions', ()=>{
|
|
32
|
+
it('maps module options to the parent IDataSourceServiceOptions shape', ()=>{
|
|
33
|
+
const configService = buildConfigService({
|
|
34
|
+
options: {
|
|
35
|
+
bootstrapAppConfig: {
|
|
36
|
+
enableCompanyFeature: true,
|
|
37
|
+
databaseMode: 'multi-tenant'
|
|
38
|
+
},
|
|
39
|
+
config: {
|
|
40
|
+
defaultDatabaseConfig: {
|
|
41
|
+
type: 'mysql',
|
|
42
|
+
host: 'db-host'
|
|
43
|
+
},
|
|
44
|
+
tenantDefaultDatabaseConfig: {
|
|
45
|
+
type: 'mysql',
|
|
46
|
+
host: 'tenant-host'
|
|
47
|
+
},
|
|
48
|
+
tenants: [
|
|
49
|
+
{
|
|
50
|
+
id: 'tenant-1',
|
|
51
|
+
database: 'tenant_db'
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
58
|
+
expect(provider.options).toEqual({
|
|
59
|
+
bootstrapAppConfig: {
|
|
60
|
+
enableCompanyFeature: true,
|
|
61
|
+
databaseMode: 'multi-tenant'
|
|
62
|
+
},
|
|
63
|
+
defaultDatabaseConfig: {
|
|
64
|
+
type: 'mysql',
|
|
65
|
+
host: 'db-host'
|
|
66
|
+
},
|
|
67
|
+
tenantDefaultDatabaseConfig: {
|
|
68
|
+
type: 'mysql',
|
|
69
|
+
host: 'tenant-host'
|
|
70
|
+
},
|
|
71
|
+
tenants: [
|
|
72
|
+
{
|
|
73
|
+
id: 'tenant-1',
|
|
74
|
+
database: 'tenant_db'
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it('builds parent options gracefully when config is not provided', ()=>{
|
|
80
|
+
const configService = buildConfigService({
|
|
81
|
+
options: {
|
|
82
|
+
bootstrapAppConfig: undefined,
|
|
83
|
+
config: undefined
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
87
|
+
expect(provider.options).toEqual({
|
|
88
|
+
bootstrapAppConfig: undefined,
|
|
89
|
+
defaultDatabaseConfig: undefined,
|
|
90
|
+
tenantDefaultDatabaseConfig: undefined,
|
|
91
|
+
tenants: undefined
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('getEnableCompanyFeatureForTenant', ()=>{
|
|
96
|
+
it('returns the tenant override when explicitly true', ()=>{
|
|
97
|
+
const configService = buildConfigService({
|
|
98
|
+
isCompanyFeatureEnabled: false
|
|
99
|
+
});
|
|
100
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
101
|
+
const result = provider.getEnableCompanyFeatureForTenant({
|
|
102
|
+
enableCompanyFeature: true
|
|
103
|
+
});
|
|
104
|
+
expect(result).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
it('returns the tenant override when explicitly false', ()=>{
|
|
107
|
+
const configService = buildConfigService({
|
|
108
|
+
isCompanyFeatureEnabled: true
|
|
109
|
+
});
|
|
110
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
111
|
+
const result = provider.getEnableCompanyFeatureForTenant({
|
|
112
|
+
enableCompanyFeature: false
|
|
113
|
+
});
|
|
114
|
+
expect(result).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
it('falls back to the config service when tenant flag is undefined', ()=>{
|
|
117
|
+
const configService = buildConfigService({
|
|
118
|
+
isCompanyFeatureEnabled: true
|
|
119
|
+
});
|
|
120
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
121
|
+
const result = provider.getEnableCompanyFeatureForTenant({});
|
|
122
|
+
expect(result).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it('falls back to the config service when no tenant is provided', ()=>{
|
|
125
|
+
const configService = buildConfigService({
|
|
126
|
+
isCompanyFeatureEnabled: false
|
|
127
|
+
});
|
|
128
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
129
|
+
const result = provider.getEnableCompanyFeatureForTenant(undefined);
|
|
130
|
+
expect(result).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('createDataSourceFromConfig', ()=>{
|
|
134
|
+
it('loads core event-manager entities when the company feature is disabled', async ()=>{
|
|
135
|
+
const configService = buildConfigService({
|
|
136
|
+
isCompanyFeatureEnabled: false
|
|
137
|
+
});
|
|
138
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
139
|
+
const spy = jest.spyOn(MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue({
|
|
140
|
+
isInitialized: true
|
|
141
|
+
});
|
|
142
|
+
const config = {
|
|
143
|
+
type: 'mysql',
|
|
144
|
+
host: 'localhost'
|
|
145
|
+
};
|
|
146
|
+
await provider.createDataSourceFromConfig(config);
|
|
147
|
+
expect(spy).toHaveBeenCalledWith(config, expect.arrayContaining([
|
|
148
|
+
Event,
|
|
149
|
+
EventParticipant
|
|
150
|
+
]));
|
|
151
|
+
expect(spy.mock.calls[0][1]).not.toContain(EventWithCompany);
|
|
152
|
+
});
|
|
153
|
+
it('loads company-scoped event-manager entities when the company feature is enabled', async ()=>{
|
|
154
|
+
const configService = buildConfigService({
|
|
155
|
+
isCompanyFeatureEnabled: true
|
|
156
|
+
});
|
|
157
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
158
|
+
const spy = jest.spyOn(MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue({
|
|
159
|
+
isInitialized: true
|
|
160
|
+
});
|
|
161
|
+
const config = {
|
|
162
|
+
type: 'mysql',
|
|
163
|
+
host: 'localhost'
|
|
164
|
+
};
|
|
165
|
+
await provider.createDataSourceFromConfig(config);
|
|
166
|
+
expect(spy).toHaveBeenCalledWith(config, expect.arrayContaining([
|
|
167
|
+
EventWithCompany,
|
|
168
|
+
EventParticipant
|
|
169
|
+
]));
|
|
170
|
+
expect(spy.mock.calls[0][1]).not.toContain(Event);
|
|
171
|
+
});
|
|
172
|
+
it('uses the current tenant override to resolve entities when a request is present', async ()=>{
|
|
173
|
+
const configService = buildConfigService({
|
|
174
|
+
isCompanyFeatureEnabled: false
|
|
175
|
+
});
|
|
176
|
+
const provider = new EventManagerDataSourceProvider(configService);
|
|
177
|
+
jest.spyOn(provider, 'getCurrentTenant').mockReturnValue({
|
|
178
|
+
enableCompanyFeature: true
|
|
179
|
+
});
|
|
180
|
+
const spy = jest.spyOn(MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue({
|
|
181
|
+
isInitialized: true
|
|
182
|
+
});
|
|
183
|
+
const config = {
|
|
184
|
+
type: 'mysql',
|
|
185
|
+
host: 'localhost'
|
|
186
|
+
};
|
|
187
|
+
await provider.createDataSourceFromConfig(config);
|
|
188
|
+
expect(spy).toHaveBeenCalledWith(config, expect.arrayContaining([
|
|
189
|
+
EventWithCompany,
|
|
190
|
+
EventParticipant
|
|
191
|
+
]));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { NotFoundException } from '@nestjs/common';
|
|
2
|
+
import { ParticipantStatus, RecurrenceType } from '@flusys/nestjs-shared';
|
|
3
|
+
import { createMockRepository } from '@test-utils/mocks/repository.mock';
|
|
4
|
+
import { EVENT_PARTICIPANT_MESSAGES } from '../config';
|
|
5
|
+
import { EventParticipant, EventWithCompany } from '../entities';
|
|
6
|
+
import { EventManagerHelperService } from './event-manager-helper.service';
|
|
7
|
+
function buildEvent(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
id: 'event-uuid-1',
|
|
10
|
+
title: 'Sprint Planning',
|
|
11
|
+
description: null,
|
|
12
|
+
eventDate: '2026-04-03',
|
|
13
|
+
startTime: '09:00',
|
|
14
|
+
endTime: '10:00',
|
|
15
|
+
isAllDay: false,
|
|
16
|
+
recurrenceType: RecurrenceType.NONE,
|
|
17
|
+
recurrenceEndDate: null,
|
|
18
|
+
weekDays: null,
|
|
19
|
+
color: '#3B82F6',
|
|
20
|
+
meetingLink: null,
|
|
21
|
+
isActive: true,
|
|
22
|
+
createdAt: new Date('2026-01-01T00:00:00Z'),
|
|
23
|
+
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
24
|
+
deletedAt: null,
|
|
25
|
+
createdById: null,
|
|
26
|
+
updatedById: null,
|
|
27
|
+
deletedById: null,
|
|
28
|
+
...overrides
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function buildParticipant(overrides = {}) {
|
|
32
|
+
return {
|
|
33
|
+
id: 'participant-uuid-1',
|
|
34
|
+
eventId: 'event-uuid-1',
|
|
35
|
+
userId: 'user-uuid-1',
|
|
36
|
+
status: ParticipantStatus.PENDING,
|
|
37
|
+
isOrganizer: false,
|
|
38
|
+
createdAt: new Date('2026-01-01T00:00:00Z'),
|
|
39
|
+
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
40
|
+
deletedAt: null,
|
|
41
|
+
createdById: null,
|
|
42
|
+
updatedById: null,
|
|
43
|
+
deletedById: null,
|
|
44
|
+
...overrides
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
describe('EventManagerHelperService', ()=>{
|
|
48
|
+
let service;
|
|
49
|
+
let mockEventRepo;
|
|
50
|
+
let mockParticipantRepo;
|
|
51
|
+
let mockConfigService;
|
|
52
|
+
let mockDataSourceProvider;
|
|
53
|
+
let mockUtilsService;
|
|
54
|
+
// `getRawMany` isn't part of the shared MockQueryBuilder — the participant queries in this
|
|
55
|
+
// service use `.getRawMany()`, so we augment the shared mock query builder instances here.
|
|
56
|
+
let mockEventQb;
|
|
57
|
+
let mockParticipantQb;
|
|
58
|
+
beforeEach(()=>{
|
|
59
|
+
mockEventRepo = createMockRepository();
|
|
60
|
+
mockParticipantRepo = createMockRepository();
|
|
61
|
+
mockEventQb = mockEventRepo.createQueryBuilder();
|
|
62
|
+
mockEventQb.getRawMany = jest.fn();
|
|
63
|
+
mockParticipantQb = mockParticipantRepo.createQueryBuilder();
|
|
64
|
+
mockParticipantQb.getRawMany = jest.fn();
|
|
65
|
+
mockConfigService = {
|
|
66
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false),
|
|
67
|
+
getDefaultColor: jest.fn().mockReturnValue('#3B82F6')
|
|
68
|
+
};
|
|
69
|
+
const fakeDataSource = {
|
|
70
|
+
getRepository: jest.fn((entity)=>entity === EventParticipant ? mockParticipantRepo : mockEventRepo)
|
|
71
|
+
};
|
|
72
|
+
mockDataSourceProvider = {
|
|
73
|
+
getDataSource: jest.fn().mockResolvedValue(fakeDataSource)
|
|
74
|
+
};
|
|
75
|
+
mockUtilsService = {
|
|
76
|
+
clearCache: jest.fn()
|
|
77
|
+
};
|
|
78
|
+
service = new EventManagerHelperService({}, mockUtilsService, mockConfigService, mockDataSourceProvider);
|
|
79
|
+
});
|
|
80
|
+
describe('createEvent', ()=>{
|
|
81
|
+
it('creates a timed event with the given fields and clears the cache', async ()=>{
|
|
82
|
+
const saved = buildEvent();
|
|
83
|
+
mockEventRepo.save.mockResolvedValue(saved);
|
|
84
|
+
mockParticipantRepo.save.mockResolvedValue([]);
|
|
85
|
+
const result = await service.createEvent({
|
|
86
|
+
title: 'Sprint Planning',
|
|
87
|
+
eventDate: '2026-04-03',
|
|
88
|
+
startTime: '09:00',
|
|
89
|
+
endTime: '10:00'
|
|
90
|
+
});
|
|
91
|
+
expect(mockEventRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
92
|
+
title: 'Sprint Planning',
|
|
93
|
+
startTime: '09:00',
|
|
94
|
+
endTime: '10:00',
|
|
95
|
+
isAllDay: false,
|
|
96
|
+
recurrenceType: RecurrenceType.NONE,
|
|
97
|
+
color: '#3B82F6'
|
|
98
|
+
}));
|
|
99
|
+
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('event', {});
|
|
100
|
+
expect(result.id).toBe(saved.id);
|
|
101
|
+
expect(result.companyId).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
it('forces symbolic 00:00 / 23:59 markers for all-day events regardless of supplied times', async ()=>{
|
|
104
|
+
const saved = buildEvent({
|
|
105
|
+
isAllDay: true,
|
|
106
|
+
startTime: '00:00',
|
|
107
|
+
endTime: '23:59'
|
|
108
|
+
});
|
|
109
|
+
mockEventRepo.save.mockResolvedValue(saved);
|
|
110
|
+
await service.createEvent({
|
|
111
|
+
title: 'Company Holiday',
|
|
112
|
+
eventDate: '2026-12-25',
|
|
113
|
+
startTime: '09:00',
|
|
114
|
+
endTime: '17:00',
|
|
115
|
+
isAllDay: true
|
|
116
|
+
});
|
|
117
|
+
expect(mockEventRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
118
|
+
isAllDay: true,
|
|
119
|
+
startTime: '00:00',
|
|
120
|
+
endTime: '23:59'
|
|
121
|
+
}));
|
|
122
|
+
});
|
|
123
|
+
it('attaches companyId only when the company feature is enabled and a companyId is supplied', async ()=>{
|
|
124
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
125
|
+
const saved = buildEvent();
|
|
126
|
+
mockEventRepo.save.mockResolvedValue(saved);
|
|
127
|
+
await service.createEvent({
|
|
128
|
+
title: 'Sprint Planning',
|
|
129
|
+
eventDate: '2026-04-03',
|
|
130
|
+
startTime: '09:00',
|
|
131
|
+
endTime: '10:00',
|
|
132
|
+
companyId: 'company-uuid-1'
|
|
133
|
+
});
|
|
134
|
+
expect(mockEventRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
135
|
+
companyId: 'company-uuid-1'
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
138
|
+
it('does not attach companyId when the company feature is disabled, even if supplied', async ()=>{
|
|
139
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
140
|
+
const saved = buildEvent();
|
|
141
|
+
mockEventRepo.save.mockResolvedValue(saved);
|
|
142
|
+
await service.createEvent({
|
|
143
|
+
title: 'Sprint Planning',
|
|
144
|
+
eventDate: '2026-04-03',
|
|
145
|
+
startTime: '09:00',
|
|
146
|
+
endTime: '10:00',
|
|
147
|
+
companyId: 'company-uuid-1'
|
|
148
|
+
});
|
|
149
|
+
const savedArg = mockEventRepo.save.mock.calls[0][0];
|
|
150
|
+
expect(savedArg).not.toHaveProperty('companyId');
|
|
151
|
+
});
|
|
152
|
+
it('adds the organizer and all participants with PENDING status, de-duplicated', async ()=>{
|
|
153
|
+
const saved = buildEvent();
|
|
154
|
+
mockEventRepo.save.mockResolvedValue(saved);
|
|
155
|
+
mockParticipantRepo.save.mockResolvedValue([]);
|
|
156
|
+
await service.createEvent({
|
|
157
|
+
title: 'Sprint Planning',
|
|
158
|
+
eventDate: '2026-04-03',
|
|
159
|
+
startTime: '09:00',
|
|
160
|
+
endTime: '10:00',
|
|
161
|
+
organizerId: 'organizer-uuid-1',
|
|
162
|
+
participantIds: [
|
|
163
|
+
'organizer-uuid-1',
|
|
164
|
+
'participant-uuid-2'
|
|
165
|
+
]
|
|
166
|
+
});
|
|
167
|
+
const savedParticipants = mockParticipantRepo.save.mock.calls[0][0];
|
|
168
|
+
expect(savedParticipants).toHaveLength(2);
|
|
169
|
+
expect(savedParticipants).toEqual(expect.arrayContaining([
|
|
170
|
+
expect.objectContaining({
|
|
171
|
+
userId: 'organizer-uuid-1',
|
|
172
|
+
isOrganizer: true,
|
|
173
|
+
status: ParticipantStatus.PENDING
|
|
174
|
+
}),
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
userId: 'participant-uuid-2',
|
|
177
|
+
isOrganizer: false,
|
|
178
|
+
status: ParticipantStatus.PENDING
|
|
179
|
+
})
|
|
180
|
+
]));
|
|
181
|
+
});
|
|
182
|
+
it('skips participant creation entirely when no organizer or participants are given', async ()=>{
|
|
183
|
+
const saved = buildEvent();
|
|
184
|
+
mockEventRepo.save.mockResolvedValue(saved);
|
|
185
|
+
await service.createEvent({
|
|
186
|
+
title: 'Sprint Planning',
|
|
187
|
+
eventDate: '2026-04-03',
|
|
188
|
+
startTime: '09:00',
|
|
189
|
+
endTime: '10:00'
|
|
190
|
+
});
|
|
191
|
+
expect(mockParticipantRepo.save).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('updateParticipantStatus', ()=>{
|
|
195
|
+
it('updates the status of an existing participant', async ()=>{
|
|
196
|
+
const participant = buildParticipant({
|
|
197
|
+
status: ParticipantStatus.PENDING
|
|
198
|
+
});
|
|
199
|
+
mockParticipantRepo.findOne.mockResolvedValue(participant);
|
|
200
|
+
mockParticipantRepo.save.mockResolvedValue({
|
|
201
|
+
...participant,
|
|
202
|
+
status: ParticipantStatus.ACCEPTED
|
|
203
|
+
});
|
|
204
|
+
await service.updateParticipantStatus('participant-uuid-1', ParticipantStatus.ACCEPTED);
|
|
205
|
+
expect(mockParticipantRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
206
|
+
status: ParticipantStatus.ACCEPTED
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
it('throws NotFoundException with the correct messageKey when the participant does not exist', async ()=>{
|
|
210
|
+
mockParticipantRepo.findOne.mockResolvedValue(null);
|
|
211
|
+
await expect(service.updateParticipantStatus('missing-uuid', ParticipantStatus.ACCEPTED)).rejects.toMatchObject({
|
|
212
|
+
response: expect.objectContaining({
|
|
213
|
+
messageKey: EVENT_PARTICIPANT_MESSAGES.NOT_FOUND
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
await expect(service.updateParticipantStatus('missing-uuid', ParticipantStatus.ACCEPTED)).rejects.toBeInstanceOf(NotFoundException);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe('getEventsForUser', ()=>{
|
|
220
|
+
it('returns an empty list without querying events when the user has no participant rows', async ()=>{
|
|
221
|
+
mockParticipantQb.getRawMany.mockResolvedValue([]);
|
|
222
|
+
const result = await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30');
|
|
223
|
+
expect(result).toEqual([]);
|
|
224
|
+
expect(mockEventQb.getMany).not.toHaveBeenCalled();
|
|
225
|
+
expect(mockEventQb.where).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
it('queries events scoped to the participant event ids and date range', async ()=>{
|
|
228
|
+
mockParticipantQb.getRawMany.mockResolvedValue([
|
|
229
|
+
{
|
|
230
|
+
ep_event_id: 'event-uuid-1'
|
|
231
|
+
}
|
|
232
|
+
]);
|
|
233
|
+
const events = [
|
|
234
|
+
buildEvent()
|
|
235
|
+
];
|
|
236
|
+
mockEventQb.getMany.mockResolvedValue(events);
|
|
237
|
+
const result = await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30');
|
|
238
|
+
const eventQb = mockEventQb;
|
|
239
|
+
expect(eventQb.where).toHaveBeenCalledWith('event.id IN (:...eventIds)', {
|
|
240
|
+
eventIds: [
|
|
241
|
+
'event-uuid-1'
|
|
242
|
+
]
|
|
243
|
+
});
|
|
244
|
+
expect(eventQb.andWhere).toHaveBeenCalledWith('event.eventDate <= :endDate', {
|
|
245
|
+
endDate: '2026-04-30'
|
|
246
|
+
});
|
|
247
|
+
expect(eventQb.andWhere).toHaveBeenCalledWith('event.eventDate >= :startDate', {
|
|
248
|
+
startDate: '2026-04-01'
|
|
249
|
+
});
|
|
250
|
+
expect(result).toHaveLength(1);
|
|
251
|
+
expect(result[0].id).toBe('event-uuid-1');
|
|
252
|
+
});
|
|
253
|
+
it('handles a zero-length range (startDate === endDate)', async ()=>{
|
|
254
|
+
mockParticipantQb.getRawMany.mockResolvedValue([
|
|
255
|
+
{
|
|
256
|
+
ep_event_id: 'event-uuid-1'
|
|
257
|
+
}
|
|
258
|
+
]);
|
|
259
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
260
|
+
buildEvent()
|
|
261
|
+
]);
|
|
262
|
+
const result = await service.getEventsForUser('user-uuid-1', '2026-04-03', '2026-04-03');
|
|
263
|
+
const eventQb = mockEventQb;
|
|
264
|
+
expect(eventQb.andWhere).toHaveBeenCalledWith('event.eventDate <= :endDate', {
|
|
265
|
+
endDate: '2026-04-03'
|
|
266
|
+
});
|
|
267
|
+
expect(eventQb.andWhere).toHaveBeenCalledWith('event.eventDate >= :startDate', {
|
|
268
|
+
startDate: '2026-04-03'
|
|
269
|
+
});
|
|
270
|
+
expect(result).toHaveLength(1);
|
|
271
|
+
});
|
|
272
|
+
it('scopes the query to the company when the company feature is enabled and a companyId is given', async ()=>{
|
|
273
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
274
|
+
mockParticipantQb.getRawMany.mockResolvedValue([
|
|
275
|
+
{
|
|
276
|
+
ep_event_id: 'event-uuid-1'
|
|
277
|
+
}
|
|
278
|
+
]);
|
|
279
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
280
|
+
buildEvent()
|
|
281
|
+
]);
|
|
282
|
+
await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30', 'company-uuid-1');
|
|
283
|
+
const eventQb = mockEventQb;
|
|
284
|
+
expect(eventQb.andWhere).toHaveBeenCalledWith('event.companyId = :companyId', {
|
|
285
|
+
companyId: 'company-uuid-1'
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
it('does not scope the query to the company when the company feature is disabled', async ()=>{
|
|
289
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
290
|
+
mockParticipantQb.getRawMany.mockResolvedValue([
|
|
291
|
+
{
|
|
292
|
+
ep_event_id: 'event-uuid-1'
|
|
293
|
+
}
|
|
294
|
+
]);
|
|
295
|
+
mockEventQb.getMany.mockResolvedValue([
|
|
296
|
+
buildEvent()
|
|
297
|
+
]);
|
|
298
|
+
await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30', 'company-uuid-1');
|
|
299
|
+
const eventQb = mockEventQb;
|
|
300
|
+
const companyCalls = eventQb.andWhere.mock.calls.filter((call)=>call[0] === 'event.companyId = :companyId');
|
|
301
|
+
expect(companyCalls).toHaveLength(0);
|
|
302
|
+
});
|
|
303
|
+
it('requests the correct entity for the company feature toggle', async ()=>{
|
|
304
|
+
mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
305
|
+
mockParticipantQb.getRawMany.mockResolvedValue([]);
|
|
306
|
+
await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30');
|
|
307
|
+
const fakeDataSource = await mockDataSourceProvider.getDataSource();
|
|
308
|
+
expect(fakeDataSource.getRepository).toHaveBeenCalledWith(EventWithCompany);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
describe('getEventById', ()=>{
|
|
312
|
+
it('returns the mapped event when found', async ()=>{
|
|
313
|
+
mockEventRepo.findOne.mockResolvedValue(buildEvent());
|
|
314
|
+
const result = await service.getEventById('event-uuid-1');
|
|
315
|
+
expect(result?.id).toBe('event-uuid-1');
|
|
316
|
+
});
|
|
317
|
+
it('returns null when the event does not exist', async ()=>{
|
|
318
|
+
mockEventRepo.findOne.mockResolvedValue(null);
|
|
319
|
+
const result = await service.getEventById('missing-uuid');
|
|
320
|
+
expect(result).toBeNull();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -355,11 +355,16 @@ export class EventService extends ApiService {
|
|
|
355
355
|
break;
|
|
356
356
|
case RecurrenceType.MONTHLY:
|
|
357
357
|
{
|
|
358
|
+
// Compute the target year/month from the *original* date before any mutation.
|
|
359
|
+
// Mutating `d` first (e.g. via setUTCMonth) can overflow past short months
|
|
360
|
+
// (Jan 31 -> Feb 31 normalizes to Mar 3), which would then compute
|
|
361
|
+
// `lastDayOfMonth` for the wrong (already-overflowed) month and silently
|
|
362
|
+
// skip target months that have fewer days than the original day-of-month.
|
|
358
363
|
const targetDay = originalDayOfMonth ?? d.getUTCDate();
|
|
359
|
-
d.
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
364
|
+
const targetYear = d.getUTCFullYear();
|
|
365
|
+
const targetMonth = d.getUTCMonth() + 1;
|
|
366
|
+
const lastDayOfMonth = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate();
|
|
367
|
+
return new Date(Date.UTC(targetYear, targetMonth, Math.min(targetDay, lastDayOfMonth))).toISOString().slice(0, 10);
|
|
363
368
|
}
|
|
364
369
|
default:
|
|
365
370
|
break;
|