@tstdl/base 0.93.145 → 0.93.146

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.
Files changed (39) hide show
  1. package/authentication/tests/authentication.client-service.test.js +15 -19
  2. package/authentication/tests/authentication.service.test.js +92 -119
  3. package/notification/tests/notification-client.test.js +39 -50
  4. package/notification/tests/notification-flow.test.js +204 -238
  5. package/notification/tests/notification-sse.service.test.js +20 -27
  6. package/notification/tests/notification-type.service.test.js +17 -20
  7. package/orm/tests/query-complex.test.js +80 -111
  8. package/orm/tests/repository-advanced.test.js +100 -143
  9. package/orm/tests/repository-attributes.test.js +30 -39
  10. package/orm/tests/repository-compound-primary-key.test.js +67 -75
  11. package/orm/tests/repository-comprehensive.test.js +76 -101
  12. package/orm/tests/repository-coverage.test.d.ts +1 -0
  13. package/orm/tests/repository-coverage.test.js +88 -149
  14. package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
  15. package/orm/tests/repository-cti-extensive.test.js +118 -147
  16. package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
  17. package/orm/tests/repository-cti-mapping.test.js +29 -42
  18. package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
  19. package/orm/tests/repository-cti-soft-delete.test.js +25 -37
  20. package/orm/tests/repository-cti-transactions.test.js +19 -33
  21. package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
  22. package/orm/tests/repository-cti-upsert-many.test.js +38 -50
  23. package/orm/tests/repository-cti.test.d.ts +1 -0
  24. package/orm/tests/repository-cti.test.js +195 -247
  25. package/orm/tests/repository-expiration.test.d.ts +1 -0
  26. package/orm/tests/repository-expiration.test.js +46 -59
  27. package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
  28. package/orm/tests/repository-extra-coverage.test.js +195 -337
  29. package/orm/tests/repository-mapping.test.d.ts +1 -0
  30. package/orm/tests/repository-mapping.test.js +20 -20
  31. package/orm/tests/repository-regression.test.js +124 -163
  32. package/orm/tests/repository-search.test.js +30 -44
  33. package/orm/tests/repository-soft-delete.test.js +54 -79
  34. package/orm/tests/repository-types.test.js +77 -111
  35. package/package.json +1 -1
  36. package/task-queue/tests/worker.test.js +5 -5
  37. package/testing/README.md +38 -16
  38. package/testing/integration-setup.d.ts +11 -0
  39. package/testing/integration-setup.js +57 -30
@@ -6,15 +6,16 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
8
8
  import { SubjectService } from '../../authentication/server/subject.service.js';
9
- import { runInInjectionContext, Singleton } from '../../injector/index.js';
9
+ import { Singleton } from '../../injector/index.js';
10
10
  import { MailService } from '../../mail/mail.service.js';
11
- import { injectRepository } from '../../orm/server/index.js';
11
+ import { getRepository } from '../../orm/server/index.js';
12
12
  import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
13
13
  import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationStatus, WebPushSubscription } from '../models/index.js';
14
14
  import { configureNotification } from '../server/module.js';
15
15
  import { EmailChannelProvider } from '../server/providers/email-channel-provider.js';
16
16
  import { NotificationAncillaryService } from '../server/services/notification-ancillary.service.js';
17
17
  import { NotificationDeliveryWorker } from '../server/services/notification-delivery.worker.js';
18
+ import { NotificationSseService } from '../server/services/notification-sse.service.js';
18
19
  import { NotificationTypeService } from '../server/services/notification-type.service.js';
19
20
  import { NotificationService } from '../server/services/notification.service.js';
20
21
  let MockNotificationAncillaryService = class MockNotificationAncillaryService extends NotificationAncillaryService {
@@ -33,6 +34,9 @@ describe('Notification Flow (Integration)', () => {
33
34
  let typeService;
34
35
  let subjectService;
35
36
  let mailServiceMock;
37
+ let logRepo;
38
+ let inAppRepo;
39
+ let webPushRepo;
36
40
  const schema = 'notification';
37
41
  let tenantId;
38
42
  beforeAll(async () => {
@@ -50,6 +54,8 @@ describe('Notification Flow (Integration)', () => {
50
54
  // Mock MailService
51
55
  mailServiceMock = { send: vi.fn() };
52
56
  injector.register(MailService, { useValue: mailServiceMock });
57
+ // Mock NotificationSseService
58
+ injector.register(NotificationSseService, { useValue: { dispatch: vi.fn() } });
53
59
  // Mock NotificationAncillaryService
54
60
  configureNotification({ injector, ancillaryService: MockNotificationAncillaryService });
55
61
  // Resolve Services
@@ -57,6 +63,9 @@ describe('Notification Flow (Integration)', () => {
57
63
  worker = injector.resolve(NotificationDeliveryWorker);
58
64
  typeService = injector.resolve(NotificationTypeService);
59
65
  subjectService = injector.resolve(SubjectService);
66
+ logRepo = injector.resolve(getRepository(NotificationLogEntity));
67
+ inAppRepo = injector.resolve(getRepository(InAppNotification));
68
+ webPushRepo = injector.resolve(getRepository(WebPushSubscription));
60
69
  // Register providers
61
70
  worker.registerProvider(NotificationChannel.Email, injector.resolve(EmailChannelProvider));
62
71
  const InAppChannelProvider = (await import('../server/providers/in-app-channel-provider.js')).InAppChannelProvider;
@@ -65,273 +74,230 @@ describe('Notification Flow (Integration)', () => {
65
74
  beforeEach(async () => {
66
75
  tenantId = crypto.randomUUID();
67
76
  vi.clearAllMocks();
77
+ await typeService.initializeTypes({
78
+ test: {
79
+ label: 'Test Category',
80
+ escalations: [{ delay: 1000, channel: NotificationChannel.Email }],
81
+ },
82
+ throttled: {
83
+ label: 'Throttled Category',
84
+ throttling: { limit: 1, interval: 60000 },
85
+ },
86
+ readTest: {
87
+ label: 'Read Test',
88
+ escalations: [{ delay: 1000, channel: NotificationChannel.Email }],
89
+ },
90
+ prefTest: { label: 'Preference Test' },
91
+ unknownTest: { label: 'Unknown Test' },
92
+ manageTest: { label: 'Manage Test' },
93
+ testType: { label: 'Test Type' },
94
+ auto: { label: 'Auto Test' },
95
+ archive: { label: 'Archive All Test' },
96
+ });
68
97
  });
69
98
  afterEach(async () => {
70
99
  await clearTenantData(database, schema, ['in_app', 'in_app_archive', 'log', 'preference', 'web_push_subscription'], tenantId);
71
100
  await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
72
101
  });
73
102
  test('should execute full notification flow with escalation', async () => {
74
- await runInInjectionContext(injector, async () => {
75
- const logRepo = injectRepository(NotificationLogEntity);
76
- const inAppRepo = injectRepository(InAppNotification);
77
- const user = await subjectService.createUser({
78
- tenantId,
79
- email: 'test@example.com',
80
- firstName: 'Test',
81
- lastName: 'User',
82
- });
83
- await typeService.initializeTypes({
84
- test: {
85
- label: 'Test Category',
86
- escalations: [{ delay: 1000, channel: NotificationChannel.Email }],
87
- },
88
- });
89
- await notificationService.send(tenantId, user.id, {
90
- type: 'test',
91
- priority: 'high',
92
- triggerSubjectId: user.id,
93
- payload: { message: 'Hello', testField: 'Test Value' },
94
- });
95
- const logs = await logRepo.loadManyByQuery({ tenantId });
96
- expect(logs).toHaveLength(1);
97
- const log = logs[0];
98
- expect(log.status).toBe(NotificationStatus.Pending);
99
- expect(log.currentStep).toBe(0);
100
- // Step 0 (In-App)
101
- const result0 = await worker.deliver(log.id);
102
- expect(result0.payload.action).toBe('reschedule');
103
- const inApps = await inAppRepo.loadManyByQuery({ tenantId });
104
- expect(inApps).toHaveLength(1);
105
- expect(inApps[0].logId).toBe(log.id);
106
- const logAfterStep0 = await logRepo.load(log.id);
107
- expect(logAfterStep0.status).toBe(NotificationStatus.Sent);
108
- expect(logAfterStep0.currentStep).toBe(1);
109
- // Step 1 (Email Escalation)
110
- const result1 = await worker.deliver(log.id);
111
- expect(result1.payload.action).toBe('complete');
112
- expect(mailServiceMock.send).toHaveBeenCalled();
113
- const mailArgs = mailServiceMock.send.mock.calls[0][0];
114
- expect(mailArgs.to).toBe('test@example.com');
115
- const logAfterStep1 = await logRepo.load(log.id);
116
- expect(logAfterStep1.currentStep).toBe(2);
103
+ const user = await subjectService.createUser({
104
+ tenantId,
105
+ email: 'test@example.com',
106
+ firstName: 'Test',
107
+ lastName: 'User',
117
108
  });
109
+ await notificationService.send(tenantId, user.id, {
110
+ type: 'test',
111
+ priority: 'high',
112
+ triggerSubjectId: user.id,
113
+ payload: { message: 'Hello', testField: 'Test Value' },
114
+ });
115
+ const logs = await logRepo.loadManyByQuery({ tenantId });
116
+ expect(logs).toHaveLength(1);
117
+ const log = logs[0];
118
+ expect(log.status).toBe(NotificationStatus.Pending);
119
+ expect(log.currentStep).toBe(0);
120
+ // Step 0 (In-App)
121
+ const result0 = await worker.deliver(log.id);
122
+ expect(result0.payload.action).toBe('reschedule');
123
+ const inApps = await inAppRepo.loadManyByQuery({ tenantId });
124
+ expect(inApps).toHaveLength(1);
125
+ expect(inApps[0].logId).toBe(log.id);
126
+ const logAfterStep0 = await logRepo.load(log.id);
127
+ expect(logAfterStep0.status).toBe(NotificationStatus.Sent);
128
+ expect(logAfterStep0.currentStep).toBe(1);
129
+ // Step 1 (Email Escalation)
130
+ const result1 = await worker.deliver(log.id);
131
+ expect(result1.payload.action).toBe('complete');
132
+ expect(mailServiceMock.send).toHaveBeenCalled();
133
+ const mailArgs = mailServiceMock.send.mock.calls[0][0];
134
+ expect(mailArgs.to).toBe('test@example.com');
135
+ const logAfterStep1 = await logRepo.load(log.id);
136
+ expect(logAfterStep1.currentStep).toBe(2);
118
137
  });
119
138
  test('should handle throttling correctly', async () => {
120
- await runInInjectionContext(injector, async () => {
121
- const logRepo = injectRepository(NotificationLogEntity);
122
- const user = await subjectService.createUser({ tenantId, email: 'throttled@example.com', firstName: 'Throttled', lastName: 'User' });
123
- await typeService.initializeTypes({
124
- throttled: {
125
- label: 'Throttled Category',
126
- throttling: { limit: 1, interval: 60000 },
127
- },
128
- });
129
- await notificationService.send(tenantId, user.id, {
130
- type: 'throttled', priority: 'medium', triggerSubjectId: user.id, payload: {},
131
- });
132
- await notificationService.send(tenantId, user.id, {
133
- type: 'throttled', priority: 'medium', triggerSubjectId: user.id, payload: {},
134
- });
135
- const logs = await logRepo.loadManyByQuery({ tenantId });
136
- logs.sort((a, b) => Number(a.timestamp) - Number(b.timestamp));
137
- // First one should pass
138
- const result1 = await worker.deliver(logs[0].id);
139
- expect(result1.payload.action).toBe('complete'); // No escalations
140
- // Second one should be throttled
141
- const result2 = await worker.deliver(logs[1].id);
142
- expect(result2.payload.action).toBe('reschedule');
139
+ const user = await subjectService.createUser({ tenantId, email: 'throttled@example.com', firstName: 'Throttled', lastName: 'User' });
140
+ await notificationService.send(tenantId, user.id, {
141
+ type: 'throttled', priority: 'medium', triggerSubjectId: user.id, payload: {},
142
+ });
143
+ await notificationService.send(tenantId, user.id, {
144
+ type: 'throttled', priority: 'medium', triggerSubjectId: user.id, payload: {},
143
145
  });
146
+ const logs = await logRepo.loadManyByQuery({ tenantId });
147
+ logs.sort((a, b) => Number(a.timestamp) - Number(b.timestamp));
148
+ // First one should pass
149
+ const result1 = await worker.deliver(logs[0].id);
150
+ expect(result1.payload.action).toBe('complete'); // No escalations
151
+ // Second one should be throttled
152
+ const result2 = await worker.deliver(logs[1].id);
153
+ expect(result2.payload.action).toBe('reschedule');
144
154
  });
145
155
  test('should skip escalation if notification is read', async () => {
146
- await runInInjectionContext(injector, async () => {
147
- const logRepo = injectRepository(NotificationLogEntity);
148
- const user = await subjectService.createUser({ tenantId, email: 'read@example.com', firstName: 'Read', lastName: 'User' });
149
- await typeService.initializeTypes({
150
- readTest: {
151
- label: 'Read Test',
152
- escalations: [{ delay: 1000, channel: NotificationChannel.Email }],
153
- },
154
- });
155
- await notificationService.send(tenantId, user.id, {
156
- type: 'readTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
157
- });
158
- const logs = await logRepo.loadManyByQuery({ tenantId });
159
- const log = logs[0];
160
- // Step 0: Deliver In-App
161
- await worker.deliver(log.id);
162
- // Mark as Read
163
- const inAppNotifications = await notificationService.listInApp(tenantId, user.id);
164
- expect(inAppNotifications).toHaveLength(1);
165
- await notificationService.markRead(tenantId, user.id, inAppNotifications[0].id);
166
- // Step 1: Attempt Escalation
167
- const result = await worker.deliver(log.id);
168
- expect(result.payload.action).toBe('complete');
169
- expect(mailServiceMock.send).not.toHaveBeenCalled();
156
+ const user = await subjectService.createUser({ tenantId, email: 'read@example.com', firstName: 'Read', lastName: 'User' });
157
+ await notificationService.send(tenantId, user.id, {
158
+ type: 'readTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
170
159
  });
160
+ const logs = await logRepo.loadManyByQuery({ tenantId });
161
+ const log = logs[0];
162
+ // Step 0: Deliver In-App
163
+ await worker.deliver(log.id);
164
+ // Mark as Read
165
+ const inAppNotifications = await notificationService.listInApp(tenantId, user.id);
166
+ expect(inAppNotifications).toHaveLength(1);
167
+ await notificationService.markRead(tenantId, user.id, inAppNotifications[0].id);
168
+ // Step 1: Attempt Escalation
169
+ const result = await worker.deliver(log.id);
170
+ expect(result.payload.action).toBe('complete');
171
+ expect(mailServiceMock.send).not.toHaveBeenCalled();
171
172
  });
172
173
  test('should respect user preferences', async () => {
173
- await runInInjectionContext(injector, async () => {
174
- const logRepo = injectRepository(NotificationLogEntity);
175
- const user = await subjectService.createUser({ tenantId, email: 'pref@example.com', firstName: 'Pref', lastName: 'User' });
176
- await typeService.initializeTypes({
177
- prefTest: { label: 'Preference Test' },
178
- });
179
- // Disable InApp, Enable Email (even though not default)
180
- await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.InApp, false);
181
- await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.Email, true);
182
- await notificationService.send(tenantId, user.id, {
183
- type: 'prefTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
184
- });
185
- const logs = await logRepo.loadManyByQuery({ tenantId });
186
- const log = logs[0];
187
- // Deliver
188
- await worker.deliver(log.id);
189
- // Should have sent Email but NOT InApp
190
- expect(mailServiceMock.send).toHaveBeenCalled();
191
- const inAppNotifications = await notificationService.listInApp(tenantId, user.id);
192
- expect(inAppNotifications).toHaveLength(0);
174
+ const user = await subjectService.createUser({ tenantId, email: 'pref@example.com', firstName: 'Pref', lastName: 'User' });
175
+ // Disable InApp, Enable Email (even though not default)
176
+ await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.InApp, false);
177
+ await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.Email, true);
178
+ await notificationService.send(tenantId, user.id, {
179
+ type: 'prefTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
193
180
  });
181
+ const logs = await logRepo.loadManyByQuery({ tenantId });
182
+ const log = logs[0];
183
+ // Deliver
184
+ await worker.deliver(log.id);
185
+ // Should have sent Email but NOT InApp
186
+ expect(mailServiceMock.send).toHaveBeenCalled();
187
+ const inAppNotifications = await notificationService.listInApp(tenantId, user.id);
188
+ expect(inAppNotifications).toHaveLength(0);
194
189
  });
195
190
  test('should handle unknown channels gracefully', async () => {
196
- await runInInjectionContext(injector, async () => {
197
- const logRepo = injectRepository(NotificationLogEntity);
198
- const user = await subjectService.createUser({ tenantId, email: 'unknown@example.com', firstName: 'Unknown', lastName: 'User' });
199
- await typeService.initializeTypes({
200
- unknownTest: { label: 'Unknown Test' },
201
- });
202
- // Force a preference for an unknown channel/unregistered provider (e.g., WebPush if not registered or some random string if type allowed)
203
- // Since NotificationChannel is an enum, we use WebPush which we haven't registered in the test setup (wait, we didn't register WebPush)
204
- await notificationService.updatePreference(tenantId, user.id, 'unknownTest', NotificationChannel.WebPush, true);
205
- // Disable InApp so only WebPush is attempted
206
- await notificationService.updatePreference(tenantId, user.id, 'unknownTest', NotificationChannel.InApp, false);
207
- await notificationService.send(tenantId, user.id, {
208
- type: 'unknownTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
209
- });
210
- const logs = await logRepo.loadManyByQuery({ tenantId });
211
- const log = logs[0];
212
- // Deliver
213
- const result = await worker.deliver(log.id);
214
- // Should complete without error, just logging a warning (which we can't easily assert on unless we spy logger)
215
- expect(result.payload.action).toBe('complete');
191
+ const user = await subjectService.createUser({ tenantId, email: 'unknown@example.com', firstName: 'Unknown', lastName: 'User' });
192
+ // Force a preference for an unknown channel/unregistered provider (e.g., WebPush if not registered or some random string if type allowed)
193
+ // Since NotificationChannel is an enum, we use WebPush which we haven't registered in the test setup (wait, we didn't register WebPush)
194
+ await notificationService.updatePreference(tenantId, user.id, 'unknownTest', NotificationChannel.WebPush, true);
195
+ // Disable InApp so only WebPush is attempted
196
+ await notificationService.updatePreference(tenantId, user.id, 'unknownTest', NotificationChannel.InApp, false);
197
+ await notificationService.send(tenantId, user.id, {
198
+ type: 'unknownTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
216
199
  });
200
+ const logs = await logRepo.loadManyByQuery({ tenantId });
201
+ const log = logs[0];
202
+ // Deliver
203
+ const result = await worker.deliver(log.id);
204
+ // Should complete without error, just logging a warning (which we can't easily assert on unless we spy logger)
205
+ expect(result.payload.action).toBe('complete');
217
206
  });
218
207
  test('should manage notification lists and status (markRead, archive)', async () => {
219
- await runInInjectionContext(injector, async () => {
220
- const logRepo = injectRepository(NotificationLogEntity);
221
- const inAppRepo = injectRepository(InAppNotification);
222
- const user = await subjectService.createUser({ tenantId, email: 'manage@example.com', firstName: 'Manage', lastName: 'User' });
223
- await typeService.initializeTypes({ manageTest: { label: 'Manage Test' } });
224
- await notificationService.send(tenantId, user.id, {
225
- type: 'manageTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
226
- });
227
- const logs = await logRepo.loadManyByQuery({ tenantId });
228
- await worker.deliver(logs[0].id);
229
- // List
230
- let list = await notificationService.listInApp(tenantId, user.id);
231
- expect(list).toHaveLength(1);
232
- expect(list[0].readTimestamp).toBeNull();
233
- expect(list[0].notification).toBeDefined();
234
- // Mark Read
235
- await notificationService.markRead(tenantId, user.id, list[0].id);
236
- list = await notificationService.listInApp(tenantId, user.id);
237
- expect(list[0].readTimestamp).not.toBeNull();
238
- // Archive
239
- await notificationService.archive(tenantId, user.id, list[0].id);
240
- // List (excludes archived)
241
- list = await notificationService.listInApp(tenantId, user.id);
242
- expect(list).toHaveLength(0);
243
- // List archived
244
- list = await notificationService.listArchivedInApp(tenantId, user.id);
245
- expect(list).toHaveLength(1);
246
- expect(list[0].archiveTimestamp).not.toBeNull();
208
+ const user = await subjectService.createUser({ tenantId, email: 'manage@example.com', firstName: 'Manage', lastName: 'User' });
209
+ await notificationService.send(tenantId, user.id, {
210
+ type: 'manageTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
247
211
  });
212
+ const logs = await logRepo.loadManyByQuery({ tenantId });
213
+ await worker.deliver(logs[0].id);
214
+ // List
215
+ let list = await notificationService.listInApp(tenantId, user.id);
216
+ expect(list).toHaveLength(1);
217
+ expect(list[0].readTimestamp).toBeNull();
218
+ expect(list[0].notification).toBeDefined();
219
+ // Mark Read
220
+ await notificationService.markRead(tenantId, user.id, list[0].id);
221
+ list = await notificationService.listInApp(tenantId, user.id);
222
+ expect(list[0].readTimestamp).not.toBeNull();
223
+ // Archive
224
+ await notificationService.archive(tenantId, user.id, list[0].id);
225
+ // List (excludes archived)
226
+ list = await notificationService.listInApp(tenantId, user.id);
227
+ expect(list).toHaveLength(0);
228
+ // List archived
229
+ list = await notificationService.listArchivedInApp(tenantId, user.id);
230
+ expect(list).toHaveLength(1);
231
+ expect(list[0].archiveTimestamp).not.toBeNull();
248
232
  });
249
233
  test('should register web push subscription', async () => {
250
- await runInInjectionContext(injector, async () => {
251
- const webPushRepo = injectRepository(WebPushSubscription);
252
- const user = await subjectService.createUser({ tenantId, email: 'push@example.com', firstName: 'Push', lastName: 'User' });
253
- await notificationService.registerWebPush(tenantId, user.id, 'https://endpoint.com', new Uint8Array(32), new Uint8Array(32));
254
- const subs = await webPushRepo.loadManyByQuery({ tenantId, userId: user.id });
255
- expect(subs).toHaveLength(1);
256
- expect(subs[0].endpoint).toBe('https://endpoint.com');
257
- });
234
+ const user = await subjectService.createUser({ tenantId, email: 'push@example.com', firstName: 'Push', lastName: 'User' });
235
+ await notificationService.registerWebPush(tenantId, user.id, 'https://endpoint.com', new Uint8Array(32), new Uint8Array(32));
236
+ const subs = await webPushRepo.loadManyByQuery({ tenantId, userId: user.id });
237
+ expect(subs).toHaveLength(1);
238
+ expect(subs[0].endpoint).toBe('https://endpoint.com');
258
239
  });
259
240
  test('should retrieve user preferences', async () => {
260
- await runInInjectionContext(injector, async () => {
261
- const user = await subjectService.createUser({ tenantId, email: 'getpref@example.com', firstName: 'GetPref', lastName: 'User' });
262
- await typeService.initializeTypes({ testType: { label: 'Test Type' } });
263
- await notificationService.updatePreference(tenantId, user.id, 'testType', NotificationChannel.Email, true);
264
- const prefs = await notificationService.getPreferences(tenantId, user.id);
265
- expect(prefs).toHaveLength(1);
266
- expect(prefs[0].type).toBe('testType');
267
- expect(prefs[0].channel).toBe(NotificationChannel.Email);
268
- expect(prefs[0].enabled).toBe(true);
269
- });
241
+ const user = await subjectService.createUser({ tenantId, email: 'getpref@example.com', firstName: 'GetPref', lastName: 'User' });
242
+ await notificationService.updatePreference(tenantId, user.id, 'testType', NotificationChannel.Email, true);
243
+ const prefs = await notificationService.getPreferences(tenantId, user.id);
244
+ expect(prefs).toHaveLength(1);
245
+ prefs.sort((a, b) => a.type.localeCompare(b.type));
246
+ expect(prefs[0].type).toBe('testType');
247
+ expect(prefs[0].channel).toBe(NotificationChannel.Email);
248
+ expect(prefs[0].enabled).toBe(true);
270
249
  });
271
250
  test('should support keyset pagination with after and orderBy', async () => {
272
- await runInInjectionContext(injector, async () => {
273
- const logRepo = injectRepository(NotificationLogEntity);
274
- const user = await subjectService.createUser({ tenantId, email: 'pagination@example.com', firstName: 'Pagination', lastName: 'User' });
275
- await typeService.initializeTypes({ test: { label: 'Pagination Test' } });
276
- // Create 3 notifications
277
- await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 1 } });
278
- await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 2 } });
279
- await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 3 } });
280
- const logs = await logRepo.loadManyByQuery({ tenantId });
281
- for (const log of logs) {
282
- await worker.deliver(log.id);
283
- }
284
- // Default list (desc timestamp, then desc id)
285
- const list = await notificationService.listInApp(tenantId, user.id);
286
- expect(list).toHaveLength(3);
287
- const firstId = list[0].id;
288
- const secondId = list[1].id;
289
- // After first
290
- const afterFirst = await notificationService.listInApp(tenantId, user.id, { after: firstId });
291
- expect(afterFirst).toHaveLength(2);
292
- expect(afterFirst[0].id).toBe(secondId);
293
- // After second
294
- const afterSecond = await notificationService.listInApp(tenantId, user.id, { after: secondId });
295
- expect(afterSecond).toHaveLength(1);
296
- expect(afterSecond[0].id).toBe(list[2].id);
297
- });
251
+ const user = await subjectService.createUser({ tenantId, email: 'pagination@example.com', firstName: 'Pagination', lastName: 'User' });
252
+ // Create 3 notifications
253
+ await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 1 } });
254
+ await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 2 } });
255
+ await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 3 } });
256
+ const logs = await logRepo.loadManyByQuery({ tenantId });
257
+ for (const log of logs) {
258
+ await worker.deliver(log.id);
259
+ }
260
+ // Default list (desc timestamp, then desc id)
261
+ const list = await notificationService.listInApp(tenantId, user.id);
262
+ expect(list).toHaveLength(3);
263
+ const firstId = list[0].id;
264
+ const secondId = list[1].id;
265
+ // After first
266
+ const afterFirst = await notificationService.listInApp(tenantId, user.id, { after: firstId });
267
+ expect(afterFirst).toHaveLength(2);
268
+ expect(afterFirst[0].id).toBe(secondId);
269
+ // After second
270
+ const afterSecond = await notificationService.listInApp(tenantId, user.id, { after: secondId });
271
+ expect(afterSecond).toHaveLength(1);
272
+ expect(afterSecond[0].id).toBe(list[2].id);
298
273
  });
299
274
  test('should auto-archive old notifications', async () => {
300
- await runInInjectionContext(injector, async () => {
301
- const logRepo = injectRepository(NotificationLogEntity);
302
- const inAppRepo = injectRepository(InAppNotification);
303
- const user = await subjectService.createUser({ tenantId, email: 'auto@example.com', firstName: 'Auto', lastName: 'User' });
304
- await typeService.initializeTypes({ test: { label: 'Auto Test' } });
305
- await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: {} });
306
- const logs = await logRepo.loadManyByQuery({ tenantId });
307
- await worker.deliver(logs[0].id);
308
- // Verify active
309
- expect(await notificationService.listInApp(tenantId, user.id)).toHaveLength(1);
310
- // Manually update timestamp to be old (31 days ago)
311
- const oldTimestamp = Date.now() - 31 * 24 * 60 * 60 * 1000;
312
- await logRepo.updateByQuery({ id: logs[0].id }, { timestamp: oldTimestamp });
313
- await inAppRepo.updateByQuery({ tenantId, logId: logs[0].id }, { timestamp: oldTimestamp });
314
- await notificationService.runAutoArchive();
315
- // Verify archived
316
- expect(await notificationService.listInApp(tenantId, user.id)).toHaveLength(0);
317
- expect(await notificationService.listArchivedInApp(tenantId, user.id)).toHaveLength(1);
318
- });
275
+ const user = await subjectService.createUser({ tenantId, email: 'auto@example.com', firstName: 'Auto', lastName: 'User' });
276
+ await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: {} });
277
+ const logs = await logRepo.loadManyByQuery({ tenantId });
278
+ await worker.deliver(logs[0].id);
279
+ // Verify active
280
+ expect(await notificationService.listInApp(tenantId, user.id)).toHaveLength(1);
281
+ // Manually update timestamp to be old (31 days ago)
282
+ const oldTimestamp = Date.now() - 31 * 24 * 60 * 60 * 1000;
283
+ await logRepo.updateByQuery({ id: logs[0].id }, { timestamp: oldTimestamp });
284
+ await inAppRepo.updateByQuery({ tenantId, logId: logs[0].id }, { timestamp: oldTimestamp });
285
+ await notificationService.runAutoArchive();
286
+ // Verify archived
287
+ expect(await notificationService.listInApp(tenantId, user.id)).toHaveLength(0);
288
+ expect(await notificationService.listArchivedInApp(tenantId, user.id)).toHaveLength(1);
319
289
  });
320
290
  test('should archive all notifications for a user', async () => {
321
- await runInInjectionContext(injector, async () => {
322
- const logRepo = injectRepository(NotificationLogEntity);
323
- const user = await subjectService.createUser({ tenantId, email: 'archiveall@example.com', firstName: 'Archive', lastName: 'All' });
324
- await typeService.initializeTypes({ test: { label: 'Archive All Test' } });
325
- await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: {} });
326
- await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: {} });
327
- const logs = await logRepo.loadManyByQuery({ tenantId });
328
- for (const log of logs) {
329
- await worker.deliver(log.id);
330
- }
331
- expect(await notificationService.listInApp(tenantId, user.id)).toHaveLength(2);
332
- await notificationService.archiveAll(tenantId, user.id);
333
- expect(await notificationService.listInApp(tenantId, user.id)).toHaveLength(0);
334
- expect(await notificationService.listArchivedInApp(tenantId, user.id)).toHaveLength(2);
335
- });
291
+ const user = await subjectService.createUser({ tenantId, email: 'archiveall@example.com', firstName: 'Archive', lastName: 'All' });
292
+ await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: {} });
293
+ await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: {} });
294
+ const logs = await logRepo.loadManyByQuery({ tenantId });
295
+ for (const log of logs) {
296
+ await worker.deliver(log.id);
297
+ }
298
+ expect(await notificationService.listInApp(tenantId, user.id)).toHaveLength(2);
299
+ await notificationService.archiveAll(tenantId, user.id);
300
+ expect(await notificationService.listInApp(tenantId, user.id)).toHaveLength(0);
301
+ expect(await notificationService.listArchivedInApp(tenantId, user.id)).toHaveLength(2);
336
302
  });
337
303
  });
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, test } from 'vitest';
2
- import { runInInjectionContext } from '../../injector/index.js';
3
2
  import { setupIntegrationTest } from '../../testing/index.js';
4
3
  import { NotificationSseService } from '../server/services/notification-sse.service.js';
5
4
  describe('NotificationSseService', () => {
@@ -8,43 +7,37 @@ describe('NotificationSseService', () => {
8
7
  const service = injector.resolve(NotificationSseService);
9
8
  const tenantId = 't1';
10
9
  const userId = 'u1';
11
- await runInInjectionContext(injector, async () => {
12
- const source = service.register(tenantId, userId);
13
- expect(source).toBeDefined();
14
- // We can't easily spy on the LocalMessageBus internals without more complex setup,
15
- // but we can verify that sending doesn't throw.
16
- const msg = { id: 'l1', tenantId, userId, logId: 'l1' };
17
- await expect(service.dispatch(tenantId, userId, { notification: msg, unreadCount: 1 })).resolves.not.toThrow();
18
- });
10
+ const source = service.register(tenantId, userId);
11
+ expect(source).toBeDefined();
12
+ // We can't easily spy on the LocalMessageBus internals without more complex setup,
13
+ // but we can verify that sending doesn't throw.
14
+ const msg = { id: 'l1', tenantId, userId, logId: 'l1' };
15
+ await expect(service.dispatch(tenantId, userId, { notification: msg, unreadCount: 1 })).resolves.not.toThrow();
19
16
  });
20
17
  test('should dispatch unread count update', async () => {
21
18
  const { injector } = await setupIntegrationTest({ modules: { messageBus: true, signals: true } });
22
19
  const service = injector.resolve(NotificationSseService);
23
20
  const tenantId = 't1';
24
21
  const userId = 'u1';
25
- await runInInjectionContext(injector, async () => {
26
- const source = service.register(tenantId, userId);
27
- const messages = [];
28
- source.subscribe((msg) => messages.push(msg));
29
- await service.dispatch(tenantId, userId, { unreadCount: 5 });
30
- expect(messages).toHaveLength(1);
31
- expect(messages[0]).toEqual({ unreadCount: 5 });
32
- });
22
+ const source = service.register(tenantId, userId);
23
+ const messages = [];
24
+ source.subscribe((msg) => messages.push(msg));
25
+ await service.dispatch(tenantId, userId, { unreadCount: 5 });
26
+ expect(messages).toHaveLength(1);
27
+ expect(messages[0]).toEqual({ unreadCount: 5 });
33
28
  });
34
29
  test('should dispatch mark read and mark all read', async () => {
35
30
  const { injector } = await setupIntegrationTest({ modules: { messageBus: true, signals: true } });
36
31
  const service = injector.resolve(NotificationSseService);
37
32
  const tenantId = 't1';
38
33
  const userId = 'u1';
39
- await runInInjectionContext(injector, async () => {
40
- const source = service.register(tenantId, userId);
41
- const messages = [];
42
- source.subscribe((msg) => messages.push(msg));
43
- await service.dispatch(tenantId, userId, { readId: 'n1', unreadCount: 2 });
44
- await service.dispatch(tenantId, userId, { readAll: true, unreadCount: 0 });
45
- expect(messages).toHaveLength(2);
46
- expect(messages[0]).toEqual({ readId: 'n1', unreadCount: 2 });
47
- expect(messages[1]).toEqual({ readAll: true, unreadCount: 0 });
48
- });
34
+ const source = service.register(tenantId, userId);
35
+ const messages = [];
36
+ source.subscribe((msg) => messages.push(msg));
37
+ await service.dispatch(tenantId, userId, { readId: 'n1', unreadCount: 2 });
38
+ await service.dispatch(tenantId, userId, { readAll: true, unreadCount: 0 });
39
+ expect(messages).toHaveLength(2);
40
+ expect(messages[0]).toEqual({ readId: 'n1', unreadCount: 2 });
41
+ expect(messages[1]).toEqual({ readAll: true, unreadCount: 0 });
49
42
  });
50
43
  });