@furystack/rest-service 12.3.5 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +47 -28
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/index.d.ts +0 -1
- package/esm/actions/index.d.ts.map +1 -1
- package/esm/actions/index.js +0 -1
- package/esm/actions/index.js.map +1 -1
- package/esm/actions/is-authenticated.spec.js +10 -6
- package/esm/actions/is-authenticated.spec.js.map +1 -1
- package/esm/actions/logout-action.spec.js +11 -9
- package/esm/actions/logout-action.spec.js.map +1 -1
- package/esm/actions/logout.js +1 -1
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/password-login-action.js +1 -1
- package/esm/actions/password-login-action.js.map +1 -1
- package/esm/actions/password-login-action.spec.js +23 -28
- package/esm/actions/password-login-action.spec.js.map +1 -1
- package/esm/authenticate.js +1 -1
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authorize.spec.js +34 -29
- package/esm/authorize.spec.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
- package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
- package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
- package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -15
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -24
- package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
- package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
- package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.js +8 -13
- package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
- package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/utils.d.ts +17 -2
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +33 -20
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts +0 -2
- package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts.map +1 -1
- package/esm/get-schema-from-api.d.ts +1 -1
- package/esm/get-schema-from-api.d.ts.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +53 -87
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +91 -92
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.d.ts +2 -1
- package/esm/helpers.spec.d.ts.map +1 -1
- package/esm/helpers.spec.js +41 -38
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +35 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +21 -29
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-server-pool.d.ts +64 -0
- package/esm/http-server-pool.d.ts.map +1 -0
- package/esm/http-server-pool.js +95 -0
- package/esm/http-server-pool.js.map +1 -0
- package/esm/http-server-pool.spec.d.ts +2 -0
- package/esm/http-server-pool.spec.d.ts.map +1 -0
- package/esm/http-server-pool.spec.js +193 -0
- package/esm/http-server-pool.spec.js.map +1 -0
- package/esm/http-user-context.d.ts +31 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +76 -95
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.d.ts +8 -2
- package/esm/http-user-context.spec.d.ts.map +1 -1
- package/esm/http-user-context.spec.js +189 -337
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/index.d.ts +6 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +6 -4
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +11 -10
- package/esm/login-response-strategy.d.ts.map +1 -1
- package/esm/login-response-strategy.js +7 -6
- package/esm/login-response-strategy.js.map +1 -1
- package/esm/login-response-strategy.spec.js +25 -29
- package/esm/login-response-strategy.spec.js.map +1 -1
- package/esm/openapi/generate-openapi-document.d.ts +0 -8
- package/esm/openapi/generate-openapi-document.d.ts.map +1 -1
- package/esm/openapi/generate-openapi-document.js +0 -2
- package/esm/openapi/generate-openapi-document.js.map +1 -1
- package/esm/proxy-manager.spec.js +152 -176
- package/esm/proxy-manager.spec.js.map +1 -1
- package/esm/proxy-runtime.d.ts +27 -0
- package/esm/proxy-runtime.d.ts.map +1 -0
- package/esm/proxy-runtime.js +45 -0
- package/esm/proxy-runtime.js.map +1 -0
- package/esm/proxy-runtime.spec.d.ts +2 -0
- package/esm/proxy-runtime.spec.d.ts.map +1 -0
- package/esm/proxy-runtime.spec.js +39 -0
- package/esm/proxy-runtime.spec.js.map +1 -0
- package/esm/rest-api-runtime.d.ts +76 -0
- package/esm/rest-api-runtime.d.ts.map +1 -0
- package/esm/rest-api-runtime.js +131 -0
- package/esm/rest-api-runtime.js.map +1 -0
- package/esm/rest-api-runtime.spec.d.ts +2 -0
- package/esm/rest-api-runtime.spec.d.ts.map +1 -0
- package/esm/rest-api-runtime.spec.js +58 -0
- package/esm/rest-api-runtime.spec.js.map +1 -0
- package/esm/rest-service.integration.spec.d.ts +1 -1
- package/esm/rest-service.integration.spec.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +60 -82
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/rest.integration.test.js +26 -84
- package/esm/rest.integration.test.js.map +1 -1
- package/esm/server-telemetry.d.ts +57 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +26 -0
- package/esm/server-telemetry.js.map +1 -0
- package/esm/server-telemetry.spec.d.ts +2 -0
- package/esm/server-telemetry.spec.d.ts.map +1 -0
- package/esm/server-telemetry.spec.js +52 -0
- package/esm/server-telemetry.spec.js.map +1 -0
- package/esm/static-server-manager.spec.js +60 -125
- package/esm/static-server-manager.spec.js.map +1 -1
- package/esm/static-site-runtime.d.ts +21 -0
- package/esm/static-site-runtime.d.ts.map +1 -0
- package/esm/static-site-runtime.js +48 -0
- package/esm/static-site-runtime.js.map +1 -0
- package/esm/static-site-runtime.spec.d.ts +2 -0
- package/esm/static-site-runtime.spec.d.ts.map +1 -0
- package/esm/static-site-runtime.spec.js +30 -0
- package/esm/static-site-runtime.spec.js.map +1 -0
- package/esm/user-store.d.ts +39 -0
- package/esm/user-store.d.ts.map +1 -0
- package/esm/user-store.js +62 -0
- package/esm/user-store.js.map +1 -0
- package/esm/user-store.spec.d.ts +2 -0
- package/esm/user-store.spec.d.ts.map +1 -0
- package/esm/user-store.spec.js +72 -0
- package/esm/user-store.spec.js.map +1 -0
- package/esm/validate.integration.spec.js +26 -83
- package/esm/validate.integration.spec.js.map +1 -1
- package/package.json +8 -8
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/index.ts +0 -1
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +1 -1
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +1 -1
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +1 -1
- package/src/authorize.spec.ts +37 -39
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
- package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
- package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
- package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -38
- package/src/endpoint-generators/create-get-schema-action.ts +1 -1
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
- package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
- package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
- package/src/endpoint-generators/create-post-endpoint.ts +13 -23
- package/src/endpoint-generators/utils.ts +36 -28
- package/src/endpoint-generators/with-schema-and-openapi-action.ts +0 -3
- package/src/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +1 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +51 -25
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +203 -383
- package/src/http-user-context.ts +114 -91
- package/src/index.ts +6 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +13 -12
- package/src/openapi/generate-openapi-document.ts +0 -3
- package/src/proxy-manager.spec.ts +152 -182
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/rest-api-runtime.spec.ts +77 -0
- package/src/rest-api-runtime.ts +204 -0
- package/src/rest-service.integration.spec.ts +68 -87
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +51 -0
- package/src/static-server-manager.spec.ts +63 -151
- package/src/static-site-runtime.spec.ts +39 -0
- package/src/static-site-runtime.ts +75 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +68 -0
- package/src/validate.integration.spec.ts +39 -97
- package/esm/actions/login-action.spec.d.ts +0 -2
- package/esm/actions/login-action.spec.d.ts.map +0 -1
- package/esm/actions/login-action.spec.js +0 -34
- package/esm/actions/login-action.spec.js.map +0 -1
- package/esm/actions/login.d.ts +0 -19
- package/esm/actions/login.d.ts.map +0 -1
- package/esm/actions/login.js +0 -27
- package/esm/actions/login.js.map +0 -1
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -180
- package/esm/api-manager.js.map +0 -1
- package/esm/proxy-manager.d.ts +0 -53
- package/esm/proxy-manager.d.ts.map +0 -1
- package/esm/proxy-manager.js +0 -84
- package/esm/proxy-manager.js.map +0 -1
- package/esm/server-manager.d.ts +0 -48
- package/esm/server-manager.d.ts.map +0 -1
- package/esm/server-manager.js +0 -100
- package/esm/server-manager.js.map +0 -1
- package/esm/static-server-manager.d.ts +0 -19
- package/esm/static-server-manager.d.ts.map +0 -1
- package/esm/static-server-manager.js +0 -65
- package/esm/static-server-manager.js.map +0 -1
- package/esm/swagger/generate-swagger-json.spec.d.ts +0 -2
- package/esm/swagger/generate-swagger-json.spec.d.ts.map +0 -1
- package/esm/swagger/generate-swagger-json.spec.js +0 -215
- package/esm/swagger/generate-swagger-json.spec.js.map +0 -1
- package/src/actions/login-action.spec.ts +0 -42
- package/src/actions/login.ts +0 -32
- package/src/api-manager.ts +0 -308
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- package/src/static-server-manager.ts +0 -90
- package/src/swagger/generate-swagger-json.spec.ts +0 -250
|
@@ -1,43 +1,51 @@
|
|
|
1
|
-
import { InMemoryStore, User
|
|
1
|
+
import { InMemoryStore, User as UserModel } from '@furystack/core'
|
|
2
2
|
import { getPort } from '@furystack/core/port-generator'
|
|
3
|
-
import {
|
|
4
|
-
import { getRepository } from '@furystack/repository'
|
|
3
|
+
import { createInjector } from '@furystack/inject'
|
|
5
4
|
import type { RestApi } from '@furystack/rest'
|
|
6
5
|
import { serializeValue } from '@furystack/rest'
|
|
6
|
+
import {
|
|
7
|
+
PasswordCredential,
|
|
8
|
+
PasswordCredentialStore,
|
|
9
|
+
PasswordResetToken,
|
|
10
|
+
PasswordResetTokenStore,
|
|
11
|
+
usePasswordPolicy,
|
|
12
|
+
} from '@furystack/security'
|
|
7
13
|
import { PathHelper, usingAsync } from '@furystack/utils'
|
|
8
14
|
import { describe, expect, it, vi } from 'vitest'
|
|
9
|
-
import { GetCurrentUser, IsAuthenticated,
|
|
10
|
-
import './helpers.js'
|
|
15
|
+
import { GetCurrentUser, IsAuthenticated, LogoutAction, createPasswordLoginAction } from './actions/index.js'
|
|
11
16
|
import { useHttpAuthentication, useRestService } from './helpers.js'
|
|
17
|
+
import { HttpServerPoolToken } from './http-server-pool.js'
|
|
18
|
+
import { createCookieLoginStrategy } from './login-response-strategy.js'
|
|
12
19
|
import { DefaultSession } from './models/default-session.js'
|
|
13
|
-
import { ServerManager } from './server-manager.js'
|
|
14
20
|
import { JsonResult } from './request-action-implementation.js'
|
|
21
|
+
import { ServerTelemetryToken } from './server-telemetry.js'
|
|
22
|
+
import { SessionStore, UserStore } from './user-store.js'
|
|
15
23
|
|
|
16
24
|
interface IntegrationTestApi extends RestApi {
|
|
17
25
|
GET: {
|
|
18
26
|
'/isAuthenticated': { result: { isAuthenticated: boolean } }
|
|
19
|
-
'/currentUser': { result:
|
|
27
|
+
'/currentUser': { result: UserModel }
|
|
20
28
|
'/testQuery': { query: { param1: string }; result: { param1Value: string } }
|
|
21
29
|
'/testUrlParams/:urlParam': { url: { urlParam: string }; result: { urlParamValue: string } }
|
|
22
30
|
}
|
|
23
31
|
POST: {
|
|
24
|
-
'/login': { result:
|
|
32
|
+
'/login': { result: UserModel; body: { username: string; password: string } }
|
|
25
33
|
'/logout': { result: unknown }
|
|
26
34
|
'/testPostBody': { body: { value: string }; result: { bodyValue: string } }
|
|
27
35
|
}
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
const createIntegrationApi = async () => {
|
|
31
|
-
const i =
|
|
39
|
+
const i = createInjector()
|
|
32
40
|
const port = getPort()
|
|
33
41
|
const root = 'test-api'
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
i.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }))
|
|
44
|
+
i.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
45
|
+
i.bind(PasswordCredentialStore, () => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
|
|
46
|
+
i.bind(PasswordResetTokenStore, () => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
|
|
47
|
+
|
|
48
|
+
usePasswordPolicy(i)
|
|
41
49
|
useHttpAuthentication(i)
|
|
42
50
|
await useRestService<IntegrationTestApi>({
|
|
43
51
|
injector: i,
|
|
@@ -56,7 +64,7 @@ const createIntegrationApi = async () => {
|
|
|
56
64
|
'/testUrlParams/:urlParam': async (options) => JsonResult({ urlParamValue: options.getUrlParams().urlParam }),
|
|
57
65
|
},
|
|
58
66
|
POST: {
|
|
59
|
-
'/login':
|
|
67
|
+
'/login': createPasswordLoginAction(createCookieLoginStrategy(i)),
|
|
60
68
|
'/logout': LogoutAction,
|
|
61
69
|
'/testPostBody': async (options) => {
|
|
62
70
|
const body = await options.getBody()
|
|
@@ -70,20 +78,19 @@ const createIntegrationApi = async () => {
|
|
|
70
78
|
return {
|
|
71
79
|
apiUrl: `http://127.0.0.1:${port}/${root}`,
|
|
72
80
|
port,
|
|
81
|
+
injector: i,
|
|
73
82
|
[Symbol.asyncDispose]: i[Symbol.asyncDispose].bind(i),
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
describe('@furystack/rest-service
|
|
78
|
-
it('
|
|
86
|
+
describe('@furystack/rest-service integration tests', () => {
|
|
87
|
+
it('starts and disposes cleanly', async () => {
|
|
79
88
|
await usingAsync(await createIntegrationApi(), async () => {})
|
|
80
89
|
})
|
|
81
90
|
|
|
82
|
-
it('
|
|
91
|
+
it('responds 404 for unknown routes', async () => {
|
|
83
92
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
84
|
-
const result = await fetch(PathHelper.joinPaths(apiUrl, 'some-route-that-does-not-
|
|
85
|
-
method: 'GET',
|
|
86
|
-
})
|
|
93
|
+
const result = await fetch(PathHelper.joinPaths(apiUrl, 'some-route-that-does-not-exist'))
|
|
87
94
|
expect(result.ok).toBe(false)
|
|
88
95
|
expect(result.status).toBe(404)
|
|
89
96
|
const response = (await result.json()) as { error: string }
|
|
@@ -91,7 +98,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
91
98
|
})
|
|
92
99
|
})
|
|
93
100
|
|
|
94
|
-
it('
|
|
101
|
+
it('responds 401 for authenticated routes hit anonymously', async () => {
|
|
95
102
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
96
103
|
const result = await fetch(PathHelper.joinPaths(apiUrl, 'currentUser'))
|
|
97
104
|
expect(result.ok).toBe(false)
|
|
@@ -101,17 +108,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
101
108
|
})
|
|
102
109
|
})
|
|
103
110
|
|
|
104
|
-
it('
|
|
105
|
-
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
106
|
-
const result = await fetch(PathHelper.joinPaths(apiUrl, 'currentUser'))
|
|
107
|
-
expect(result.ok).toBe(false)
|
|
108
|
-
expect(result.status).toBe(401)
|
|
109
|
-
const response = (await result.json()) as { error: string }
|
|
110
|
-
expect(response).toEqual({ error: 'unauthorized' })
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('Should respond with the correct result body', async () => {
|
|
111
|
+
it('returns JSON bodies from async actions', async () => {
|
|
115
112
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
116
113
|
const response = await fetch(PathHelper.joinPaths(apiUrl, 'isAuthenticated'))
|
|
117
114
|
expect(response.status).toBe(200)
|
|
@@ -120,9 +117,8 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
120
117
|
})
|
|
121
118
|
})
|
|
122
119
|
|
|
123
|
-
it('
|
|
120
|
+
it('deserialises query parameters', async () => {
|
|
124
121
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
125
|
-
console.log('apiUrl', apiUrl)
|
|
126
122
|
const response = await fetch(PathHelper.joinPaths(apiUrl, `testQuery?param1=${serializeValue('foo')}`))
|
|
127
123
|
expect(response.status).toBe(200)
|
|
128
124
|
const result = (await response.json()) as { param1Value: string }
|
|
@@ -130,7 +126,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
130
126
|
})
|
|
131
127
|
})
|
|
132
128
|
|
|
133
|
-
it('
|
|
129
|
+
it('extracts url parameters', async () => {
|
|
134
130
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
135
131
|
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testUrlParams/bar'))
|
|
136
132
|
expect(response.status).toBe(200)
|
|
@@ -139,7 +135,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
139
135
|
})
|
|
140
136
|
})
|
|
141
137
|
|
|
142
|
-
it('
|
|
138
|
+
it('reads the POST body once and memoises subsequent reads', async () => {
|
|
143
139
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
144
140
|
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testPostBody'), {
|
|
145
141
|
method: 'POST',
|
|
@@ -151,19 +147,14 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
151
147
|
})
|
|
152
148
|
})
|
|
153
149
|
|
|
154
|
-
it('
|
|
150
|
+
it('responds 200 to OPTIONS preflight requests', async () => {
|
|
155
151
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
156
|
-
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testPostBody'), {
|
|
157
|
-
method: 'OPTIONS',
|
|
158
|
-
}).catch((e) => {
|
|
159
|
-
console.log(e)
|
|
160
|
-
throw e
|
|
161
|
-
})
|
|
152
|
+
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testPostBody'), { method: 'OPTIONS' })
|
|
162
153
|
expect(response.status).toBe(200)
|
|
163
154
|
})
|
|
164
155
|
})
|
|
165
156
|
|
|
166
|
-
it('
|
|
157
|
+
it('rejects requests outside the API root with a dropped socket', async () => {
|
|
167
158
|
await usingAsync(await createIntegrationApi(), async ({ port }) => {
|
|
168
159
|
await expect(fetch(PathHelper.joinPaths(`http://127.0.0.1:${port}`, 'not-my-api-root'))).rejects.toThrowError(
|
|
169
160
|
'fetch failed',
|
|
@@ -171,7 +162,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
171
162
|
})
|
|
172
163
|
})
|
|
173
164
|
|
|
174
|
-
it('
|
|
165
|
+
it('responds 400 for malformed query-parameter values', async () => {
|
|
175
166
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
176
167
|
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testQuery?param1=not-valid-base64!!!'))
|
|
177
168
|
expect(response.status).toBe(400)
|
|
@@ -180,7 +171,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
180
171
|
})
|
|
181
172
|
})
|
|
182
173
|
|
|
183
|
-
it('
|
|
174
|
+
it('responds 400 for malformed percent-encoded path parameters', async () => {
|
|
184
175
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
185
176
|
const response = await fetch(`${apiUrl}/testUrlParams/%E0%A4%A`)
|
|
186
177
|
expect(response.status).toBe(400)
|
|
@@ -190,68 +181,58 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
190
181
|
})
|
|
191
182
|
})
|
|
192
183
|
|
|
193
|
-
describe('
|
|
194
|
-
it('
|
|
195
|
-
await usingAsync(
|
|
196
|
-
const
|
|
184
|
+
describe('HttpServerPool telemetry', () => {
|
|
185
|
+
it('emits onServerListening with the url / port / host for newly-opened servers', async () => {
|
|
186
|
+
await usingAsync(createInjector(), async (i) => {
|
|
187
|
+
const telemetry = i.get(ServerTelemetryToken)
|
|
197
188
|
const handler = vi.fn()
|
|
198
|
-
|
|
189
|
+
telemetry.addListener('onServerListening', handler)
|
|
199
190
|
|
|
200
191
|
const port = getPort()
|
|
201
|
-
await
|
|
192
|
+
await i.get(HttpServerPoolToken).acquire({ port })
|
|
202
193
|
|
|
203
194
|
expect(handler).toHaveBeenCalledTimes(1)
|
|
204
|
-
expect(handler).toHaveBeenCalledWith({
|
|
205
|
-
url: `http://localhost:${port}`,
|
|
206
|
-
port,
|
|
207
|
-
hostName: undefined,
|
|
208
|
-
})
|
|
195
|
+
expect(handler).toHaveBeenCalledWith({ url: `http://localhost:${port}`, port, hostName: undefined })
|
|
209
196
|
})
|
|
210
197
|
})
|
|
211
198
|
|
|
212
|
-
it('
|
|
213
|
-
await usingAsync(
|
|
214
|
-
const
|
|
199
|
+
it('forwards the configured host name into the emitted event', async () => {
|
|
200
|
+
await usingAsync(createInjector(), async (i) => {
|
|
201
|
+
const telemetry = i.get(ServerTelemetryToken)
|
|
215
202
|
const handler = vi.fn()
|
|
216
|
-
|
|
203
|
+
telemetry.addListener('onServerListening', handler)
|
|
217
204
|
|
|
218
205
|
const port = getPort()
|
|
219
|
-
await
|
|
206
|
+
await i.get(HttpServerPoolToken).acquire({ port, hostName: '127.0.0.1' })
|
|
220
207
|
|
|
221
|
-
expect(handler).toHaveBeenCalledWith({
|
|
222
|
-
url: `http://127.0.0.1:${port}`,
|
|
223
|
-
port,
|
|
224
|
-
hostName: '127.0.0.1',
|
|
225
|
-
})
|
|
208
|
+
expect(handler).toHaveBeenCalledWith({ url: `http://127.0.0.1:${port}`, port, hostName: '127.0.0.1' })
|
|
226
209
|
})
|
|
227
210
|
})
|
|
228
211
|
|
|
229
|
-
it('
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const port = getPort()
|
|
236
|
-
await serverManager.getOrCreate({ port })
|
|
212
|
+
it('emits onServerClosed when the pool is disposed', async () => {
|
|
213
|
+
const i = createInjector()
|
|
214
|
+
const telemetry = i.get(ServerTelemetryToken)
|
|
215
|
+
const handler = vi.fn()
|
|
216
|
+
telemetry.addListener('onServerClosed', handler)
|
|
237
217
|
|
|
238
|
-
|
|
218
|
+
const port = getPort()
|
|
219
|
+
await i.get(HttpServerPoolToken).acquire({ port })
|
|
220
|
+
await i[Symbol.asyncDispose]()
|
|
239
221
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
})
|
|
222
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
223
|
+
expect(handler).toHaveBeenCalledWith({ url: `http://localhost:${port}` })
|
|
243
224
|
})
|
|
244
225
|
|
|
245
|
-
it('
|
|
246
|
-
await usingAsync(
|
|
247
|
-
const serverManager = i.getInstance(ServerManager)
|
|
226
|
+
it('does not emit onServerListening a second time for the same url', async () => {
|
|
227
|
+
await usingAsync(createInjector(), async (i) => {
|
|
248
228
|
const port = getPort()
|
|
249
|
-
await
|
|
229
|
+
await i.get(HttpServerPoolToken).acquire({ port })
|
|
250
230
|
|
|
231
|
+
const telemetry = i.get(ServerTelemetryToken)
|
|
251
232
|
const handler = vi.fn()
|
|
252
|
-
|
|
233
|
+
telemetry.addListener('onServerListening', handler)
|
|
253
234
|
|
|
254
|
-
await
|
|
235
|
+
await i.get(HttpServerPoolToken).acquire({ port })
|
|
255
236
|
|
|
256
237
|
expect(handler).not.toHaveBeenCalled()
|
|
257
238
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getPort } from '@furystack/core/port-generator'
|
|
2
|
-
import {
|
|
2
|
+
import { createInjector } from '@furystack/inject'
|
|
3
3
|
import type { RestApi } from '@furystack/rest'
|
|
4
4
|
import { createClient } from '@furystack/rest-client-fetch'
|
|
5
5
|
import { usingAsync } from '@furystack/utils'
|
|
@@ -32,7 +32,7 @@ export interface EchoApi extends RestApi {
|
|
|
32
32
|
const createEchoApiServer = async () => {
|
|
33
33
|
const port = getPort()
|
|
34
34
|
const root = '/api'
|
|
35
|
-
const injector =
|
|
35
|
+
const injector = createInjector()
|
|
36
36
|
await useRestService<EchoApi>({
|
|
37
37
|
injector,
|
|
38
38
|
port,
|
|
@@ -67,122 +67,62 @@ const createEchoApiServer = async () => {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
describe('REST Integration tests with FETCH client', () => {
|
|
70
|
-
it('
|
|
70
|
+
it('executes a parameterless GET request', async () => {
|
|
71
71
|
await usingAsync(await createEchoApiServer(), async ({ client }) => {
|
|
72
|
-
const result = await client({
|
|
73
|
-
method: 'GET',
|
|
74
|
-
action: '/plain',
|
|
75
|
-
})
|
|
72
|
+
const result = await client({ method: 'GET', action: '/plain' })
|
|
76
73
|
expect(result.response.status).toBe(200)
|
|
77
74
|
expect(result.result).toEqual({})
|
|
78
75
|
})
|
|
79
76
|
})
|
|
80
77
|
|
|
81
|
-
it('
|
|
78
|
+
it('passes headers through', async () => {
|
|
82
79
|
await usingAsync(await createEchoApiServer(), async ({ client }) => {
|
|
83
|
-
const
|
|
84
|
-
const result = await client({
|
|
85
|
-
method: 'GET',
|
|
86
|
-
action: '/headers',
|
|
87
|
-
headers: {
|
|
88
|
-
value,
|
|
89
|
-
},
|
|
90
|
-
})
|
|
80
|
+
const result = await client({ method: 'GET', action: '/headers', headers: { value: 'hello' } })
|
|
91
81
|
expect(result.response.status).toBe(200)
|
|
92
|
-
expect(result.result.headers.value).
|
|
82
|
+
expect(result.result.headers.value).toBe('hello')
|
|
93
83
|
})
|
|
94
84
|
})
|
|
95
85
|
|
|
96
|
-
it('
|
|
86
|
+
it('serialises url params', async () => {
|
|
97
87
|
await usingAsync(await createEchoApiServer(), async ({ client }) => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
method: 'GET',
|
|
101
|
-
action: '/query',
|
|
102
|
-
query: {
|
|
103
|
-
someObject: {
|
|
104
|
-
foo: value,
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
})
|
|
108
|
-
expect(result.response.status).toBe(200)
|
|
109
|
-
expect(result.result.query.someObject.foo).toEqual(value)
|
|
110
|
-
})
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('Should execute a request with URL parameters', async () => {
|
|
114
|
-
await usingAsync(await createEchoApiServer(), async ({ client }) => {
|
|
115
|
-
const value = 'value3'
|
|
116
|
-
const result = await client({
|
|
117
|
-
method: 'GET',
|
|
118
|
-
action: '/urlParams/:id',
|
|
119
|
-
url: {
|
|
120
|
-
id: value,
|
|
121
|
-
},
|
|
122
|
-
})
|
|
123
|
-
expect(result.response.status).toBe(200)
|
|
124
|
-
expect(result.result.url.id).toEqual(value)
|
|
88
|
+
const result = await client({ method: 'GET', action: '/urlParams/:id', url: { id: '42' } })
|
|
89
|
+
expect(result.result.url.id).toBe('42')
|
|
125
90
|
})
|
|
126
91
|
})
|
|
127
92
|
|
|
128
|
-
it('
|
|
93
|
+
it('serialises a structured query', async () => {
|
|
129
94
|
await usingAsync(await createEchoApiServer(), async ({ client }) => {
|
|
130
95
|
const result = await client({
|
|
131
96
|
method: 'GET',
|
|
132
|
-
action: '/
|
|
133
|
-
|
|
134
|
-
expect(result.response.status).toBe(200)
|
|
135
|
-
expect(result.result.name).toEqual('segment')
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('should execute a request for a subsegment', async () => {
|
|
140
|
-
await usingAsync(await createEchoApiServer(), async ({ client }) => {
|
|
141
|
-
const result = await client({
|
|
142
|
-
method: 'GET',
|
|
143
|
-
action: '/segment/subsegment',
|
|
97
|
+
action: '/query',
|
|
98
|
+
query: { someObject: { foo: 'bar' } },
|
|
144
99
|
})
|
|
145
|
-
expect(result.
|
|
146
|
-
expect(result.result.name).toEqual('segment-subsegment')
|
|
100
|
+
expect(result.result.query.someObject.foo).toBe('bar')
|
|
147
101
|
})
|
|
148
102
|
})
|
|
149
103
|
|
|
150
|
-
it('
|
|
104
|
+
it('dispatches plain and multi-segment URLs correctly', async () => {
|
|
151
105
|
await usingAsync(await createEchoApiServer(), async ({ client }) => {
|
|
152
|
-
|
|
153
|
-
|
|
106
|
+
expect((await client({ method: 'GET', action: '/segment' })).result.name).toBe('segment')
|
|
107
|
+
expect((await client({ method: 'GET', action: '/segment/subsegment' })).result.name).toBe('segment-subsegment')
|
|
108
|
+
const subsegment = await client({
|
|
154
109
|
method: 'GET',
|
|
155
110
|
action: '/segment/:id/subsegment',
|
|
156
|
-
url: {
|
|
157
|
-
id: value,
|
|
158
|
-
},
|
|
111
|
+
url: { id: 'abc' },
|
|
159
112
|
})
|
|
160
|
-
expect(result.
|
|
161
|
-
expect(
|
|
162
|
-
expect(result.result.url.name).toEqual('segment-subsegment')
|
|
113
|
+
expect(subsegment.result.url.id).toBe('abc')
|
|
114
|
+
expect(subsegment.result.url.name).toBe('segment-subsegment')
|
|
163
115
|
})
|
|
164
116
|
})
|
|
165
|
-
|
|
117
|
+
|
|
118
|
+
it('forwards POST bodies', async () => {
|
|
166
119
|
await usingAsync(await createEchoApiServer(), async ({ client }) => {
|
|
167
120
|
const result = await client({
|
|
168
|
-
method: '
|
|
169
|
-
action: '/
|
|
170
|
-
|
|
171
|
-
})
|
|
172
|
-
expect(result.response.status).toBe(200)
|
|
173
|
-
expect(result.result.url.optionalId).toBeUndefined()
|
|
174
|
-
expect(result.result.name).toEqual('optional-id')
|
|
175
|
-
|
|
176
|
-
const result2 = await client({
|
|
177
|
-
method: 'GET',
|
|
178
|
-
action: '/segment{/:optionalId}?/optionalSubsegment/',
|
|
179
|
-
url: {
|
|
180
|
-
optionalId: 'value',
|
|
181
|
-
},
|
|
121
|
+
method: 'POST',
|
|
122
|
+
action: '/body',
|
|
123
|
+
body: { foo: 'hi', bar: 42 },
|
|
182
124
|
})
|
|
183
|
-
expect(
|
|
184
|
-
expect(result2.result.url.optionalId).toEqual('value')
|
|
185
|
-
expect(result2.result.name).toEqual('optional-id')
|
|
125
|
+
expect(result.result.body).toEqual({ foo: 'hi', bar: 42 })
|
|
186
126
|
})
|
|
187
127
|
})
|
|
188
128
|
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createInjector } from '@furystack/inject'
|
|
2
|
+
import { usingAsync } from '@furystack/utils'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { ServerTelemetry, ServerTelemetryToken } from './server-telemetry.js'
|
|
5
|
+
|
|
6
|
+
describe('ServerTelemetry', () => {
|
|
7
|
+
it('ServerTelemetryToken resolves to a ServerTelemetry EventHub and is cached per scope', async () => {
|
|
8
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
9
|
+
const a = injector.get(ServerTelemetryToken)
|
|
10
|
+
const b = injector.get(ServerTelemetryToken)
|
|
11
|
+
expect(a).toBe(b)
|
|
12
|
+
expect(a).toBeInstanceOf(ServerTelemetry)
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('Emitted events are dispatched to subscribers', async () => {
|
|
17
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
18
|
+
const telemetry = injector.get(ServerTelemetryToken)
|
|
19
|
+
const onListening = vi.fn()
|
|
20
|
+
telemetry.subscribe('onServerListening', onListening)
|
|
21
|
+
telemetry.emit('onServerListening', { url: 'http://localhost:1', port: 1 })
|
|
22
|
+
expect(onListening).toHaveBeenCalledWith({ url: 'http://localhost:1', port: 1 })
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('Disposes cleanly with the injector and stops routing events', async () => {
|
|
27
|
+
const injector = createInjector()
|
|
28
|
+
const telemetry = injector.get(ServerTelemetryToken)
|
|
29
|
+
const onClosed = vi.fn()
|
|
30
|
+
telemetry.subscribe('onServerClosed', onClosed)
|
|
31
|
+
|
|
32
|
+
await injector[Symbol.asyncDispose]()
|
|
33
|
+
|
|
34
|
+
// After the owning injector is disposed, emit on the captured reference
|
|
35
|
+
// should still work (the EventHub is disposed separately via onDispose,
|
|
36
|
+
// but listeners are cleared). The key guarantee: previously-attached
|
|
37
|
+
// listeners receive nothing new after dispose.
|
|
38
|
+
telemetry.emit('onServerClosed', { url: 'http://localhost:2' })
|
|
39
|
+
expect(onClosed).not.toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('Scopes have isolated telemetry instances', async () => {
|
|
43
|
+
await usingAsync(createInjector(), async (root) => {
|
|
44
|
+
const scope1 = root.createScope()
|
|
45
|
+
const scope2 = root.createScope()
|
|
46
|
+
try {
|
|
47
|
+
const t1 = scope1.get(ServerTelemetryToken)
|
|
48
|
+
const t2 = scope2.get(ServerTelemetryToken)
|
|
49
|
+
expect(t1).not.toBe(t2)
|
|
50
|
+
} finally {
|
|
51
|
+
await scope1[Symbol.asyncDispose]()
|
|
52
|
+
await scope2[Symbol.asyncDispose]()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineService, type Token } from '@furystack/inject'
|
|
2
|
+
import { EventHub, type ListenerErrorPayload } from '@furystack/utils'
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from 'http'
|
|
4
|
+
import type { Data, WebSocket } from 'ws'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Union of every server-lifecycle event the rest-service runtime emits. Replaces
|
|
8
|
+
* the `EventHub`s that used to live on the deleted `ServerManager`,
|
|
9
|
+
* `ProxyManager` and `WebSocketApi` classes — consumers subscribe once to the
|
|
10
|
+
* telemetry singleton and receive events from every source.
|
|
11
|
+
*/
|
|
12
|
+
export type ServerTelemetryEvents = {
|
|
13
|
+
/** Emitted when the pool successfully opens an HTTP listener. */
|
|
14
|
+
onServerListening: { url: string; port: number; hostName?: string }
|
|
15
|
+
/** Emitted when the pool closes an HTTP listener during disposal. */
|
|
16
|
+
onServerClosed: { url: string }
|
|
17
|
+
/** Emitted when an API's `onRequest` rejects. Tuple matches the original `ServerManager` payload. */
|
|
18
|
+
onRequestFailed: [unknown, IncomingMessage, ServerResponse<IncomingMessage>]
|
|
19
|
+
/** Emitted when an HTTP proxy round-trip fails. */
|
|
20
|
+
onProxyFailed: { from: string; to: string; error: unknown }
|
|
21
|
+
/** Emitted when a WebSocket proxy upgrade fails. */
|
|
22
|
+
onWebSocketProxyFailed: { from: string; to: string; error: unknown }
|
|
23
|
+
/** Emitted when a WebSocket API action fails during lookup or execution. */
|
|
24
|
+
onWebSocketActionFailed: { error: unknown; data?: Data; socket?: WebSocket }
|
|
25
|
+
/** Emitted when a listener registered on this telemetry hub throws. */
|
|
26
|
+
onListenerError: ListenerErrorPayload
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Telemetry surface exposed to applications. Extending {@link EventHub}
|
|
31
|
+
* gives subscribers the same `addListener` / `subscribe` / `emit` API they
|
|
32
|
+
* were using on the pre-migration managers.
|
|
33
|
+
*/
|
|
34
|
+
export class ServerTelemetry extends EventHub<ServerTelemetryEvents> {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* DI token for the shared {@link ServerTelemetry} instance. Scoped so each
|
|
38
|
+
* injector tree (in particular, each test) gets an isolated event stream.
|
|
39
|
+
*/
|
|
40
|
+
export const ServerTelemetryToken: Token<ServerTelemetry, 'scoped'> = defineService({
|
|
41
|
+
name: 'furystack/rest-service/ServerTelemetry',
|
|
42
|
+
lifetime: 'scoped',
|
|
43
|
+
factory: ({ onDispose }) => {
|
|
44
|
+
const telemetry = new ServerTelemetry()
|
|
45
|
+
// Disposal is delegated to the injector via `onDispose`; the telemetry
|
|
46
|
+
// outlives this factory invocation and is torn down on scope teardown.
|
|
47
|
+
// eslint-disable-next-line furystack/prefer-using-wrapper
|
|
48
|
+
onDispose(() => telemetry[Symbol.dispose]())
|
|
49
|
+
return telemetry
|
|
50
|
+
},
|
|
51
|
+
})
|