@furystack/rest-service 13.0.0 → 14.1.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 +124 -0
- package/README.md +47 -28
- package/esm/actions/error-action.d.ts +7 -2
- package/esm/actions/error-action.d.ts.map +1 -1
- package/esm/actions/error-action.js +7 -2
- package/esm/actions/error-action.js.map +1 -1
- package/esm/actions/get-current-user.d.ts +1 -4
- package/esm/actions/get-current-user.d.ts.map +1 -1
- package/esm/actions/get-current-user.js +1 -4
- package/esm/actions/get-current-user.js.map +1 -1
- 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.d.ts +3 -4
- package/esm/actions/is-authenticated.d.ts.map +1 -1
- package/esm/actions/is-authenticated.js +3 -4
- package/esm/actions/is-authenticated.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.d.ts +2 -6
- package/esm/actions/logout.d.ts.map +1 -1
- package/esm/actions/logout.js +3 -7
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/not-found-action.d.ts +1 -3
- package/esm/actions/not-found-action.d.ts.map +1 -1
- package/esm/actions/not-found-action.js +1 -3
- package/esm/actions/not-found-action.js.map +1 -1
- package/esm/actions/password-login-action.d.ts +5 -12
- package/esm/actions/password-login-action.d.ts.map +1 -1
- package/esm/actions/password-login-action.js +6 -13
- 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.d.ts +5 -11
- package/esm/authenticate.d.ts.map +1 -1
- package/esm/authenticate.js +6 -12
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authentication-providers/authentication-provider.d.ts +17 -0
- package/esm/authentication-providers/authentication-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.d.ts +7 -0
- package/esm/authentication-providers/basic-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.js +7 -0
- package/esm/authentication-providers/basic-auth-provider.js.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.js +8 -0
- package/esm/authentication-providers/cookie-auth-provider.js.map +1 -1
- package/esm/authorize.d.ts +5 -17
- package/esm/authorize.d.ts.map +1 -1
- package/esm/authorize.js +5 -17
- package/esm/authorize.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.js +1 -0
- package/esm/get-schema-from-api.js.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +54 -88
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +90 -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 +56 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +27 -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 +34 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +75 -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 +226 -332
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/identity-cache-keys.d.ts +28 -0
- package/esm/identity-cache-keys.d.ts.map +1 -0
- package/esm/identity-cache-keys.js +13 -0
- package/esm/identity-cache-keys.js.map +1 -0
- package/esm/identity-cache-keys.spec.d.ts +2 -0
- package/esm/identity-cache-keys.spec.d.ts.map +1 -0
- package/esm/identity-cache-keys.spec.js +21 -0
- package/esm/identity-cache-keys.spec.js.map +1 -0
- package/esm/identity-event-bus.d.ts +102 -0
- package/esm/identity-event-bus.d.ts.map +1 -0
- package/esm/identity-event-bus.js +151 -0
- package/esm/identity-event-bus.js.map +1 -0
- package/esm/identity-event-bus.spec.d.ts +2 -0
- package/esm/identity-event-bus.spec.d.ts.map +1 -0
- package/esm/identity-event-bus.spec.js +392 -0
- package/esm/identity-event-bus.spec.js.map +1 -0
- package/esm/index.d.ts +9 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +9 -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.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/request-action-implementation.d.ts +25 -0
- package/esm/request-action-implementation.d.ts.map +1 -1
- package/esm/request-action-implementation.js +9 -0
- package/esm/request-action-implementation.js.map +1 -1
- 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 +49 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +25 -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-resolution-cache.d.ts +65 -0
- package/esm/user-resolution-cache.d.ts.map +1 -0
- package/esm/user-resolution-cache.js +101 -0
- package/esm/user-resolution-cache.js.map +1 -0
- package/esm/user-resolution-cache.spec.d.ts +2 -0
- package/esm/user-resolution-cache.spec.d.ts.map +1 -0
- package/esm/user-resolution-cache.spec.js +220 -0
- package/esm/user-resolution-cache.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.d.ts +13 -12
- package/esm/validate.d.ts.map +1 -1
- package/esm/validate.integration.spec.js +27 -28
- package/esm/validate.integration.spec.js.map +1 -1
- package/esm/validate.js +12 -0
- package/esm/validate.js.map +1 -1
- package/package.json +14 -12
- package/src/actions/error-action.ts +7 -3
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/get-current-user.ts +1 -4
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/is-authenticated.ts +3 -4
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +3 -7
- package/src/actions/not-found-action.ts +1 -3
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +6 -13
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +6 -12
- package/src/authentication-providers/authentication-provider.ts +17 -0
- package/src/authentication-providers/basic-auth-provider.ts +7 -0
- package/src/authentication-providers/cookie-auth-provider.ts +8 -0
- package/src/authorize.spec.ts +37 -39
- package/src/authorize.ts +6 -17
- 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 +2 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +78 -24
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +246 -378
- package/src/http-user-context.ts +122 -92
- package/src/identity-cache-keys.spec.ts +24 -0
- package/src/identity-cache-keys.ts +31 -0
- package/src/identity-event-bus.spec.ts +285 -0
- package/src/identity-event-bus.ts +230 -0
- package/src/index.ts +9 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +14 -12
- package/src/openapi/generate-openapi-document.spec.ts +2 -2
- package/src/proxy-manager.spec.ts +158 -188
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/request-action-implementation.ts +25 -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 +43 -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-resolution-cache.spec.ts +241 -0
- package/src/user-resolution-cache.ts +164 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +69 -0
- package/src/validate.integration.spec.ts +40 -35
- package/src/validate.ts +14 -12
- 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,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
|
+
})
|
|
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
|
-
}
|
|
223
|
-
).rejects.
|
|
200
|
+
}),
|
|
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
|
|
234
|
-
}
|
|
235
|
-
).rejects.
|
|
211
|
+
headers: { cookie: `${ctx.authentication.cookieName}=666;` },
|
|
212
|
+
}),
|
|
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
|
|
272
|
-
}
|
|
273
|
-
|
|
225
|
+
headers: { cookie: `${ctx.authentication.cookieName}=666;` },
|
|
226
|
+
})
|
|
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,168 @@ 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.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
expect(ctx.authenticateRequest).toBeCalledTimes(1)
|
|
331
|
-
expect(result).toBe(testUser)
|
|
332
|
-
expect(result2).toBe(testUser)
|
|
333
|
-
})
|
|
334
|
-
})
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
describe('cookieLogin', () => {
|
|
338
|
-
it('Should return the current user from authenticateRequest() once per request', async () => {
|
|
339
|
-
await usingAsync(new Injector(), async (i) => {
|
|
340
|
-
await prepareInjector(i)
|
|
341
|
-
const ctx = i.getInstance(HttpUserContext)
|
|
342
|
-
const setHeader = vi.fn()
|
|
343
|
-
const addMock = vi.fn(async () => ({}))
|
|
344
|
-
// @ts-expect-error
|
|
345
|
-
ctx.getSessionDataSet = vi.fn(() => ({ add: addMock }))
|
|
346
|
-
const authResult = await ctx.cookieLogin(testUser, { setHeader })
|
|
347
|
-
expect(authResult).toBe(testUser)
|
|
348
|
-
expect(setHeader).toBeCalled()
|
|
349
|
-
expect(addMock).toBeCalled()
|
|
350
|
-
})
|
|
351
|
-
})
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
describe('cookieLogout', () => {
|
|
355
|
-
it('Should invalidate the current session id cookie', async () => {
|
|
356
|
-
await usingAsync(new Injector(), async (i) => {
|
|
357
|
-
await prepareInjector(i)
|
|
358
|
-
const ctx = i.getInstance(HttpUserContext)
|
|
359
|
-
const setHeader = vi.fn()
|
|
360
|
-
const removeMock = vi.fn(async () => undefined)
|
|
361
|
-
const sessionDataSetMock = {
|
|
362
|
-
add: vi.fn(async () => ({})),
|
|
363
|
-
find: vi.fn(async () => [{ sessionId: 'example-session-id' }]),
|
|
364
|
-
remove: removeMock,
|
|
365
|
-
primaryKey: 'sessionId' as const,
|
|
366
|
-
}
|
|
367
|
-
// @ts-expect-error
|
|
368
|
-
ctx.getSessionDataSet = vi.fn(() => sessionDataSetMock)
|
|
276
|
+
it('caches the result of authenticateRequest by session id when the cookie provider yields a key', async () => {
|
|
277
|
+
await usingAsync(createInjector(), async (i) => {
|
|
278
|
+
prepareInjector(i)
|
|
279
|
+
const ctx = i.get(HttpUserContext)
|
|
280
|
+
const cookiedRequest = {
|
|
281
|
+
headers: { cookie: `${ctx.authentication.cookieName}=session-1;` },
|
|
282
|
+
} as IncomingMessage
|
|
369
283
|
ctx.authenticateRequest = vi.fn(async () => testUser)
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
expect(
|
|
375
|
-
expect(removeMock).toBeCalled()
|
|
284
|
+
const first = await ctx.getCurrentUser(cookiedRequest)
|
|
285
|
+
const second = await ctx.getCurrentUser(cookiedRequest)
|
|
286
|
+
expect(ctx.authenticateRequest).toHaveBeenCalledTimes(1)
|
|
287
|
+
expect(first).toBe(testUser)
|
|
288
|
+
expect(second).toBe(testUser)
|
|
376
289
|
})
|
|
377
290
|
})
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
describe('EventHub events', () => {
|
|
381
|
-
it('Should emit onLogin when cookieLogin succeeds', async () => {
|
|
382
|
-
await usingAsync(new Injector(), async (i) => {
|
|
383
|
-
await prepareInjector(i)
|
|
384
|
-
const ctx = i.getInstance(HttpUserContext)
|
|
385
|
-
const handler = vi.fn()
|
|
386
|
-
ctx.addListener('onLogin', handler)
|
|
387
291
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
292
|
+
it('treats distinct sessions as distinct cache entries', async () => {
|
|
293
|
+
await usingAsync(createInjector(), async (i) => {
|
|
294
|
+
prepareInjector(i)
|
|
295
|
+
const ctx = i.get(HttpUserContext)
|
|
296
|
+
const otherUser: User = { username: 'otherUser', roles: [] }
|
|
297
|
+
const requestA = {
|
|
298
|
+
headers: { cookie: `${ctx.authentication.cookieName}=session-a;` },
|
|
299
|
+
} as IncomingMessage
|
|
300
|
+
const requestB = {
|
|
301
|
+
headers: { cookie: `${ctx.authentication.cookieName}=session-b;` },
|
|
302
|
+
} as IncomingMessage
|
|
303
|
+
const authenticateRequest = vi
|
|
304
|
+
.fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
|
|
305
|
+
.mockResolvedValueOnce(testUser)
|
|
306
|
+
.mockResolvedValueOnce(otherUser)
|
|
307
|
+
ctx.authenticateRequest = authenticateRequest
|
|
308
|
+
expect(await ctx.getCurrentUser(requestA)).toBe(testUser)
|
|
309
|
+
expect(await ctx.getCurrentUser(requestB)).toBe(otherUser)
|
|
310
|
+
expect(await ctx.getCurrentUser(requestA)).toBe(testUser)
|
|
311
|
+
expect(authenticateRequest).toHaveBeenCalledTimes(2)
|
|
396
312
|
})
|
|
397
313
|
})
|
|
398
314
|
|
|
399
|
-
it('
|
|
400
|
-
await usingAsync(
|
|
401
|
-
|
|
402
|
-
const ctx = i.
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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)
|
|
315
|
+
it('bypasses the cache when no provider yields a key (e.g. Basic Auth, anonymous)', async () => {
|
|
316
|
+
await usingAsync(createInjector(), async (i) => {
|
|
317
|
+
prepareInjector(i)
|
|
318
|
+
const ctx = i.get(HttpUserContext)
|
|
319
|
+
const anonRequest = { headers: {} } as IncomingMessage
|
|
320
|
+
const authenticateRequest = vi
|
|
321
|
+
.fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
|
|
322
|
+
.mockResolvedValue(testUser)
|
|
323
|
+
ctx.authenticateRequest = authenticateRequest
|
|
324
|
+
await ctx.getCurrentUser(anonRequest)
|
|
325
|
+
await ctx.getCurrentUser(anonRequest)
|
|
326
|
+
expect(authenticateRequest).toHaveBeenCalledTimes(2)
|
|
420
327
|
})
|
|
421
328
|
})
|
|
422
329
|
|
|
423
|
-
it('
|
|
424
|
-
await usingAsync(
|
|
425
|
-
|
|
426
|
-
const ctx = i.
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
await
|
|
330
|
+
it('drops the local cache entry on cookieLogout so the next request re-authenticates', async () => {
|
|
331
|
+
await usingAsync(createInjector(), async (i) => {
|
|
332
|
+
prepareInjector(i)
|
|
333
|
+
const ctx = i.get(HttpUserContext)
|
|
334
|
+
const sessionId = 'session-logout'
|
|
335
|
+
const cookiedRequest = {
|
|
336
|
+
headers: { cookie: `${ctx.authentication.cookieName}=${sessionId};` },
|
|
337
|
+
} as IncomingMessage
|
|
338
|
+
await i.get(SessionStore).add({ sessionId, username: testUser.username })
|
|
339
|
+
await i.get(UserStore).add(testUser)
|
|
432
340
|
|
|
433
|
-
const
|
|
434
|
-
|
|
341
|
+
const authenticateRequest = vi
|
|
342
|
+
.fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
|
|
343
|
+
.mockResolvedValue(testUser)
|
|
344
|
+
ctx.authenticateRequest = authenticateRequest
|
|
435
345
|
|
|
436
|
-
await ctx.
|
|
346
|
+
await ctx.getCurrentUser(cookiedRequest)
|
|
347
|
+
expect(authenticateRequest).toHaveBeenCalledTimes(1)
|
|
437
348
|
|
|
438
|
-
|
|
439
|
-
const userDataSet = getDataSetFor(systemInjector, User, 'username')
|
|
440
|
-
await userDataSet.remove(systemInjector, testUser.username)
|
|
349
|
+
await ctx.cookieLogout(cookiedRequest, { setHeader: vi.fn() })
|
|
441
350
|
|
|
442
|
-
|
|
351
|
+
await ctx.getCurrentUser(cookiedRequest)
|
|
352
|
+
expect(authenticateRequest).toHaveBeenCalledTimes(2)
|
|
443
353
|
})
|
|
444
354
|
})
|
|
445
355
|
|
|
446
|
-
it('
|
|
447
|
-
await usingAsync(
|
|
448
|
-
|
|
449
|
-
const ctx = i.
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
let sessionId = ''
|
|
460
|
-
await ctx.cookieLogin(testUser, {
|
|
461
|
-
setHeader: (_headerName, headerValue) => {
|
|
462
|
-
sessionId = headerValue.split('=')[1].split(';')[0]
|
|
463
|
-
},
|
|
464
|
-
})
|
|
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)
|
|
356
|
+
it('re-walks providers after a failed authenticateRequest', async () => {
|
|
357
|
+
await usingAsync(createInjector(), async (i) => {
|
|
358
|
+
prepareInjector(i)
|
|
359
|
+
const ctx = i.get(HttpUserContext)
|
|
360
|
+
const authenticateRequest = vi
|
|
361
|
+
.fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
|
|
362
|
+
.mockRejectedValueOnce(new UnauthenticatedError())
|
|
363
|
+
.mockResolvedValueOnce(testUser)
|
|
364
|
+
ctx.authenticateRequest = authenticateRequest
|
|
365
|
+
await expect(ctx.getCurrentUser(request)).rejects.toBeInstanceOf(UnauthenticatedError)
|
|
366
|
+
expect(await ctx.getCurrentUser(request)).toBe(testUser)
|
|
367
|
+
expect(authenticateRequest).toHaveBeenCalledTimes(2)
|
|
471
368
|
})
|
|
472
369
|
})
|
|
473
370
|
})
|
|
474
371
|
|
|
475
|
-
describe('
|
|
476
|
-
it('
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const ctx = i.
|
|
480
|
-
const
|
|
481
|
-
|
|
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)
|
|
372
|
+
describe('cookieLogin / cookieLogout', () => {
|
|
373
|
+
it('persists the session, sets the cookie and emits onLogin', async () => {
|
|
374
|
+
await usingAsync(createInjector(), async (i) => {
|
|
375
|
+
prepareInjector(i)
|
|
376
|
+
const ctx = i.get(HttpUserContext)
|
|
377
|
+
const setHeader = vi.fn()
|
|
378
|
+
const onLogin = vi.fn()
|
|
379
|
+
ctx.addListener('onLogin', onLogin)
|
|
518
380
|
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
381
|
+
const result = await ctx.cookieLogin(testUser, { setHeader })
|
|
382
|
+
expect(result).toBe(testUser)
|
|
383
|
+
expect(setHeader).toHaveBeenCalledTimes(1)
|
|
384
|
+
expect(onLogin).toHaveBeenCalledWith({ user: testUser })
|
|
522
385
|
|
|
523
|
-
await
|
|
386
|
+
await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
|
|
387
|
+
const sessionDataSet = systemScope.get(SessionDataSet)
|
|
388
|
+
expect(await sessionDataSet.count(systemScope)).toBe(1)
|
|
389
|
+
})
|
|
524
390
|
})
|
|
525
391
|
})
|
|
526
392
|
|
|
527
|
-
it('
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const ctx = i.
|
|
531
|
-
const
|
|
532
|
-
|
|
393
|
+
it('clears the session cookie, removes the stored session and emits onLogout', async () => {
|
|
394
|
+
await usingAsync(createInjector(), async (i) => {
|
|
395
|
+
prepareInjector(i)
|
|
396
|
+
const ctx = i.get(HttpUserContext)
|
|
397
|
+
const onLogout = vi.fn()
|
|
398
|
+
ctx.addListener('onLogout', onLogout)
|
|
533
399
|
|
|
534
400
|
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
401
|
await ctx.cookieLogin(testUser, {
|
|
540
|
-
setHeader: (
|
|
541
|
-
sessionId =
|
|
542
|
-
return {} as ServerResponse
|
|
402
|
+
setHeader: (_name, value) => {
|
|
403
|
+
sessionId = value.split('=')[1].split(';')[0]
|
|
543
404
|
},
|
|
544
405
|
})
|
|
545
406
|
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
407
|
+
const logoutSetHeader = vi.fn()
|
|
408
|
+
await ctx.cookieLogout(
|
|
409
|
+
{ headers: { cookie: `${ctx.authentication.cookieName}=${sessionId};` } },
|
|
410
|
+
{ setHeader: logoutSetHeader },
|
|
411
|
+
)
|
|
412
|
+
expect(logoutSetHeader).toHaveBeenCalledWith(
|
|
413
|
+
'Set-Cookie',
|
|
414
|
+
`${ctx.authentication.cookieName}=; Path=/; HttpOnly`,
|
|
415
|
+
)
|
|
416
|
+
expect(onLogout).toHaveBeenCalledWith(undefined)
|
|
552
417
|
|
|
553
|
-
await
|
|
418
|
+
await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
|
|
419
|
+
const sessionDataSet = systemScope.get(SessionDataSet)
|
|
420
|
+
expect(await sessionDataSet.count(systemScope)).toBe(0)
|
|
421
|
+
})
|
|
554
422
|
})
|
|
555
423
|
})
|
|
556
424
|
})
|