@furystack/rest-service 13.0.0 → 14.0.0
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/CHANGELOG.md +17 -0
- package/README.md +47 -28
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/is-authenticated.spec.js +10 -6
- package/esm/actions/is-authenticated.spec.js.map +1 -1
- package/esm/actions/logout-action.spec.js +11 -9
- package/esm/actions/logout-action.spec.js.map +1 -1
- package/esm/actions/logout.js +1 -1
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/password-login-action.js +1 -1
- package/esm/actions/password-login-action.js.map +1 -1
- package/esm/actions/password-login-action.spec.js +23 -28
- package/esm/actions/password-login-action.spec.js.map +1 -1
- package/esm/authenticate.js +1 -1
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authorize.spec.js +34 -29
- package/esm/authorize.spec.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
- package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
- package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
- package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
- package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
- package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.js +8 -13
- package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
- package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/utils.d.ts +17 -2
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +33 -20
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/get-schema-from-api.d.ts +1 -1
- package/esm/get-schema-from-api.d.ts.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +53 -87
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +91 -92
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.d.ts +2 -1
- package/esm/helpers.spec.d.ts.map +1 -1
- package/esm/helpers.spec.js +41 -38
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +35 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +21 -29
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-server-pool.d.ts +64 -0
- package/esm/http-server-pool.d.ts.map +1 -0
- package/esm/http-server-pool.js +95 -0
- package/esm/http-server-pool.js.map +1 -0
- package/esm/http-server-pool.spec.d.ts +2 -0
- package/esm/http-server-pool.spec.d.ts.map +1 -0
- package/esm/http-server-pool.spec.js +193 -0
- package/esm/http-server-pool.spec.js.map +1 -0
- package/esm/http-user-context.d.ts +31 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +76 -95
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.d.ts +8 -2
- package/esm/http-user-context.spec.d.ts.map +1 -1
- package/esm/http-user-context.spec.js +189 -337
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/index.d.ts +6 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +6 -4
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +11 -10
- package/esm/login-response-strategy.d.ts.map +1 -1
- package/esm/login-response-strategy.js +7 -6
- package/esm/login-response-strategy.js.map +1 -1
- package/esm/login-response-strategy.spec.js +25 -29
- package/esm/login-response-strategy.spec.js.map +1 -1
- package/esm/proxy-manager.spec.js +152 -176
- package/esm/proxy-manager.spec.js.map +1 -1
- package/esm/proxy-runtime.d.ts +27 -0
- package/esm/proxy-runtime.d.ts.map +1 -0
- package/esm/proxy-runtime.js +45 -0
- package/esm/proxy-runtime.js.map +1 -0
- package/esm/proxy-runtime.spec.d.ts +2 -0
- package/esm/proxy-runtime.spec.d.ts.map +1 -0
- package/esm/proxy-runtime.spec.js +39 -0
- package/esm/proxy-runtime.spec.js.map +1 -0
- package/esm/rest-api-runtime.d.ts +76 -0
- package/esm/rest-api-runtime.d.ts.map +1 -0
- package/esm/rest-api-runtime.js +131 -0
- package/esm/rest-api-runtime.js.map +1 -0
- package/esm/rest-api-runtime.spec.d.ts +2 -0
- package/esm/rest-api-runtime.spec.d.ts.map +1 -0
- package/esm/rest-api-runtime.spec.js +58 -0
- package/esm/rest-api-runtime.spec.js.map +1 -0
- package/esm/rest-service.integration.spec.d.ts +1 -1
- package/esm/rest-service.integration.spec.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +57 -80
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/rest.integration.test.js +26 -84
- package/esm/rest.integration.test.js.map +1 -1
- package/esm/server-telemetry.d.ts +57 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +26 -0
- package/esm/server-telemetry.js.map +1 -0
- package/esm/server-telemetry.spec.d.ts +2 -0
- package/esm/server-telemetry.spec.d.ts.map +1 -0
- package/esm/server-telemetry.spec.js +52 -0
- package/esm/server-telemetry.spec.js.map +1 -0
- package/esm/static-server-manager.spec.js +60 -125
- package/esm/static-server-manager.spec.js.map +1 -1
- package/esm/static-site-runtime.d.ts +21 -0
- package/esm/static-site-runtime.d.ts.map +1 -0
- package/esm/static-site-runtime.js +48 -0
- package/esm/static-site-runtime.js.map +1 -0
- package/esm/static-site-runtime.spec.d.ts +2 -0
- package/esm/static-site-runtime.spec.d.ts.map +1 -0
- package/esm/static-site-runtime.spec.js +30 -0
- package/esm/static-site-runtime.spec.js.map +1 -0
- package/esm/user-store.d.ts +39 -0
- package/esm/user-store.d.ts.map +1 -0
- package/esm/user-store.js +62 -0
- package/esm/user-store.js.map +1 -0
- package/esm/user-store.spec.d.ts +2 -0
- package/esm/user-store.spec.d.ts.map +1 -0
- package/esm/user-store.spec.js +72 -0
- package/esm/user-store.spec.js.map +1 -0
- package/esm/validate.integration.spec.js +26 -28
- package/esm/validate.integration.spec.js.map +1 -1
- package/package.json +8 -8
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +1 -1
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +1 -1
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +1 -1
- package/src/authorize.spec.ts +37 -39
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
- package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
- package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
- package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -2
- package/src/endpoint-generators/create-get-schema-action.ts +1 -1
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
- package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
- package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
- package/src/endpoint-generators/create-post-endpoint.ts +13 -23
- package/src/endpoint-generators/utils.ts +36 -28
- package/src/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +1 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +51 -25
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +203 -383
- package/src/http-user-context.ts +114 -91
- package/src/index.ts +6 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +13 -12
- package/src/proxy-manager.spec.ts +152 -182
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/rest-api-runtime.spec.ts +77 -0
- package/src/rest-api-runtime.ts +204 -0
- package/src/rest-service.integration.spec.ts +65 -85
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +51 -0
- package/src/static-server-manager.spec.ts +63 -151
- package/src/static-site-runtime.spec.ts +39 -0
- package/src/static-site-runtime.ts +75 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +68 -0
- package/src/validate.integration.spec.ts +39 -35
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -179
- package/esm/api-manager.js.map +0 -1
- package/esm/proxy-manager.d.ts +0 -53
- package/esm/proxy-manager.d.ts.map +0 -1
- package/esm/proxy-manager.js +0 -84
- package/esm/proxy-manager.js.map +0 -1
- package/esm/server-manager.d.ts +0 -48
- package/esm/server-manager.d.ts.map +0 -1
- package/esm/server-manager.js +0 -100
- package/esm/server-manager.js.map +0 -1
- package/esm/static-server-manager.d.ts +0 -19
- package/esm/static-server-manager.d.ts.map +0 -1
- package/esm/static-server-manager.js +0 -65
- package/esm/static-server-manager.js.map +0 -1
- package/src/api-manager.ts +0 -304
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- package/src/static-server-manager.ts +0 -90
|
@@ -1,233 +1,192 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { getDataSetFor, getRepository } from '@furystack/repository';
|
|
5
|
-
import { PasswordAuthenticator, PasswordCredential, PasswordResetToken, UnauthenticatedError, usePasswordPolicy, } from '@furystack/security';
|
|
1
|
+
import { InMemoryStore, User as UserModel, useSystemIdentityContext } from '@furystack/core';
|
|
2
|
+
import { createInjector } from '@furystack/inject';
|
|
3
|
+
import { PasswordAuthenticator, PasswordCredential, PasswordCredentialStore, PasswordResetToken, PasswordResetTokenStore, UnauthenticatedError, usePasswordPolicy, } from '@furystack/security';
|
|
6
4
|
import { usingAsync } from '@furystack/utils';
|
|
7
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
8
6
|
import { useHttpAuthentication } from './helpers.js';
|
|
9
7
|
import { HttpUserContext } from './http-user-context.js';
|
|
10
8
|
import { DefaultSession } from './models/default-session.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
import { SessionDataSet, SessionStore, UserStore } from './user-store.js';
|
|
10
|
+
/**
|
|
11
|
+
* Binds in-memory implementations for every persistent token required by
|
|
12
|
+
* the authentication subsystem (users, sessions, credentials, reset tokens)
|
|
13
|
+
* on the provided injector, then installs the default password policy and
|
|
14
|
+
* HTTP-authentication stack.
|
|
15
|
+
*/
|
|
16
|
+
export const prepareInjector = (i, options) => {
|
|
17
|
+
i.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }));
|
|
18
|
+
i.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }));
|
|
19
|
+
i.bind(PasswordCredentialStore, () => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }));
|
|
20
|
+
i.bind(PasswordResetTokenStore, () => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }));
|
|
21
21
|
usePasswordPolicy(i);
|
|
22
22
|
useHttpAuthentication(i, { enableBasicAuth: options?.enableBasicAuth ?? true });
|
|
23
23
|
};
|
|
24
24
|
const setupUser = async (i, userName, password) => {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
await
|
|
29
|
-
await sm.getStoreFor(User, 'username').add({ username: userName, roles: [] });
|
|
25
|
+
const authenticator = i.get(PasswordAuthenticator);
|
|
26
|
+
const credential = await authenticator.hasher.createCredential(userName, password);
|
|
27
|
+
await i.get(PasswordCredentialStore).add(credential);
|
|
28
|
+
await i.get(UserStore).add({ username: userName, roles: [] });
|
|
30
29
|
};
|
|
31
30
|
describe('HttpUserContext', () => {
|
|
32
31
|
const request = { headers: {} };
|
|
33
|
-
const response = {};
|
|
34
32
|
const testUser = { username: 'testUser', roles: ['grantedRole1', 'grantedRole2'] };
|
|
35
|
-
it('
|
|
36
|
-
await usingAsync(
|
|
37
|
-
|
|
38
|
-
const ctx = i.
|
|
39
|
-
expect(ctx).
|
|
33
|
+
it('resolves to an HttpUserContext via the DI token', async () => {
|
|
34
|
+
await usingAsync(createInjector(), async (i) => {
|
|
35
|
+
prepareInjector(i);
|
|
36
|
+
const ctx = i.get(HttpUserContext);
|
|
37
|
+
expect(ctx).toBeDefined();
|
|
38
|
+
expect(ctx.authentication).toBeDefined();
|
|
40
39
|
});
|
|
41
40
|
});
|
|
42
41
|
describe('isAuthenticated', () => {
|
|
43
|
-
it('
|
|
44
|
-
await usingAsync(
|
|
45
|
-
|
|
46
|
-
const ctx = i.
|
|
42
|
+
it('returns true when the cached user is present', async () => {
|
|
43
|
+
await usingAsync(createInjector(), async (i) => {
|
|
44
|
+
prepareInjector(i);
|
|
45
|
+
const ctx = i.get(HttpUserContext);
|
|
47
46
|
ctx.getCurrentUser = vi.fn(async () => testUser);
|
|
48
|
-
|
|
49
|
-
expect(
|
|
50
|
-
expect(ctx.getCurrentUser).toBeCalled();
|
|
47
|
+
expect(await ctx.isAuthenticated(request)).toBe(true);
|
|
48
|
+
expect(ctx.getCurrentUser).toHaveBeenCalled();
|
|
51
49
|
});
|
|
52
50
|
});
|
|
53
|
-
it('
|
|
54
|
-
await usingAsync(
|
|
55
|
-
|
|
56
|
-
const ctx = i.
|
|
51
|
+
it('returns false when getCurrentUser rejects', async () => {
|
|
52
|
+
await usingAsync(createInjector(), async (i) => {
|
|
53
|
+
prepareInjector(i);
|
|
54
|
+
const ctx = i.get(HttpUserContext);
|
|
57
55
|
ctx.getCurrentUser = vi.fn(async () => {
|
|
58
|
-
throw Error(':(');
|
|
56
|
+
throw new Error(':(');
|
|
59
57
|
});
|
|
60
|
-
await
|
|
61
|
-
expect(ctx.getCurrentUser).toBeCalled();
|
|
58
|
+
expect(await ctx.isAuthenticated(request)).toBe(false);
|
|
62
59
|
});
|
|
63
60
|
});
|
|
64
61
|
});
|
|
65
62
|
describe('isAuthorized', () => {
|
|
66
|
-
it('
|
|
67
|
-
await usingAsync(
|
|
68
|
-
|
|
69
|
-
const ctx = i.
|
|
63
|
+
it('returns true when the current user has every requested role', async () => {
|
|
64
|
+
await usingAsync(createInjector(), async (i) => {
|
|
65
|
+
prepareInjector(i);
|
|
66
|
+
const ctx = i.get(HttpUserContext);
|
|
70
67
|
ctx.getCurrentUser = vi.fn(async () => testUser);
|
|
71
|
-
|
|
72
|
-
expect(value).toBe(true);
|
|
73
|
-
expect(ctx.getCurrentUser).toBeCalled();
|
|
68
|
+
expect(await ctx.isAuthorized(request, 'grantedRole1', 'grantedRole2')).toBe(true);
|
|
74
69
|
});
|
|
75
70
|
});
|
|
76
|
-
it('
|
|
77
|
-
await usingAsync(
|
|
78
|
-
|
|
79
|
-
const ctx = i.
|
|
71
|
+
it('returns false when any requested role is missing', async () => {
|
|
72
|
+
await usingAsync(createInjector(), async (i) => {
|
|
73
|
+
prepareInjector(i);
|
|
74
|
+
const ctx = i.get(HttpUserContext);
|
|
80
75
|
ctx.getCurrentUser = vi.fn(async () => testUser);
|
|
81
|
-
|
|
82
|
-
expect(value).toBe(false);
|
|
83
|
-
expect(ctx.getCurrentUser).toBeCalled();
|
|
76
|
+
expect(await ctx.isAuthorized(request, 'grantedRole1', 'otherRole')).toBe(false);
|
|
84
77
|
});
|
|
85
78
|
});
|
|
86
79
|
});
|
|
87
80
|
describe('authenticateUser', () => {
|
|
88
|
-
it('
|
|
89
|
-
await usingAsync(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await expect(ctx.authenticateUser('user', 'password')).rejects.toThrowError(UnauthenticatedError);
|
|
81
|
+
it('throws UnauthenticatedError when the credential store is empty', async () => {
|
|
82
|
+
await usingAsync(createInjector(), async (i) => {
|
|
83
|
+
prepareInjector(i);
|
|
84
|
+
await expect(i.get(HttpUserContext).authenticateUser('user', 'password')).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
93
85
|
});
|
|
94
86
|
});
|
|
95
|
-
it('
|
|
96
|
-
await usingAsync(
|
|
97
|
-
|
|
87
|
+
it('throws UnauthenticatedError when the password does not match', async () => {
|
|
88
|
+
await usingAsync(createInjector(), async (i) => {
|
|
89
|
+
prepareInjector(i);
|
|
98
90
|
await setupUser(i, 'user', 'pass123');
|
|
99
|
-
await expect(i.
|
|
91
|
+
await expect(i.get(HttpUserContext).authenticateUser('user', 'wrong')).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
100
92
|
});
|
|
101
93
|
});
|
|
102
|
-
it('
|
|
103
|
-
await usingAsync(
|
|
104
|
-
|
|
94
|
+
it('throws UnauthenticatedError for an unknown username', async () => {
|
|
95
|
+
await usingAsync(createInjector(), async (i) => {
|
|
96
|
+
prepareInjector(i);
|
|
105
97
|
await setupUser(i, 'otherUser', 'pass123');
|
|
106
|
-
await expect(i.
|
|
98
|
+
await expect(i.get(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
107
99
|
});
|
|
108
100
|
});
|
|
109
|
-
it('
|
|
110
|
-
await usingAsync(
|
|
111
|
-
|
|
101
|
+
it('throws UnauthenticatedError when the user exists only as a credential', async () => {
|
|
102
|
+
await usingAsync(createInjector(), async (i) => {
|
|
103
|
+
prepareInjector(i);
|
|
112
104
|
await setupUser(i, 'user', 'pass123');
|
|
113
|
-
await
|
|
105
|
+
await i.get(UserStore).remove('user');
|
|
106
|
+
await expect(i.get(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
114
107
|
});
|
|
115
108
|
});
|
|
116
|
-
it('
|
|
117
|
-
await usingAsync(
|
|
118
|
-
|
|
109
|
+
it('returns the user on valid credentials', async () => {
|
|
110
|
+
await usingAsync(createInjector(), async (i) => {
|
|
111
|
+
prepareInjector(i);
|
|
119
112
|
await setupUser(i, 'user', 'pass123');
|
|
120
|
-
await i.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
it('Should return the user when the username and password matches', async () => {
|
|
125
|
-
await usingAsync(new Injector(), async (i) => {
|
|
126
|
-
await prepareInjector(i);
|
|
127
|
-
await setupUser(i, 'user', 'pass123');
|
|
128
|
-
const ctx = i.getInstance(HttpUserContext);
|
|
129
|
-
const value = await ctx.authenticateUser('user', 'pass123');
|
|
130
|
-
expect(value).toEqual({ username: 'user', roles: [] });
|
|
113
|
+
expect(await i.get(HttpUserContext).authenticateUser('user', 'pass123')).toEqual({
|
|
114
|
+
username: 'user',
|
|
115
|
+
roles: [],
|
|
116
|
+
});
|
|
131
117
|
});
|
|
132
118
|
});
|
|
133
119
|
});
|
|
134
120
|
describe('getSessionIdFromRequest', () => {
|
|
135
|
-
it('
|
|
136
|
-
await usingAsync(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const sid = ctx.getSessionIdFromRequest(request);
|
|
140
|
-
expect(sid).toBeNull();
|
|
121
|
+
it('returns null when no headers are present', async () => {
|
|
122
|
+
await usingAsync(createInjector(), async (i) => {
|
|
123
|
+
prepareInjector(i);
|
|
124
|
+
expect(i.get(HttpUserContext).getSessionIdFromRequest(request)).toBeNull();
|
|
141
125
|
});
|
|
142
126
|
});
|
|
143
|
-
it('
|
|
144
|
-
await usingAsync(
|
|
145
|
-
|
|
146
|
-
const requestWithCookie = {
|
|
147
|
-
|
|
148
|
-
const sid = ctx.getSessionIdFromRequest(requestWithCookie);
|
|
149
|
-
expect(sid).toBeNull();
|
|
127
|
+
it('returns null when no session cookie is present', async () => {
|
|
128
|
+
await usingAsync(createInjector(), async (i) => {
|
|
129
|
+
prepareInjector(i);
|
|
130
|
+
const requestWithCookie = { headers: { cookie: 'a=2;b=3;' } };
|
|
131
|
+
expect(i.get(HttpUserContext).getSessionIdFromRequest(requestWithCookie)).toBeNull();
|
|
150
132
|
});
|
|
151
133
|
});
|
|
152
|
-
it('
|
|
153
|
-
await usingAsync(
|
|
154
|
-
|
|
155
|
-
const ctx = i.
|
|
134
|
+
it('returns the session id when the configured cookie is present', async () => {
|
|
135
|
+
await usingAsync(createInjector(), async (i) => {
|
|
136
|
+
prepareInjector(i);
|
|
137
|
+
const ctx = i.get(HttpUserContext);
|
|
156
138
|
const requestWithAuthCookie = {
|
|
157
|
-
...request,
|
|
158
139
|
headers: { cookie: `a=2;b=3;${ctx.authentication.cookieName}=666;c=4;` },
|
|
159
140
|
};
|
|
160
|
-
|
|
161
|
-
expect(sid).toBe('666');
|
|
141
|
+
expect(ctx.getSessionIdFromRequest(requestWithAuthCookie)).toBe('666');
|
|
162
142
|
});
|
|
163
143
|
});
|
|
164
144
|
});
|
|
165
145
|
describe('authenticateRequest', () => {
|
|
166
|
-
it('
|
|
167
|
-
await usingAsync(
|
|
168
|
-
|
|
146
|
+
it('authenticates with Basic Auth when enabled and credentials are valid', async () => {
|
|
147
|
+
await usingAsync(createInjector(), async (i) => {
|
|
148
|
+
prepareInjector(i);
|
|
169
149
|
await setupUser(i, 'testuser', 'password');
|
|
170
|
-
const
|
|
171
|
-
const result = await ctx.authenticateRequest({
|
|
150
|
+
const result = await i.get(HttpUserContext).authenticateRequest({
|
|
172
151
|
headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
|
|
173
152
|
});
|
|
174
153
|
expect(result.username).toBe('testuser');
|
|
175
154
|
});
|
|
176
155
|
});
|
|
177
|
-
it('
|
|
178
|
-
await usingAsync(
|
|
179
|
-
|
|
156
|
+
it('does not attempt Basic Auth when it is disabled', async () => {
|
|
157
|
+
await usingAsync(createInjector(), async (i) => {
|
|
158
|
+
prepareInjector(i, { enableBasicAuth: false });
|
|
180
159
|
await setupUser(i, 'testuser', 'password');
|
|
181
|
-
|
|
182
|
-
await expect(ctx.authenticateRequest({
|
|
160
|
+
await expect(i.get(HttpUserContext).authenticateRequest({
|
|
183
161
|
headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
|
|
184
|
-
})).rejects.
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
it('Should fail with no session in the store', async () => {
|
|
188
|
-
await usingAsync(new Injector(), async (i) => {
|
|
189
|
-
await prepareInjector(i);
|
|
190
|
-
const ctx = i.getInstance(HttpUserContext);
|
|
191
|
-
await expect(ctx.authenticateRequest({
|
|
192
|
-
headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
|
|
193
|
-
})).rejects.toThrowError(UnauthenticatedError);
|
|
162
|
+
})).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
194
163
|
});
|
|
195
164
|
});
|
|
196
|
-
it('
|
|
197
|
-
await usingAsync(
|
|
198
|
-
|
|
199
|
-
const ctx = i.
|
|
200
|
-
await i.getInstance(StoreManager).getStoreFor(DefaultSession, 'sessionId').add({
|
|
201
|
-
sessionId: '666',
|
|
202
|
-
username: testUser.username,
|
|
203
|
-
});
|
|
165
|
+
it('throws when the cookie session id does not exist', async () => {
|
|
166
|
+
await usingAsync(createInjector(), async (i) => {
|
|
167
|
+
prepareInjector(i);
|
|
168
|
+
const ctx = i.get(HttpUserContext);
|
|
204
169
|
await expect(ctx.authenticateRequest({
|
|
205
|
-
headers: { cookie: `${ctx.authentication.cookieName}=666
|
|
206
|
-
})).rejects.
|
|
170
|
+
headers: { cookie: `${ctx.authentication.cookieName}=666;` },
|
|
171
|
+
})).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
207
172
|
});
|
|
208
173
|
});
|
|
209
|
-
it('
|
|
210
|
-
await usingAsync(
|
|
211
|
-
|
|
212
|
-
const ctx = i.
|
|
213
|
-
await i.
|
|
214
|
-
|
|
215
|
-
username: testUser.username,
|
|
216
|
-
});
|
|
217
|
-
await i
|
|
218
|
-
.getInstance(StoreManager)
|
|
219
|
-
.getStoreFor(User, 'username')
|
|
220
|
-
.add({ ...testUser });
|
|
174
|
+
it('authenticates via cookie when the session id matches a stored user', async () => {
|
|
175
|
+
await usingAsync(createInjector(), async (i) => {
|
|
176
|
+
prepareInjector(i);
|
|
177
|
+
const ctx = i.get(HttpUserContext);
|
|
178
|
+
await i.get(SessionStore).add({ sessionId: '666', username: testUser.username });
|
|
179
|
+
await i.get(UserStore).add({ ...testUser });
|
|
221
180
|
const result = await ctx.authenticateRequest({
|
|
222
|
-
headers: { cookie: `${ctx.authentication.cookieName}=666
|
|
181
|
+
headers: { cookie: `${ctx.authentication.cookieName}=666;` },
|
|
223
182
|
});
|
|
224
183
|
expect(result).toEqual(testUser);
|
|
225
184
|
});
|
|
226
185
|
});
|
|
227
|
-
it('
|
|
228
|
-
await usingAsync(
|
|
229
|
-
|
|
230
|
-
const ctx = i.
|
|
186
|
+
it('walks providers in order and stops at the first match', async () => {
|
|
187
|
+
await usingAsync(createInjector(), async (i) => {
|
|
188
|
+
prepareInjector(i);
|
|
189
|
+
const ctx = i.get(HttpUserContext);
|
|
231
190
|
const provider1 = vi.fn(async () => null);
|
|
232
191
|
const provider2 = vi.fn(async () => testUser);
|
|
233
192
|
ctx.authentication.authenticationProviders = [
|
|
@@ -240,10 +199,10 @@ describe('HttpUserContext', () => {
|
|
|
240
199
|
expect(result).toBe(testUser);
|
|
241
200
|
});
|
|
242
201
|
});
|
|
243
|
-
it('
|
|
244
|
-
await usingAsync(
|
|
245
|
-
|
|
246
|
-
const ctx = i.
|
|
202
|
+
it('propagates a provider-thrown error and skips remaining providers', async () => {
|
|
203
|
+
await usingAsync(createInjector(), async (i) => {
|
|
204
|
+
prepareInjector(i);
|
|
205
|
+
const ctx = i.get(HttpUserContext);
|
|
247
206
|
const provider1 = vi.fn(async () => {
|
|
248
207
|
throw new UnauthenticatedError();
|
|
249
208
|
});
|
|
@@ -252,210 +211,103 @@ describe('HttpUserContext', () => {
|
|
|
252
211
|
{ name: 'test-1', authenticate: provider1 },
|
|
253
212
|
{ name: 'test-2', authenticate: provider2 },
|
|
254
213
|
];
|
|
255
|
-
await expect(ctx.authenticateRequest(request)).rejects.
|
|
214
|
+
await expect(ctx.authenticateRequest(request)).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
256
215
|
expect(provider2).not.toHaveBeenCalled();
|
|
257
216
|
});
|
|
258
217
|
});
|
|
259
|
-
it('
|
|
260
|
-
await usingAsync(
|
|
261
|
-
|
|
262
|
-
const ctx = i.
|
|
218
|
+
it('throws UnauthenticatedError when no provider returns a user', async () => {
|
|
219
|
+
await usingAsync(createInjector(), async (i) => {
|
|
220
|
+
prepareInjector(i);
|
|
221
|
+
const ctx = i.get(HttpUserContext);
|
|
263
222
|
ctx.authentication.authenticationProviders = [{ name: 'test-1', authenticate: async () => null }];
|
|
264
|
-
await expect(ctx.authenticateRequest(request)).rejects.
|
|
223
|
+
await expect(ctx.authenticateRequest(request)).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
265
224
|
});
|
|
266
225
|
});
|
|
267
226
|
});
|
|
268
227
|
describe('getCurrentUser', () => {
|
|
269
|
-
it('
|
|
270
|
-
await usingAsync(
|
|
271
|
-
|
|
272
|
-
const ctx = i.
|
|
228
|
+
it('caches the result of authenticateRequest within the request scope', async () => {
|
|
229
|
+
await usingAsync(createInjector(), async (i) => {
|
|
230
|
+
prepareInjector(i);
|
|
231
|
+
const ctx = i.get(HttpUserContext);
|
|
273
232
|
ctx.authenticateRequest = vi.fn(async () => testUser);
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
expect(ctx.authenticateRequest).
|
|
277
|
-
expect(
|
|
278
|
-
expect(
|
|
233
|
+
const first = await ctx.getCurrentUser(request);
|
|
234
|
+
const second = await ctx.getCurrentUser(request);
|
|
235
|
+
expect(ctx.authenticateRequest).toHaveBeenCalledTimes(1);
|
|
236
|
+
expect(first).toBe(testUser);
|
|
237
|
+
expect(second).toBe(testUser);
|
|
279
238
|
});
|
|
280
239
|
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
expect(
|
|
294
|
-
expect(
|
|
240
|
+
it('does not leak a cached user between distinct requests on the same instance', async () => {
|
|
241
|
+
await usingAsync(createInjector(), async (i) => {
|
|
242
|
+
prepareInjector(i);
|
|
243
|
+
const ctx = i.get(HttpUserContext);
|
|
244
|
+
const otherUser = { username: 'otherUser', roles: [] };
|
|
245
|
+
const requestA = { headers: {} };
|
|
246
|
+
const requestB = { headers: {} };
|
|
247
|
+
const authenticateRequest = vi
|
|
248
|
+
.fn()
|
|
249
|
+
.mockResolvedValueOnce(testUser)
|
|
250
|
+
.mockResolvedValueOnce(otherUser);
|
|
251
|
+
ctx.authenticateRequest = authenticateRequest;
|
|
252
|
+
expect(await ctx.getCurrentUser(requestA)).toBe(testUser);
|
|
253
|
+
expect(await ctx.getCurrentUser(requestB)).toBe(otherUser);
|
|
254
|
+
expect(await ctx.getCurrentUser(requestA)).toBe(testUser);
|
|
255
|
+
expect(authenticateRequest).toHaveBeenCalledTimes(2);
|
|
295
256
|
});
|
|
296
257
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
primaryKey: 'sessionId',
|
|
310
|
-
};
|
|
311
|
-
// @ts-expect-error
|
|
312
|
-
ctx.getSessionDataSet = vi.fn(() => sessionDataSetMock);
|
|
313
|
-
ctx.authenticateRequest = vi.fn(async () => testUser);
|
|
314
|
-
ctx.getSessionIdFromRequest = () => 'example-session-id';
|
|
315
|
-
response.setHeader = vi.fn(() => response);
|
|
316
|
-
await ctx.cookieLogin(testUser, { setHeader });
|
|
317
|
-
await ctx.cookieLogout(request, response);
|
|
318
|
-
expect(response.setHeader).toBeCalledWith('Set-Cookie', 'fss=; Path=/; HttpOnly');
|
|
319
|
-
expect(removeMock).toBeCalled();
|
|
258
|
+
it('re-walks providers after a failed authenticateRequest', async () => {
|
|
259
|
+
await usingAsync(createInjector(), async (i) => {
|
|
260
|
+
prepareInjector(i);
|
|
261
|
+
const ctx = i.get(HttpUserContext);
|
|
262
|
+
const authenticateRequest = vi
|
|
263
|
+
.fn()
|
|
264
|
+
.mockRejectedValueOnce(new UnauthenticatedError())
|
|
265
|
+
.mockResolvedValueOnce(testUser);
|
|
266
|
+
ctx.authenticateRequest = authenticateRequest;
|
|
267
|
+
await expect(ctx.getCurrentUser(request)).rejects.toBeInstanceOf(UnauthenticatedError);
|
|
268
|
+
expect(await ctx.getCurrentUser(request)).toBe(testUser);
|
|
269
|
+
expect(authenticateRequest).toHaveBeenCalledTimes(2);
|
|
320
270
|
});
|
|
321
271
|
});
|
|
322
272
|
});
|
|
323
|
-
describe('
|
|
324
|
-
it('
|
|
325
|
-
await usingAsync(
|
|
326
|
-
|
|
327
|
-
const ctx = i.
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
expect(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
it('Should emit onLogout when cookieLogout is called', async () => {
|
|
339
|
-
await usingAsync(new Injector(), async (i) => {
|
|
340
|
-
await prepareInjector(i);
|
|
341
|
-
const ctx = i.getInstance(HttpUserContext);
|
|
342
|
-
const handler = vi.fn();
|
|
343
|
-
ctx.addListener('onLogout', handler);
|
|
344
|
-
const sessionDataSetMock = {
|
|
345
|
-
add: vi.fn(async () => ({})),
|
|
346
|
-
find: vi.fn(async () => [{ sessionId: 'sid' }]),
|
|
347
|
-
remove: vi.fn(async () => undefined),
|
|
348
|
-
primaryKey: 'sessionId',
|
|
349
|
-
};
|
|
350
|
-
// @ts-expect-error
|
|
351
|
-
ctx.getSessionDataSet = vi.fn(() => sessionDataSetMock);
|
|
352
|
-
ctx.getSessionIdFromRequest = () => 'sid';
|
|
353
|
-
await ctx.cookieLogout(request, { setHeader: vi.fn() });
|
|
354
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
355
|
-
expect(handler).toHaveBeenCalledWith(undefined);
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
it('Should emit onSessionInvalidated when the user entity is removed', async () => {
|
|
359
|
-
await usingAsync(new Injector(), async (i) => {
|
|
360
|
-
await prepareInjector(i);
|
|
361
|
-
const ctx = i.getInstance(HttpUserContext);
|
|
362
|
-
const sm = i.getInstance(StoreManager);
|
|
363
|
-
await sm.getStoreFor(User, 'username').add(testUser);
|
|
364
|
-
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test');
|
|
365
|
-
await sm.getStoreFor(PasswordCredential, 'userName').add(pw);
|
|
366
|
-
const handler = vi.fn();
|
|
367
|
-
ctx.addListener('onSessionInvalidated', handler);
|
|
368
|
-
await ctx.cookieLogin(testUser, { setHeader: vi.fn() });
|
|
369
|
-
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' });
|
|
370
|
-
const userDataSet = getDataSetFor(systemInjector, User, 'username');
|
|
371
|
-
await userDataSet.remove(systemInjector, testUser.username);
|
|
372
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
it('Should emit onSessionInvalidated when the session entity is removed', async () => {
|
|
376
|
-
await usingAsync(new Injector(), async (i) => {
|
|
377
|
-
await prepareInjector(i);
|
|
378
|
-
const ctx = i.getInstance(HttpUserContext);
|
|
379
|
-
const sm = i.getInstance(StoreManager);
|
|
380
|
-
await sm.getStoreFor(User, 'username').add(testUser);
|
|
381
|
-
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test');
|
|
382
|
-
await sm.getStoreFor(PasswordCredential, 'userName').add(pw);
|
|
383
|
-
const handler = vi.fn();
|
|
384
|
-
ctx.addListener('onSessionInvalidated', handler);
|
|
385
|
-
let sessionId = '';
|
|
386
|
-
await ctx.cookieLogin(testUser, {
|
|
387
|
-
setHeader: (_headerName, headerValue) => {
|
|
388
|
-
sessionId = headerValue.split('=')[1].split(';')[0];
|
|
389
|
-
},
|
|
273
|
+
describe('cookieLogin / cookieLogout', () => {
|
|
274
|
+
it('persists the session, sets the cookie and emits onLogin', async () => {
|
|
275
|
+
await usingAsync(createInjector(), async (i) => {
|
|
276
|
+
prepareInjector(i);
|
|
277
|
+
const ctx = i.get(HttpUserContext);
|
|
278
|
+
const setHeader = vi.fn();
|
|
279
|
+
const onLogin = vi.fn();
|
|
280
|
+
ctx.addListener('onLogin', onLogin);
|
|
281
|
+
const result = await ctx.cookieLogin(testUser, { setHeader });
|
|
282
|
+
expect(result).toBe(testUser);
|
|
283
|
+
expect(setHeader).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(onLogin).toHaveBeenCalledWith({ user: testUser });
|
|
285
|
+
await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
|
|
286
|
+
const sessionDataSet = systemScope.get(SessionDataSet);
|
|
287
|
+
expect(await sessionDataSet.count(systemScope)).toBe(1);
|
|
390
288
|
});
|
|
391
|
-
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' });
|
|
392
|
-
const sessionDataSet = getDataSetFor(systemInjector, DefaultSession, 'sessionId');
|
|
393
|
-
await sessionDataSet.remove(systemInjector, sessionId);
|
|
394
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
395
289
|
});
|
|
396
290
|
});
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const sm = i.getInstance(StoreManager);
|
|
404
|
-
await sm.getStoreFor(User, 'username').add(testUser);
|
|
405
|
-
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test');
|
|
406
|
-
await sm.getStoreFor(PasswordCredential, 'userName').add(pw);
|
|
407
|
-
await ctx.cookieLogin(testUser, { setHeader: vi.fn() });
|
|
408
|
-
const originalUser = await ctx.getCurrentUser(request);
|
|
409
|
-
expect(originalUser).toEqual(testUser);
|
|
410
|
-
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' });
|
|
411
|
-
const userDataSet = getDataSetFor(systemInjector, User, 'username');
|
|
412
|
-
const updatedUser = { ...testUser, roles: ['newFancyRole'] };
|
|
413
|
-
await userDataSet.update(systemInjector, testUser.username, updatedUser);
|
|
414
|
-
const updatedUserFromContext = await ctx.getCurrentUser(request);
|
|
415
|
-
expect(updatedUserFromContext.roles).toEqual(['newFancyRole']);
|
|
416
|
-
await userDataSet.update(systemInjector, testUser.username, { ...updatedUser, roles: [] });
|
|
417
|
-
const reloadedUserFromContext = await ctx.getCurrentUser(request);
|
|
418
|
-
expect(reloadedUserFromContext.roles).toEqual([]);
|
|
419
|
-
});
|
|
420
|
-
});
|
|
421
|
-
it('Should remove current user when the user is removed from the store', () => {
|
|
422
|
-
return usingAsync(new Injector(), async (i) => {
|
|
423
|
-
await prepareInjector(i);
|
|
424
|
-
const ctx = i.getInstance(HttpUserContext);
|
|
425
|
-
const sm = i.getInstance(StoreManager);
|
|
426
|
-
await sm.getStoreFor(User, 'username').add(testUser);
|
|
427
|
-
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test');
|
|
428
|
-
await sm.getStoreFor(PasswordCredential, 'userName').add(pw);
|
|
429
|
-
await ctx.cookieLogin(testUser, { setHeader: vi.fn() });
|
|
430
|
-
const originalUser = await ctx.getCurrentUser(request);
|
|
431
|
-
expect(originalUser).toEqual(testUser);
|
|
432
|
-
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' });
|
|
433
|
-
const userDataSet = getDataSetFor(systemInjector, User, 'username');
|
|
434
|
-
await userDataSet.remove(systemInjector, testUser.username);
|
|
435
|
-
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError);
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
it('Should remove current user when the session is removed from the store', () => {
|
|
439
|
-
return usingAsync(new Injector(), async (i) => {
|
|
440
|
-
await prepareInjector(i);
|
|
441
|
-
const ctx = i.getInstance(HttpUserContext);
|
|
442
|
-
const sm = i.getInstance(StoreManager);
|
|
443
|
-
await sm.getStoreFor(User, 'username').add(testUser);
|
|
291
|
+
it('clears the session cookie, removes the stored session and emits onLogout', async () => {
|
|
292
|
+
await usingAsync(createInjector(), async (i) => {
|
|
293
|
+
prepareInjector(i);
|
|
294
|
+
const ctx = i.get(HttpUserContext);
|
|
295
|
+
const onLogout = vi.fn();
|
|
296
|
+
ctx.addListener('onLogout', onLogout);
|
|
444
297
|
let sessionId = '';
|
|
445
|
-
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test');
|
|
446
|
-
await sm.getStoreFor(PasswordCredential, 'userName').add(pw);
|
|
447
298
|
await ctx.cookieLogin(testUser, {
|
|
448
|
-
setHeader: (
|
|
449
|
-
sessionId =
|
|
450
|
-
return {};
|
|
299
|
+
setHeader: (_name, value) => {
|
|
300
|
+
sessionId = value.split('=')[1].split(';')[0];
|
|
451
301
|
},
|
|
452
302
|
});
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
await
|
|
458
|
-
|
|
303
|
+
const logoutSetHeader = vi.fn();
|
|
304
|
+
await ctx.cookieLogout({ headers: { cookie: `${ctx.authentication.cookieName}=${sessionId};` } }, { setHeader: logoutSetHeader });
|
|
305
|
+
expect(logoutSetHeader).toHaveBeenCalledWith('Set-Cookie', `${ctx.authentication.cookieName}=; Path=/; HttpOnly`);
|
|
306
|
+
expect(onLogout).toHaveBeenCalledWith(undefined);
|
|
307
|
+
await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
|
|
308
|
+
const sessionDataSet = systemScope.get(SessionDataSet);
|
|
309
|
+
expect(await sessionDataSet.count(systemScope)).toBe(0);
|
|
310
|
+
});
|
|
459
311
|
});
|
|
460
312
|
});
|
|
461
313
|
});
|