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