@furystack/rest-service 13.0.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +47 -28
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/is-authenticated.spec.js +10 -6
- package/esm/actions/is-authenticated.spec.js.map +1 -1
- package/esm/actions/logout-action.spec.js +11 -9
- package/esm/actions/logout-action.spec.js.map +1 -1
- package/esm/actions/logout.js +1 -1
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/password-login-action.js +1 -1
- package/esm/actions/password-login-action.js.map +1 -1
- package/esm/actions/password-login-action.spec.js +23 -28
- package/esm/actions/password-login-action.spec.js.map +1 -1
- package/esm/authenticate.js +1 -1
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authorize.spec.js +34 -29
- package/esm/authorize.spec.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
- package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
- package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
- package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
- package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
- package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.js +8 -13
- package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
- package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/utils.d.ts +17 -2
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +33 -20
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/get-schema-from-api.d.ts +1 -1
- package/esm/get-schema-from-api.d.ts.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +53 -87
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +91 -92
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.d.ts +2 -1
- package/esm/helpers.spec.d.ts.map +1 -1
- package/esm/helpers.spec.js +41 -38
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +35 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +21 -29
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-server-pool.d.ts +64 -0
- package/esm/http-server-pool.d.ts.map +1 -0
- package/esm/http-server-pool.js +95 -0
- package/esm/http-server-pool.js.map +1 -0
- package/esm/http-server-pool.spec.d.ts +2 -0
- package/esm/http-server-pool.spec.d.ts.map +1 -0
- package/esm/http-server-pool.spec.js +193 -0
- package/esm/http-server-pool.spec.js.map +1 -0
- package/esm/http-user-context.d.ts +31 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +76 -95
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.d.ts +8 -2
- package/esm/http-user-context.spec.d.ts.map +1 -1
- package/esm/http-user-context.spec.js +189 -337
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/index.d.ts +6 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +6 -4
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +11 -10
- package/esm/login-response-strategy.d.ts.map +1 -1
- package/esm/login-response-strategy.js +7 -6
- package/esm/login-response-strategy.js.map +1 -1
- package/esm/login-response-strategy.spec.js +25 -29
- package/esm/login-response-strategy.spec.js.map +1 -1
- package/esm/proxy-manager.spec.js +152 -176
- package/esm/proxy-manager.spec.js.map +1 -1
- package/esm/proxy-runtime.d.ts +27 -0
- package/esm/proxy-runtime.d.ts.map +1 -0
- package/esm/proxy-runtime.js +45 -0
- package/esm/proxy-runtime.js.map +1 -0
- package/esm/proxy-runtime.spec.d.ts +2 -0
- package/esm/proxy-runtime.spec.d.ts.map +1 -0
- package/esm/proxy-runtime.spec.js +39 -0
- package/esm/proxy-runtime.spec.js.map +1 -0
- package/esm/rest-api-runtime.d.ts +76 -0
- package/esm/rest-api-runtime.d.ts.map +1 -0
- package/esm/rest-api-runtime.js +131 -0
- package/esm/rest-api-runtime.js.map +1 -0
- package/esm/rest-api-runtime.spec.d.ts +2 -0
- package/esm/rest-api-runtime.spec.d.ts.map +1 -0
- package/esm/rest-api-runtime.spec.js +58 -0
- package/esm/rest-api-runtime.spec.js.map +1 -0
- package/esm/rest-service.integration.spec.d.ts +1 -1
- package/esm/rest-service.integration.spec.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +57 -80
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/rest.integration.test.js +26 -84
- package/esm/rest.integration.test.js.map +1 -1
- package/esm/server-telemetry.d.ts +57 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +26 -0
- package/esm/server-telemetry.js.map +1 -0
- package/esm/server-telemetry.spec.d.ts +2 -0
- package/esm/server-telemetry.spec.d.ts.map +1 -0
- package/esm/server-telemetry.spec.js +52 -0
- package/esm/server-telemetry.spec.js.map +1 -0
- package/esm/static-server-manager.spec.js +60 -125
- package/esm/static-server-manager.spec.js.map +1 -1
- package/esm/static-site-runtime.d.ts +21 -0
- package/esm/static-site-runtime.d.ts.map +1 -0
- package/esm/static-site-runtime.js +48 -0
- package/esm/static-site-runtime.js.map +1 -0
- package/esm/static-site-runtime.spec.d.ts +2 -0
- package/esm/static-site-runtime.spec.d.ts.map +1 -0
- package/esm/static-site-runtime.spec.js +30 -0
- package/esm/static-site-runtime.spec.js.map +1 -0
- package/esm/user-store.d.ts +39 -0
- package/esm/user-store.d.ts.map +1 -0
- package/esm/user-store.js +62 -0
- package/esm/user-store.js.map +1 -0
- package/esm/user-store.spec.d.ts +2 -0
- package/esm/user-store.spec.d.ts.map +1 -0
- package/esm/user-store.spec.js +72 -0
- package/esm/user-store.spec.js.map +1 -0
- package/esm/validate.integration.spec.js +26 -28
- package/esm/validate.integration.spec.js.map +1 -1
- package/package.json +8 -8
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +1 -1
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +1 -1
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +1 -1
- package/src/authorize.spec.ts +37 -39
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
- package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
- package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
- package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -2
- package/src/endpoint-generators/create-get-schema-action.ts +1 -1
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
- package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
- package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
- package/src/endpoint-generators/create-post-endpoint.ts +13 -23
- package/src/endpoint-generators/utils.ts +36 -28
- package/src/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +1 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +51 -25
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +203 -383
- package/src/http-user-context.ts +114 -91
- package/src/index.ts +6 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +13 -12
- package/src/proxy-manager.spec.ts +152 -182
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/rest-api-runtime.spec.ts +77 -0
- package/src/rest-api-runtime.ts +204 -0
- package/src/rest-service.integration.spec.ts +65 -85
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +51 -0
- package/src/static-server-manager.spec.ts +63 -151
- package/src/static-site-runtime.spec.ts +39 -0
- package/src/static-site-runtime.ts +75 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +68 -0
- package/src/validate.integration.spec.ts +39 -35
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -179
- package/esm/api-manager.js.map +0 -1
- package/esm/proxy-manager.d.ts +0 -53
- package/esm/proxy-manager.d.ts.map +0 -1
- package/esm/proxy-manager.js +0 -84
- package/esm/proxy-manager.js.map +0 -1
- package/esm/server-manager.d.ts +0 -48
- package/esm/server-manager.d.ts.map +0 -1
- package/esm/server-manager.js +0 -100
- package/esm/server-manager.js.map +0 -1
- package/esm/static-server-manager.d.ts +0 -19
- package/esm/static-server-manager.d.ts.map +0 -1
- package/esm/static-server-manager.js +0 -65
- package/esm/static-server-manager.js.map +0 -1
- package/src/api-manager.ts +0 -304
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- package/src/static-server-manager.ts +0 -90
|
@@ -1,44 +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
15
|
import { GetCurrentUser, IsAuthenticated, LogoutAction, createPasswordLoginAction } from './actions/index.js'
|
|
10
|
-
import './helpers.js'
|
|
11
16
|
import { useHttpAuthentication, useRestService } from './helpers.js'
|
|
17
|
+
import { HttpServerPoolToken } from './http-server-pool.js'
|
|
12
18
|
import { createCookieLoginStrategy } from './login-response-strategy.js'
|
|
13
19
|
import { DefaultSession } from './models/default-session.js'
|
|
14
|
-
import { ServerManager } from './server-manager.js'
|
|
15
20
|
import { JsonResult } from './request-action-implementation.js'
|
|
21
|
+
import { ServerTelemetryToken } from './server-telemetry.js'
|
|
22
|
+
import { SessionStore, UserStore } from './user-store.js'
|
|
16
23
|
|
|
17
24
|
interface IntegrationTestApi extends RestApi {
|
|
18
25
|
GET: {
|
|
19
26
|
'/isAuthenticated': { result: { isAuthenticated: boolean } }
|
|
20
|
-
'/currentUser': { result:
|
|
27
|
+
'/currentUser': { result: UserModel }
|
|
21
28
|
'/testQuery': { query: { param1: string }; result: { param1Value: string } }
|
|
22
29
|
'/testUrlParams/:urlParam': { url: { urlParam: string }; result: { urlParamValue: string } }
|
|
23
30
|
}
|
|
24
31
|
POST: {
|
|
25
|
-
'/login': { result:
|
|
32
|
+
'/login': { result: UserModel; body: { username: string; password: string } }
|
|
26
33
|
'/logout': { result: unknown }
|
|
27
34
|
'/testPostBody': { body: { value: string }; result: { bodyValue: string } }
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const createIntegrationApi = async () => {
|
|
32
|
-
const i =
|
|
39
|
+
const i = createInjector()
|
|
33
40
|
const port = getPort()
|
|
34
41
|
const root = 'test-api'
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
|
42
49
|
useHttpAuthentication(i)
|
|
43
50
|
await useRestService<IntegrationTestApi>({
|
|
44
51
|
injector: i,
|
|
@@ -71,20 +78,19 @@ const createIntegrationApi = async () => {
|
|
|
71
78
|
return {
|
|
72
79
|
apiUrl: `http://127.0.0.1:${port}/${root}`,
|
|
73
80
|
port,
|
|
81
|
+
injector: i,
|
|
74
82
|
[Symbol.asyncDispose]: i[Symbol.asyncDispose].bind(i),
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
|
|
78
|
-
describe('@furystack/rest-service
|
|
79
|
-
it('
|
|
86
|
+
describe('@furystack/rest-service integration tests', () => {
|
|
87
|
+
it('starts and disposes cleanly', async () => {
|
|
80
88
|
await usingAsync(await createIntegrationApi(), async () => {})
|
|
81
89
|
})
|
|
82
90
|
|
|
83
|
-
it('
|
|
91
|
+
it('responds 404 for unknown routes', async () => {
|
|
84
92
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
85
|
-
const result = await fetch(PathHelper.joinPaths(apiUrl, 'some-route-that-does-not-
|
|
86
|
-
method: 'GET',
|
|
87
|
-
})
|
|
93
|
+
const result = await fetch(PathHelper.joinPaths(apiUrl, 'some-route-that-does-not-exist'))
|
|
88
94
|
expect(result.ok).toBe(false)
|
|
89
95
|
expect(result.status).toBe(404)
|
|
90
96
|
const response = (await result.json()) as { error: string }
|
|
@@ -92,7 +98,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
92
98
|
})
|
|
93
99
|
})
|
|
94
100
|
|
|
95
|
-
it('
|
|
101
|
+
it('responds 401 for authenticated routes hit anonymously', async () => {
|
|
96
102
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
97
103
|
const result = await fetch(PathHelper.joinPaths(apiUrl, 'currentUser'))
|
|
98
104
|
expect(result.ok).toBe(false)
|
|
@@ -102,17 +108,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
102
108
|
})
|
|
103
109
|
})
|
|
104
110
|
|
|
105
|
-
it('
|
|
106
|
-
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
107
|
-
const result = await fetch(PathHelper.joinPaths(apiUrl, 'currentUser'))
|
|
108
|
-
expect(result.ok).toBe(false)
|
|
109
|
-
expect(result.status).toBe(401)
|
|
110
|
-
const response = (await result.json()) as { error: string }
|
|
111
|
-
expect(response).toEqual({ error: 'unauthorized' })
|
|
112
|
-
})
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('Should respond with the correct result body', async () => {
|
|
111
|
+
it('returns JSON bodies from async actions', async () => {
|
|
116
112
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
117
113
|
const response = await fetch(PathHelper.joinPaths(apiUrl, 'isAuthenticated'))
|
|
118
114
|
expect(response.status).toBe(200)
|
|
@@ -121,9 +117,8 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
121
117
|
})
|
|
122
118
|
})
|
|
123
119
|
|
|
124
|
-
it('
|
|
120
|
+
it('deserialises query parameters', async () => {
|
|
125
121
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
126
|
-
console.log('apiUrl', apiUrl)
|
|
127
122
|
const response = await fetch(PathHelper.joinPaths(apiUrl, `testQuery?param1=${serializeValue('foo')}`))
|
|
128
123
|
expect(response.status).toBe(200)
|
|
129
124
|
const result = (await response.json()) as { param1Value: string }
|
|
@@ -131,7 +126,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
131
126
|
})
|
|
132
127
|
})
|
|
133
128
|
|
|
134
|
-
it('
|
|
129
|
+
it('extracts url parameters', async () => {
|
|
135
130
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
136
131
|
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testUrlParams/bar'))
|
|
137
132
|
expect(response.status).toBe(200)
|
|
@@ -140,7 +135,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
140
135
|
})
|
|
141
136
|
})
|
|
142
137
|
|
|
143
|
-
it('
|
|
138
|
+
it('reads the POST body once and memoises subsequent reads', async () => {
|
|
144
139
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
145
140
|
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testPostBody'), {
|
|
146
141
|
method: 'POST',
|
|
@@ -152,19 +147,14 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
152
147
|
})
|
|
153
148
|
})
|
|
154
149
|
|
|
155
|
-
it('
|
|
150
|
+
it('responds 200 to OPTIONS preflight requests', async () => {
|
|
156
151
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
157
|
-
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testPostBody'), {
|
|
158
|
-
method: 'OPTIONS',
|
|
159
|
-
}).catch((e) => {
|
|
160
|
-
console.log(e)
|
|
161
|
-
throw e
|
|
162
|
-
})
|
|
152
|
+
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testPostBody'), { method: 'OPTIONS' })
|
|
163
153
|
expect(response.status).toBe(200)
|
|
164
154
|
})
|
|
165
155
|
})
|
|
166
156
|
|
|
167
|
-
it('
|
|
157
|
+
it('rejects requests outside the API root with a dropped socket', async () => {
|
|
168
158
|
await usingAsync(await createIntegrationApi(), async ({ port }) => {
|
|
169
159
|
await expect(fetch(PathHelper.joinPaths(`http://127.0.0.1:${port}`, 'not-my-api-root'))).rejects.toThrowError(
|
|
170
160
|
'fetch failed',
|
|
@@ -172,7 +162,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
172
162
|
})
|
|
173
163
|
})
|
|
174
164
|
|
|
175
|
-
it('
|
|
165
|
+
it('responds 400 for malformed query-parameter values', async () => {
|
|
176
166
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
177
167
|
const response = await fetch(PathHelper.joinPaths(apiUrl, 'testQuery?param1=not-valid-base64!!!'))
|
|
178
168
|
expect(response.status).toBe(400)
|
|
@@ -181,7 +171,7 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
181
171
|
})
|
|
182
172
|
})
|
|
183
173
|
|
|
184
|
-
it('
|
|
174
|
+
it('responds 400 for malformed percent-encoded path parameters', async () => {
|
|
185
175
|
await usingAsync(await createIntegrationApi(), async ({ apiUrl }) => {
|
|
186
176
|
const response = await fetch(`${apiUrl}/testUrlParams/%E0%A4%A`)
|
|
187
177
|
expect(response.status).toBe(400)
|
|
@@ -191,68 +181,58 @@ describe('@furystack/rest-service inregration tests', () => {
|
|
|
191
181
|
})
|
|
192
182
|
})
|
|
193
183
|
|
|
194
|
-
describe('
|
|
195
|
-
it('
|
|
196
|
-
await usingAsync(
|
|
197
|
-
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)
|
|
198
188
|
const handler = vi.fn()
|
|
199
|
-
|
|
189
|
+
telemetry.addListener('onServerListening', handler)
|
|
200
190
|
|
|
201
191
|
const port = getPort()
|
|
202
|
-
await
|
|
192
|
+
await i.get(HttpServerPoolToken).acquire({ port })
|
|
203
193
|
|
|
204
194
|
expect(handler).toHaveBeenCalledTimes(1)
|
|
205
|
-
expect(handler).toHaveBeenCalledWith({
|
|
206
|
-
url: `http://localhost:${port}`,
|
|
207
|
-
port,
|
|
208
|
-
hostName: undefined,
|
|
209
|
-
})
|
|
195
|
+
expect(handler).toHaveBeenCalledWith({ url: `http://localhost:${port}`, port, hostName: undefined })
|
|
210
196
|
})
|
|
211
197
|
})
|
|
212
198
|
|
|
213
|
-
it('
|
|
214
|
-
await usingAsync(
|
|
215
|
-
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)
|
|
216
202
|
const handler = vi.fn()
|
|
217
|
-
|
|
203
|
+
telemetry.addListener('onServerListening', handler)
|
|
218
204
|
|
|
219
205
|
const port = getPort()
|
|
220
|
-
await
|
|
206
|
+
await i.get(HttpServerPoolToken).acquire({ port, hostName: '127.0.0.1' })
|
|
221
207
|
|
|
222
|
-
expect(handler).toHaveBeenCalledWith({
|
|
223
|
-
url: `http://127.0.0.1:${port}`,
|
|
224
|
-
port,
|
|
225
|
-
hostName: '127.0.0.1',
|
|
226
|
-
})
|
|
208
|
+
expect(handler).toHaveBeenCalledWith({ url: `http://127.0.0.1:${port}`, port, hostName: '127.0.0.1' })
|
|
227
209
|
})
|
|
228
210
|
})
|
|
229
211
|
|
|
230
|
-
it('
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const port = getPort()
|
|
237
|
-
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)
|
|
238
217
|
|
|
239
|
-
|
|
218
|
+
const port = getPort()
|
|
219
|
+
await i.get(HttpServerPoolToken).acquire({ port })
|
|
220
|
+
await i[Symbol.asyncDispose]()
|
|
240
221
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
})
|
|
222
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
223
|
+
expect(handler).toHaveBeenCalledWith({ url: `http://localhost:${port}` })
|
|
244
224
|
})
|
|
245
225
|
|
|
246
|
-
it('
|
|
247
|
-
await usingAsync(
|
|
248
|
-
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) => {
|
|
249
228
|
const port = getPort()
|
|
250
|
-
await
|
|
229
|
+
await i.get(HttpServerPoolToken).acquire({ port })
|
|
251
230
|
|
|
231
|
+
const telemetry = i.get(ServerTelemetryToken)
|
|
252
232
|
const handler = vi.fn()
|
|
253
|
-
|
|
233
|
+
telemetry.addListener('onServerListening', handler)
|
|
254
234
|
|
|
255
|
-
await
|
|
235
|
+
await i.get(HttpServerPoolToken).acquire({ port })
|
|
256
236
|
|
|
257
237
|
expect(handler).not.toHaveBeenCalled()
|
|
258
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
|
+
})
|