@tstdl/base 0.93.145 → 0.93.147
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/authentication/tests/authentication.client-service.test.js +15 -19
- package/authentication/tests/authentication.service.test.js +92 -119
- package/notification/tests/notification-client.test.js +39 -50
- package/notification/tests/notification-flow.test.js +204 -238
- package/notification/tests/notification-sse.service.test.js +20 -27
- package/notification/tests/notification-type.service.test.js +17 -20
- package/orm/repository.types.d.ts +13 -2
- package/orm/server/repository.d.ts +60 -4
- package/orm/server/repository.js +126 -25
- package/orm/tests/query-complex.test.js +80 -111
- package/orm/tests/repository-advanced.test.js +100 -143
- package/orm/tests/repository-attributes.test.js +30 -39
- package/orm/tests/repository-compound-primary-key.test.js +67 -75
- package/orm/tests/repository-comprehensive.test.js +76 -101
- package/orm/tests/repository-coverage.test.d.ts +1 -0
- package/orm/tests/repository-coverage.test.js +88 -149
- package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
- package/orm/tests/repository-cti-extensive.test.js +118 -147
- package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
- package/orm/tests/repository-cti-mapping.test.js +29 -42
- package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
- package/orm/tests/repository-cti-soft-delete.test.js +25 -37
- package/orm/tests/repository-cti-transactions.test.js +19 -33
- package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
- package/orm/tests/repository-cti-upsert-many.test.js +38 -50
- package/orm/tests/repository-cti.test.d.ts +1 -0
- package/orm/tests/repository-cti.test.js +195 -247
- package/orm/tests/repository-expiration.test.d.ts +1 -0
- package/orm/tests/repository-expiration.test.js +46 -59
- package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
- package/orm/tests/repository-extra-coverage.test.js +195 -337
- package/orm/tests/repository-mapping.test.d.ts +1 -0
- package/orm/tests/repository-mapping.test.js +20 -20
- package/orm/tests/repository-regression.test.js +124 -163
- package/orm/tests/repository-search.test.js +30 -44
- package/orm/tests/repository-soft-delete.test.js +54 -79
- package/orm/tests/repository-types.test.js +77 -111
- package/orm/tests/repository-undelete.test.d.ts +2 -0
- package/orm/tests/repository-undelete.test.js +201 -0
- package/package.json +3 -3
- package/task-queue/tests/worker.test.js +5 -5
- package/testing/README.md +38 -16
- package/testing/integration-setup.d.ts +11 -0
- 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 {
|
|
9
|
+
import { Singleton } from '../../injector/index.js';
|
|
10
10
|
import { MailService } from '../../mail/mail.service.js';
|
|
11
|
-
import {
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
await
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
await
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
});
|