@furystack/rest-service 12.3.5 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +47 -28
  3. package/esm/actions/get-current-user.spec.js +10 -6
  4. package/esm/actions/get-current-user.spec.js.map +1 -1
  5. package/esm/actions/index.d.ts +0 -1
  6. package/esm/actions/index.d.ts.map +1 -1
  7. package/esm/actions/index.js +0 -1
  8. package/esm/actions/index.js.map +1 -1
  9. package/esm/actions/is-authenticated.spec.js +10 -6
  10. package/esm/actions/is-authenticated.spec.js.map +1 -1
  11. package/esm/actions/logout-action.spec.js +11 -9
  12. package/esm/actions/logout-action.spec.js.map +1 -1
  13. package/esm/actions/logout.js +1 -1
  14. package/esm/actions/logout.js.map +1 -1
  15. package/esm/actions/password-login-action.js +1 -1
  16. package/esm/actions/password-login-action.js.map +1 -1
  17. package/esm/actions/password-login-action.spec.js +23 -28
  18. package/esm/actions/password-login-action.spec.js.map +1 -1
  19. package/esm/authenticate.js +1 -1
  20. package/esm/authenticate.js.map +1 -1
  21. package/esm/authenticate.spec.js +41 -33
  22. package/esm/authenticate.spec.js.map +1 -1
  23. package/esm/authorize.spec.js +34 -29
  24. package/esm/authorize.spec.js.map +1 -1
  25. package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
  26. package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
  27. package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
  28. package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
  29. package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
  30. package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
  31. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
  32. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
  33. package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
  34. package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
  35. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
  36. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
  37. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
  38. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
  39. package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
  40. package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
  41. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
  42. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
  43. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -15
  44. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
  45. package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -24
  46. package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
  47. package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
  48. package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
  49. package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
  50. package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
  51. package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
  52. package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
  53. package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
  54. package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
  55. package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
  56. package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
  57. package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
  58. package/esm/endpoint-generators/create-post-endpoint.js +8 -13
  59. package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
  60. package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
  61. package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
  62. package/esm/endpoint-generators/utils.d.ts +17 -2
  63. package/esm/endpoint-generators/utils.d.ts.map +1 -1
  64. package/esm/endpoint-generators/utils.js +33 -20
  65. package/esm/endpoint-generators/utils.js.map +1 -1
  66. package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts +0 -2
  67. package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts.map +1 -1
  68. package/esm/get-schema-from-api.d.ts +1 -1
  69. package/esm/get-schema-from-api.d.ts.map +1 -1
  70. package/esm/get-schema-from-api.spec.js.map +1 -1
  71. package/esm/helpers.d.ts +53 -87
  72. package/esm/helpers.d.ts.map +1 -1
  73. package/esm/helpers.js +91 -92
  74. package/esm/helpers.js.map +1 -1
  75. package/esm/helpers.spec.d.ts +2 -1
  76. package/esm/helpers.spec.d.ts.map +1 -1
  77. package/esm/helpers.spec.js +41 -38
  78. package/esm/helpers.spec.js.map +1 -1
  79. package/esm/http-authentication-settings.d.ts +35 -12
  80. package/esm/http-authentication-settings.d.ts.map +1 -1
  81. package/esm/http-authentication-settings.js +21 -29
  82. package/esm/http-authentication-settings.js.map +1 -1
  83. package/esm/http-server-pool.d.ts +64 -0
  84. package/esm/http-server-pool.d.ts.map +1 -0
  85. package/esm/http-server-pool.js +95 -0
  86. package/esm/http-server-pool.js.map +1 -0
  87. package/esm/http-server-pool.spec.d.ts +2 -0
  88. package/esm/http-server-pool.spec.d.ts.map +1 -0
  89. package/esm/http-server-pool.spec.js +193 -0
  90. package/esm/http-server-pool.spec.js.map +1 -0
  91. package/esm/http-user-context.d.ts +31 -44
  92. package/esm/http-user-context.d.ts.map +1 -1
  93. package/esm/http-user-context.js +76 -95
  94. package/esm/http-user-context.js.map +1 -1
  95. package/esm/http-user-context.spec.d.ts +8 -2
  96. package/esm/http-user-context.spec.d.ts.map +1 -1
  97. package/esm/http-user-context.spec.js +189 -337
  98. package/esm/http-user-context.spec.js.map +1 -1
  99. package/esm/index.d.ts +6 -4
  100. package/esm/index.d.ts.map +1 -1
  101. package/esm/index.js +6 -4
  102. package/esm/index.js.map +1 -1
  103. package/esm/login-response-strategy.d.ts +11 -10
  104. package/esm/login-response-strategy.d.ts.map +1 -1
  105. package/esm/login-response-strategy.js +7 -6
  106. package/esm/login-response-strategy.js.map +1 -1
  107. package/esm/login-response-strategy.spec.js +25 -29
  108. package/esm/login-response-strategy.spec.js.map +1 -1
  109. package/esm/openapi/generate-openapi-document.d.ts +0 -8
  110. package/esm/openapi/generate-openapi-document.d.ts.map +1 -1
  111. package/esm/openapi/generate-openapi-document.js +0 -2
  112. package/esm/openapi/generate-openapi-document.js.map +1 -1
  113. package/esm/proxy-manager.spec.js +152 -176
  114. package/esm/proxy-manager.spec.js.map +1 -1
  115. package/esm/proxy-runtime.d.ts +27 -0
  116. package/esm/proxy-runtime.d.ts.map +1 -0
  117. package/esm/proxy-runtime.js +45 -0
  118. package/esm/proxy-runtime.js.map +1 -0
  119. package/esm/proxy-runtime.spec.d.ts +2 -0
  120. package/esm/proxy-runtime.spec.d.ts.map +1 -0
  121. package/esm/proxy-runtime.spec.js +39 -0
  122. package/esm/proxy-runtime.spec.js.map +1 -0
  123. package/esm/rest-api-runtime.d.ts +76 -0
  124. package/esm/rest-api-runtime.d.ts.map +1 -0
  125. package/esm/rest-api-runtime.js +131 -0
  126. package/esm/rest-api-runtime.js.map +1 -0
  127. package/esm/rest-api-runtime.spec.d.ts +2 -0
  128. package/esm/rest-api-runtime.spec.d.ts.map +1 -0
  129. package/esm/rest-api-runtime.spec.js +58 -0
  130. package/esm/rest-api-runtime.spec.js.map +1 -0
  131. package/esm/rest-service.integration.spec.d.ts +1 -1
  132. package/esm/rest-service.integration.spec.d.ts.map +1 -1
  133. package/esm/rest-service.integration.spec.js +60 -82
  134. package/esm/rest-service.integration.spec.js.map +1 -1
  135. package/esm/rest.integration.test.js +26 -84
  136. package/esm/rest.integration.test.js.map +1 -1
  137. package/esm/server-telemetry.d.ts +57 -0
  138. package/esm/server-telemetry.d.ts.map +1 -0
  139. package/esm/server-telemetry.js +26 -0
  140. package/esm/server-telemetry.js.map +1 -0
  141. package/esm/server-telemetry.spec.d.ts +2 -0
  142. package/esm/server-telemetry.spec.d.ts.map +1 -0
  143. package/esm/server-telemetry.spec.js +52 -0
  144. package/esm/server-telemetry.spec.js.map +1 -0
  145. package/esm/static-server-manager.spec.js +60 -125
  146. package/esm/static-server-manager.spec.js.map +1 -1
  147. package/esm/static-site-runtime.d.ts +21 -0
  148. package/esm/static-site-runtime.d.ts.map +1 -0
  149. package/esm/static-site-runtime.js +48 -0
  150. package/esm/static-site-runtime.js.map +1 -0
  151. package/esm/static-site-runtime.spec.d.ts +2 -0
  152. package/esm/static-site-runtime.spec.d.ts.map +1 -0
  153. package/esm/static-site-runtime.spec.js +30 -0
  154. package/esm/static-site-runtime.spec.js.map +1 -0
  155. package/esm/user-store.d.ts +39 -0
  156. package/esm/user-store.d.ts.map +1 -0
  157. package/esm/user-store.js +62 -0
  158. package/esm/user-store.js.map +1 -0
  159. package/esm/user-store.spec.d.ts +2 -0
  160. package/esm/user-store.spec.d.ts.map +1 -0
  161. package/esm/user-store.spec.js +72 -0
  162. package/esm/user-store.spec.js.map +1 -0
  163. package/esm/validate.integration.spec.js +26 -83
  164. package/esm/validate.integration.spec.js.map +1 -1
  165. package/package.json +8 -8
  166. package/src/actions/get-current-user.spec.ts +11 -10
  167. package/src/actions/index.ts +0 -1
  168. package/src/actions/is-authenticated.spec.ts +12 -7
  169. package/src/actions/logout-action.spec.ts +12 -12
  170. package/src/actions/logout.ts +1 -1
  171. package/src/actions/password-login-action.spec.ts +30 -30
  172. package/src/actions/password-login-action.ts +1 -1
  173. package/src/authenticate.spec.ts +45 -51
  174. package/src/authenticate.ts +1 -1
  175. package/src/authorize.spec.ts +37 -39
  176. package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
  177. package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
  178. package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
  179. package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
  180. package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
  181. package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
  182. package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -38
  183. package/src/endpoint-generators/create-get-schema-action.ts +1 -1
  184. package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
  185. package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
  186. package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
  187. package/src/endpoint-generators/create-post-endpoint.ts +13 -23
  188. package/src/endpoint-generators/utils.ts +36 -28
  189. package/src/endpoint-generators/with-schema-and-openapi-action.ts +0 -3
  190. package/src/get-schema-from-api.spec.ts +1 -1
  191. package/src/get-schema-from-api.ts +1 -1
  192. package/src/helpers.spec.ts +51 -41
  193. package/src/helpers.ts +140 -109
  194. package/src/http-authentication-settings.ts +51 -25
  195. package/src/http-server-pool.spec.ts +220 -0
  196. package/src/http-server-pool.ts +169 -0
  197. package/src/http-user-context.spec.ts +203 -383
  198. package/src/http-user-context.ts +114 -91
  199. package/src/index.ts +6 -4
  200. package/src/login-response-strategy.spec.ts +32 -31
  201. package/src/login-response-strategy.ts +13 -12
  202. package/src/openapi/generate-openapi-document.ts +0 -3
  203. package/src/proxy-manager.spec.ts +152 -182
  204. package/src/proxy-runtime.spec.ts +50 -0
  205. package/src/proxy-runtime.ts +75 -0
  206. package/src/rest-api-runtime.spec.ts +77 -0
  207. package/src/rest-api-runtime.ts +204 -0
  208. package/src/rest-service.integration.spec.ts +68 -87
  209. package/src/rest.integration.test.ts +27 -87
  210. package/src/server-telemetry.spec.ts +56 -0
  211. package/src/server-telemetry.ts +51 -0
  212. package/src/static-server-manager.spec.ts +63 -151
  213. package/src/static-site-runtime.spec.ts +39 -0
  214. package/src/static-site-runtime.ts +75 -0
  215. package/src/user-store.spec.ts +86 -0
  216. package/src/user-store.ts +68 -0
  217. package/src/validate.integration.spec.ts +39 -97
  218. package/esm/actions/login-action.spec.d.ts +0 -2
  219. package/esm/actions/login-action.spec.d.ts.map +0 -1
  220. package/esm/actions/login-action.spec.js +0 -34
  221. package/esm/actions/login-action.spec.js.map +0 -1
  222. package/esm/actions/login.d.ts +0 -19
  223. package/esm/actions/login.d.ts.map +0 -1
  224. package/esm/actions/login.js +0 -27
  225. package/esm/actions/login.js.map +0 -1
  226. package/esm/api-manager.d.ts +0 -104
  227. package/esm/api-manager.d.ts.map +0 -1
  228. package/esm/api-manager.js +0 -180
  229. package/esm/api-manager.js.map +0 -1
  230. package/esm/proxy-manager.d.ts +0 -53
  231. package/esm/proxy-manager.d.ts.map +0 -1
  232. package/esm/proxy-manager.js +0 -84
  233. package/esm/proxy-manager.js.map +0 -1
  234. package/esm/server-manager.d.ts +0 -48
  235. package/esm/server-manager.d.ts.map +0 -1
  236. package/esm/server-manager.js +0 -100
  237. package/esm/server-manager.js.map +0 -1
  238. package/esm/static-server-manager.d.ts +0 -19
  239. package/esm/static-server-manager.d.ts.map +0 -1
  240. package/esm/static-server-manager.js +0 -65
  241. package/esm/static-server-manager.js.map +0 -1
  242. package/esm/swagger/generate-swagger-json.spec.d.ts +0 -2
  243. package/esm/swagger/generate-swagger-json.spec.d.ts.map +0 -1
  244. package/esm/swagger/generate-swagger-json.spec.js +0 -215
  245. package/esm/swagger/generate-swagger-json.spec.js.map +0 -1
  246. package/src/actions/login-action.spec.ts +0 -42
  247. package/src/actions/login.ts +0 -32
  248. package/src/api-manager.ts +0 -308
  249. package/src/proxy-manager.ts +0 -102
  250. package/src/server-manager.ts +0 -135
  251. package/src/static-server-manager.ts +0 -90
  252. package/src/swagger/generate-swagger-json.spec.ts +0 -250
@@ -1,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
188
  } as IncomingMessage)
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
200
  } as IncomingMessage),
223
- ).rejects.toThrowError(UnauthenticatedError)
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` },
211
+ headers: { cookie: `${ctx.authentication.cookieName}=666;` },
234
212
  } as IncomingMessage),
235
- ).rejects.toThrowError(UnauthenticatedError)
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` },
225
+ headers: { cookie: `${ctx.authentication.cookieName}=666;` },
272
226
  } as IncomingMessage)
273
-
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,120 @@ 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)
276
+ it('caches the result of authenticateRequest within the request scope', async () => {
277
+ await usingAsync(createInjector(), async (i) => {
278
+ prepareInjector(i)
279
+ const ctx = i.get(HttpUserContext)
327
280
  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)
281
+ const first = await ctx.getCurrentUser(request)
282
+ const second = await ctx.getCurrentUser(request)
283
+ expect(ctx.authenticateRequest).toHaveBeenCalledTimes(1)
284
+ expect(first).toBe(testUser)
285
+ expect(second).toBe(testUser)
333
286
  })
334
287
  })
335
- })
336
288
 
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()
289
+ it('does not leak a cached user between distinct requests on the same instance', async () => {
290
+ await usingAsync(createInjector(), async (i) => {
291
+ prepareInjector(i)
292
+ const ctx = i.get(HttpUserContext)
293
+ const otherUser: User = { username: 'otherUser', roles: [] }
294
+ const requestA = { headers: {} } as IncomingMessage
295
+ const requestB = { headers: {} } as IncomingMessage
296
+ const authenticateRequest = vi
297
+ .fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
298
+ .mockResolvedValueOnce(testUser)
299
+ .mockResolvedValueOnce(otherUser)
300
+ ctx.authenticateRequest = authenticateRequest
301
+ expect(await ctx.getCurrentUser(requestA)).toBe(testUser)
302
+ expect(await ctx.getCurrentUser(requestB)).toBe(otherUser)
303
+ expect(await ctx.getCurrentUser(requestA)).toBe(testUser)
304
+ expect(authenticateRequest).toHaveBeenCalledTimes(2)
350
305
  })
351
306
  })
352
- })
353
307
 
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)
369
- 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()
308
+ it('re-walks providers after a failed authenticateRequest', async () => {
309
+ await usingAsync(createInjector(), async (i) => {
310
+ prepareInjector(i)
311
+ const ctx = i.get(HttpUserContext)
312
+ const authenticateRequest = vi
313
+ .fn<(req: Pick<IncomingMessage, 'headers'>) => Promise<User>>()
314
+ .mockRejectedValueOnce(new UnauthenticatedError())
315
+ .mockResolvedValueOnce(testUser)
316
+ ctx.authenticateRequest = authenticateRequest
317
+ await expect(ctx.getCurrentUser(request)).rejects.toBeInstanceOf(UnauthenticatedError)
318
+ expect(await ctx.getCurrentUser(request)).toBe(testUser)
319
+ expect(authenticateRequest).toHaveBeenCalledTimes(2)
376
320
  })
377
321
  })
378
322
  })
379
323
 
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
-
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 })
396
- })
397
- })
398
-
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)
420
- })
421
- })
422
-
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)
432
-
433
- const handler = vi.fn()
434
- ctx.addListener('onSessionInvalidated', handler)
435
-
436
- await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
437
-
438
- const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
439
- const userDataSet = getDataSetFor(systemInjector, User, 'username')
440
- await userDataSet.remove(systemInjector, testUser.username)
441
-
442
- expect(handler).toHaveBeenCalledTimes(1)
443
- })
444
- })
445
-
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)
324
+ describe('cookieLogin / cookieLogout', () => {
325
+ it('persists the session, sets the cookie and emits onLogin', async () => {
326
+ await usingAsync(createInjector(), async (i) => {
327
+ prepareInjector(i)
328
+ const ctx = i.get(HttpUserContext)
329
+ const setHeader = vi.fn()
330
+ const onLogin = vi.fn()
331
+ ctx.addListener('onLogin', onLogin)
455
332
 
456
- const handler = vi.fn()
457
- ctx.addListener('onSessionInvalidated', handler)
333
+ const result = await ctx.cookieLogin(testUser, { setHeader })
334
+ expect(result).toBe(testUser)
335
+ expect(setHeader).toHaveBeenCalledTimes(1)
336
+ expect(onLogin).toHaveBeenCalledWith({ user: testUser })
458
337
 
459
- let sessionId = ''
460
- await ctx.cookieLogin(testUser, {
461
- setHeader: (_headerName, headerValue) => {
462
- sessionId = headerValue.split('=')[1].split(';')[0]
463
- },
338
+ await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
339
+ const sessionDataSet = systemScope.get(SessionDataSet)
340
+ expect(await sessionDataSet.count(systemScope)).toBe(1)
464
341
  })
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)
471
- })
472
- })
473
- })
474
-
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)
518
-
519
- const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
520
- const userDataSet = getDataSetFor(systemInjector, User, 'username')
521
- await userDataSet.remove(systemInjector, testUser.username)
522
-
523
- await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
524
342
  })
525
343
  })
526
344
 
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)
345
+ it('clears the session cookie, removes the stored session and emits onLogout', async () => {
346
+ await usingAsync(createInjector(), async (i) => {
347
+ prepareInjector(i)
348
+ const ctx = i.get(HttpUserContext)
349
+ const onLogout = vi.fn()
350
+ ctx.addListener('onLogout', onLogout)
533
351
 
534
352
  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
353
  await ctx.cookieLogin(testUser, {
540
- setHeader: (_headerName, headerValue) => {
541
- sessionId = headerValue.split('=')[1].split(';')[0]
542
- return {} as ServerResponse
354
+ setHeader: (_name, value) => {
355
+ sessionId = value.split('=')[1].split(';')[0]
543
356
  },
544
357
  })
545
358
 
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)
359
+ const logoutSetHeader = vi.fn()
360
+ await ctx.cookieLogout(
361
+ { headers: { cookie: `${ctx.authentication.cookieName}=${sessionId};` } } as IncomingMessage,
362
+ { setHeader: logoutSetHeader },
363
+ )
364
+ expect(logoutSetHeader).toHaveBeenCalledWith(
365
+ 'Set-Cookie',
366
+ `${ctx.authentication.cookieName}=; Path=/; HttpOnly`,
367
+ )
368
+ expect(onLogout).toHaveBeenCalledWith(undefined)
552
369
 
553
- await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
370
+ await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
371
+ const sessionDataSet = systemScope.get(SessionDataSet)
372
+ expect(await sessionDataSet.count(systemScope)).toBe(0)
373
+ })
554
374
  })
555
375
  })
556
376
  })