@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,224 +1,137 @@
|
|
|
1
|
-
import { Injector } from '@furystack/inject'
|
|
2
|
-
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
3
|
-
import { ServerManager } from './server-manager.js'
|
|
4
|
-
import { StaticServerManager } from './static-server-manager.js'
|
|
5
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
6
1
|
import { getPort } from '@furystack/core/port-generator'
|
|
2
|
+
import { createInjector } from '@furystack/inject'
|
|
3
|
+
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { useStaticFiles } from './helpers.js'
|
|
6
|
+
import { HttpServerPoolToken } from './http-server-pool.js'
|
|
7
|
+
import { buildStaticSiteServerApi } from './static-site-runtime.js'
|
|
7
8
|
|
|
8
|
-
describe('
|
|
9
|
+
describe('useStaticFiles / buildStaticSiteServerApi', () => {
|
|
9
10
|
describe('Top level routing', () => {
|
|
10
|
-
it('
|
|
11
|
-
await usingAsync(
|
|
12
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
11
|
+
it('responds 404 without a fallback', async () => {
|
|
12
|
+
await usingAsync(createInjector(), async (i) => {
|
|
13
13
|
const port = getPort()
|
|
14
|
-
await
|
|
15
|
-
baseUrl: '/',
|
|
16
|
-
path: '.',
|
|
17
|
-
port,
|
|
18
|
-
})
|
|
14
|
+
await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
|
|
19
15
|
|
|
20
16
|
const result = await fetch(`http://127.0.0.1:${port}/not-found.html`)
|
|
21
17
|
expect(result.ok).toBe(false)
|
|
22
18
|
expect(result.status).toBe(404)
|
|
23
|
-
expect(result
|
|
24
|
-
|
|
25
|
-
expect(body).toBe('Not found')
|
|
19
|
+
expect(result.headers.get('content-type')).toBe('text/plain')
|
|
20
|
+
expect(await result.text()).toBe('Not found')
|
|
26
21
|
})
|
|
27
22
|
})
|
|
28
23
|
|
|
29
|
-
it('
|
|
30
|
-
await usingAsync(
|
|
31
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
24
|
+
it('falls back to the configured file and returns its content-type', async () => {
|
|
25
|
+
await usingAsync(createInjector(), async (i) => {
|
|
32
26
|
const port = getPort()
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
await useStaticFiles({
|
|
28
|
+
injector: i,
|
|
35
29
|
baseUrl: '/',
|
|
36
30
|
path: '.',
|
|
37
31
|
fallback: 'package.json',
|
|
38
32
|
port,
|
|
39
|
-
headers: {
|
|
40
|
-
'custom-header': 'custom-value',
|
|
41
|
-
},
|
|
33
|
+
headers: { 'custom-header': 'custom-value' },
|
|
42
34
|
})
|
|
43
35
|
|
|
44
36
|
const result = await fetch(`http://localhost:${port}/not-found.html`)
|
|
45
|
-
|
|
46
37
|
expect(result.headers.get('content-type')).toBe('application/json')
|
|
47
38
|
expect(result.headers.get('custom-header')).toBe('custom-value')
|
|
48
39
|
})
|
|
49
40
|
})
|
|
50
41
|
|
|
51
|
-
it('
|
|
52
|
-
await usingAsync(
|
|
53
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
42
|
+
it('falls back for the root url when no index exists', async () => {
|
|
43
|
+
await usingAsync(createInjector(), async (i) => {
|
|
54
44
|
const port = getPort()
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
await useStaticFiles({
|
|
46
|
+
injector: i,
|
|
57
47
|
baseUrl: '/',
|
|
58
48
|
path: '.',
|
|
59
49
|
fallback: 'package.json',
|
|
60
50
|
port,
|
|
61
|
-
headers: {
|
|
62
|
-
'custom-header': 'custom-value',
|
|
63
|
-
},
|
|
51
|
+
headers: { 'custom-header': 'custom-value' },
|
|
64
52
|
})
|
|
65
53
|
|
|
66
54
|
const result = await fetch(`http://localhost:${port}`)
|
|
67
|
-
|
|
68
55
|
expect(result.headers.get('content-type')).toBe('application/json')
|
|
69
56
|
expect(result.headers.get('custom-header')).toBe('custom-value')
|
|
70
57
|
})
|
|
71
58
|
})
|
|
72
59
|
|
|
73
|
-
it('
|
|
74
|
-
await usingAsync(
|
|
75
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
60
|
+
it('serves an existing file from the root directory', async () => {
|
|
61
|
+
await usingAsync(createInjector(), async (i) => {
|
|
76
62
|
const port = getPort()
|
|
77
|
-
|
|
78
|
-
await staticServerManager.addStaticSite({
|
|
79
|
-
baseUrl: '/',
|
|
80
|
-
path: '.',
|
|
81
|
-
port,
|
|
82
|
-
headers: {
|
|
83
|
-
'custom-header': 'custom-value',
|
|
84
|
-
},
|
|
85
|
-
})
|
|
86
|
-
|
|
63
|
+
await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
|
|
87
64
|
const result = await fetch(`http://localhost:${port}/README.md`)
|
|
88
|
-
|
|
89
65
|
expect(result.headers.get('content-type')).toBe('text/markdown')
|
|
90
|
-
expect(result.headers.get('custom-header')).toBe('custom-value')
|
|
91
66
|
})
|
|
92
67
|
})
|
|
93
68
|
|
|
94
|
-
it('
|
|
95
|
-
await usingAsync(
|
|
96
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
69
|
+
it('serves files from subdirectories', async () => {
|
|
70
|
+
await usingAsync(createInjector(), async (i) => {
|
|
97
71
|
const port = getPort()
|
|
98
|
-
|
|
99
|
-
await staticServerManager.addStaticSite({
|
|
100
|
-
baseUrl: '/',
|
|
101
|
-
path: '.',
|
|
102
|
-
port,
|
|
103
|
-
})
|
|
104
|
-
|
|
72
|
+
await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
|
|
105
73
|
const result = await fetch(`http://localhost:${port}/packages/utils/README.md`)
|
|
106
|
-
|
|
107
74
|
expect(result.headers.get('content-type')).toBe('text/markdown')
|
|
108
75
|
})
|
|
109
76
|
})
|
|
110
77
|
})
|
|
111
78
|
|
|
112
79
|
describe('Non-top level routing', () => {
|
|
113
|
-
it('
|
|
114
|
-
await usingAsync(
|
|
115
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
80
|
+
it('leaves requests outside the base url to the next api in the chain', async () => {
|
|
81
|
+
await usingAsync(createInjector(), async (i) => {
|
|
116
82
|
const port = getPort()
|
|
83
|
+
const serverApi = await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', port })
|
|
117
84
|
|
|
118
|
-
await
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const server = [...injector.getInstance(ServerManager).servers.values()][0]
|
|
125
|
-
|
|
126
|
-
server.apis[0].onRequest = vi.fn()
|
|
85
|
+
const record = await i.get(HttpServerPoolToken).acquire({ port })
|
|
86
|
+
const onRequest = vi.fn()
|
|
87
|
+
// Swap in a spy to observe non-matching requests never reach the handler.
|
|
88
|
+
serverApi.onRequest = onRequest
|
|
89
|
+
record.apis[0] = serverApi
|
|
127
90
|
|
|
128
91
|
fetch(`http://localhost:${port}/bundleToAnotherFolder/not-found.html`).catch(() => {
|
|
129
|
-
|
|
92
|
+
// Expected to fail: the server has no matching API and drops the socket.
|
|
130
93
|
})
|
|
131
|
-
|
|
132
94
|
await sleepAsync(100)
|
|
133
|
-
|
|
134
|
-
expect(server.apis[0].onRequest).not.toHaveBeenCalled()
|
|
95
|
+
expect(onRequest).not.toHaveBeenCalled()
|
|
135
96
|
})
|
|
136
97
|
})
|
|
137
98
|
|
|
138
|
-
it('
|
|
139
|
-
await usingAsync(
|
|
140
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
99
|
+
it('returns 404 under a sub-base url when the file is missing and no fallback is set', async () => {
|
|
100
|
+
await usingAsync(createInjector(), async (i) => {
|
|
141
101
|
const port = getPort()
|
|
142
|
-
|
|
143
|
-
await staticServerManager.addStaticSite({
|
|
144
|
-
baseUrl: '/bundle',
|
|
145
|
-
path: '.',
|
|
146
|
-
port,
|
|
147
|
-
})
|
|
102
|
+
await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', port })
|
|
148
103
|
|
|
149
104
|
const result = await fetch(`http://localhost:${port}/bundle/not-found.html`)
|
|
150
105
|
expect(result.ok).toBe(false)
|
|
151
106
|
expect(result.status).toBe(404)
|
|
152
107
|
expect(result.headers.get('content-type')).toBe('text/plain')
|
|
153
|
-
|
|
154
|
-
expect(body).toBe('Not found')
|
|
108
|
+
expect(await result.text()).toBe('Not found')
|
|
155
109
|
})
|
|
156
110
|
})
|
|
157
111
|
|
|
158
|
-
it('
|
|
159
|
-
await usingAsync(
|
|
160
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
112
|
+
it('serves the configured fallback under a sub-base url', async () => {
|
|
113
|
+
await usingAsync(createInjector(), async (i) => {
|
|
161
114
|
const port = getPort()
|
|
162
|
-
|
|
163
|
-
await staticServerManager.addStaticSite({
|
|
164
|
-
baseUrl: '/bundle',
|
|
165
|
-
path: '.',
|
|
166
|
-
fallback: 'package.json',
|
|
167
|
-
port,
|
|
168
|
-
})
|
|
115
|
+
await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', fallback: 'package.json', port })
|
|
169
116
|
|
|
170
117
|
const result = await fetch(`http://localhost:${port}/bundle/not-found.html`)
|
|
171
|
-
|
|
172
118
|
expect(result.headers.get('content-type')).toBe('application/json')
|
|
173
119
|
})
|
|
174
120
|
})
|
|
175
|
-
|
|
176
|
-
it('Should return a defined file from a root directory', async () => {
|
|
177
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
178
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
179
|
-
const port = getPort()
|
|
180
|
-
|
|
181
|
-
await staticServerManager.addStaticSite({
|
|
182
|
-
baseUrl: '/',
|
|
183
|
-
path: '.',
|
|
184
|
-
port,
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
const result = await fetch(`http://localhost:${port}/README.md`)
|
|
188
|
-
|
|
189
|
-
expect(result.headers.get('content-type')).toBe('text/markdown')
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('Should return a defined file from a subdirectory', async () => {
|
|
194
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
195
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
196
|
-
const port = getPort()
|
|
197
|
-
|
|
198
|
-
await staticServerManager.addStaticSite({
|
|
199
|
-
baseUrl: '/',
|
|
200
|
-
path: '.',
|
|
201
|
-
port,
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
const result = await fetch(`http://localhost:${port}/packages/utils/README.md`)
|
|
205
|
-
|
|
206
|
-
expect(result.headers.get('content-type')).toBe('text/markdown')
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
121
|
})
|
|
210
122
|
|
|
211
123
|
describe('shouldExec', () => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
124
|
+
const api = (baseUrl: string) => buildStaticSiteServerApi({ baseUrl, path: '.', port: 0 })
|
|
125
|
+
|
|
126
|
+
it('does not match when the request method is missing', () => {
|
|
127
|
+
expect(api('/').shouldExec({ req: { url: '/' } as never, res: {} as never })).toBe(false)
|
|
215
128
|
})
|
|
216
129
|
|
|
217
|
-
it('
|
|
218
|
-
|
|
219
|
-
expect(staticServerManager.shouldExec('/')({ req: { method: 'POST', url: '/' } })).toBe(false)
|
|
130
|
+
it('does not match non-GET methods', () => {
|
|
131
|
+
expect(api('/').shouldExec({ req: { method: 'POST', url: '/' } as never, res: {} as never })).toBe(false)
|
|
220
132
|
})
|
|
221
|
-
|
|
133
|
+
|
|
134
|
+
const positiveCases: Array<[string, string]> = [
|
|
222
135
|
['/', '/'],
|
|
223
136
|
['/', '/index.html'],
|
|
224
137
|
['/', '/subdir'],
|
|
@@ -228,22 +141,21 @@ describe('StaticServerManager', () => {
|
|
|
228
141
|
['/subdir', '/subdir/'],
|
|
229
142
|
['/subdir', '/subdir/file.js'],
|
|
230
143
|
['/subdir', '/subdir/s2/file.js'],
|
|
231
|
-
]
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
expect(shouldExec).toBe(true)
|
|
144
|
+
]
|
|
145
|
+
positiveCases.forEach(([baseUrl, url]) =>
|
|
146
|
+
it(`matches GET '${url}' against base '${baseUrl}'`, () => {
|
|
147
|
+
expect(api(baseUrl).shouldExec({ req: { method: 'GET', url } as never, res: {} as never })).toBe(true)
|
|
236
148
|
}),
|
|
237
149
|
)
|
|
238
150
|
|
|
239
|
-
it('
|
|
240
|
-
const
|
|
241
|
-
expect(
|
|
242
|
-
expect(
|
|
151
|
+
it('does not match unrelated urls under a non-root base', () => {
|
|
152
|
+
const subdir = api('/subdir')
|
|
153
|
+
expect(subdir.shouldExec({ req: { method: 'GET', url: '/' } as never, res: {} as never })).toBe(false)
|
|
154
|
+
expect(subdir.shouldExec({ req: { method: 'GET', url: '/other/index.html' } as never, res: {} as never })).toBe(
|
|
243
155
|
false,
|
|
244
156
|
)
|
|
245
|
-
expect(
|
|
246
|
-
expect(
|
|
157
|
+
expect(subdir.shouldExec({ req: { method: 'GET', url: '/subdir2' } as never, res: {} as never })).toBe(false)
|
|
158
|
+
expect(subdir.shouldExec({ req: { method: 'GET', url: '/subdir2/index.html' } as never, res: {} as never })).toBe(
|
|
247
159
|
false,
|
|
248
160
|
)
|
|
249
161
|
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'http'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { buildStaticSiteServerApi } from './static-site-runtime.js'
|
|
4
|
+
|
|
5
|
+
const makeRequest = (url: string | undefined, method: string | undefined = 'GET'): IncomingMessage =>
|
|
6
|
+
({ url, method, headers: {} }) as unknown as IncomingMessage
|
|
7
|
+
|
|
8
|
+
// Full onRequest / sendFile behaviour (streaming, fallback, 404) is covered
|
|
9
|
+
// end-to-end via `static-server-manager.spec.ts` which drives the real HTTP
|
|
10
|
+
// pool. These tests cover the pure-function `shouldExec` gate which is
|
|
11
|
+
// exercised by the server pool before any request is handed to the API.
|
|
12
|
+
describe('buildStaticSiteServerApi / shouldExec', () => {
|
|
13
|
+
const api = buildStaticSiteServerApi({ baseUrl: '/static', path: '/tmp', port: 0 })
|
|
14
|
+
|
|
15
|
+
it('matches GET requests under the baseUrl', () => {
|
|
16
|
+
expect(api.shouldExec({ req: makeRequest('/static/index.html'), res: {} as ServerResponse })).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('is case-insensitive about the method (GET only)', () => {
|
|
20
|
+
expect(api.shouldExec({ req: makeRequest('/static/a.html', 'get'), res: {} as ServerResponse })).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('rejects non-GET methods', () => {
|
|
24
|
+
expect(api.shouldExec({ req: makeRequest('/static/index.html', 'POST'), res: {} as ServerResponse })).toBe(false)
|
|
25
|
+
expect(api.shouldExec({ req: makeRequest('/static/index.html', 'DELETE'), res: {} as ServerResponse })).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects URLs that do not match the baseUrl', () => {
|
|
29
|
+
expect(api.shouldExec({ req: makeRequest('/other/index.html'), res: {} as ServerResponse })).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('rejects requests without a URL', () => {
|
|
33
|
+
expect(api.shouldExec({ req: makeRequest(undefined), res: {} as ServerResponse })).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('never exposes an onUpgrade hook (static sites do not handle upgrades)', () => {
|
|
37
|
+
expect(api.onUpgrade).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PathHelper } from '@furystack/utils'
|
|
2
|
+
import { createReadStream } from 'fs'
|
|
3
|
+
import { access, stat } from 'fs/promises'
|
|
4
|
+
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'http'
|
|
5
|
+
import { join, normalize, sep } from 'path'
|
|
6
|
+
import type { ServerApi } from './http-server-pool.js'
|
|
7
|
+
import { getMimeForFile } from './mime-types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options accepted by {@link buildStaticSiteServerApi} and
|
|
11
|
+
* {@link useStaticFiles}.
|
|
12
|
+
*/
|
|
13
|
+
export interface StaticServerOptions {
|
|
14
|
+
baseUrl: string
|
|
15
|
+
path: string
|
|
16
|
+
hostName?: string
|
|
17
|
+
port: number
|
|
18
|
+
fallback?: string
|
|
19
|
+
headers?: OutgoingHttpHeaders
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sendFile = async ({
|
|
23
|
+
fullPath,
|
|
24
|
+
headers,
|
|
25
|
+
res,
|
|
26
|
+
}: {
|
|
27
|
+
fullPath: string
|
|
28
|
+
res: ServerResponse
|
|
29
|
+
headers?: OutgoingHttpHeaders
|
|
30
|
+
}): Promise<void> => {
|
|
31
|
+
const { size } = await stat(fullPath)
|
|
32
|
+
await access(fullPath)
|
|
33
|
+
const head = {
|
|
34
|
+
...headers,
|
|
35
|
+
'Content-Length': size,
|
|
36
|
+
'Content-Type': getMimeForFile(fullPath),
|
|
37
|
+
}
|
|
38
|
+
Object.entries(head).forEach(([key, value]) => res.setHeader(key, value))
|
|
39
|
+
await new Promise<void>((resolve, reject) =>
|
|
40
|
+
createReadStream(fullPath, { autoClose: true }).once('finish', resolve).once('error', reject).pipe(res),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds a {@link ServerApi} that serves files under `options.path` for every
|
|
46
|
+
* GET request whose URL matches `options.baseUrl`. Falls back to
|
|
47
|
+
* `options.fallback` (if set) for missing files, otherwise responds with 404.
|
|
48
|
+
*/
|
|
49
|
+
export const buildStaticSiteServerApi = (options: StaticServerOptions): ServerApi => {
|
|
50
|
+
const onRequest = async ({ req, res }: { req: IncomingMessage; res: ServerResponse }): Promise<void> => {
|
|
51
|
+
const extractedPath = PathHelper.extractPath(req.url as string, options.baseUrl)
|
|
52
|
+
const filePath = (extractedPath || '/').replaceAll('/', sep)
|
|
53
|
+
const fullPath = normalize(join(options.path, filePath))
|
|
54
|
+
try {
|
|
55
|
+
await sendFile({ fullPath, res, headers: options.headers })
|
|
56
|
+
} catch {
|
|
57
|
+
if (options.fallback) {
|
|
58
|
+
await sendFile({ fullPath: normalize(join(options.path, options.fallback)), res, headers: options.headers })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
62
|
+
res.end('Not found')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
shouldExec: ({ req }) =>
|
|
68
|
+
req.url !== undefined &&
|
|
69
|
+
req.method?.toUpperCase() === 'GET' &&
|
|
70
|
+
PathHelper.matchesBaseUrl(req.url, options.baseUrl)
|
|
71
|
+
? true
|
|
72
|
+
: false,
|
|
73
|
+
onRequest,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { User } from '@furystack/core'
|
|
2
|
+
import { createInjector } from '@furystack/inject'
|
|
3
|
+
import { usingAsync } from '@furystack/utils'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { HttpAuthenticationSettings, defaultHttpAuthenticationSettings } from './http-authentication-settings.js'
|
|
6
|
+
import { resolveUserCacheKey, UserResolutionCache } from './user-resolution-cache.js'
|
|
7
|
+
|
|
8
|
+
const testUser: User = { username: 'alice', roles: ['reader'] }
|
|
9
|
+
const otherUser: User = { username: 'bob', roles: [] }
|
|
10
|
+
|
|
11
|
+
describe('UserResolutionCache', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.useFakeTimers()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.useRealTimers()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('resolve', () => {
|
|
21
|
+
it('returns the loader result on first call and reuses it on subsequent calls within TTL', async () => {
|
|
22
|
+
await usingAsync(createInjector(), async (i) => {
|
|
23
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
24
|
+
...defaultHttpAuthenticationSettings(),
|
|
25
|
+
userCacheTtlMs: 30_000,
|
|
26
|
+
}))
|
|
27
|
+
const cache = i.get(UserResolutionCache)
|
|
28
|
+
const loader = vi.fn(async () => testUser)
|
|
29
|
+
const first = await cache.resolve('cookie:abc', loader)
|
|
30
|
+
const second = await cache.resolve('cookie:abc', loader)
|
|
31
|
+
expect(first).toBe(testUser)
|
|
32
|
+
expect(second).toBe(testUser)
|
|
33
|
+
expect(loader).toHaveBeenCalledTimes(1)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('treats distinct cache keys independently', async () => {
|
|
38
|
+
await usingAsync(createInjector(), async (i) => {
|
|
39
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
40
|
+
...defaultHttpAuthenticationSettings(),
|
|
41
|
+
userCacheTtlMs: 30_000,
|
|
42
|
+
}))
|
|
43
|
+
const cache = i.get(UserResolutionCache)
|
|
44
|
+
const loaderA = vi.fn(async () => testUser)
|
|
45
|
+
const loaderB = vi.fn(async () => otherUser)
|
|
46
|
+
expect(await cache.resolve('cookie:a', loaderA)).toBe(testUser)
|
|
47
|
+
expect(await cache.resolve('cookie:b', loaderB)).toBe(otherUser)
|
|
48
|
+
expect(await cache.resolve('cookie:a', loaderA)).toBe(testUser)
|
|
49
|
+
expect(loaderA).toHaveBeenCalledTimes(1)
|
|
50
|
+
expect(loaderB).toHaveBeenCalledTimes(1)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('drops the entry once cacheTimeMs has elapsed', async () => {
|
|
55
|
+
await usingAsync(createInjector(), async (i) => {
|
|
56
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
57
|
+
...defaultHttpAuthenticationSettings(),
|
|
58
|
+
userCacheTtlMs: 100,
|
|
59
|
+
}))
|
|
60
|
+
const cache = i.get(UserResolutionCache)
|
|
61
|
+
const loader = vi.fn(async () => testUser)
|
|
62
|
+
await cache.resolve('cookie:ttl', loader)
|
|
63
|
+
await vi.advanceTimersByTimeAsync(150)
|
|
64
|
+
await cache.resolve('cookie:ttl', loader)
|
|
65
|
+
expect(loader).toHaveBeenCalledTimes(2)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('runs the loader on every call when ttl is 0', async () => {
|
|
70
|
+
await usingAsync(createInjector(), async (i) => {
|
|
71
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
72
|
+
...defaultHttpAuthenticationSettings(),
|
|
73
|
+
userCacheTtlMs: 0,
|
|
74
|
+
}))
|
|
75
|
+
const cache = i.get(UserResolutionCache)
|
|
76
|
+
const loader = vi.fn(async () => testUser)
|
|
77
|
+
await cache.resolve('cookie:disabled', loader)
|
|
78
|
+
await cache.resolve('cookie:disabled', loader)
|
|
79
|
+
expect(loader).toHaveBeenCalledTimes(2)
|
|
80
|
+
expect(cache.size).toBe(0)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('dedupes concurrent loaders for the same key', async () => {
|
|
85
|
+
await usingAsync(createInjector(), async (i) => {
|
|
86
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
87
|
+
...defaultHttpAuthenticationSettings(),
|
|
88
|
+
userCacheTtlMs: 30_000,
|
|
89
|
+
}))
|
|
90
|
+
const cache = i.get(UserResolutionCache)
|
|
91
|
+
const loader = vi.fn(
|
|
92
|
+
() =>
|
|
93
|
+
new Promise<User>((resolve) => {
|
|
94
|
+
setTimeout(() => resolve(testUser), 100)
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
const both = Promise.all([cache.resolve('cookie:dedupe', loader), cache.resolve('cookie:dedupe', loader)])
|
|
98
|
+
await vi.advanceTimersByTimeAsync(150)
|
|
99
|
+
const [a, b] = await both
|
|
100
|
+
expect(a).toBe(testUser)
|
|
101
|
+
expect(b).toBe(testUser)
|
|
102
|
+
expect(loader).toHaveBeenCalledTimes(1)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('rejects (without caching) when the loader throws', async () => {
|
|
107
|
+
await usingAsync(createInjector(), async (i) => {
|
|
108
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
109
|
+
...defaultHttpAuthenticationSettings(),
|
|
110
|
+
userCacheTtlMs: 30_000,
|
|
111
|
+
}))
|
|
112
|
+
const cache = i.get(UserResolutionCache)
|
|
113
|
+
const loader = vi
|
|
114
|
+
.fn<() => Promise<User>>()
|
|
115
|
+
.mockRejectedValueOnce(new Error('boom'))
|
|
116
|
+
.mockResolvedValueOnce(testUser)
|
|
117
|
+
await expect(cache.resolve('cookie:err', loader)).rejects.toThrow('boom')
|
|
118
|
+
await expect(cache.resolve('cookie:err', loader)).resolves.toBe(testUser)
|
|
119
|
+
expect(loader).toHaveBeenCalledTimes(2)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('invalidate', () => {
|
|
125
|
+
it('drops the targeted entry only', async () => {
|
|
126
|
+
await usingAsync(createInjector(), async (i) => {
|
|
127
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
128
|
+
...defaultHttpAuthenticationSettings(),
|
|
129
|
+
userCacheTtlMs: 30_000,
|
|
130
|
+
}))
|
|
131
|
+
const cache = i.get(UserResolutionCache)
|
|
132
|
+
const loaderA = vi.fn(async () => testUser)
|
|
133
|
+
const loaderB = vi.fn(async () => otherUser)
|
|
134
|
+
await cache.resolve('cookie:a', loaderA)
|
|
135
|
+
await cache.resolve('cookie:b', loaderB)
|
|
136
|
+
cache.invalidate('cookie:a')
|
|
137
|
+
await cache.resolve('cookie:a', loaderA)
|
|
138
|
+
await cache.resolve('cookie:b', loaderB)
|
|
139
|
+
expect(loaderA).toHaveBeenCalledTimes(2)
|
|
140
|
+
expect(loaderB).toHaveBeenCalledTimes(1)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('invalidateAll', () => {
|
|
146
|
+
it('flushes every entry', async () => {
|
|
147
|
+
await usingAsync(createInjector(), async (i) => {
|
|
148
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
149
|
+
...defaultHttpAuthenticationSettings(),
|
|
150
|
+
userCacheTtlMs: 30_000,
|
|
151
|
+
}))
|
|
152
|
+
const cache = i.get(UserResolutionCache)
|
|
153
|
+
await cache.resolve('cookie:a', async () => testUser)
|
|
154
|
+
await cache.resolve('cookie:b', async () => otherUser)
|
|
155
|
+
expect(cache.size).toBe(2)
|
|
156
|
+
cache.invalidateAll()
|
|
157
|
+
expect(cache.size).toBe(0)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('invalidateByUser', () => {
|
|
163
|
+
it('drops every cached session that resolved to the supplied username', async () => {
|
|
164
|
+
await usingAsync(createInjector(), async (i) => {
|
|
165
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
166
|
+
...defaultHttpAuthenticationSettings(),
|
|
167
|
+
userCacheTtlMs: 30_000,
|
|
168
|
+
}))
|
|
169
|
+
const cache = i.get(UserResolutionCache)
|
|
170
|
+
const aliceLoaderA = vi.fn(async () => testUser)
|
|
171
|
+
const aliceLoaderB = vi.fn(async () => testUser)
|
|
172
|
+
const bobLoader = vi.fn(async () => otherUser)
|
|
173
|
+
await cache.resolve('cookie:alice-1', aliceLoaderA)
|
|
174
|
+
await cache.resolve('cookie:alice-2', aliceLoaderB)
|
|
175
|
+
await cache.resolve('cookie:bob-1', bobLoader)
|
|
176
|
+
expect(cache.size).toBe(3)
|
|
177
|
+
|
|
178
|
+
cache.invalidateByUser('alice')
|
|
179
|
+
|
|
180
|
+
expect(cache.size).toBe(1)
|
|
181
|
+
await cache.resolve('cookie:alice-1', aliceLoaderA)
|
|
182
|
+
await cache.resolve('cookie:alice-2', aliceLoaderB)
|
|
183
|
+
await cache.resolve('cookie:bob-1', bobLoader)
|
|
184
|
+
expect(aliceLoaderA).toHaveBeenCalledTimes(2)
|
|
185
|
+
expect(aliceLoaderB).toHaveBeenCalledTimes(2)
|
|
186
|
+
expect(bobLoader).toHaveBeenCalledTimes(1)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('is a no-op when the username is unknown', async () => {
|
|
191
|
+
await usingAsync(createInjector(), async (i) => {
|
|
192
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
193
|
+
...defaultHttpAuthenticationSettings(),
|
|
194
|
+
userCacheTtlMs: 30_000,
|
|
195
|
+
}))
|
|
196
|
+
const cache = i.get(UserResolutionCache)
|
|
197
|
+
await cache.resolve('cookie:alice-1', async () => testUser)
|
|
198
|
+
await cache.resolve('cookie:bob-1', async () => otherUser)
|
|
199
|
+
cache.invalidateByUser('charlie')
|
|
200
|
+
expect(cache.size).toBe(2)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('is a no-op when caching is disabled (ttl = 0)', async () => {
|
|
205
|
+
await usingAsync(createInjector(), async (i) => {
|
|
206
|
+
i.bind(HttpAuthenticationSettings, () => ({
|
|
207
|
+
...defaultHttpAuthenticationSettings(),
|
|
208
|
+
userCacheTtlMs: 0,
|
|
209
|
+
}))
|
|
210
|
+
const cache = i.get(UserResolutionCache)
|
|
211
|
+
expect(() => cache.invalidateByUser('alice')).not.toThrow()
|
|
212
|
+
expect(cache.size).toBe(0)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe('resolveUserCacheKey', () => {
|
|
219
|
+
it('returns the first non-null key from the configured providers', () => {
|
|
220
|
+
const settings = {
|
|
221
|
+
...defaultHttpAuthenticationSettings(),
|
|
222
|
+
authenticationProviders: [
|
|
223
|
+
{ name: 'first', authenticate: async () => null, getCacheKey: () => null },
|
|
224
|
+
{ name: 'second', authenticate: async () => null, getCacheKey: () => 'cookie:matched' },
|
|
225
|
+
{ name: 'third', authenticate: async () => null, getCacheKey: () => 'jwt:not-reached' },
|
|
226
|
+
],
|
|
227
|
+
}
|
|
228
|
+
expect(resolveUserCacheKey(settings, { headers: {} })).toBe('cookie:matched')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('returns null when every provider opts out', () => {
|
|
232
|
+
const settings = {
|
|
233
|
+
...defaultHttpAuthenticationSettings(),
|
|
234
|
+
authenticationProviders: [
|
|
235
|
+
{ name: 'a', authenticate: async () => null },
|
|
236
|
+
{ name: 'b', authenticate: async () => null, getCacheKey: () => null },
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
expect(resolveUserCacheKey(settings, { headers: {} })).toBeNull()
|
|
240
|
+
})
|
|
241
|
+
})
|