@furystack/rest-service 13.0.0 → 14.1.0

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