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