@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.
- package/authentication/authentication.api.d.ts +9 -0
- package/authentication/authentication.api.js +3 -0
- package/authentication/client/authentication.service.js +5 -5
- package/authentication/client/http-client.middleware.js +6 -2
- package/authentication/tests/authentication.client-middleware.test.js +35 -0
- package/authentication/tests/authentication.client-service-refresh.test.js +7 -0
- 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/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/package.json +1 -1
- 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
|
@@ -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 {};
|
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
});
|