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