@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.
Files changed (224) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +47 -28
  3. package/esm/actions/get-current-user.spec.js +10 -6
  4. package/esm/actions/get-current-user.spec.js.map +1 -1
  5. package/esm/actions/is-authenticated.spec.js +10 -6
  6. package/esm/actions/is-authenticated.spec.js.map +1 -1
  7. package/esm/actions/logout-action.spec.js +11 -9
  8. package/esm/actions/logout-action.spec.js.map +1 -1
  9. package/esm/actions/logout.js +1 -1
  10. package/esm/actions/logout.js.map +1 -1
  11. package/esm/actions/password-login-action.js +1 -1
  12. package/esm/actions/password-login-action.js.map +1 -1
  13. package/esm/actions/password-login-action.spec.js +23 -28
  14. package/esm/actions/password-login-action.spec.js.map +1 -1
  15. package/esm/authenticate.js +1 -1
  16. package/esm/authenticate.js.map +1 -1
  17. package/esm/authenticate.spec.js +41 -33
  18. package/esm/authenticate.spec.js.map +1 -1
  19. package/esm/authorize.spec.js +34 -29
  20. package/esm/authorize.spec.js.map +1 -1
  21. package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
  22. package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
  23. package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
  24. package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
  25. package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
  26. package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
  27. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
  28. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
  29. package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
  30. package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
  31. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
  32. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
  33. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
  34. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
  35. package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
  36. package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
  37. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
  38. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
  39. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -1
  40. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
  41. package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -1
  42. package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
  43. package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
  44. package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
  45. package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
  46. package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
  47. package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
  48. package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
  49. package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
  50. package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
  51. package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
  52. package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
  53. package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
  54. package/esm/endpoint-generators/create-post-endpoint.js +8 -13
  55. package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
  56. package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
  57. package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
  58. package/esm/endpoint-generators/utils.d.ts +17 -2
  59. package/esm/endpoint-generators/utils.d.ts.map +1 -1
  60. package/esm/endpoint-generators/utils.js +33 -20
  61. package/esm/endpoint-generators/utils.js.map +1 -1
  62. package/esm/get-schema-from-api.d.ts +1 -1
  63. package/esm/get-schema-from-api.d.ts.map +1 -1
  64. package/esm/get-schema-from-api.spec.js.map +1 -1
  65. package/esm/helpers.d.ts +53 -87
  66. package/esm/helpers.d.ts.map +1 -1
  67. package/esm/helpers.js +91 -92
  68. package/esm/helpers.js.map +1 -1
  69. package/esm/helpers.spec.d.ts +2 -1
  70. package/esm/helpers.spec.d.ts.map +1 -1
  71. package/esm/helpers.spec.js +41 -38
  72. package/esm/helpers.spec.js.map +1 -1
  73. package/esm/http-authentication-settings.d.ts +35 -12
  74. package/esm/http-authentication-settings.d.ts.map +1 -1
  75. package/esm/http-authentication-settings.js +21 -29
  76. package/esm/http-authentication-settings.js.map +1 -1
  77. package/esm/http-server-pool.d.ts +64 -0
  78. package/esm/http-server-pool.d.ts.map +1 -0
  79. package/esm/http-server-pool.js +95 -0
  80. package/esm/http-server-pool.js.map +1 -0
  81. package/esm/http-server-pool.spec.d.ts +2 -0
  82. package/esm/http-server-pool.spec.d.ts.map +1 -0
  83. package/esm/http-server-pool.spec.js +193 -0
  84. package/esm/http-server-pool.spec.js.map +1 -0
  85. package/esm/http-user-context.d.ts +31 -44
  86. package/esm/http-user-context.d.ts.map +1 -1
  87. package/esm/http-user-context.js +76 -95
  88. package/esm/http-user-context.js.map +1 -1
  89. package/esm/http-user-context.spec.d.ts +8 -2
  90. package/esm/http-user-context.spec.d.ts.map +1 -1
  91. package/esm/http-user-context.spec.js +189 -337
  92. package/esm/http-user-context.spec.js.map +1 -1
  93. package/esm/index.d.ts +6 -4
  94. package/esm/index.d.ts.map +1 -1
  95. package/esm/index.js +6 -4
  96. package/esm/index.js.map +1 -1
  97. package/esm/login-response-strategy.d.ts +11 -10
  98. package/esm/login-response-strategy.d.ts.map +1 -1
  99. package/esm/login-response-strategy.js +7 -6
  100. package/esm/login-response-strategy.js.map +1 -1
  101. package/esm/login-response-strategy.spec.js +25 -29
  102. package/esm/login-response-strategy.spec.js.map +1 -1
  103. package/esm/proxy-manager.spec.js +152 -176
  104. package/esm/proxy-manager.spec.js.map +1 -1
  105. package/esm/proxy-runtime.d.ts +27 -0
  106. package/esm/proxy-runtime.d.ts.map +1 -0
  107. package/esm/proxy-runtime.js +45 -0
  108. package/esm/proxy-runtime.js.map +1 -0
  109. package/esm/proxy-runtime.spec.d.ts +2 -0
  110. package/esm/proxy-runtime.spec.d.ts.map +1 -0
  111. package/esm/proxy-runtime.spec.js +39 -0
  112. package/esm/proxy-runtime.spec.js.map +1 -0
  113. package/esm/rest-api-runtime.d.ts +76 -0
  114. package/esm/rest-api-runtime.d.ts.map +1 -0
  115. package/esm/rest-api-runtime.js +131 -0
  116. package/esm/rest-api-runtime.js.map +1 -0
  117. package/esm/rest-api-runtime.spec.d.ts +2 -0
  118. package/esm/rest-api-runtime.spec.d.ts.map +1 -0
  119. package/esm/rest-api-runtime.spec.js +58 -0
  120. package/esm/rest-api-runtime.spec.js.map +1 -0
  121. package/esm/rest-service.integration.spec.d.ts +1 -1
  122. package/esm/rest-service.integration.spec.d.ts.map +1 -1
  123. package/esm/rest-service.integration.spec.js +57 -80
  124. package/esm/rest-service.integration.spec.js.map +1 -1
  125. package/esm/rest.integration.test.js +26 -84
  126. package/esm/rest.integration.test.js.map +1 -1
  127. package/esm/server-telemetry.d.ts +57 -0
  128. package/esm/server-telemetry.d.ts.map +1 -0
  129. package/esm/server-telemetry.js +26 -0
  130. package/esm/server-telemetry.js.map +1 -0
  131. package/esm/server-telemetry.spec.d.ts +2 -0
  132. package/esm/server-telemetry.spec.d.ts.map +1 -0
  133. package/esm/server-telemetry.spec.js +52 -0
  134. package/esm/server-telemetry.spec.js.map +1 -0
  135. package/esm/static-server-manager.spec.js +60 -125
  136. package/esm/static-server-manager.spec.js.map +1 -1
  137. package/esm/static-site-runtime.d.ts +21 -0
  138. package/esm/static-site-runtime.d.ts.map +1 -0
  139. package/esm/static-site-runtime.js +48 -0
  140. package/esm/static-site-runtime.js.map +1 -0
  141. package/esm/static-site-runtime.spec.d.ts +2 -0
  142. package/esm/static-site-runtime.spec.d.ts.map +1 -0
  143. package/esm/static-site-runtime.spec.js +30 -0
  144. package/esm/static-site-runtime.spec.js.map +1 -0
  145. package/esm/user-store.d.ts +39 -0
  146. package/esm/user-store.d.ts.map +1 -0
  147. package/esm/user-store.js +62 -0
  148. package/esm/user-store.js.map +1 -0
  149. package/esm/user-store.spec.d.ts +2 -0
  150. package/esm/user-store.spec.d.ts.map +1 -0
  151. package/esm/user-store.spec.js +72 -0
  152. package/esm/user-store.spec.js.map +1 -0
  153. package/esm/validate.integration.spec.js +26 -28
  154. package/esm/validate.integration.spec.js.map +1 -1
  155. package/package.json +8 -8
  156. package/src/actions/get-current-user.spec.ts +11 -10
  157. package/src/actions/is-authenticated.spec.ts +12 -7
  158. package/src/actions/logout-action.spec.ts +12 -12
  159. package/src/actions/logout.ts +1 -1
  160. package/src/actions/password-login-action.spec.ts +30 -30
  161. package/src/actions/password-login-action.ts +1 -1
  162. package/src/authenticate.spec.ts +45 -51
  163. package/src/authenticate.ts +1 -1
  164. package/src/authorize.spec.ts +37 -39
  165. package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
  166. package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
  167. package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
  168. package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
  169. package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
  170. package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
  171. package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -2
  172. package/src/endpoint-generators/create-get-schema-action.ts +1 -1
  173. package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
  174. package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
  175. package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
  176. package/src/endpoint-generators/create-post-endpoint.ts +13 -23
  177. package/src/endpoint-generators/utils.ts +36 -28
  178. package/src/get-schema-from-api.spec.ts +1 -1
  179. package/src/get-schema-from-api.ts +1 -1
  180. package/src/helpers.spec.ts +51 -41
  181. package/src/helpers.ts +140 -109
  182. package/src/http-authentication-settings.ts +51 -25
  183. package/src/http-server-pool.spec.ts +220 -0
  184. package/src/http-server-pool.ts +169 -0
  185. package/src/http-user-context.spec.ts +203 -383
  186. package/src/http-user-context.ts +114 -91
  187. package/src/index.ts +6 -4
  188. package/src/login-response-strategy.spec.ts +32 -31
  189. package/src/login-response-strategy.ts +13 -12
  190. package/src/proxy-manager.spec.ts +152 -182
  191. package/src/proxy-runtime.spec.ts +50 -0
  192. package/src/proxy-runtime.ts +75 -0
  193. package/src/rest-api-runtime.spec.ts +77 -0
  194. package/src/rest-api-runtime.ts +204 -0
  195. package/src/rest-service.integration.spec.ts +65 -85
  196. package/src/rest.integration.test.ts +27 -87
  197. package/src/server-telemetry.spec.ts +56 -0
  198. package/src/server-telemetry.ts +51 -0
  199. package/src/static-server-manager.spec.ts +63 -151
  200. package/src/static-site-runtime.spec.ts +39 -0
  201. package/src/static-site-runtime.ts +75 -0
  202. package/src/user-store.spec.ts +86 -0
  203. package/src/user-store.ts +68 -0
  204. package/src/validate.integration.spec.ts +39 -35
  205. package/esm/api-manager.d.ts +0 -104
  206. package/esm/api-manager.d.ts.map +0 -1
  207. package/esm/api-manager.js +0 -179
  208. package/esm/api-manager.js.map +0 -1
  209. package/esm/proxy-manager.d.ts +0 -53
  210. package/esm/proxy-manager.d.ts.map +0 -1
  211. package/esm/proxy-manager.js +0 -84
  212. package/esm/proxy-manager.js.map +0 -1
  213. package/esm/server-manager.d.ts +0 -48
  214. package/esm/server-manager.d.ts.map +0 -1
  215. package/esm/server-manager.js +0 -100
  216. package/esm/server-manager.js.map +0 -1
  217. package/esm/static-server-manager.d.ts +0 -19
  218. package/esm/static-server-manager.d.ts.map +0 -1
  219. package/esm/static-server-manager.js +0 -65
  220. package/esm/static-server-manager.js.map +0 -1
  221. package/src/api-manager.ts +0 -304
  222. package/src/proxy-manager.ts +0 -102
  223. package/src/server-manager.ts +0 -135
  224. package/src/static-server-manager.ts +0 -90
@@ -1,44 +1,51 @@
1
- import { InMemoryStore, User, addStore } from '@furystack/core'
1
+ import { InMemoryStore, User as UserModel } from '@furystack/core'
2
2
  import { getPort } from '@furystack/core/port-generator'
3
- import { Injector } from '@furystack/inject'
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: User }
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: User; body: { username: string; password: string } }
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 = new Injector()
39
+ const i = createInjector()
33
40
  const port = getPort()
34
41
  const root = 'test-api'
35
42
 
36
- addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' })).addStore(
37
- new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }),
38
- )
39
- const repo = getRepository(i)
40
- repo.createDataSet(User, 'username')
41
- repo.createDataSet(DefaultSession, 'sessionId')
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 inregration tests', () => {
79
- it('Should be started and disposed', async () => {
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('Should respond with 404 when a route is not found', async () => {
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-exists'), {
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('Should respond with 401 for unauthorized request errors', async () => {
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('Should respond with 401 for unauthorized request errors', async () => {
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('Should be able to read query parameters', async () => {
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('Should be able to read url parameters', async () => {
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('Should be able to read post body', async () => {
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('Should respond with OK to OPTIONS requests', async () => {
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('Should reject requests outside of the API Root', async () => {
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('Should respond with 400 for malformed query parameter values', async () => {
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('Should respond with 400 for malformed percent-encoded path parameters', async () => {
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('ServerManager events', () => {
195
- it('Should emit onServerListening when a server is created', async () => {
196
- await usingAsync(new Injector(), async (i) => {
197
- const serverManager = i.getInstance(ServerManager)
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
- serverManager.addListener('onServerListening', handler)
189
+ telemetry.addListener('onServerListening', handler)
200
190
 
201
191
  const port = getPort()
202
- await serverManager.getOrCreate({ port })
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('Should emit onServerListening with hostName when provided', async () => {
214
- await usingAsync(new Injector(), async (i) => {
215
- const serverManager = i.getInstance(ServerManager)
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
- serverManager.addListener('onServerListening', handler)
203
+ telemetry.addListener('onServerListening', handler)
218
204
 
219
205
  const port = getPort()
220
- await serverManager.getOrCreate({ port, hostName: '127.0.0.1' })
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('Should emit onServerClosed when the server manager is disposed', async () => {
231
- await usingAsync(new Injector(), async (i) => {
232
- const serverManager = i.getInstance(ServerManager)
233
- const handler = vi.fn()
234
- serverManager.addListener('onServerClosed', handler)
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
- await serverManager[Symbol.asyncDispose]()
218
+ const port = getPort()
219
+ await i.get(HttpServerPoolToken).acquire({ port })
220
+ await i[Symbol.asyncDispose]()
240
221
 
241
- expect(handler).toHaveBeenCalledTimes(1)
242
- expect(handler).toHaveBeenCalledWith({ url: `http://localhost:${port}` })
243
- })
222
+ expect(handler).toHaveBeenCalledTimes(1)
223
+ expect(handler).toHaveBeenCalledWith({ url: `http://localhost:${port}` })
244
224
  })
245
225
 
246
- it('Should not emit onServerListening for an existing server', async () => {
247
- await usingAsync(new Injector(), async (i) => {
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 serverManager.getOrCreate({ port })
229
+ await i.get(HttpServerPoolToken).acquire({ port })
251
230
 
231
+ const telemetry = i.get(ServerTelemetryToken)
252
232
  const handler = vi.fn()
253
- serverManager.addListener('onServerListening', handler)
233
+ telemetry.addListener('onServerListening', handler)
254
234
 
255
- await serverManager.getOrCreate({ port })
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 { Injector } from '@furystack/inject'
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 = new 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('Should execute a single parameterless GET query', async () => {
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('Should execute a request with headers', async () => {
78
+ it('passes headers through', async () => {
82
79
  await usingAsync(await createEchoApiServer(), async ({ client }) => {
83
- const value = 'value'
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).toEqual(value)
82
+ expect(result.result.headers.value).toBe('hello')
93
83
  })
94
84
  })
95
85
 
96
- it('Should execute a request with query', async () => {
86
+ it('serialises url params', async () => {
97
87
  await usingAsync(await createEchoApiServer(), async ({ client }) => {
98
- const value = 'value2'
99
- const result = await client({
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('should execute a request for a segment', async () => {
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: '/segment',
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.response.status).toBe(200)
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('should execute a request for a subsegment with URL parameters', async () => {
104
+ it('dispatches plain and multi-segment URLs correctly', async () => {
151
105
  await usingAsync(await createEchoApiServer(), async ({ client }) => {
152
- const value = 'value4'
153
- const result = await client({
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.response.status).toBe(200)
161
- expect(result.result.url.id).toEqual(value)
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
- it('should evaluate optional parameters in the URL', async () => {
117
+
118
+ it('forwards POST bodies', async () => {
166
119
  await usingAsync(await createEchoApiServer(), async ({ client }) => {
167
120
  const result = await client({
168
- method: 'GET',
169
- action: '/segment{/:optionalId}?/optionalSubsegment/',
170
- url: {},
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(result2.response.status).toBe(200)
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
+ })