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