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