@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
|
@@ -47,27 +47,23 @@ describe('AuthenticationClientService Integration', () => {
|
|
|
47
47
|
await clearTenantData(database, schema, ['credentials', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
|
|
48
48
|
});
|
|
49
49
|
test('login and logout should work', async () => {
|
|
50
|
-
await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(service.isLoggedIn()).toBe(false);
|
|
59
|
-
});
|
|
50
|
+
const user = await subjectService.createUser({ tenantId, email: 'client-test@example.com', firstName: 'C', lastName: 'T' });
|
|
51
|
+
await serverService.setCredentials(user, 'Strong-Pass-2026!');
|
|
52
|
+
expect(service.isLoggedIn()).toBe(false);
|
|
53
|
+
await service.login({ tenantId, subject: user.id }, 'Strong-Pass-2026!');
|
|
54
|
+
expect(service.isLoggedIn()).toBe(true);
|
|
55
|
+
expect(service.subjectId()).toBe(user.id);
|
|
56
|
+
await service.logout();
|
|
57
|
+
expect(service.isLoggedIn()).toBe(false);
|
|
60
58
|
});
|
|
61
59
|
test('refresh should work', async () => {
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
expect(service.isLoggedIn()).toBe(true);
|
|
70
|
-
});
|
|
60
|
+
const user = await subjectService.createUser({ tenantId, email: 'refresh-test@example.com', firstName: 'R', lastName: 'T' });
|
|
61
|
+
await serverService.setCredentials(user, 'Strong-Pass-2026!');
|
|
62
|
+
await service.login({ tenantId, subject: user.id }, 'Strong-Pass-2026!');
|
|
63
|
+
const initialToken = service.token()?.jti;
|
|
64
|
+
await service.refresh();
|
|
65
|
+
expect(service.token()?.jti).not.toBe(initialToken);
|
|
66
|
+
expect(service.isLoggedIn()).toBe(true);
|
|
71
67
|
});
|
|
72
68
|
test('checkSecret should work', async () => {
|
|
73
69
|
const result = await service.checkSecret('123');
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
|
2
2
|
import { ActorType, Auditor } from '../../audit/index.js';
|
|
3
3
|
import { NIL_UUID } from '../../constants.js';
|
|
4
|
-
import { runInInjectionContext } from '../../injector/index.js';
|
|
5
4
|
import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
|
|
6
5
|
import { AuthenticationAncillaryService } from '../server/authentication-ancillary.service.js';
|
|
7
6
|
import { AuthenticationService } from '../server/authentication.service.js';
|
|
@@ -33,151 +32,125 @@ describe('AuthenticationService', () => {
|
|
|
33
32
|
await clearTenantData(database, schema, ['credentials', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
|
|
34
33
|
});
|
|
35
34
|
test('login should create a session and listSessions should return it', async () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
lastName: 'Doe',
|
|
42
|
-
});
|
|
43
|
-
await authenticationService.setCredentials(user, 'Strong-Password-2026!');
|
|
44
|
-
const tokenResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor.with({ actor: user.id, actorType: ActorType.Subject }));
|
|
45
|
-
expect(tokenResult.token).toBeDefined();
|
|
46
|
-
const sessions = await authenticationService.listSessions(tenantId, user.id);
|
|
47
|
-
expect(sessions).toHaveLength(1);
|
|
48
|
-
expect(sessions[0]?.id).toBe(tokenResult.jsonToken.payload.session);
|
|
35
|
+
const user = await subjectService.createUser({
|
|
36
|
+
tenantId,
|
|
37
|
+
email: 'test@example.com',
|
|
38
|
+
firstName: 'John',
|
|
39
|
+
lastName: 'Doe',
|
|
49
40
|
});
|
|
41
|
+
await authenticationService.setCredentials(user, 'Strong-Password-2026!');
|
|
42
|
+
const tokenResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor.with({ actor: user.id, actorType: ActorType.Subject }));
|
|
43
|
+
expect(tokenResult.token).toBeDefined();
|
|
44
|
+
const sessions = await authenticationService.listSessions(tenantId, user.id);
|
|
45
|
+
expect(sessions).toHaveLength(1);
|
|
46
|
+
expect(sessions[0]?.id).toBe(tokenResult.jsonToken.payload.session);
|
|
50
47
|
});
|
|
51
48
|
test('invalidateAllSessions should end all active sessions', async () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
lastName: 'Doe',
|
|
58
|
-
});
|
|
59
|
-
await authenticationService.setCredentials(user, 'Strong-Password-2026!');
|
|
60
|
-
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
61
|
-
await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
|
|
62
|
-
await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
|
|
63
|
-
let sessions = await authenticationService.listSessions(tenantId, user.id);
|
|
64
|
-
expect(sessions).toHaveLength(2);
|
|
65
|
-
const now = Date.now();
|
|
66
|
-
expect(sessions.every((s) => s.end > now)).toBe(true);
|
|
67
|
-
await authenticationService.invalidateAllSessions(tenantId, user.id, userAuditor);
|
|
68
|
-
sessions = await authenticationService.listSessions(tenantId, user.id);
|
|
69
|
-
expect(sessions.every((s) => s.end <= now + 1000)).toBe(true); // small buffer for test execution time
|
|
49
|
+
const user = await subjectService.createUser({
|
|
50
|
+
tenantId,
|
|
51
|
+
email: 'test@example.com',
|
|
52
|
+
firstName: 'John',
|
|
53
|
+
lastName: 'Doe',
|
|
70
54
|
});
|
|
55
|
+
await authenticationService.setCredentials(user, 'Strong-Password-2026!');
|
|
56
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
57
|
+
await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
|
|
58
|
+
await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
|
|
59
|
+
let sessions = await authenticationService.listSessions(tenantId, user.id);
|
|
60
|
+
expect(sessions).toHaveLength(2);
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
expect(sessions.every((s) => s.end > now)).toBe(true);
|
|
63
|
+
await authenticationService.invalidateAllSessions(tenantId, user.id, userAuditor);
|
|
64
|
+
sessions = await authenticationService.listSessions(tenantId, user.id);
|
|
65
|
+
expect(sessions.every((s) => s.end <= now + 1000)).toBe(true); // small buffer for test execution time
|
|
71
66
|
});
|
|
72
67
|
test('getSession and tryGetSession', async () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
lastName: 'Doe',
|
|
79
|
-
});
|
|
80
|
-
await authenticationService.setCredentials(user, 'Strong-Password-2026!');
|
|
81
|
-
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
82
|
-
const tokenResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
|
|
83
|
-
const sessionId = tokenResult.jsonToken.payload.session;
|
|
84
|
-
const session = await authenticationService.getSession(sessionId);
|
|
85
|
-
expect(session.id).toBe(sessionId);
|
|
86
|
-
const triedSession = await authenticationService.tryGetSession(sessionId);
|
|
87
|
-
expect(triedSession?.id).toBe(sessionId);
|
|
88
|
-
const nonExistent = await authenticationService.tryGetSession(NIL_UUID);
|
|
89
|
-
expect(nonExistent).toBeUndefined();
|
|
68
|
+
const user = await subjectService.createUser({
|
|
69
|
+
tenantId,
|
|
70
|
+
email: 'test@example.com',
|
|
71
|
+
firstName: 'John',
|
|
72
|
+
lastName: 'Doe',
|
|
90
73
|
});
|
|
74
|
+
await authenticationService.setCredentials(user, 'Strong-Password-2026!');
|
|
75
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
76
|
+
const tokenResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
|
|
77
|
+
const sessionId = tokenResult.jsonToken.payload.session;
|
|
78
|
+
const session = await authenticationService.getSession(sessionId);
|
|
79
|
+
expect(session.id).toBe(sessionId);
|
|
80
|
+
const triedSession = await authenticationService.tryGetSession(sessionId);
|
|
81
|
+
expect(triedSession?.id).toBe(sessionId);
|
|
82
|
+
const nonExistent = await authenticationService.tryGetSession(NIL_UUID);
|
|
83
|
+
expect(nonExistent).toBeUndefined();
|
|
91
84
|
});
|
|
92
85
|
test('refresh should issue new token and session', async () => {
|
|
93
|
-
await
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expect(refreshResult.refreshToken).not.toBe(loginResult.refreshToken);
|
|
101
|
-
});
|
|
86
|
+
const user = await subjectService.createUser({ tenantId, email: 'refresh@example.com', firstName: 'R', lastName: 'F' });
|
|
87
|
+
await authenticationService.setCredentials(user, 'Strong-Password-2026!');
|
|
88
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
89
|
+
const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
|
|
90
|
+
const refreshResult = await authenticationService.refresh(loginResult.refreshToken, undefined, {}, userAuditor);
|
|
91
|
+
expect(refreshResult.token).toBeDefined();
|
|
92
|
+
expect(refreshResult.refreshToken).not.toBe(loginResult.refreshToken);
|
|
102
93
|
});
|
|
103
94
|
test('changeSecret should update credentials', async () => {
|
|
104
|
-
await
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
expect(authResult.success).toBe(true);
|
|
111
|
-
});
|
|
95
|
+
const user = await subjectService.createUser({ tenantId, email: 'change@example.com', firstName: 'C', lastName: 'S' });
|
|
96
|
+
await authenticationService.setCredentials(user, 'Old-Password-2026!');
|
|
97
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
98
|
+
await authenticationService.changeSecret({ tenantId, subject: user.id }, 'Old-Password-2026!', 'New-Password-2026!', userAuditor);
|
|
99
|
+
const authResult = await authenticationService.authenticate({ tenantId, subject: user.id }, 'New-Password-2026!');
|
|
100
|
+
expect(authResult.success).toBe(true);
|
|
112
101
|
});
|
|
113
102
|
test('checkSecret, testSecret, and validateSecret', async () => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
await expect(authenticationService.validateSecret(strong)).resolves.not.toThrow();
|
|
123
|
-
});
|
|
103
|
+
const weak = 'abc';
|
|
104
|
+
const strong = 'Very-Strong-Password-2026-!@#$';
|
|
105
|
+
expect((await authenticationService.checkSecret(weak)).strength).toBeLessThan(2);
|
|
106
|
+
expect((await authenticationService.checkSecret(strong)).strength).toBeGreaterThanOrEqual(2);
|
|
107
|
+
expect((await authenticationService.testSecret(weak)).success).toBe(false);
|
|
108
|
+
expect((await authenticationService.testSecret(strong)).success).toBe(true);
|
|
109
|
+
await expect(authenticationService.validateSecret(weak)).rejects.toThrow();
|
|
110
|
+
await expect(authenticationService.validateSecret(strong)).resolves.not.toThrow();
|
|
124
111
|
});
|
|
125
112
|
test('tryResolveSubject and resolveSubject', async () => {
|
|
126
|
-
await
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await expect(authenticationService.resolveSubject({ tenantId, subject: 'missing' })).rejects.toThrow();
|
|
133
|
-
});
|
|
113
|
+
const user = await subjectService.createUser({ tenantId, email: 'resolve@example.com', firstName: 'R', lastName: 'S' });
|
|
114
|
+
const resolved = await authenticationService.tryResolveSubject({ tenantId, subject: user.id });
|
|
115
|
+
expect(resolved?.id).toBe(user.id);
|
|
116
|
+
const resolvedByEmail = await authenticationService.resolveSubject({ tenantId, subject: 'resolve@example.com' });
|
|
117
|
+
expect(resolvedByEmail.id).toBe(user.id);
|
|
118
|
+
await expect(authenticationService.resolveSubject({ tenantId, subject: 'missing' })).rejects.toThrow();
|
|
134
119
|
});
|
|
135
120
|
test('impersonation and unimpersonation', async () => {
|
|
136
|
-
await
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
expect(unimpersonated.jsonToken.payload.impersonator).toBeUndefined();
|
|
147
|
-
});
|
|
121
|
+
const admin = await subjectService.createUser({ tenantId, email: 'admin@example.com', firstName: 'A', lastName: 'D' });
|
|
122
|
+
const user = await subjectService.createUser({ tenantId, email: 'user@example.com', firstName: 'U', lastName: 'S' });
|
|
123
|
+
const adminAuditor = auditor.with({ actor: admin.id, actorType: ActorType.Subject });
|
|
124
|
+
const adminToken = await authenticationService.getToken(admin, undefined);
|
|
125
|
+
const impersonated = await authenticationService.impersonate(adminToken.token, adminToken.refreshToken, user.id, undefined, adminAuditor);
|
|
126
|
+
expect(impersonated.jsonToken.payload.subject).toBe(user.id);
|
|
127
|
+
expect(impersonated.jsonToken.payload.impersonator).toBe(admin.id);
|
|
128
|
+
const unimpersonated = await authenticationService.unimpersonate(impersonated.impersonatorRefreshToken, undefined, adminAuditor);
|
|
129
|
+
expect(unimpersonated.jsonToken.payload.subject).toBe(admin.id);
|
|
130
|
+
expect(unimpersonated.jsonToken.payload.impersonator).toBeUndefined();
|
|
148
131
|
});
|
|
149
132
|
test('refresh should throw on invalid token', async () => {
|
|
150
|
-
await
|
|
151
|
-
await expect(authenticationService.refresh('invalid', undefined, {}, auditor)).rejects.toThrow();
|
|
152
|
-
});
|
|
133
|
+
await expect(authenticationService.refresh('invalid', undefined, {}, auditor)).rejects.toThrow();
|
|
153
134
|
});
|
|
154
135
|
test('login should throw on invalid credentials', async () => {
|
|
155
|
-
await
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await expect(authenticationService.login({ tenantId, subject: user.id }, 'wrong', undefined, auditor)).rejects.toThrow();
|
|
159
|
-
});
|
|
136
|
+
const user = await subjectService.createUser({ tenantId, email: 'fail@example.com', firstName: 'F', lastName: 'L' });
|
|
137
|
+
await authenticationService.setCredentials(user, 'Very-Strong-Password-2026!');
|
|
138
|
+
await expect(authenticationService.login({ tenantId, subject: user.id }, 'wrong', undefined, auditor)).rejects.toThrow();
|
|
160
139
|
});
|
|
161
140
|
test('endSession should handle non-existent session gracefully', async () => {
|
|
162
|
-
await
|
|
163
|
-
await expect(authenticationService.endSession(NIL_UUID, auditor)).resolves.not.toThrow();
|
|
164
|
-
});
|
|
141
|
+
await expect(authenticationService.endSession(NIL_UUID, auditor)).resolves.not.toThrow();
|
|
165
142
|
});
|
|
166
143
|
test('resolveSubject should throw if not found', async () => {
|
|
167
|
-
await
|
|
168
|
-
await expect(authenticationService.resolveSubject({ tenantId, subject: 'missing' })).rejects.toThrow();
|
|
169
|
-
});
|
|
144
|
+
await expect(authenticationService.resolveSubject({ tenantId, subject: 'missing' })).rejects.toThrow();
|
|
170
145
|
});
|
|
171
146
|
test('secret reset flow', async () => {
|
|
172
|
-
await
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
expect(authResult.success).toBe(true);
|
|
180
|
-
});
|
|
147
|
+
const user = await subjectService.createUser({ tenantId, email: 'reset@example.com', firstName: 'R', lastName: 'E' });
|
|
148
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
149
|
+
await authenticationService.initSecretReset({ tenantId, subject: user.id }, undefined, userAuditor);
|
|
150
|
+
expect(ancillaryService.lastResetData).toBeDefined();
|
|
151
|
+
await authenticationService.resetSecret(ancillaryService.lastResetData.token, 'New-Password-Reset-2026!', userAuditor);
|
|
152
|
+
const authResult = await authenticationService.authenticate({ tenantId, subject: user.id }, 'New-Password-Reset-2026!');
|
|
153
|
+
expect(authResult.success).toBe(true);
|
|
181
154
|
});
|
|
182
155
|
test('deriveSigningSecrets should work', async () => {
|
|
183
156
|
// This is mostly covered by initialize, but we can test it implicitly by ensuring service works after init
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
2
|
import { AuthenticationClientService } from '../../authentication/client/authentication.service.js';
|
|
3
|
-
import { Injector
|
|
3
|
+
import { Injector } from '../../injector/index.js';
|
|
4
4
|
import { NotificationApiClient } from '../../notification/api/index.js';
|
|
5
5
|
import { NotificationClient } from '../../notification/client/notification-client.js';
|
|
6
6
|
import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
|
|
7
|
+
import { timeout } from '../../utils/timing.js';
|
|
7
8
|
import { BehaviorSubject, of, Subject } from 'rxjs';
|
|
8
9
|
describe('NotificationClient', () => {
|
|
9
10
|
let injector;
|
|
@@ -33,11 +34,9 @@ describe('NotificationClient', () => {
|
|
|
33
34
|
sessionId$.next(undefined);
|
|
34
35
|
});
|
|
35
36
|
test('should initialize with empty state', () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
expect(notificationClient.types()).toEqual({});
|
|
40
|
-
});
|
|
37
|
+
expect(notificationClient.notifications()).toEqual([]);
|
|
38
|
+
expect(notificationClient.unreadCount()).toBe(0);
|
|
39
|
+
expect(notificationClient.types()).toEqual({});
|
|
41
40
|
});
|
|
42
41
|
test('should load notifications on session start', async () => {
|
|
43
42
|
const notifications = [{ id: '1', type: 'test' }];
|
|
@@ -46,52 +45,44 @@ describe('NotificationClient', () => {
|
|
|
46
45
|
notificationApiClientMock.listInApp.mockResolvedValue(notifications);
|
|
47
46
|
notificationApiClientMock.unreadCount.mockResolvedValue(unreadCount);
|
|
48
47
|
notificationApiClientMock.types.mockResolvedValue(types);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(notificationApiClientMock.stream).toHaveBeenCalled();
|
|
60
|
-
});
|
|
48
|
+
sessionId$.next('session-1');
|
|
49
|
+
// Wait for async operations (microtasks)
|
|
50
|
+
await timeout(0);
|
|
51
|
+
expect(notificationClient.notifications()).toEqual(notifications);
|
|
52
|
+
expect(notificationClient.unreadCount()).toBe(unreadCount);
|
|
53
|
+
expect(notificationClient.types()).toEqual(types);
|
|
54
|
+
expect(notificationApiClientMock.listInApp).toHaveBeenCalledWith({ limit: 20 });
|
|
55
|
+
expect(notificationApiClientMock.unreadCount).toHaveBeenCalled();
|
|
56
|
+
expect(notificationApiClientMock.types).toHaveBeenCalled();
|
|
57
|
+
expect(notificationApiClientMock.stream).toHaveBeenCalled();
|
|
61
58
|
});
|
|
62
59
|
test('should clear notifications on session end', async () => {
|
|
63
60
|
const notifications = [{ id: '1', type: 'test' }];
|
|
64
61
|
notificationApiClientMock.listInApp.mockResolvedValue(notifications);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
expect(notificationClient.unreadCount()).toBe(0);
|
|
73
|
-
});
|
|
62
|
+
sessionId$.next('session-1');
|
|
63
|
+
await timeout(0);
|
|
64
|
+
expect(notificationClient.notifications()).toHaveLength(1);
|
|
65
|
+
sessionId$.next(undefined);
|
|
66
|
+
await timeout(0);
|
|
67
|
+
expect(notificationClient.notifications()).toEqual([]);
|
|
68
|
+
expect(notificationClient.unreadCount()).toBe(0);
|
|
74
69
|
});
|
|
75
70
|
test('should handle new notification from stream', async () => {
|
|
76
71
|
const initialNotifications = [{ id: '1', type: 'test' }];
|
|
77
72
|
notificationApiClientMock.listInApp.mockResolvedValue(initialNotifications);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
expect(notificationClient.unreadCount()).toBe(1);
|
|
85
|
-
});
|
|
73
|
+
sessionId$.next('session-1');
|
|
74
|
+
await timeout(0);
|
|
75
|
+
const newNotification = { id: '2', type: 'test' };
|
|
76
|
+
stream$.next({ notification: newNotification, unreadCount: 1 });
|
|
77
|
+
expect(notificationClient.notifications()).toEqual([newNotification, ...initialNotifications]);
|
|
78
|
+
expect(notificationClient.unreadCount()).toBe(1);
|
|
86
79
|
});
|
|
87
80
|
test('should handle unread count update from stream', async () => {
|
|
88
81
|
notificationApiClientMock.listInApp.mockResolvedValue([]);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(notificationClient.unreadCount()).toBe(10);
|
|
94
|
-
});
|
|
82
|
+
sessionId$.next('session-1');
|
|
83
|
+
await timeout(0);
|
|
84
|
+
stream$.next({ unreadCount: 10 });
|
|
85
|
+
expect(notificationClient.unreadCount()).toBe(10);
|
|
95
86
|
});
|
|
96
87
|
test('should load next page of notifications', async () => {
|
|
97
88
|
const page1 = [{ id: '2', type: 'test' }];
|
|
@@ -99,14 +90,12 @@ describe('NotificationClient', () => {
|
|
|
99
90
|
notificationApiClientMock.listInApp
|
|
100
91
|
.mockResolvedValueOnce(page1) // Initial load
|
|
101
92
|
.mockResolvedValueOnce(page2); // Pagination
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(notificationApiClientMock.listInApp).toHaveBeenCalledWith({ limit: 10, after: '2' });
|
|
110
|
-
});
|
|
93
|
+
sessionId$.next('session-1');
|
|
94
|
+
await timeout(0);
|
|
95
|
+
expect(notificationClient.notifications()).toEqual(page1);
|
|
96
|
+
notificationClient.loadNext(10);
|
|
97
|
+
await timeout(0);
|
|
98
|
+
expect(notificationClient.notifications()).toEqual([...page1, ...page2]);
|
|
99
|
+
expect(notificationApiClientMock.listInApp).toHaveBeenCalledWith({ limit: 10, after: '2' });
|
|
111
100
|
});
|
|
112
101
|
});
|