@flusys/nestjs-notification 6.0.1 → 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/notification.adapter.spec.js +73 -0
- package/cjs/controllers/notification.controller.spec.js +214 -0
- package/cjs/docs/notification-swagger.config.spec.js +68 -0
- package/cjs/entities/index.spec.js +13 -0
- package/cjs/gateways/notification.gateway.spec.js +359 -0
- package/cjs/modules/notification.module.spec.js +152 -0
- package/cjs/services/notification-config.service.spec.js +113 -0
- package/cjs/services/notification-datasource.provider.spec.js +136 -0
- package/cjs/services/notification-helper.service.spec.js +306 -0
- package/cjs/services/notification.service.spec.js +490 -0
- package/fesm/adapters/notification.adapter.spec.js +69 -0
- package/fesm/controllers/notification.controller.spec.js +210 -0
- package/fesm/docs/notification-swagger.config.spec.js +64 -0
- package/fesm/entities/index.spec.js +9 -0
- package/fesm/gateways/notification.gateway.spec.js +355 -0
- package/fesm/modules/notification.module.spec.js +148 -0
- package/fesm/services/notification-config.service.spec.js +109 -0
- package/fesm/services/notification-datasource.provider.spec.js +132 -0
- package/fesm/services/notification-helper.service.spec.js +302 -0
- package/fesm/services/notification.service.spec.js +486 -0
- package/package.json +3 -3
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _testing = require("@nestjs/testing");
|
|
6
|
+
const _modules = require("@flusys/nestjs-shared/modules");
|
|
7
|
+
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
8
|
+
const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
9
|
+
const _notificationservice = require("./notification.service");
|
|
10
|
+
const _notificationconfigservice = require("./notification-config.service");
|
|
11
|
+
const _notificationdatasourceprovider = require("./notification-datasource.provider");
|
|
12
|
+
const _gateways = require("../gateways");
|
|
13
|
+
const _entities = require("../entities");
|
|
14
|
+
function buildNotification(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
id: 'notif-uuid-1',
|
|
17
|
+
userId: 'user-uuid-1',
|
|
18
|
+
type: 'info',
|
|
19
|
+
title: 'Title',
|
|
20
|
+
message: 'Message',
|
|
21
|
+
data: null,
|
|
22
|
+
isRead: false,
|
|
23
|
+
readAt: null,
|
|
24
|
+
createdAt: new Date(),
|
|
25
|
+
updatedAt: new Date(),
|
|
26
|
+
deletedAt: null,
|
|
27
|
+
createdById: null,
|
|
28
|
+
updatedById: null,
|
|
29
|
+
deletedById: null,
|
|
30
|
+
...overrides
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createMockQuery() {
|
|
34
|
+
return {
|
|
35
|
+
andWhere: jest.fn().mockReturnThis(),
|
|
36
|
+
addSelect: jest.fn().mockReturnThis(),
|
|
37
|
+
orderBy: jest.fn().mockReturnThis()
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
describe('NotificationService', ()=>{
|
|
41
|
+
let service;
|
|
42
|
+
let mockRepo;
|
|
43
|
+
let mockConfig;
|
|
44
|
+
let mockGateway;
|
|
45
|
+
async function buildService(withGateway = true) {
|
|
46
|
+
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
47
|
+
const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
48
|
+
mockConfig = {
|
|
49
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
|
|
50
|
+
};
|
|
51
|
+
mockGateway = {
|
|
52
|
+
emitToUser: jest.fn(),
|
|
53
|
+
emitToUserInCompany: jest.fn()
|
|
54
|
+
};
|
|
55
|
+
const providers = [
|
|
56
|
+
_notificationservice.NotificationService,
|
|
57
|
+
{
|
|
58
|
+
provide: 'CACHE_INSTANCE',
|
|
59
|
+
useValue: {}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
provide: _modules.UtilsService,
|
|
63
|
+
useValue: {
|
|
64
|
+
clearCache: jest.fn().mockResolvedValue(undefined)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
provide: _notificationconfigservice.NotificationConfigService,
|
|
69
|
+
useValue: mockConfig
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
provide: _notificationdatasourceprovider.NotificationDataSourceProvider,
|
|
73
|
+
useValue: mockDataSourceProvider
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
if (withGateway) {
|
|
77
|
+
providers.push({
|
|
78
|
+
provide: _gateways.NotificationGateway,
|
|
79
|
+
useValue: mockGateway
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const module = await _testing.Test.createTestingModule({
|
|
83
|
+
providers
|
|
84
|
+
}).compile();
|
|
85
|
+
const resolved = await module.resolve(_notificationservice.NotificationService);
|
|
86
|
+
resolved.repository = mockRepo;
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
beforeEach(async ()=>{
|
|
90
|
+
service = await buildService();
|
|
91
|
+
});
|
|
92
|
+
describe('resolveEntity', ()=>{
|
|
93
|
+
it('returns Notification when the company feature is disabled', ()=>{
|
|
94
|
+
expect(service.resolveEntity()).toBe(_entities.Notification);
|
|
95
|
+
});
|
|
96
|
+
it('returns NotificationWithCompany when the company feature is enabled', ()=>{
|
|
97
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
98
|
+
expect(service.resolveEntity()).toBe(_entities.NotificationWithCompany);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('convertSingleDtoToEntity', ()=>{
|
|
102
|
+
it('builds a new entity for create (no id present)', async ()=>{
|
|
103
|
+
const user = (0, _loggedusermock.buildMockUser)();
|
|
104
|
+
const dto = {
|
|
105
|
+
userId: user.id,
|
|
106
|
+
title: 'Hello',
|
|
107
|
+
type: 'success'
|
|
108
|
+
};
|
|
109
|
+
const entity = await service.convertSingleDtoToEntity(dto, user);
|
|
110
|
+
expect(entity).toMatchObject({
|
|
111
|
+
userId: user.id,
|
|
112
|
+
title: 'Hello',
|
|
113
|
+
type: 'success',
|
|
114
|
+
isRead: false
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
it('defaults type to "info" when not provided', async ()=>{
|
|
118
|
+
const user = (0, _loggedusermock.buildMockUser)();
|
|
119
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
120
|
+
userId: user.id,
|
|
121
|
+
title: 'Hi'
|
|
122
|
+
}, user);
|
|
123
|
+
expect(entity.type).toBe('info');
|
|
124
|
+
});
|
|
125
|
+
it('merges into the existing entity when updating by id', async ()=>{
|
|
126
|
+
const existing = buildNotification({
|
|
127
|
+
id: 'notif-1',
|
|
128
|
+
title: 'Old title'
|
|
129
|
+
});
|
|
130
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
131
|
+
const user = (0, _loggedusermock.buildMockUser)();
|
|
132
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
133
|
+
id: 'notif-1',
|
|
134
|
+
title: 'New title'
|
|
135
|
+
}, user);
|
|
136
|
+
expect(mockRepo.findOne).toHaveBeenCalledWith({
|
|
137
|
+
where: {
|
|
138
|
+
id: 'notif-1'
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
expect(entity.title).toBe('New title');
|
|
142
|
+
expect(entity.isRead).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
it('throws NotFoundException when updating an id that does not exist', async ()=>{
|
|
145
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
146
|
+
await expect(service.convertSingleDtoToEntity({
|
|
147
|
+
id: 'missing-id',
|
|
148
|
+
title: 'X'
|
|
149
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
|
|
150
|
+
});
|
|
151
|
+
it('includes companyId from the dto when the company feature is enabled', async ()=>{
|
|
152
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
153
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
154
|
+
companyId: 'company-user'
|
|
155
|
+
});
|
|
156
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
157
|
+
userId: user.id,
|
|
158
|
+
title: 'Hi',
|
|
159
|
+
companyId: 'company-dto'
|
|
160
|
+
}, user);
|
|
161
|
+
expect(entity.companyId).toBe('company-dto');
|
|
162
|
+
});
|
|
163
|
+
it('falls back to the user companyId when the dto has none and the company feature is enabled', async ()=>{
|
|
164
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
165
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
166
|
+
companyId: 'company-user'
|
|
167
|
+
});
|
|
168
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
169
|
+
userId: user.id,
|
|
170
|
+
title: 'Hi'
|
|
171
|
+
}, user);
|
|
172
|
+
expect(entity.companyId).toBe('company-user');
|
|
173
|
+
});
|
|
174
|
+
it('does not set companyId when the company feature is disabled', async ()=>{
|
|
175
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
176
|
+
companyId: 'company-user'
|
|
177
|
+
});
|
|
178
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
179
|
+
userId: user.id,
|
|
180
|
+
title: 'Hi',
|
|
181
|
+
companyId: 'company-dto'
|
|
182
|
+
}, user);
|
|
183
|
+
expect(entity.companyId).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe('getFilterQuery', ()=>{
|
|
187
|
+
it('adds an isRead filter when provided as boolean', async ()=>{
|
|
188
|
+
const query = createMockQuery();
|
|
189
|
+
await service.getFilterQuery(query, {
|
|
190
|
+
isRead: true
|
|
191
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
192
|
+
expect(query.andWhere).toHaveBeenCalledWith('notification.isRead = :isRead', {
|
|
193
|
+
isRead: true
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
it('adds a type filter when provided', async ()=>{
|
|
197
|
+
const query = createMockQuery();
|
|
198
|
+
await service.getFilterQuery(query, {
|
|
199
|
+
type: 'success'
|
|
200
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
201
|
+
expect(query.andWhere).toHaveBeenCalledWith('notification.type = :type', {
|
|
202
|
+
type: 'success'
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
it('adds a userId filter when provided', async ()=>{
|
|
206
|
+
const query = createMockQuery();
|
|
207
|
+
await service.getFilterQuery(query, {
|
|
208
|
+
userId: 'user-9'
|
|
209
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
210
|
+
expect(query.andWhere).toHaveBeenCalledWith('notification.userId = :userId', {
|
|
211
|
+
userId: 'user-9'
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
it('ignores unrecognized filter keys', async ()=>{
|
|
215
|
+
const query = createMockQuery();
|
|
216
|
+
await service.getFilterQuery(query, {
|
|
217
|
+
unknownField: 'x'
|
|
218
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
219
|
+
expect(query.andWhere).not.toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
it('ignores a non-boolean isRead value', async ()=>{
|
|
222
|
+
const query = createMockQuery();
|
|
223
|
+
await service.getFilterQuery(query, {
|
|
224
|
+
isRead: 'true'
|
|
225
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
226
|
+
expect(query.andWhere).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe('getSelectQuery', ()=>{
|
|
230
|
+
it('always selects createdAt and updatedAt in addition to requested fields', async ()=>{
|
|
231
|
+
const query = createMockQuery();
|
|
232
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), [
|
|
233
|
+
'title',
|
|
234
|
+
'message'
|
|
235
|
+
]);
|
|
236
|
+
expect(query.addSelect).toHaveBeenCalledWith([
|
|
237
|
+
'notification.title',
|
|
238
|
+
'notification.message',
|
|
239
|
+
'notification.createdAt',
|
|
240
|
+
'notification.updatedAt'
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
it('selects only createdAt/updatedAt when no fields are requested', async ()=>{
|
|
244
|
+
const query = createMockQuery();
|
|
245
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), undefined);
|
|
246
|
+
expect(query.addSelect).toHaveBeenCalledWith([
|
|
247
|
+
'notification.createdAt',
|
|
248
|
+
'notification.updatedAt'
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
it('deduplicates fields already included in the always-selected set', async ()=>{
|
|
252
|
+
const query = createMockQuery();
|
|
253
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), [
|
|
254
|
+
'createdAt'
|
|
255
|
+
]);
|
|
256
|
+
expect(query.addSelect).toHaveBeenCalledWith([
|
|
257
|
+
'notification.createdAt',
|
|
258
|
+
'notification.updatedAt'
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
describe('getExtraManipulateQuery', ()=>{
|
|
263
|
+
it('scopes the query to the current user', async ()=>{
|
|
264
|
+
const query = createMockQuery();
|
|
265
|
+
const user = (0, _loggedusermock.buildMockUser)();
|
|
266
|
+
await service.getExtraManipulateQuery(query, {}, user);
|
|
267
|
+
expect(query.andWhere).toHaveBeenCalledWith('notification.userId = :currentUserId', {
|
|
268
|
+
currentUserId: user.id
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
it('applies the company filter when the company feature is enabled', async ()=>{
|
|
272
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
273
|
+
const query = createMockQuery();
|
|
274
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
275
|
+
companyId: 'company-1'
|
|
276
|
+
});
|
|
277
|
+
await service.getExtraManipulateQuery(query, {}, user);
|
|
278
|
+
expect(query.andWhere).toHaveBeenCalledWith('notification.companyId = :companyId', {
|
|
279
|
+
companyId: 'company-1'
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
it('defaults to ordering by createdAt DESC when no sort is provided', async ()=>{
|
|
283
|
+
const query = createMockQuery();
|
|
284
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)());
|
|
285
|
+
expect(query.orderBy).toHaveBeenCalledWith('notification.createdAt', 'DESC');
|
|
286
|
+
});
|
|
287
|
+
it('does not apply a default sort when the caller already specified one', async ()=>{
|
|
288
|
+
const query = createMockQuery();
|
|
289
|
+
await service.getExtraManipulateQuery(query, {
|
|
290
|
+
sort: {
|
|
291
|
+
title: 'ASC'
|
|
292
|
+
}
|
|
293
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
294
|
+
expect(query.orderBy).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
it('does not scope by user when no user is provided', async ()=>{
|
|
297
|
+
const query = createMockQuery();
|
|
298
|
+
await service.getExtraManipulateQuery(query, {}, null);
|
|
299
|
+
expect(query.andWhere).not.toHaveBeenCalledWith(expect.stringContaining('currentUserId'), expect.anything());
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
describe('afterInsertOperation', ()=>{
|
|
303
|
+
it('emits a notification:new event for each inserted entity', async ()=>{
|
|
304
|
+
const entities = [
|
|
305
|
+
buildNotification({
|
|
306
|
+
id: 'n1'
|
|
307
|
+
}),
|
|
308
|
+
buildNotification({
|
|
309
|
+
id: 'n2'
|
|
310
|
+
})
|
|
311
|
+
];
|
|
312
|
+
await service.afterInsertOperation(entities, {}, (0, _loggedusermock.buildMockUser)(), {});
|
|
313
|
+
expect(mockGateway.emitToUser).toHaveBeenCalledTimes(2);
|
|
314
|
+
expect(mockGateway.emitToUser).toHaveBeenCalledWith('user-uuid-1', 'notification:new', expect.objectContaining({
|
|
315
|
+
id: 'n1'
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
it('uses emitToUserInCompany when the entity has a companyId and the company feature is enabled', async ()=>{
|
|
319
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
320
|
+
const entities = [
|
|
321
|
+
buildNotification({
|
|
322
|
+
id: 'n1',
|
|
323
|
+
companyId: 'company-1'
|
|
324
|
+
})
|
|
325
|
+
];
|
|
326
|
+
await service.afterInsertOperation(entities, {}, (0, _loggedusermock.buildMockUser)(), {});
|
|
327
|
+
expect(mockGateway.emitToUserInCompany).toHaveBeenCalledWith('user-uuid-1', 'company-1', 'notification:new', expect.any(Object));
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
describe('markAsRead', ()=>{
|
|
331
|
+
it('marks a matching notification as read and returns it', async ()=>{
|
|
332
|
+
const user = (0, _loggedusermock.buildMockUser)();
|
|
333
|
+
const notification = buildNotification({
|
|
334
|
+
id: 'notif-1',
|
|
335
|
+
userId: user.id,
|
|
336
|
+
isRead: false
|
|
337
|
+
});
|
|
338
|
+
mockRepo.findOne.mockResolvedValue(notification);
|
|
339
|
+
mockRepo.save.mockResolvedValue({
|
|
340
|
+
...notification,
|
|
341
|
+
isRead: true
|
|
342
|
+
});
|
|
343
|
+
const result = await service.markAsRead('notif-1', user);
|
|
344
|
+
expect(mockRepo.findOne).toHaveBeenCalledWith({
|
|
345
|
+
where: {
|
|
346
|
+
id: 'notif-1',
|
|
347
|
+
userId: user.id
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
expect(mockRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
351
|
+
isRead: true,
|
|
352
|
+
readAt: expect.any(Date)
|
|
353
|
+
}));
|
|
354
|
+
expect(result.isRead).toBe(true);
|
|
355
|
+
expect(mockGateway.emitToUser).toHaveBeenCalledWith(user.id, 'notification:read', {
|
|
356
|
+
id: 'notif-1'
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
it('throws NotFoundException when the notification does not belong to the user or does not exist', async ()=>{
|
|
360
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
361
|
+
await expect(service.markAsRead('missing-id', (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
|
|
362
|
+
expect(mockRepo.save).not.toHaveBeenCalled();
|
|
363
|
+
});
|
|
364
|
+
it('emits to the company-scoped room when the notification has a companyId', async ()=>{
|
|
365
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
366
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
367
|
+
companyId: 'company-1'
|
|
368
|
+
});
|
|
369
|
+
const notification = buildNotification({
|
|
370
|
+
id: 'notif-1',
|
|
371
|
+
userId: user.id,
|
|
372
|
+
companyId: 'company-1'
|
|
373
|
+
});
|
|
374
|
+
mockRepo.findOne.mockResolvedValue(notification);
|
|
375
|
+
mockRepo.save.mockResolvedValue({
|
|
376
|
+
...notification,
|
|
377
|
+
isRead: true
|
|
378
|
+
});
|
|
379
|
+
await service.markAsRead('notif-1', user);
|
|
380
|
+
expect(mockGateway.emitToUserInCompany).toHaveBeenCalledWith(user.id, 'company-1', 'notification:read', {
|
|
381
|
+
id: 'notif-1'
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
describe('markAllAsRead', ()=>{
|
|
386
|
+
it('marks all unread notifications as read for the user and returns the affected count', async ()=>{
|
|
387
|
+
const user = (0, _loggedusermock.buildMockUser)();
|
|
388
|
+
mockRepo.update.mockResolvedValue({
|
|
389
|
+
affected: 4,
|
|
390
|
+
raw: {},
|
|
391
|
+
generatedMaps: []
|
|
392
|
+
});
|
|
393
|
+
const count = await service.markAllAsRead(user);
|
|
394
|
+
expect(mockRepo.update).toHaveBeenCalledWith({
|
|
395
|
+
userId: user.id,
|
|
396
|
+
isRead: false
|
|
397
|
+
}, {
|
|
398
|
+
isRead: true,
|
|
399
|
+
readAt: expect.any(Date)
|
|
400
|
+
});
|
|
401
|
+
expect(count).toBe(4);
|
|
402
|
+
expect(mockGateway.emitToUser).toHaveBeenCalledWith(user.id, 'notification:read-all', {});
|
|
403
|
+
});
|
|
404
|
+
it('returns 0 when there is nothing to update (zero unread notifications)', async ()=>{
|
|
405
|
+
mockRepo.update.mockResolvedValue({
|
|
406
|
+
affected: 0,
|
|
407
|
+
raw: {},
|
|
408
|
+
generatedMaps: []
|
|
409
|
+
});
|
|
410
|
+
const count = await service.markAllAsRead((0, _loggedusermock.buildMockUser)());
|
|
411
|
+
expect(count).toBe(0);
|
|
412
|
+
});
|
|
413
|
+
it('returns 0 when affected is undefined/null', async ()=>{
|
|
414
|
+
mockRepo.update.mockResolvedValue({
|
|
415
|
+
affected: undefined,
|
|
416
|
+
raw: {},
|
|
417
|
+
generatedMaps: []
|
|
418
|
+
});
|
|
419
|
+
const count = await service.markAllAsRead((0, _loggedusermock.buildMockUser)());
|
|
420
|
+
expect(count).toBe(0);
|
|
421
|
+
});
|
|
422
|
+
it('scopes the update by companyId when the company feature is enabled', async ()=>{
|
|
423
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
424
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
425
|
+
companyId: 'company-1'
|
|
426
|
+
});
|
|
427
|
+
mockRepo.update.mockResolvedValue({
|
|
428
|
+
affected: 1,
|
|
429
|
+
raw: {},
|
|
430
|
+
generatedMaps: []
|
|
431
|
+
});
|
|
432
|
+
await service.markAllAsRead(user);
|
|
433
|
+
expect(mockRepo.update).toHaveBeenCalledWith({
|
|
434
|
+
userId: user.id,
|
|
435
|
+
isRead: false,
|
|
436
|
+
companyId: 'company-1'
|
|
437
|
+
}, expect.any(Object));
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
describe('getUnreadCount', ()=>{
|
|
441
|
+
it('returns the unread count for the user', async ()=>{
|
|
442
|
+
const user = (0, _loggedusermock.buildMockUser)();
|
|
443
|
+
mockRepo.count.mockResolvedValue(7);
|
|
444
|
+
const count = await service.getUnreadCount(user);
|
|
445
|
+
expect(mockRepo.count).toHaveBeenCalledWith({
|
|
446
|
+
where: {
|
|
447
|
+
userId: user.id,
|
|
448
|
+
isRead: false
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
expect(count).toBe(7);
|
|
452
|
+
});
|
|
453
|
+
it('returns 0 for a user with no unread notifications', async ()=>{
|
|
454
|
+
mockRepo.count.mockResolvedValue(0);
|
|
455
|
+
const count = await service.getUnreadCount((0, _loggedusermock.buildMockUser)());
|
|
456
|
+
expect(count).toBe(0);
|
|
457
|
+
});
|
|
458
|
+
it('scopes the count by companyId when the company feature is enabled', async ()=>{
|
|
459
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
460
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
461
|
+
companyId: 'company-1'
|
|
462
|
+
});
|
|
463
|
+
mockRepo.count.mockResolvedValue(2);
|
|
464
|
+
await service.getUnreadCount(user);
|
|
465
|
+
expect(mockRepo.count).toHaveBeenCalledWith({
|
|
466
|
+
where: {
|
|
467
|
+
userId: user.id,
|
|
468
|
+
isRead: false,
|
|
469
|
+
companyId: 'company-1'
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
describe('without a bound gateway', ()=>{
|
|
475
|
+
it('does not throw when marking as read and no gateway is registered', async ()=>{
|
|
476
|
+
const noGatewayService = await buildService(false);
|
|
477
|
+
const user = (0, _loggedusermock.buildMockUser)();
|
|
478
|
+
const notification = buildNotification({
|
|
479
|
+
id: 'notif-1',
|
|
480
|
+
userId: user.id
|
|
481
|
+
});
|
|
482
|
+
mockRepo.findOne.mockResolvedValue(notification);
|
|
483
|
+
mockRepo.save.mockResolvedValue({
|
|
484
|
+
...notification,
|
|
485
|
+
isRead: true
|
|
486
|
+
});
|
|
487
|
+
await expect(noGatewayService.markAsRead('notif-1', user)).resolves.toBeDefined();
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NotificationAdapter } from './notification.adapter';
|
|
2
|
+
describe('NotificationAdapter', ()=>{
|
|
3
|
+
let adapter;
|
|
4
|
+
let mockHelper;
|
|
5
|
+
beforeEach(()=>{
|
|
6
|
+
mockHelper = {
|
|
7
|
+
send: jest.fn(),
|
|
8
|
+
sendToMany: jest.fn(),
|
|
9
|
+
isUserOnline: jest.fn()
|
|
10
|
+
};
|
|
11
|
+
adapter = new NotificationAdapter(mockHelper);
|
|
12
|
+
});
|
|
13
|
+
describe('send', ()=>{
|
|
14
|
+
it('delegates to NotificationHelperService.send with the same options', async ()=>{
|
|
15
|
+
const options = {
|
|
16
|
+
userId: 'user-uuid-1',
|
|
17
|
+
title: 'Hello',
|
|
18
|
+
message: 'World'
|
|
19
|
+
};
|
|
20
|
+
mockHelper.send.mockResolvedValue(undefined);
|
|
21
|
+
await adapter.send(options);
|
|
22
|
+
expect(mockHelper.send).toHaveBeenCalledWith(options);
|
|
23
|
+
expect(mockHelper.send).toHaveBeenCalledTimes(1);
|
|
24
|
+
});
|
|
25
|
+
it('propagates errors thrown by the helper service', async ()=>{
|
|
26
|
+
const options = {
|
|
27
|
+
userId: 'user-uuid-1',
|
|
28
|
+
title: 'Hello'
|
|
29
|
+
};
|
|
30
|
+
mockHelper.send.mockRejectedValue(new Error('boom'));
|
|
31
|
+
await expect(adapter.send(options)).rejects.toThrow('boom');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('sendToMany', ()=>{
|
|
35
|
+
it('delegates to NotificationHelperService.sendToMany with the same options', async ()=>{
|
|
36
|
+
const options = {
|
|
37
|
+
userIds: [
|
|
38
|
+
'user-uuid-1',
|
|
39
|
+
'user-uuid-2'
|
|
40
|
+
],
|
|
41
|
+
title: 'Broadcast'
|
|
42
|
+
};
|
|
43
|
+
mockHelper.sendToMany.mockResolvedValue(undefined);
|
|
44
|
+
await adapter.sendToMany(options);
|
|
45
|
+
expect(mockHelper.sendToMany).toHaveBeenCalledWith(options);
|
|
46
|
+
expect(mockHelper.sendToMany).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
it('handles an empty userIds array without throwing', async ()=>{
|
|
49
|
+
const options = {
|
|
50
|
+
userIds: [],
|
|
51
|
+
title: 'Nobody'
|
|
52
|
+
};
|
|
53
|
+
mockHelper.sendToMany.mockResolvedValue(undefined);
|
|
54
|
+
await expect(adapter.sendToMany(options)).resolves.toBeUndefined();
|
|
55
|
+
expect(mockHelper.sendToMany).toHaveBeenCalledWith(options);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('isUserOnline', ()=>{
|
|
59
|
+
it('returns true when the helper reports the user online', ()=>{
|
|
60
|
+
mockHelper.isUserOnline.mockReturnValue(true);
|
|
61
|
+
expect(adapter.isUserOnline('user-uuid-1')).toBe(true);
|
|
62
|
+
expect(mockHelper.isUserOnline).toHaveBeenCalledWith('user-uuid-1');
|
|
63
|
+
});
|
|
64
|
+
it('returns false when the helper reports the user offline', ()=>{
|
|
65
|
+
mockHelper.isUserOnline.mockReturnValue(false);
|
|
66
|
+
expect(adapter.isUserOnline('user-uuid-2')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|