@tstdl/base 0.93.144 → 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 (45) hide show
  1. package/authentication/authentication.api.d.ts +9 -0
  2. package/authentication/authentication.api.js +3 -0
  3. package/authentication/client/authentication.service.js +5 -5
  4. package/authentication/client/http-client.middleware.js +6 -2
  5. package/authentication/tests/authentication.client-middleware.test.js +35 -0
  6. package/authentication/tests/authentication.client-service-refresh.test.js +7 -0
  7. package/authentication/tests/authentication.client-service.test.js +15 -19
  8. package/authentication/tests/authentication.service.test.js +92 -119
  9. package/notification/tests/notification-client.test.js +39 -50
  10. package/notification/tests/notification-flow.test.js +204 -238
  11. package/notification/tests/notification-sse.service.test.js +20 -27
  12. package/notification/tests/notification-type.service.test.js +17 -20
  13. package/orm/tests/query-complex.test.js +80 -111
  14. package/orm/tests/repository-advanced.test.js +100 -143
  15. package/orm/tests/repository-attributes.test.js +30 -39
  16. package/orm/tests/repository-compound-primary-key.test.js +67 -75
  17. package/orm/tests/repository-comprehensive.test.js +76 -101
  18. package/orm/tests/repository-coverage.test.d.ts +1 -0
  19. package/orm/tests/repository-coverage.test.js +88 -149
  20. package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
  21. package/orm/tests/repository-cti-extensive.test.js +118 -147
  22. package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
  23. package/orm/tests/repository-cti-mapping.test.js +29 -42
  24. package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
  25. package/orm/tests/repository-cti-soft-delete.test.js +25 -37
  26. package/orm/tests/repository-cti-transactions.test.js +19 -33
  27. package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
  28. package/orm/tests/repository-cti-upsert-many.test.js +38 -50
  29. package/orm/tests/repository-cti.test.d.ts +1 -0
  30. package/orm/tests/repository-cti.test.js +195 -247
  31. package/orm/tests/repository-expiration.test.d.ts +1 -0
  32. package/orm/tests/repository-expiration.test.js +46 -59
  33. package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
  34. package/orm/tests/repository-extra-coverage.test.js +195 -337
  35. package/orm/tests/repository-mapping.test.d.ts +1 -0
  36. package/orm/tests/repository-mapping.test.js +20 -20
  37. package/orm/tests/repository-regression.test.js +124 -163
  38. package/orm/tests/repository-search.test.js +30 -44
  39. package/orm/tests/repository-soft-delete.test.js +54 -79
  40. package/orm/tests/repository-types.test.js +77 -111
  41. package/package.json +1 -1
  42. package/task-queue/tests/worker.test.js +5 -5
  43. package/testing/README.md +38 -16
  44. package/testing/integration-setup.d.ts +11 -0
  45. package/testing/integration-setup.js +57 -30
@@ -126,6 +126,9 @@ export declare const authenticationApiDefinition: {
126
126
  timestamp: {
127
127
  resource: string;
128
128
  result: import("../schema/index.js").NumberSchema;
129
+ data: {
130
+ [dontWaitForValidToken]: boolean;
131
+ };
129
132
  };
130
133
  };
131
134
  };
@@ -248,6 +251,9 @@ export declare function getAuthenticationApiDefinition<AdditionalTokenPayload ex
248
251
  timestamp: {
249
252
  resource: string;
250
253
  result: import("../schema/index.js").NumberSchema;
254
+ data: {
255
+ [dontWaitForValidToken]: boolean;
256
+ };
251
257
  };
252
258
  };
253
259
  };
@@ -365,6 +371,9 @@ export declare function getAuthenticationApiEndpointsDefinition<AdditionalTokenP
365
371
  timestamp: {
366
372
  resource: string;
367
373
  result: import("../schema/index.js").NumberSchema;
374
+ data: {
375
+ [dontWaitForValidToken]: boolean;
376
+ };
368
377
  };
369
378
  };
370
379
  export {};
@@ -147,6 +147,9 @@ export function getAuthenticationApiEndpointsDefinition(additionalTokenPayloadSc
147
147
  timestamp: {
148
148
  resource: 'timestamp',
149
149
  result: number(),
150
+ data: {
151
+ [dontWaitForValidToken]: true,
152
+ },
150
153
  },
151
154
  };
152
155
  }
@@ -420,7 +420,7 @@ let AuthenticationClientService = class AuthenticationClientService {
420
420
  if (stillNeedsRefresh) {
421
421
  await this.refresh();
422
422
  }
423
- if (this.forceRefreshRequested() && (this.token() != currentToken)) {
423
+ if (this.forceRefreshRequested() && (this.token()?.jti != currentToken?.jti)) {
424
424
  this.forceRefreshRequested.set(false);
425
425
  }
426
426
  });
@@ -428,7 +428,7 @@ let AuthenticationClientService = class AuthenticationClientService {
428
428
  // Lock held by another instance, wait 5 seconds or until token changes (Passive Sync)
429
429
  const changeReason = await firstValueFrom(race([
430
430
  timer(5000).pipe(map(() => 'timer')),
431
- this.token$.pipe(filter((t) => t != token), map(() => 'token')),
431
+ this.token$.pipe(filter((t) => t?.jti != token.jti), map(() => 'token')),
432
432
  from(this.disposeSignal),
433
433
  ]), { defaultValue: undefined });
434
434
  if (changeReason == 'token') {
@@ -442,7 +442,7 @@ let AuthenticationClientService = class AuthenticationClientService {
442
442
  const delay = Math.min(maxRefreshDelay, ((currentToken?.exp ?? 0) - this.estimatedServerTimestampSeconds() - currentRefreshBufferSeconds) * millisecondsPerSecond);
443
443
  const wakeUpSignals = [
444
444
  from(this.disposeSignal),
445
- this.token$.pipe(filter((t) => t != currentToken)),
445
+ this.token$.pipe(filter((t) => t?.jti != currentToken?.jti)),
446
446
  ];
447
447
  if (!forceRefresh) {
448
448
  wakeUpSignals.push(this.forceRefreshRequested$.pipe(filter((requested) => requested)));
@@ -451,7 +451,7 @@ let AuthenticationClientService = class AuthenticationClientService {
451
451
  await firstValueFrom(race([timer(delay), ...wakeUpSignals]), { defaultValue: undefined });
452
452
  }
453
453
  else {
454
- await firstValueFrom(race([timer(5000), ...wakeUpSignals]), { defaultValue: undefined });
454
+ await firstValueFrom(race([timer(2500), ...wakeUpSignals]), { defaultValue: undefined });
455
455
  }
456
456
  }
457
457
  catch (error) {
@@ -460,7 +460,7 @@ let AuthenticationClientService = class AuthenticationClientService {
460
460
  await firstValueFrom(race([
461
461
  timer(2500),
462
462
  from(this.disposeSignal),
463
- this.token$.pipe(filter((t) => t != currentToken)),
463
+ this.token$.pipe(filter((t) => t?.jti != currentToken?.jti)),
464
464
  this.forceRefreshRequested$.pipe(filter((requested) => requested), skip(this.forceRefreshRequested() ? 1 : 0)),
465
465
  ]), { defaultValue: undefined });
466
466
  }
@@ -1,5 +1,6 @@
1
- import { firstValueFrom, race, timeout } from 'rxjs';
1
+ import { firstValueFrom, race, timeout as rxjsTimeout } from 'rxjs';
2
2
  import { HttpError } from '../../http/index.js';
3
+ import { timeout } from '../../utils/timing.js';
3
4
  import { isDefined } from '../../utils/type-guards.js';
4
5
  import { cacheValueOrAsyncProvider } from '../../utils/value-or-provider.js';
5
6
  import { dontWaitForValidToken } from '../authentication.api.js';
@@ -19,10 +20,13 @@ export function waitForAuthenticationCredentialsMiddleware(authenticationService
19
20
  authenticationService.validToken$,
20
21
  request.cancellationSignal,
21
22
  ]);
22
- await firstValueFrom(race$.pipe(timeout(30000))).catch(() => undefined);
23
+ await firstValueFrom(race$.pipe(rxjsTimeout(30000))).catch(() => undefined);
23
24
  if (request.cancellationSignal.isSet) {
24
25
  break;
25
26
  }
27
+ if (!authenticationService.hasValidToken && authenticationService.isLoggedIn()) {
28
+ await timeout(100);
29
+ }
26
30
  }
27
31
  }
28
32
  await next();
@@ -21,6 +21,41 @@ describe('waitForAuthenticationCredentialsMiddleware', () => {
21
21
  await middleware({ request }, next);
22
22
  expect(next).toHaveBeenCalled();
23
23
  });
24
+ test('should NOT wait if endpoint has dontWaitForValidToken', async () => {
25
+ const authenticationServiceMock = {
26
+ isLoggedIn: vi.fn().mockReturnValue(true),
27
+ hasValidToken: false,
28
+ };
29
+ const middleware = waitForAuthenticationCredentialsMiddleware(authenticationServiceMock);
30
+ const request = new HttpClientRequest('http://localhost');
31
+ request.context = {
32
+ endpoint: {
33
+ credentials: true,
34
+ data: {
35
+ [dontWaitForValidToken]: true,
36
+ },
37
+ },
38
+ };
39
+ const next = vi.fn().mockResolvedValue(undefined);
40
+ await middleware({ request }, next);
41
+ expect(next).toHaveBeenCalled();
42
+ });
43
+ test('should NOT wait if credentials is NOT true', async () => {
44
+ const authenticationServiceMock = {
45
+ isLoggedIn: vi.fn().mockReturnValue(true),
46
+ hasValidToken: false,
47
+ };
48
+ const middleware = waitForAuthenticationCredentialsMiddleware(authenticationServiceMock);
49
+ const request = new HttpClientRequest('http://localhost');
50
+ request.context = {
51
+ endpoint: {
52
+ credentials: false,
53
+ },
54
+ };
55
+ const next = vi.fn().mockResolvedValue(undefined);
56
+ await middleware({ request }, next);
57
+ expect(next).toHaveBeenCalled();
58
+ });
24
59
  });
25
60
  describe('logoutOnUnauthorizedMiddleware', () => {
26
61
  test('should call logout on 401 error', async () => {
@@ -123,4 +123,11 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
123
123
  // If it busy loops, this will be much higher than 1.
124
124
  expect(mockLock.tryUse.mock.calls.length).toBeLessThan(5);
125
125
  });
126
+ test('refresh() should call timestamp() to sync clock', async () => {
127
+ service = injector.resolve(AuthenticationClientService);
128
+ const newToken = { iat: 1000, exp: 2000, jti: 'new' };
129
+ mockApiClient.refresh.mockResolvedValue(newToken);
130
+ await service.refresh();
131
+ expect(mockApiClient.timestamp).toHaveBeenCalled();
132
+ });
126
133
  });
@@ -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
  });