@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,284 +1,237 @@
1
- /* eslint-disable @typescript-eslint/ban-ts-comment */
2
- import { InMemoryStore, StoreManager, User, addStore, useSystemIdentityContext } from '@furystack/core'
3
- import { Injector } from '@furystack/inject'
4
- import { getDataSetFor, getRepository } from '@furystack/repository'
1
+ import type { User } from '@furystack/core'
2
+ import { InMemoryStore, User as UserModel, useSystemIdentityContext } from '@furystack/core'
3
+ import { createInjector, type Injector } from '@furystack/inject'
5
4
  import {
6
5
  PasswordAuthenticator,
7
6
  PasswordCredential,
7
+ PasswordCredentialStore,
8
8
  PasswordResetToken,
9
+ PasswordResetTokenStore,
9
10
  UnauthenticatedError,
10
11
  usePasswordPolicy,
11
12
  } from '@furystack/security'
12
13
  import { usingAsync } from '@furystack/utils'
13
- import type { IncomingMessage, ServerResponse } from 'http'
14
+ import type { IncomingMessage } from 'http'
14
15
  import { describe, expect, it, vi } from 'vitest'
15
16
  import { useHttpAuthentication } from './helpers.js'
16
17
  import { HttpUserContext } from './http-user-context.js'
17
18
  import { DefaultSession } from './models/default-session.js'
18
-
19
- export const prepareInjector = async (i: Injector, options?: { enableBasicAuth?: boolean }) => {
20
- addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' }))
21
- .addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
22
- .addStore(new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
23
- .addStore(new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
24
-
25
- const repo = getRepository(i)
26
- repo.createDataSet(User, 'username')
27
- repo.createDataSet(DefaultSession, 'sessionId')
28
- repo.createDataSet(PasswordCredential, 'userName')
29
- repo.createDataSet(PasswordResetToken, 'token')
19
+ import { SessionDataSet, SessionStore, UserStore } from './user-store.js'
20
+
21
+ /**
22
+ * Binds in-memory implementations for every persistent token required by
23
+ * the authentication subsystem (users, sessions, credentials, reset tokens)
24
+ * on the provided injector, then installs the default password policy and
25
+ * HTTP-authentication stack.
26
+ */
27
+ export const prepareInjector = (i: Injector, options?: { enableBasicAuth?: boolean }): void => {
28
+ i.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }))
29
+ i.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
30
+ i.bind(PasswordCredentialStore, () => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
31
+ i.bind(PasswordResetTokenStore, () => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
30
32
 
31
33
  usePasswordPolicy(i)
32
34
  useHttpAuthentication(i, { enableBasicAuth: options?.enableBasicAuth ?? true })
33
35
  }
34
36
 
35
- const setupUser = async (i: Injector, userName: string, password: string) => {
36
- const sm = i.getInstance(StoreManager)
37
- const pw = i.getInstance(PasswordAuthenticator)
38
- const cred = await pw.hasher.createCredential(userName, password)
39
- await sm.getStoreFor(PasswordCredential, 'userName').add(cred)
40
- await sm.getStoreFor(User, 'username').add({ username: userName, roles: [] })
37
+ const setupUser = async (i: Injector, userName: string, password: string): Promise<void> => {
38
+ const authenticator = i.get(PasswordAuthenticator)
39
+ const credential = await authenticator.hasher.createCredential(userName, password)
40
+ await i.get(PasswordCredentialStore).add(credential)
41
+ await i.get(UserStore).add({ username: userName, roles: [] })
41
42
  }
42
43
 
43
44
  describe('HttpUserContext', () => {
44
45
  const request = { headers: {} } as IncomingMessage
45
- const response = {} as any as ServerResponse
46
-
47
46
  const testUser: User = { username: 'testUser', roles: ['grantedRole1', 'grantedRole2'] }
48
47
 
49
- it('Should be constructed with the extension method', async () => {
50
- await usingAsync(new Injector(), async (i) => {
51
- await prepareInjector(i)
52
- const ctx = i.getInstance(HttpUserContext)
53
- expect(ctx).toBeInstanceOf(HttpUserContext)
48
+ it('resolves to an HttpUserContext via the DI token', async () => {
49
+ await usingAsync(createInjector(), async (i) => {
50
+ prepareInjector(i)
51
+ const ctx = i.get(HttpUserContext)
52
+ expect(ctx).toBeDefined()
53
+ expect(ctx.authentication).toBeDefined()
54
54
  })
55
55
  })
56
56
 
57
57
  describe('isAuthenticated', () => {
58
- it('Should return true for authenticated users', async () => {
59
- await usingAsync(new Injector(), async (i) => {
60
- await prepareInjector(i)
61
- const ctx = i.getInstance(HttpUserContext)
58
+ it('returns true when the cached user is present', async () => {
59
+ await usingAsync(createInjector(), async (i) => {
60
+ prepareInjector(i)
61
+ const ctx = i.get(HttpUserContext)
62
62
  ctx.getCurrentUser = vi.fn(async () => testUser)
63
- const value = await ctx.isAuthenticated(request)
64
- expect(value).toBe(true)
65
- expect(ctx.getCurrentUser).toBeCalled()
63
+ expect(await ctx.isAuthenticated(request)).toBe(true)
64
+ expect(ctx.getCurrentUser).toHaveBeenCalled()
66
65
  })
67
66
  })
68
67
 
69
- it('Should return false for unauthenticated users', async () => {
70
- await usingAsync(new Injector(), async (i) => {
71
- await prepareInjector(i)
72
- const ctx = i.getInstance(HttpUserContext)
68
+ it('returns false when getCurrentUser rejects', async () => {
69
+ await usingAsync(createInjector(), async (i) => {
70
+ prepareInjector(i)
71
+ const ctx = i.get(HttpUserContext)
73
72
  ctx.getCurrentUser = vi.fn(async () => {
74
- throw Error(':(')
73
+ throw new Error(':(')
75
74
  })
76
- await expect(ctx.isAuthenticated(request)).resolves.toEqual(false)
77
- expect(ctx.getCurrentUser).toBeCalled()
75
+ expect(await ctx.isAuthenticated(request)).toBe(false)
78
76
  })
79
77
  })
80
78
  })
81
79
 
82
80
  describe('isAuthorized', () => {
83
- it('Should return true if all roles are authorized', async () => {
84
- await usingAsync(new Injector(), async (i) => {
85
- await prepareInjector(i)
86
- const ctx = i.getInstance(HttpUserContext)
81
+ it('returns true when the current user has every requested role', async () => {
82
+ await usingAsync(createInjector(), async (i) => {
83
+ prepareInjector(i)
84
+ const ctx = i.get(HttpUserContext)
87
85
  ctx.getCurrentUser = vi.fn(async () => testUser)
88
- const value = await ctx.isAuthorized(request, 'grantedRole1', 'grantedRole2')
89
- expect(value).toBe(true)
90
- expect(ctx.getCurrentUser).toBeCalled()
86
+ expect(await ctx.isAuthorized(request, 'grantedRole1', 'grantedRole2')).toBe(true)
91
87
  })
92
88
  })
93
89
 
94
- it('Should return false if not all roles are authorized', async () => {
95
- await usingAsync(new Injector(), async (i) => {
96
- await prepareInjector(i)
97
- const ctx = i.getInstance(HttpUserContext)
90
+ it('returns false when any requested role is missing', async () => {
91
+ await usingAsync(createInjector(), async (i) => {
92
+ prepareInjector(i)
93
+ const ctx = i.get(HttpUserContext)
98
94
  ctx.getCurrentUser = vi.fn(async () => testUser)
99
- const value = await ctx.isAuthorized(request, 'grantedRole1', 'nonGrantedRole2')
100
- expect(value).toBe(false)
101
- expect(ctx.getCurrentUser).toBeCalled()
95
+ expect(await ctx.isAuthorized(request, 'grantedRole1', 'otherRole')).toBe(false)
102
96
  })
103
97
  })
104
98
  })
105
99
 
106
100
  describe('authenticateUser', () => {
107
- it('Should fail when the store is empty', async () => {
108
- await usingAsync(new Injector(), async (i) => {
109
- await prepareInjector(i)
110
- const ctx = i.getInstance(HttpUserContext)
111
- await expect(ctx.authenticateUser('user', 'password')).rejects.toThrowError(UnauthenticatedError)
112
- })
113
- })
114
-
115
- it('Should fail when the password not equals', async () => {
116
- await usingAsync(new Injector(), async (i) => {
117
- await prepareInjector(i)
118
- await setupUser(i, 'user', 'pass123')
119
- await expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass321')).rejects.toThrowError(
101
+ it('throws UnauthenticatedError when the credential store is empty', async () => {
102
+ await usingAsync(createInjector(), async (i) => {
103
+ prepareInjector(i)
104
+ await expect(i.get(HttpUserContext).authenticateUser('user', 'password')).rejects.toBeInstanceOf(
120
105
  UnauthenticatedError,
121
106
  )
122
107
  })
123
108
  })
124
109
 
125
- it('Should fail when the username not equals', async () => {
126
- await usingAsync(new Injector(), async (i) => {
127
- await prepareInjector(i)
128
- await setupUser(i, 'otherUser', 'pass123')
129
- await expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toThrowError(
110
+ it('throws UnauthenticatedError when the password does not match', async () => {
111
+ await usingAsync(createInjector(), async (i) => {
112
+ prepareInjector(i)
113
+ await setupUser(i, 'user', 'pass123')
114
+ await expect(i.get(HttpUserContext).authenticateUser('user', 'wrong')).rejects.toBeInstanceOf(
130
115
  UnauthenticatedError,
131
116
  )
132
117
  })
133
118
  })
134
119
 
135
- it('Should fail when password not provided', async () => {
136
- await usingAsync(new Injector(), async (i) => {
137
- await prepareInjector(i)
138
- await setupUser(i, 'user', 'pass123')
139
- await expect(i.getInstance(HttpUserContext).authenticateUser('user', '')).rejects.toThrowError(
120
+ it('throws UnauthenticatedError for an unknown username', async () => {
121
+ await usingAsync(createInjector(), async (i) => {
122
+ prepareInjector(i)
123
+ await setupUser(i, 'otherUser', 'pass123')
124
+ await expect(i.get(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toBeInstanceOf(
140
125
  UnauthenticatedError,
141
126
  )
142
127
  })
143
128
  })
144
129
 
145
- it('Should fail when the user is not in the user store', async () => {
146
- await usingAsync(new Injector(), async (i) => {
147
- await prepareInjector(i)
130
+ it('throws UnauthenticatedError when the user exists only as a credential', async () => {
131
+ await usingAsync(createInjector(), async (i) => {
132
+ prepareInjector(i)
148
133
  await setupUser(i, 'user', 'pass123')
149
- await i.getInstance(StoreManager).getStoreFor(User, 'username').remove('user')
150
- await expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toThrowError(
134
+ await i.get(UserStore).remove('user')
135
+ await expect(i.get(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toBeInstanceOf(
151
136
  UnauthenticatedError,
152
137
  )
153
138
  })
154
139
  })
155
140
 
156
- it('Should return the user when the username and password matches', async () => {
157
- await usingAsync(new Injector(), async (i) => {
158
- await prepareInjector(i)
141
+ it('returns the user on valid credentials', async () => {
142
+ await usingAsync(createInjector(), async (i) => {
143
+ prepareInjector(i)
159
144
  await setupUser(i, 'user', 'pass123')
160
- const ctx = i.getInstance(HttpUserContext)
161
- const value = await ctx.authenticateUser('user', 'pass123')
162
- expect(value).toEqual({ username: 'user', roles: [] })
145
+ expect(await i.get(HttpUserContext).authenticateUser('user', 'pass123')).toEqual({
146
+ username: 'user',
147
+ roles: [],
148
+ })
163
149
  })
164
150
  })
165
151
  })
166
152
 
167
153
  describe('getSessionIdFromRequest', () => {
168
- it('Should return null if no headers present', async () => {
169
- await usingAsync(new Injector(), async (i) => {
170
- await prepareInjector(i)
171
- const ctx = i.getInstance(HttpUserContext)
172
- const sid = ctx.getSessionIdFromRequest(request)
173
- expect(sid).toBeNull()
154
+ it('returns null when no headers are present', async () => {
155
+ await usingAsync(createInjector(), async (i) => {
156
+ prepareInjector(i)
157
+ expect(i.get(HttpUserContext).getSessionIdFromRequest(request)).toBeNull()
174
158
  })
175
159
  })
176
160
 
177
- it('Should return null if no session ID cookie present', async () => {
178
- await usingAsync(new Injector(), async (i) => {
179
- await prepareInjector(i)
180
- const requestWithCookie = { ...request, cookie: 'a=2;b=3;c=4;' } as unknown as IncomingMessage
181
- const ctx = i.getInstance(HttpUserContext)
182
- const sid = ctx.getSessionIdFromRequest(requestWithCookie)
183
- expect(sid).toBeNull()
161
+ it('returns null when no session cookie is present', async () => {
162
+ await usingAsync(createInjector(), async (i) => {
163
+ prepareInjector(i)
164
+ const requestWithCookie = { headers: { cookie: 'a=2;b=3;' } } as unknown as IncomingMessage
165
+ expect(i.get(HttpUserContext).getSessionIdFromRequest(requestWithCookie)).toBeNull()
184
166
  })
185
167
  })
186
- it('Should return the Session ID value if session ID cookie present', async () => {
187
- await usingAsync(new Injector(), async (i) => {
188
- await prepareInjector(i)
189
- const ctx = i.getInstance(HttpUserContext)
168
+
169
+ it('returns the session id when the configured cookie is present', async () => {
170
+ await usingAsync(createInjector(), async (i) => {
171
+ prepareInjector(i)
172
+ const ctx = i.get(HttpUserContext)
190
173
  const requestWithAuthCookie = {
191
- ...request,
192
174
  headers: { cookie: `a=2;b=3;${ctx.authentication.cookieName}=666;c=4;` },
193
175
  } as unknown as IncomingMessage
194
-
195
- const sid = ctx.getSessionIdFromRequest(requestWithAuthCookie)
196
- expect(sid).toBe('666')
176
+ expect(ctx.getSessionIdFromRequest(requestWithAuthCookie)).toBe('666')
197
177
  })
198
178
  })
199
179
  })
200
180
 
201
181
  describe('authenticateRequest', () => {
202
- it('Should authenticate with Basic Auth when enabled and valid credentials provided', async () => {
203
- await usingAsync(new Injector(), async (i) => {
204
- await prepareInjector(i)
182
+ it('authenticates with Basic Auth when enabled and credentials are valid', async () => {
183
+ await usingAsync(createInjector(), async (i) => {
184
+ prepareInjector(i)
205
185
  await setupUser(i, 'testuser', 'password')
206
- const ctx = i.getInstance(HttpUserContext)
207
- const result = await ctx.authenticateRequest({
186
+ const result = await i.get(HttpUserContext).authenticateRequest({
208
187
  headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
209
- } as IncomingMessage)
188
+ })
210
189
  expect(result.username).toBe('testuser')
211
190
  })
212
191
  })
213
192
 
214
- it('Should NOT try to authenticate with Basic when disabled', async () => {
215
- await usingAsync(new Injector(), async (i) => {
216
- await prepareInjector(i, { enableBasicAuth: false })
193
+ it('does not attempt Basic Auth when it is disabled', async () => {
194
+ await usingAsync(createInjector(), async (i) => {
195
+ prepareInjector(i, { enableBasicAuth: false })
217
196
  await setupUser(i, 'testuser', 'password')
218
- const ctx = i.getInstance(HttpUserContext)
219
197
  await expect(
220
- ctx.authenticateRequest({
198
+ i.get(HttpUserContext).authenticateRequest({
221
199
  headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
222
- } as IncomingMessage),
223
- ).rejects.toThrowError(UnauthenticatedError)
200
+ }),
201
+ ).rejects.toBeInstanceOf(UnauthenticatedError)
224
202
  })
225
203
  })
226
204
 
227
- it('Should fail with no session in the store', async () => {
228
- await usingAsync(new Injector(), async (i) => {
229
- await prepareInjector(i)
230
- const ctx = i.getInstance(HttpUserContext)
205
+ it('throws when the cookie session id does not exist', async () => {
206
+ await usingAsync(createInjector(), async (i) => {
207
+ prepareInjector(i)
208
+ const ctx = i.get(HttpUserContext)
231
209
  await expect(
232
210
  ctx.authenticateRequest({
233
- headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
234
- } as IncomingMessage),
235
- ).rejects.toThrowError(UnauthenticatedError)
211
+ headers: { cookie: `${ctx.authentication.cookieName}=666;` },
212
+ }),
213
+ ).rejects.toBeInstanceOf(UnauthenticatedError)
236
214
  })
237
215
  })
238
216
 
239
- it('Should fail with valid session Id but no user', async () => {
240
- await usingAsync(new Injector(), async (i) => {
241
- await prepareInjector(i)
242
- const ctx = i.getInstance(HttpUserContext)
243
- await i.getInstance(StoreManager).getStoreFor(DefaultSession, 'sessionId').add({
244
- sessionId: '666',
245
- username: testUser.username,
246
- })
247
- await expect(
248
- ctx.authenticateRequest({
249
- headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
250
- } as IncomingMessage),
251
- ).rejects.toThrowError(UnauthenticatedError)
252
- })
253
- })
254
-
255
- it('Should authenticate with cookie, if the session IDs matches', async () => {
256
- await usingAsync(new Injector(), async (i) => {
257
- await prepareInjector(i)
258
-
259
- const ctx = i.getInstance(HttpUserContext)
260
- await i.getInstance(StoreManager).getStoreFor(DefaultSession, 'sessionId').add({
261
- sessionId: '666',
262
- username: testUser.username,
263
- })
264
-
265
- await i
266
- .getInstance(StoreManager)
267
- .getStoreFor(User, 'username')
268
- .add({ ...testUser })
217
+ it('authenticates via cookie when the session id matches a stored user', async () => {
218
+ await usingAsync(createInjector(), async (i) => {
219
+ prepareInjector(i)
220
+ const ctx = i.get(HttpUserContext)
221
+ await i.get(SessionStore).add({ sessionId: '666', username: testUser.username })
222
+ await i.get(UserStore).add({ ...testUser })
269
223
 
270
224
  const result = await ctx.authenticateRequest({
271
- headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
272
- } as IncomingMessage)
273
-
225
+ headers: { cookie: `${ctx.authentication.cookieName}=666;` },
226
+ })
274
227
  expect(result).toEqual(testUser)
275
228
  })
276
229
  })
277
230
 
278
- it('Should iterate providers and return null results pass to next', async () => {
279
- await usingAsync(new Injector(), async (i) => {
280
- await prepareInjector(i)
281
- const ctx = i.getInstance(HttpUserContext)
231
+ it('walks providers in order and stops at the first match', async () => {
232
+ await usingAsync(createInjector(), async (i) => {
233
+ prepareInjector(i)
234
+ const ctx = i.get(HttpUserContext)
282
235
  const provider1 = vi.fn(async () => null)
283
236
  const provider2 = vi.fn(async () => testUser)
284
237
  ctx.authentication.authenticationProviders = [
@@ -292,10 +245,10 @@ describe('HttpUserContext', () => {
292
245
  })
293
246
  })
294
247
 
295
- it('Should throw if provider throws (skipping remaining providers)', async () => {
296
- await usingAsync(new Injector(), async (i) => {
297
- await prepareInjector(i)
298
- const ctx = i.getInstance(HttpUserContext)
248
+ it('propagates a provider-thrown error and skips remaining providers', async () => {
249
+ await usingAsync(createInjector(), async (i) => {
250
+ prepareInjector(i)
251
+ const ctx = i.get(HttpUserContext)
299
252
  const provider1 = vi.fn(async () => {
300
253
  throw new UnauthenticatedError()
301
254
  })
@@ -304,253 +257,168 @@ describe('HttpUserContext', () => {
304
257
  { name: 'test-1', authenticate: provider1 },
305
258
  { name: 'test-2', authenticate: provider2 },
306
259
  ]
307
- await expect(ctx.authenticateRequest(request)).rejects.toThrowError(UnauthenticatedError)
260
+ await expect(ctx.authenticateRequest(request)).rejects.toBeInstanceOf(UnauthenticatedError)
308
261
  expect(provider2).not.toHaveBeenCalled()
309
262
  })
310
263
  })
311
264
 
312
- it('Should throw UnauthenticatedError if no provider returns a user', async () => {
313
- await usingAsync(new Injector(), async (i) => {
314
- await prepareInjector(i)
315
- const ctx = i.getInstance(HttpUserContext)
265
+ it('throws UnauthenticatedError when no provider returns a user', async () => {
266
+ await usingAsync(createInjector(), async (i) => {
267
+ prepareInjector(i)
268
+ const ctx = i.get(HttpUserContext)
316
269
  ctx.authentication.authenticationProviders = [{ name: 'test-1', authenticate: async () => null }]
317
- await expect(ctx.authenticateRequest(request)).rejects.toThrowError(UnauthenticatedError)
270
+ await expect(ctx.authenticateRequest(request)).rejects.toBeInstanceOf(UnauthenticatedError)
318
271
  })
319
272
  })
320
273
  })
321
274
 
322
275
  describe('getCurrentUser', () => {
323
- it('Should return the current user from authenticateRequest() once per request', async () => {
324
- await usingAsync(new Injector(), async (i) => {
325
- await prepareInjector(i)
326
- const ctx = i.getInstance(HttpUserContext)
327
- ctx.authenticateRequest = vi.fn(async () => testUser)
328
- const result = await ctx.getCurrentUser(request)
329
- const result2 = await ctx.getCurrentUser(request)
330
- expect(ctx.authenticateRequest).toBeCalledTimes(1)
331
- expect(result).toBe(testUser)
332
- expect(result2).toBe(testUser)
333
- })
334
- })
335
- })
336
-
337
- describe('cookieLogin', () => {
338
- it('Should return the current user from authenticateRequest() once per request', async () => {
339
- await usingAsync(new Injector(), async (i) => {
340
- await prepareInjector(i)
341
- const ctx = i.getInstance(HttpUserContext)
342
- const setHeader = vi.fn()
343
- const addMock = vi.fn(async () => ({}))
344
- // @ts-expect-error
345
- ctx.getSessionDataSet = vi.fn(() => ({ add: addMock }))
346
- const authResult = await ctx.cookieLogin(testUser, { setHeader })
347
- expect(authResult).toBe(testUser)
348
- expect(setHeader).toBeCalled()
349
- expect(addMock).toBeCalled()
350
- })
351
- })
352
- })
353
-
354
- describe('cookieLogout', () => {
355
- it('Should invalidate the current session id cookie', async () => {
356
- await usingAsync(new Injector(), async (i) => {
357
- await prepareInjector(i)
358
- const ctx = i.getInstance(HttpUserContext)
359
- const setHeader = vi.fn()
360
- const removeMock = vi.fn(async () => undefined)
361
- const sessionDataSetMock = {
362
- add: vi.fn(async () => ({})),
363
- find: vi.fn(async () => [{ sessionId: 'example-session-id' }]),
364
- remove: removeMock,
365
- primaryKey: 'sessionId' as const,
366
- }
367
- // @ts-expect-error
368
- ctx.getSessionDataSet = vi.fn(() => sessionDataSetMock)
276
+ it('caches the result of authenticateRequest by session id when the cookie provider yields a key', async () => {
277
+ await usingAsync(createInjector(), async (i) => {
278
+ prepareInjector(i)
279
+ const ctx = i.get(HttpUserContext)
280
+ const cookiedRequest = {
281
+ headers: { cookie: `${ctx.authentication.cookieName}=session-1;` },
282
+ } as IncomingMessage
369
283
  ctx.authenticateRequest = vi.fn(async () => testUser)
370
- ctx.getSessionIdFromRequest = () => 'example-session-id'
371
- response.setHeader = vi.fn(() => response)
372
- await ctx.cookieLogin(testUser, { setHeader })
373
- await ctx.cookieLogout(request, response)
374
- expect(response.setHeader).toBeCalledWith('Set-Cookie', 'fss=; Path=/; HttpOnly')
375
- expect(removeMock).toBeCalled()
284
+ const first = await ctx.getCurrentUser(cookiedRequest)
285
+ const second = await ctx.getCurrentUser(cookiedRequest)
286
+ expect(ctx.authenticateRequest).toHaveBeenCalledTimes(1)
287
+ expect(first).toBe(testUser)
288
+ expect(second).toBe(testUser)
376
289
  })
377
290
  })
378
- })
379
-
380
- describe('EventHub events', () => {
381
- it('Should emit onLogin when cookieLogin succeeds', async () => {
382
- await usingAsync(new Injector(), async (i) => {
383
- await prepareInjector(i)
384
- const ctx = i.getInstance(HttpUserContext)
385
- const handler = vi.fn()
386
- ctx.addListener('onLogin', handler)
387
291
 
388
- const addMock = vi.fn(async () => ({}))
389
- // @ts-expect-error
390
- ctx.getSessionDataSet = vi.fn(() => ({ add: addMock }))
391
-
392
- await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
393
-
394
- expect(handler).toHaveBeenCalledTimes(1)
395
- expect(handler).toHaveBeenCalledWith({ user: testUser })
292
+ it('treats distinct sessions as distinct cache entries', async () => {
293
+ await usingAsync(createInjector(), async (i) => {
294
+ prepareInjector(i)
295
+ const ctx = i.get(HttpUserContext)
296
+ const otherUser: User = { username: 'otherUser', roles: [] }
297
+ const requestA = {
298
+ headers: { cookie: `${ctx.authentication.cookieName}=session-a;` },
299
+ } as IncomingMessage
300
+ const requestB = {
301
+ headers: { cookie: `${ctx.authentication.cookieName}=session-b;` },
302
+ } as IncomingMessage
303
+ const authenticateRequest = vi
304
+ .fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
305
+ .mockResolvedValueOnce(testUser)
306
+ .mockResolvedValueOnce(otherUser)
307
+ ctx.authenticateRequest = authenticateRequest
308
+ expect(await ctx.getCurrentUser(requestA)).toBe(testUser)
309
+ expect(await ctx.getCurrentUser(requestB)).toBe(otherUser)
310
+ expect(await ctx.getCurrentUser(requestA)).toBe(testUser)
311
+ expect(authenticateRequest).toHaveBeenCalledTimes(2)
396
312
  })
397
313
  })
398
314
 
399
- it('Should emit onLogout when cookieLogout is called', async () => {
400
- await usingAsync(new Injector(), async (i) => {
401
- await prepareInjector(i)
402
- const ctx = i.getInstance(HttpUserContext)
403
- const handler = vi.fn()
404
- ctx.addListener('onLogout', handler)
405
-
406
- const sessionDataSetMock = {
407
- add: vi.fn(async () => ({})),
408
- find: vi.fn(async () => [{ sessionId: 'sid' }]),
409
- remove: vi.fn(async () => undefined),
410
- primaryKey: 'sessionId' as const,
411
- }
412
- // @ts-expect-error
413
- ctx.getSessionDataSet = vi.fn(() => sessionDataSetMock)
414
- ctx.getSessionIdFromRequest = () => 'sid'
415
-
416
- await ctx.cookieLogout(request, { setHeader: vi.fn() })
417
-
418
- expect(handler).toHaveBeenCalledTimes(1)
419
- expect(handler).toHaveBeenCalledWith(undefined)
315
+ it('bypasses the cache when no provider yields a key (e.g. Basic Auth, anonymous)', async () => {
316
+ await usingAsync(createInjector(), async (i) => {
317
+ prepareInjector(i)
318
+ const ctx = i.get(HttpUserContext)
319
+ const anonRequest = { headers: {} } as IncomingMessage
320
+ const authenticateRequest = vi
321
+ .fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
322
+ .mockResolvedValue(testUser)
323
+ ctx.authenticateRequest = authenticateRequest
324
+ await ctx.getCurrentUser(anonRequest)
325
+ await ctx.getCurrentUser(anonRequest)
326
+ expect(authenticateRequest).toHaveBeenCalledTimes(2)
420
327
  })
421
328
  })
422
329
 
423
- it('Should emit onSessionInvalidated when the user entity is removed', async () => {
424
- await usingAsync(new Injector(), async (i) => {
425
- await prepareInjector(i)
426
- const ctx = i.getInstance(HttpUserContext)
427
- const sm = i.getInstance(StoreManager)
428
- await sm.getStoreFor(User, 'username').add(testUser)
429
-
430
- const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
431
- await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
330
+ it('drops the local cache entry on cookieLogout so the next request re-authenticates', async () => {
331
+ await usingAsync(createInjector(), async (i) => {
332
+ prepareInjector(i)
333
+ const ctx = i.get(HttpUserContext)
334
+ const sessionId = 'session-logout'
335
+ const cookiedRequest = {
336
+ headers: { cookie: `${ctx.authentication.cookieName}=${sessionId};` },
337
+ } as IncomingMessage
338
+ await i.get(SessionStore).add({ sessionId, username: testUser.username })
339
+ await i.get(UserStore).add(testUser)
432
340
 
433
- const handler = vi.fn()
434
- ctx.addListener('onSessionInvalidated', handler)
341
+ const authenticateRequest = vi
342
+ .fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
343
+ .mockResolvedValue(testUser)
344
+ ctx.authenticateRequest = authenticateRequest
435
345
 
436
- await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
346
+ await ctx.getCurrentUser(cookiedRequest)
347
+ expect(authenticateRequest).toHaveBeenCalledTimes(1)
437
348
 
438
- const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
439
- const userDataSet = getDataSetFor(systemInjector, User, 'username')
440
- await userDataSet.remove(systemInjector, testUser.username)
349
+ await ctx.cookieLogout(cookiedRequest, { setHeader: vi.fn() })
441
350
 
442
- expect(handler).toHaveBeenCalledTimes(1)
351
+ await ctx.getCurrentUser(cookiedRequest)
352
+ expect(authenticateRequest).toHaveBeenCalledTimes(2)
443
353
  })
444
354
  })
445
355
 
446
- it('Should emit onSessionInvalidated when the session entity is removed', async () => {
447
- await usingAsync(new Injector(), async (i) => {
448
- await prepareInjector(i)
449
- const ctx = i.getInstance(HttpUserContext)
450
- const sm = i.getInstance(StoreManager)
451
- await sm.getStoreFor(User, 'username').add(testUser)
452
-
453
- const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
454
- await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
455
-
456
- const handler = vi.fn()
457
- ctx.addListener('onSessionInvalidated', handler)
458
-
459
- let sessionId = ''
460
- await ctx.cookieLogin(testUser, {
461
- setHeader: (_headerName, headerValue) => {
462
- sessionId = headerValue.split('=')[1].split(';')[0]
463
- },
464
- })
465
-
466
- const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
467
- const sessionDataSet = getDataSetFor(systemInjector, DefaultSession, 'sessionId')
468
- await sessionDataSet.remove(systemInjector, sessionId)
469
-
470
- expect(handler).toHaveBeenCalledTimes(1)
356
+ it('re-walks providers after a failed authenticateRequest', async () => {
357
+ await usingAsync(createInjector(), async (i) => {
358
+ prepareInjector(i)
359
+ const ctx = i.get(HttpUserContext)
360
+ const authenticateRequest = vi
361
+ .fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
362
+ .mockRejectedValueOnce(new UnauthenticatedError())
363
+ .mockResolvedValueOnce(testUser)
364
+ ctx.authenticateRequest = authenticateRequest
365
+ await expect(ctx.getCurrentUser(request)).rejects.toBeInstanceOf(UnauthenticatedError)
366
+ expect(await ctx.getCurrentUser(request)).toBe(testUser)
367
+ expect(authenticateRequest).toHaveBeenCalledTimes(2)
471
368
  })
472
369
  })
473
370
  })
474
371
 
475
- describe('Changes in the store during the context lifetime', () => {
476
- it('Should update user roles', () => {
477
- return usingAsync(new Injector(), async (i) => {
478
- await prepareInjector(i)
479
- const ctx = i.getInstance(HttpUserContext)
480
- const sm = i.getInstance(StoreManager)
481
- await sm.getStoreFor(User, 'username').add(testUser)
482
-
483
- const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
484
- await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
485
-
486
- await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
487
-
488
- const originalUser = await ctx.getCurrentUser(request)
489
- expect(originalUser).toEqual(testUser)
490
-
491
- const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
492
- const userDataSet = getDataSetFor(systemInjector, User, 'username')
493
- const updatedUser = { ...testUser, roles: ['newFancyRole'] }
494
- await userDataSet.update(systemInjector, testUser.username, updatedUser)
495
- const updatedUserFromContext = await ctx.getCurrentUser(request)
496
- expect(updatedUserFromContext.roles).toEqual(['newFancyRole'])
497
-
498
- await userDataSet.update(systemInjector, testUser.username, { ...updatedUser, roles: [] })
499
- const reloadedUserFromContext = await ctx.getCurrentUser(request)
500
- expect(reloadedUserFromContext.roles).toEqual([])
501
- })
502
- })
503
-
504
- it('Should remove current user when the user is removed from the store', () => {
505
- return usingAsync(new Injector(), async (i) => {
506
- await prepareInjector(i)
507
- const ctx = i.getInstance(HttpUserContext)
508
- const sm = i.getInstance(StoreManager)
509
- await sm.getStoreFor(User, 'username').add(testUser)
510
-
511
- const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
512
- await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
513
-
514
- await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
515
-
516
- const originalUser = await ctx.getCurrentUser(request)
517
- expect(originalUser).toEqual(testUser)
372
+ describe('cookieLogin / cookieLogout', () => {
373
+ it('persists the session, sets the cookie and emits onLogin', async () => {
374
+ await usingAsync(createInjector(), async (i) => {
375
+ prepareInjector(i)
376
+ const ctx = i.get(HttpUserContext)
377
+ const setHeader = vi.fn()
378
+ const onLogin = vi.fn()
379
+ ctx.addListener('onLogin', onLogin)
518
380
 
519
- const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
520
- const userDataSet = getDataSetFor(systemInjector, User, 'username')
521
- await userDataSet.remove(systemInjector, testUser.username)
381
+ const result = await ctx.cookieLogin(testUser, { setHeader })
382
+ expect(result).toBe(testUser)
383
+ expect(setHeader).toHaveBeenCalledTimes(1)
384
+ expect(onLogin).toHaveBeenCalledWith({ user: testUser })
522
385
 
523
- await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
386
+ await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
387
+ const sessionDataSet = systemScope.get(SessionDataSet)
388
+ expect(await sessionDataSet.count(systemScope)).toBe(1)
389
+ })
524
390
  })
525
391
  })
526
392
 
527
- it('Should remove current user when the session is removed from the store', () => {
528
- return usingAsync(new Injector(), async (i) => {
529
- await prepareInjector(i)
530
- const ctx = i.getInstance(HttpUserContext)
531
- const sm = i.getInstance(StoreManager)
532
- await sm.getStoreFor(User, 'username').add(testUser)
393
+ it('clears the session cookie, removes the stored session and emits onLogout', async () => {
394
+ await usingAsync(createInjector(), async (i) => {
395
+ prepareInjector(i)
396
+ const ctx = i.get(HttpUserContext)
397
+ const onLogout = vi.fn()
398
+ ctx.addListener('onLogout', onLogout)
533
399
 
534
400
  let sessionId = ''
535
-
536
- const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
537
- await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
538
-
539
401
  await ctx.cookieLogin(testUser, {
540
- setHeader: (_headerName, headerValue) => {
541
- sessionId = headerValue.split('=')[1].split(';')[0]
542
- return {} as ServerResponse
402
+ setHeader: (_name, value) => {
403
+ sessionId = value.split('=')[1].split(';')[0]
543
404
  },
544
405
  })
545
406
 
546
- const originalUser = await ctx.getCurrentUser(request)
547
- expect(originalUser).toEqual(testUser)
548
-
549
- const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
550
- const sessionDataSet = getDataSetFor(systemInjector, DefaultSession, 'sessionId')
551
- await sessionDataSet.remove(systemInjector, sessionId)
407
+ const logoutSetHeader = vi.fn()
408
+ await ctx.cookieLogout(
409
+ { headers: { cookie: `${ctx.authentication.cookieName}=${sessionId};` } },
410
+ { setHeader: logoutSetHeader },
411
+ )
412
+ expect(logoutSetHeader).toHaveBeenCalledWith(
413
+ 'Set-Cookie',
414
+ `${ctx.authentication.cookieName}=; Path=/; HttpOnly`,
415
+ )
416
+ expect(onLogout).toHaveBeenCalledWith(undefined)
552
417
 
553
- await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
418
+ await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
419
+ const sessionDataSet = systemScope.get(SessionDataSet)
420
+ expect(await sessionDataSet.count(systemScope)).toBe(0)
421
+ })
554
422
  })
555
423
  })
556
424
  })