@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.
Files changed (44) 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/repository.types.d.ts +13 -2
  8. package/orm/server/repository.d.ts +60 -4
  9. package/orm/server/repository.js +126 -25
  10. package/orm/tests/query-complex.test.js +80 -111
  11. package/orm/tests/repository-advanced.test.js +100 -143
  12. package/orm/tests/repository-attributes.test.js +30 -39
  13. package/orm/tests/repository-compound-primary-key.test.js +67 -75
  14. package/orm/tests/repository-comprehensive.test.js +76 -101
  15. package/orm/tests/repository-coverage.test.d.ts +1 -0
  16. package/orm/tests/repository-coverage.test.js +88 -149
  17. package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
  18. package/orm/tests/repository-cti-extensive.test.js +118 -147
  19. package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
  20. package/orm/tests/repository-cti-mapping.test.js +29 -42
  21. package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
  22. package/orm/tests/repository-cti-soft-delete.test.js +25 -37
  23. package/orm/tests/repository-cti-transactions.test.js +19 -33
  24. package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
  25. package/orm/tests/repository-cti-upsert-many.test.js +38 -50
  26. package/orm/tests/repository-cti.test.d.ts +1 -0
  27. package/orm/tests/repository-cti.test.js +195 -247
  28. package/orm/tests/repository-expiration.test.d.ts +1 -0
  29. package/orm/tests/repository-expiration.test.js +46 -59
  30. package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
  31. package/orm/tests/repository-extra-coverage.test.js +195 -337
  32. package/orm/tests/repository-mapping.test.d.ts +1 -0
  33. package/orm/tests/repository-mapping.test.js +20 -20
  34. package/orm/tests/repository-regression.test.js +124 -163
  35. package/orm/tests/repository-search.test.js +30 -44
  36. package/orm/tests/repository-soft-delete.test.js +54 -79
  37. package/orm/tests/repository-types.test.js +77 -111
  38. package/orm/tests/repository-undelete.test.d.ts +2 -0
  39. package/orm/tests/repository-undelete.test.js +201 -0
  40. package/package.json +3 -3
  41. package/task-queue/tests/worker.test.js +5 -5
  42. package/testing/README.md +38 -16
  43. package/testing/integration-setup.d.ts +11 -0
  44. 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 runInInjectionContext(injector, async () => {
51
- const user = await subjectService.createUser({ tenantId, email: 'client-test@example.com', firstName: 'C', lastName: 'T' });
52
- await serverService.setCredentials(user, 'Strong-Pass-2026!');
53
- expect(service.isLoggedIn()).toBe(false);
54
- await service.login({ tenantId, subject: user.id }, 'Strong-Pass-2026!');
55
- expect(service.isLoggedIn()).toBe(true);
56
- expect(service.subjectId()).toBe(user.id);
57
- await service.logout();
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 runInInjectionContext(injector, async () => {
63
- const user = await subjectService.createUser({ tenantId, email: 'refresh-test@example.com', firstName: 'R', lastName: 'T' });
64
- await serverService.setCredentials(user, 'Strong-Pass-2026!');
65
- await service.login({ tenantId, subject: user.id }, 'Strong-Pass-2026!');
66
- const initialToken = service.token()?.jti;
67
- await service.refresh();
68
- expect(service.token()?.jti).not.toBe(initialToken);
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
- await runInInjectionContext(injector, async () => {
37
- const user = await subjectService.createUser({
38
- tenantId,
39
- email: 'test@example.com',
40
- firstName: 'John',
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
- await runInInjectionContext(injector, async () => {
53
- const user = await subjectService.createUser({
54
- tenantId,
55
- email: 'test@example.com',
56
- firstName: 'John',
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
- await runInInjectionContext(injector, async () => {
74
- const user = await subjectService.createUser({
75
- tenantId,
76
- email: 'test@example.com',
77
- firstName: 'John',
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 runInInjectionContext(injector, async () => {
94
- const user = await subjectService.createUser({ tenantId, email: 'refresh@example.com', firstName: 'R', lastName: 'F' });
95
- await authenticationService.setCredentials(user, 'Strong-Password-2026!');
96
- const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
97
- const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
98
- const refreshResult = await authenticationService.refresh(loginResult.refreshToken, undefined, {}, userAuditor);
99
- expect(refreshResult.token).toBeDefined();
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 runInInjectionContext(injector, async () => {
105
- const user = await subjectService.createUser({ tenantId, email: 'change@example.com', firstName: 'C', lastName: 'S' });
106
- await authenticationService.setCredentials(user, 'Old-Password-2026!');
107
- const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
108
- await authenticationService.changeSecret({ tenantId, subject: user.id }, 'Old-Password-2026!', 'New-Password-2026!', userAuditor);
109
- const authResult = await authenticationService.authenticate({ tenantId, subject: user.id }, 'New-Password-2026!');
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
- await runInInjectionContext(injector, async () => {
115
- const weak = 'abc';
116
- const strong = 'Very-Strong-Password-2026-!@#$';
117
- expect((await authenticationService.checkSecret(weak)).strength).toBeLessThan(2);
118
- expect((await authenticationService.checkSecret(strong)).strength).toBeGreaterThanOrEqual(2);
119
- expect((await authenticationService.testSecret(weak)).success).toBe(false);
120
- expect((await authenticationService.testSecret(strong)).success).toBe(true);
121
- await expect(authenticationService.validateSecret(weak)).rejects.toThrow();
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 runInInjectionContext(injector, async () => {
127
- const user = await subjectService.createUser({ tenantId, email: 'resolve@example.com', firstName: 'R', lastName: 'S' });
128
- const resolved = await authenticationService.tryResolveSubject({ tenantId, subject: user.id });
129
- expect(resolved?.id).toBe(user.id);
130
- const resolvedByEmail = await authenticationService.resolveSubject({ tenantId, subject: 'resolve@example.com' });
131
- expect(resolvedByEmail.id).toBe(user.id);
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 runInInjectionContext(injector, async () => {
137
- const admin = await subjectService.createUser({ tenantId, email: 'admin@example.com', firstName: 'A', lastName: 'D' });
138
- const user = await subjectService.createUser({ tenantId, email: 'user@example.com', firstName: 'U', lastName: 'S' });
139
- const adminAuditor = auditor.with({ actor: admin.id, actorType: ActorType.Subject });
140
- const adminToken = await authenticationService.getToken(admin, undefined);
141
- const impersonated = await authenticationService.impersonate(adminToken.token, adminToken.refreshToken, user.id, undefined, adminAuditor);
142
- expect(impersonated.jsonToken.payload.subject).toBe(user.id);
143
- expect(impersonated.jsonToken.payload.impersonator).toBe(admin.id);
144
- const unimpersonated = await authenticationService.unimpersonate(impersonated.impersonatorRefreshToken, undefined, adminAuditor);
145
- expect(unimpersonated.jsonToken.payload.subject).toBe(admin.id);
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 runInInjectionContext(injector, async () => {
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 runInInjectionContext(injector, async () => {
156
- const user = await subjectService.createUser({ tenantId, email: 'fail@example.com', firstName: 'F', lastName: 'L' });
157
- await authenticationService.setCredentials(user, 'Very-Strong-Password-2026!');
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 runInInjectionContext(injector, async () => {
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 runInInjectionContext(injector, async () => {
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 runInInjectionContext(injector, async () => {
173
- const user = await subjectService.createUser({ tenantId, email: 'reset@example.com', firstName: 'R', lastName: 'E' });
174
- const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
175
- await authenticationService.initSecretReset({ tenantId, subject: user.id }, undefined, userAuditor);
176
- expect(ancillaryService.lastResetData).toBeDefined();
177
- await authenticationService.resetSecret(ancillaryService.lastResetData.token, 'New-Password-Reset-2026!', userAuditor);
178
- const authResult = await authenticationService.authenticate({ tenantId, subject: user.id }, 'New-Password-Reset-2026!');
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, runInInjectionContext } from '../../injector/index.js';
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
- runInInjectionContext(injector, () => {
37
- expect(notificationClient.notifications()).toEqual([]);
38
- expect(notificationClient.unreadCount()).toBe(0);
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
- await runInInjectionContext(injector, async () => {
50
- sessionId$.next('session-1');
51
- // Wait for async operations (microtasks)
52
- await new Promise(resolve => setTimeout(resolve, 0));
53
- expect(notificationClient.notifications()).toEqual(notifications);
54
- expect(notificationClient.unreadCount()).toBe(unreadCount);
55
- expect(notificationClient.types()).toEqual(types);
56
- expect(notificationApiClientMock.listInApp).toHaveBeenCalledWith({ limit: 20 });
57
- expect(notificationApiClientMock.unreadCount).toHaveBeenCalled();
58
- expect(notificationApiClientMock.types).toHaveBeenCalled();
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
- await runInInjectionContext(injector, async () => {
66
- sessionId$.next('session-1');
67
- await new Promise(resolve => setTimeout(resolve, 0));
68
- expect(notificationClient.notifications()).toHaveLength(1);
69
- sessionId$.next(undefined);
70
- await new Promise(resolve => setTimeout(resolve, 0));
71
- expect(notificationClient.notifications()).toEqual([]);
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
- await runInInjectionContext(injector, async () => {
79
- sessionId$.next('session-1');
80
- await new Promise(resolve => setTimeout(resolve, 0));
81
- const newNotification = { id: '2', type: 'test' };
82
- stream$.next({ notification: newNotification, unreadCount: 1 });
83
- expect(notificationClient.notifications()).toEqual([newNotification, ...initialNotifications]);
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
- await runInInjectionContext(injector, async () => {
90
- sessionId$.next('session-1');
91
- await new Promise(resolve => setTimeout(resolve, 0));
92
- stream$.next({ unreadCount: 10 });
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
- await runInInjectionContext(injector, async () => {
103
- sessionId$.next('session-1');
104
- await new Promise(resolve => setTimeout(resolve, 0));
105
- expect(notificationClient.notifications()).toEqual(page1);
106
- notificationClient.loadNext(10);
107
- await new Promise(resolve => setTimeout(resolve, 0));
108
- expect(notificationClient.notifications()).toEqual([...page1, ...page2]);
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
  });