@o3co/create-auth-provider 0.17.0-rc.2 → 0.17.0-rc.4

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 (43) hide show
  1. package/README.ja.md +3 -1
  2. package/README.md +5 -2
  3. package/package.json +3 -3
  4. package/templates/standalone/.env.example +7 -2
  5. package/templates/standalone/README.ja.md +17 -11
  6. package/templates/standalone/README.md +47 -21
  7. package/templates/standalone/config/application.conf +1 -0
  8. package/templates/standalone/config/clients.yaml.example +8 -0
  9. package/templates/standalone/config/development.conf +2 -2
  10. package/templates/standalone/config/mailpit.conf +10 -5
  11. package/templates/standalone/config/reference.conf +9 -3
  12. package/templates/standalone/docker-compose.mailpit.yml +3 -0
  13. package/templates/standalone/package.json +6 -6
  14. package/templates/standalone/src/__tests__/adapters.test.mts +12 -6
  15. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +13 -5
  16. package/templates/standalone/src/__tests__/config-notices.test.mts +4 -1
  17. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +48 -0
  18. package/templates/standalone/src/__tests__/core-federations.test.mts +9 -6
  19. package/templates/standalone/src/__tests__/core-sections.test.mts +7 -5
  20. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +91 -87
  21. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +4 -0
  22. package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +113 -0
  23. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +81 -14
  24. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +8 -4
  25. package/templates/standalone/src/__tests__/mfa-switch.test.mts +79 -21
  26. package/templates/standalone/src/__tests__/own-modules.test.mts +38 -12
  27. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +214 -0
  28. package/templates/standalone/src/__tests__/replica-safety.test.mts +4 -0
  29. package/templates/standalone/src/__tests__/repositories-logger.test.mts +119 -0
  30. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +10 -3
  31. package/templates/standalone/src/__tests__/sections.test.mts +24 -0
  32. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +4 -0
  33. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +148 -0
  34. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +8 -4
  35. package/templates/standalone/src/__tests__/two-phase-config.test.mts +2 -1
  36. package/templates/standalone/src/adapters.mts +2 -2
  37. package/templates/standalone/src/mfaSwitch.mts +6 -7
  38. package/templates/standalone/src/modules.mts +21 -9
  39. package/templates/standalone/src/rootRenames.mts +12 -13
  40. package/templates/standalone/src/sections.mts +12 -2
  41. package/templates/standalone/tests/package-lock.json +95 -95
  42. package/templates/standalone/tests/package.json +1 -1
  43. package/templates/versions.json +18 -18
@@ -78,6 +78,10 @@ vi.mock("ioredis", () => {
78
78
  quit: async () => "OK",
79
79
  disconnect: () => undefined,
80
80
  ping: async () => "PONG",
81
+ // The Redis stores' eviction gate reads the policy from INFO memory: a
82
+ // default server's.
83
+ info: async (section: string) =>
84
+ section === "memory" ? "# Memory\r\nmaxmemory_policy:noeviction\r\n" : null,
81
85
  };
82
86
  const makeMockRedis = (): object =>
83
87
  new Proxy(
@@ -182,12 +186,16 @@ describe('every module on, every shared store on Redis, core.deployment.mode = "
182
186
  expect(current.handle.components.deploymentMode).toBe("multi");
183
187
  });
184
188
 
185
- it("boots as multi on DEPLOYMENT_MODE=multi beside CORE_DEPLOYMENT_MODE=multi, as the umbrella sets them", async () => {
186
- current = await compose({
187
- ...MULTI,
188
- env: { ...MULTI_ENV, DEPLOYMENT_MODE: "multi", CORE_DEPLOYMENT_MODE: "multi" },
189
+ it("refuses DEPLOYMENT_MODE=multi beside CORE_DEPLOYMENT_MODE=multi: the old name must be unset", async () => {
190
+ await expect(
191
+ compose({
192
+ ...MULTI,
193
+ env: { ...MULTI_ENV, DEPLOYMENT_MODE: "multi", CORE_DEPLOYMENT_MODE: "multi" },
194
+ }),
195
+ ).rejects.toMatchObject({
196
+ reason: "environment-variable-renamed",
197
+ details: { renamed: [{ from: "DEPLOYMENT_MODE", state: "different" }] },
189
198
  });
190
- expect(current.handle.components.deploymentMode).toBe("multi");
191
199
  });
192
200
 
193
201
  /**
@@ -149,6 +149,9 @@ describe("configDefaultsFor", () => {
149
149
 
150
150
  it("takes nothing from the process's environment", () => {
151
151
  vi.stubEnv("REDIS_CLIENTS_URL", "redis://operator.test:6379");
152
- expect(configDefaultsFor(modules)[NOT_LOADED]).toEqual({ url: "redis://localhost:6379" });
152
+ expect(configDefaultsFor(modules)[NOT_LOADED]).toEqual({
153
+ url: "redis://localhost:6379",
154
+ assumeNoEviction: false,
155
+ });
153
156
  });
154
157
  });
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright 2026 1o1 Co. Ltd.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * The MFA switch, `mfaMode`, as this project's own configuration files write
19
+ * it with `MFA_MODE` unset: `{environment}.conf` over `application.conf` over
20
+ * the template's `config/reference.conf`. The template ships it `required`;
21
+ * a project `create-auth-provider --no-mfa` scaffolds writes it `off` at the
22
+ * end of its `application.conf`. A suite whose subject depends on the switch
23
+ * asserts what MFA on (`required`, as the template ships it) or off (as
24
+ * `--no-mfa` writes it) implies, so the suite that ships with a scaffold holds
25
+ * in either project. A project whose files write another mode, such as
26
+ * `optional`, is not what these suites claim to cover.
27
+ *
28
+ * Read with the HOCON parser alone, not through phase one (`readSwitches`),
29
+ * so that a suite about phase one can state what phase one should answer.
30
+ */
31
+
32
+ import { join } from "node:path";
33
+ import { fileURLToPath } from "node:url";
34
+ import { type Config, empty, parseFile } from "@o3co/ts.hocon";
35
+ import { type MfaSwitch, mfaSwitchSchema } from "#/sections.mjs";
36
+
37
+ const configDir = fileURLToPath(new URL("../../config", import.meta.url));
38
+
39
+ /** `mfaMode` as the files selected by `environment` write it, with no variable set. */
40
+ export function configuredMfaMode(environment: string): MfaSwitch {
41
+ const resolved = [`${environment}.conf`, "application.conf", "reference.conf"]
42
+ .reduce<Config>(
43
+ (layered, file) => layered.withFallback(parseFile(join(configDir, file), { env: {} })),
44
+ empty(),
45
+ )
46
+ .toObject() as Record<string, unknown>;
47
+ return mfaSwitchSchema.parse(resolved.mfaMode);
48
+ }
@@ -22,9 +22,8 @@
22
22
  * bundles are listed whatever the map says (`federation-types.test.mts`
23
23
  * boots them). A variable the
24
24
  * template bound before, `FEDERATIONS_<NAME>_<KEY>`, set alone or beside its
25
- * new name at a different value is refused before any module is chosen,
26
- * naming the new variable and path and never a value; the two at one value
27
- * are accepted. The map written at the top level refuses boot, naming its
25
+ * new name, at any value, is refused before any module is chosen, naming the
26
+ * new variable and path and never a value. The map written at the top level refuses boot, naming its
28
27
  * paths under `core.federations`.
29
28
  */
30
29
 
@@ -206,9 +205,13 @@ describe("a variable the template bound before, FEDERATIONS_<NAME>_<KEY>", () =>
206
205
  );
207
206
 
208
207
  it.each(RENAMED)(
209
- "core.federations.%s.%s: %s beside its new name at the same value is read",
210
- (name, key, old, variable, value) => {
211
- expect(String(federationsOf({ [old]: value, [variable]: value })[name]?.[key])).toBe(value);
208
+ "core.federations.%s.%s: %s beside its new name at the same value is refused all the same",
209
+ (_name, _key, old, variable, value) => {
210
+ const err = refusal({ [old]: value, [variable]: value });
211
+ expect(err).toMatchObject({
212
+ reason: "environment-variable-renamed",
213
+ details: { renamed: [{ from: old, state: "different" }] },
214
+ });
212
215
  },
213
216
  );
214
217
  });
@@ -20,8 +20,8 @@
20
20
  * environment, phase one's switches, then the layers over every loaded
21
21
  * package's `reference.conf` handed to boot. A path they moved from, written
22
22
  * in the operator's own layer, refuses boot naming the new one;
23
- * `DEPLOYMENT_MODE`, renamed `CORE_DEPLOYMENT_MODE`, refuses boot unless the
24
- * new name carries the same value.
23
+ * `DEPLOYMENT_MODE`, renamed `CORE_DEPLOYMENT_MODE`, refuses boot while it is
24
+ * set, whatever the new name holds.
25
25
  */
26
26
 
27
27
  import { BootError } from "@o3co/auth-provider-core";
@@ -176,10 +176,12 @@ describe("DEPLOYMENT_MODE, renamed CORE_DEPLOYMENT_MODE, through the template's
176
176
  expect(err.message).not.toContain("new-mode-c81a");
177
177
  });
178
178
 
179
- it("set beside the new name at the same value: boots with that mode", async () => {
180
- const composition = await boot({ DEPLOYMENT_MODE: "single", CORE_DEPLOYMENT_MODE: "single" });
179
+ it("set beside the new name at the same value: refused all the same", async () => {
180
+ const err = await refused({ DEPLOYMENT_MODE: "single", CORE_DEPLOYMENT_MODE: "single" });
181
181
 
182
- expect(composition.handle.components.deploymentMode).toBe("single");
182
+ expect(err.details).toMatchObject({
183
+ renamed: [{ from: "DEPLOYMENT_MODE", to: "CORE_DEPLOYMENT_MODE", state: "different" }],
184
+ });
183
185
  });
184
186
 
185
187
  it("unset, with the new name set: boots with its mode", async () => {
@@ -44,7 +44,12 @@ import {
44
44
  resolveLayers,
45
45
  type Switches,
46
46
  } from "../configPath.mjs";
47
- import { httpModule, keyStoreModule, templateReference } from "../modules.mjs";
47
+ import {
48
+ httpModule,
49
+ keyStoreModule,
50
+ standaloneRedisClientsModule,
51
+ templateReference,
52
+ } from "../modules.mjs";
48
53
 
49
54
  /**
50
55
  * Boots the shipped config with EVERY documented override supplied the way an
@@ -134,7 +139,6 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
134
139
  OAUTH_SESSION_ENABLED: "false",
135
140
  OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED: "true",
136
141
  OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED: "true",
137
- OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY: "reject",
138
142
  OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED: "true",
139
143
  OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED: "false",
140
144
 
@@ -195,6 +199,7 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
195
199
  REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT: "3",
196
200
  REDIS_CLIENTS_URL: "redis://redis:6379",
197
201
  REDIS_CLIENTS_PASSWORD: "rt-family-password",
202
+ REDIS_CLIENTS_ASSUME_NO_EVICTION: "true",
198
203
  // The federation token store's Redis branch.
199
204
  REDIS_FEDERATION_TOKEN_STORE_KEY_PREFIX: "ft:",
200
205
  REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_MODE: "required",
@@ -311,139 +316,141 @@ const DELIBERATELY_UNSET: Readonly<Record<string, string>> = {
311
316
  OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS:
312
317
  "the variable of a removed key, and only captured — any value fails boot",
313
318
  OAUTH_ACCESS_TOKEN_EXPIRES_IN:
314
- "renamed OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN, and only captured — set alone, or to another value, it fails boot",
319
+ "renamed OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN, and only captured — set at any value, beside its new name or not, it fails boot",
315
320
  DEPLOYMENT_MODE:
316
- "renamed CORE_DEPLOYMENT_MODE, and only captured — set alone, or to another value, it fails boot",
321
+ "renamed CORE_DEPLOYMENT_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
317
322
  MEMORY_RATE_LIMITER_MAX_BUCKETS:
318
- "renamed CORE_RATE_LIMITER_MEMORY_MAX_BUCKETS, and only captured — set alone, or to another value, it fails boot",
323
+ "renamed CORE_RATE_LIMITER_MEMORY_MAX_BUCKETS, and only captured — set at any value, beside its new name or not, it fails boot",
319
324
  RATE_LIMIT_FAIL_MODE:
320
- "renamed REDIS_RATE_LIMITER_FAIL_MODE, and only captured — set alone, or to another value, it fails boot",
325
+ "renamed REDIS_RATE_LIMITER_FAIL_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
321
326
  REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX:
322
- "renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX, and only captured — set alone, or to another value, it fails boot",
327
+ "renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX, and only captured — set at any value, beside its new name or not, it fails boot",
323
328
  REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT:
324
- "renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT, and only captured — set alone, or to another value, it fails boot",
329
+ "renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT, and only captured — set at any value, beside its new name or not, it fails boot",
325
330
  FEDERATION_GRANTS_ENCRYPTION_MODE:
326
- "renamed REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, and only captured — set alone, or to another value, it fails boot",
331
+ "renamed REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
327
332
  OAUTH_TOKEN_BINDING_DISPATCH_POLICY:
328
- "renamed CORE_TOKEN_BINDING_DISPATCH_POLICY, and only captured — set alone, or to another value, it fails boot",
333
+ "renamed CORE_TOKEN_BINDING_DISPATCH_POLICY, and only captured — set at any value, beside its new name or not, it fails boot",
329
334
  OAUTH_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS:
330
- "renamed CORE_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS, and only captured — set alone, or to another value, it fails boot",
335
+ "renamed CORE_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS, and only captured — set at any value, beside its new name or not, it fails boot",
331
336
  OAUTH_GRANTS_SESSION_ENABLED:
332
- "renamed OAUTH_SESSION_ENABLED, and only captured — set alone, or to another value, it fails boot",
337
+ "renamed OAUTH_SESSION_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
333
338
  OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED:
334
- "renamed OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED, and only captured — set alone, or to another value, it fails boot",
339
+ "renamed OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
335
340
  OAUTH_GRANTS_REFRESH_TOKEN_ENABLED:
336
- "renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED, and only captured — set alone, or to another value, it fails boot",
341
+ "renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
337
342
  OAUTH_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY:
338
- "renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY, and only captured — set alone, or to another value, it fails boot",
343
+ "the variable of a removed key, and only captured — any value fails boot",
344
+ OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY:
345
+ "the variable of a removed key, and only captured — any value fails boot",
339
346
  OAUTH_GRANTS_CLIENT_CREDENTIALS_ENABLED:
340
- "renamed OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED, and only captured — set alone, or to another value, it fails boot",
347
+ "renamed OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
341
348
  OAUTH_GRANTS_JWT_BEARER_ENABLED:
342
- "renamed OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED, and only captured — set alone, or to another value, it fails boot",
349
+ "renamed OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
343
350
  ENDPOINTS_CONSENT_URL:
344
- "renamed OAUTH_CONSENT_PAGE_URL, and only captured — set alone, or to another value, it fails boot",
351
+ "renamed OAUTH_CONSENT_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
345
352
  OAUTH_CIMD_ENABLED:
346
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED, and only captured — set alone, or to another value, it fails boot",
353
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
347
354
  OAUTH_CIMD_ALLOWED_SCOPES:
348
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_SCOPES, and only captured — set alone, or to another value, it fails boot",
355
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_SCOPES, and only captured — set at any value, beside its new name or not, it fails boot",
349
356
  OAUTH_CIMD_ALLOWED_AUDIENCES:
350
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_AUDIENCES, and only captured — set alone, or to another value, it fails boot",
357
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_AUDIENCES, and only captured — set at any value, beside its new name or not, it fails boot",
351
358
  OAUTH_CIMD_ALLOWED_HOSTS:
352
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_HOSTS, and only captured — set alone, or to another value, it fails boot",
359
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_HOSTS, and only captured — set at any value, beside its new name or not, it fails boot",
353
360
  OAUTH_CIMD_DENIED_HOSTS:
354
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_DENIED_HOSTS, and only captured — set alone, or to another value, it fails boot",
361
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_DENIED_HOSTS, and only captured — set at any value, beside its new name or not, it fails boot",
355
362
  OAUTH_CIMD_MAX_BYTES:
356
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_BYTES, and only captured — set alone, or to another value, it fails boot",
363
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_BYTES, and only captured — set at any value, beside its new name or not, it fails boot",
357
364
  OAUTH_CIMD_TIMEOUT_MS:
358
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_TIMEOUT_MS, and only captured — set alone, or to another value, it fails boot",
365
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_TIMEOUT_MS, and only captured — set at any value, beside its new name or not, it fails boot",
359
366
  OAUTH_CIMD_CACHE_MAX_AGE_MS:
360
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_CACHE_MAX_AGE_MS, and only captured — set alone, or to another value, it fails boot",
367
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_CACHE_MAX_AGE_MS, and only captured — set at any value, beside its new name or not, it fails boot",
361
368
  OAUTH_CIMD_MAX_CACHE_ENTRIES:
362
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CACHE_ENTRIES, and only captured — set alone, or to another value, it fails boot",
369
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CACHE_ENTRIES, and only captured — set at any value, beside its new name or not, it fails boot",
363
370
  OAUTH_CIMD_STALE_IF_ERROR_MS:
364
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_STALE_IF_ERROR_MS, and only captured — set alone, or to another value, it fails boot",
371
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_STALE_IF_ERROR_MS, and only captured — set at any value, beside its new name or not, it fails boot",
365
372
  OAUTH_CIMD_NEGATIVE_CACHE_MS:
366
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_NEGATIVE_CACHE_MS, and only captured — set alone, or to another value, it fails boot",
373
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_NEGATIVE_CACHE_MS, and only captured — set at any value, beside its new name or not, it fails boot",
367
374
  OAUTH_CIMD_MAX_CONCURRENT_FETCHES:
368
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CONCURRENT_FETCHES, and only captured — set alone, or to another value, it fails boot",
375
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CONCURRENT_FETCHES, and only captured — set at any value, beside its new name or not, it fails boot",
369
376
  OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256:
370
377
  "the authorization-code grant's pkce block was removed, and this is only captured — set at all, it fails boot",
371
378
  ENDPOINTS_LOGIN_URL:
372
- "renamed SESSION_LOGIN_PAGE_URL, and only captured — set alone, or to another value, it fails boot",
379
+ "renamed SESSION_LOGIN_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
373
380
  SESSION_SECRET:
374
- "renamed SESSION_STORE_SECRET, and only captured — set alone, or to another value, it fails boot",
381
+ "renamed SESSION_STORE_SECRET, and only captured — set at any value, beside its new name or not, it fails boot",
375
382
  SESSION_NAME:
376
- "renamed SESSION_STORE_NAME, and only captured — set alone, or to another value, it fails boot",
383
+ "renamed SESSION_STORE_NAME, and only captured — set at any value, beside its new name or not, it fails boot",
377
384
  SESSION_MAX_AGE:
378
- "renamed SESSION_STORE_MAX_AGE, and only captured — set alone, or to another value, it fails boot",
385
+ "renamed SESSION_STORE_MAX_AGE, and only captured — set at any value, beside its new name or not, it fails boot",
379
386
  SESSION_SECURE:
380
- "renamed SESSION_STORE_SECURE, and only captured — set alone, or to another value, it fails boot",
387
+ "renamed SESSION_STORE_SECURE, and only captured — set at any value, beside its new name or not, it fails boot",
381
388
  SESSION_SAME_SITE:
382
- "renamed SESSION_STORE_SAME_SITE, and only captured — set alone, or to another value, it fails boot",
389
+ "renamed SESSION_STORE_SAME_SITE, and only captured — set at any value, beside its new name or not, it fails boot",
383
390
  SESSION_DOMAIN:
384
- "renamed SESSION_STORE_DOMAIN, and only captured — set alone, or to another value, it fails boot",
391
+ "renamed SESSION_STORE_DOMAIN, and only captured — set at any value, beside its new name or not, it fails boot",
385
392
  SESSION_STORAGE_TYPE:
386
- "renamed SESSION_STORE_STORAGE_TYPE, and only captured — set alone, or to another value, it fails boot",
393
+ "renamed SESSION_STORE_STORAGE_TYPE, and only captured — set at any value, beside its new name or not, it fails boot",
387
394
  SESSION_STORAGE_REDIS_URL:
388
- "renamed SESSION_STORE_STORAGE_REDIS_URL, and only captured — set alone, or to another value, it fails boot",
395
+ "renamed SESSION_STORE_STORAGE_REDIS_URL, and only captured — set at any value, beside its new name or not, it fails boot",
389
396
  SESSION_STORAGE_REDIS_PASSWORD:
390
- "renamed SESSION_STORE_STORAGE_REDIS_PASSWORD, and only captured — set alone, or to another value, it fails boot",
397
+ "renamed SESSION_STORE_STORAGE_REDIS_PASSWORD, and only captured — set at any value, beside its new name or not, it fails boot",
391
398
  LOG_LEVEL:
392
- "renamed LOGGING_LEVEL, and only captured — set alone, or to another value, it fails boot",
399
+ "renamed LOGGING_LEVEL, and only captured — set at any value, beside its new name or not, it fails boot",
393
400
  CORS_ALLOWED_ORIGINS:
394
- "renamed HTTP_CORS_ALLOWED_ORIGINS, and only captured — set alone, or to another value, it fails boot",
401
+ "renamed HTTP_CORS_ALLOWED_ORIGINS, and only captured — set at any value, beside its new name or not, it fails boot",
395
402
  OAUTH_JWT_SIGNING_KEY_PROVIDER:
396
- "renamed KEY_STORE_PROVIDER, and only captured — set alone, or to another value, it fails boot",
403
+ "renamed KEY_STORE_PROVIDER, and only captured — set at any value, beside its new name or not, it fails boot",
397
404
  OAUTH_JWT_ALGORITHM:
398
- "renamed KEY_STORE_LOCAL_ALGORITHM, and only captured — set alone, or to another value, it fails boot",
405
+ "renamed KEY_STORE_LOCAL_ALGORITHM, and only captured — set at any value, beside its new name or not, it fails boot",
399
406
  OAUTH_JWT_KID:
400
- "renamed KEY_STORE_LOCAL_KID, and only captured — set alone, or to another value, it fails boot",
407
+ "renamed KEY_STORE_LOCAL_KID, and only captured — set at any value, beside its new name or not, it fails boot",
401
408
  OAUTH_JWT_SECRET:
402
- "renamed KEY_STORE_LOCAL_SECRET, and only captured — set alone, or to another value, it fails boot",
409
+ "renamed KEY_STORE_LOCAL_SECRET, and only captured — set at any value, beside its new name or not, it fails boot",
403
410
  OAUTH_JWT_PRIVATE_KEY_PATH:
404
- "renamed KEY_STORE_LOCAL_PRIVATE_KEY_PATH, and only captured — set alone, or to another value, it fails boot",
411
+ "renamed KEY_STORE_LOCAL_PRIVATE_KEY_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
405
412
  OAUTH_JWT_PUBLIC_KEY_PATH:
406
- "renamed KEY_STORE_LOCAL_PUBLIC_KEY_PATH, and only captured — set alone, or to another value, it fails boot",
413
+ "renamed KEY_STORE_LOCAL_PUBLIC_KEY_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
407
414
  OAUTH_JWT_PRIVATE_KEY:
408
- "renamed KEY_STORE_LOCAL_PRIVATE_KEY, and only captured — set alone, or to another value, it fails boot",
415
+ "renamed KEY_STORE_LOCAL_PRIVATE_KEY, and only captured — set at any value, beside its new name or not, it fails boot",
409
416
  OAUTH_JWT_PUBLIC_KEY:
410
- "renamed KEY_STORE_LOCAL_PUBLIC_KEY, and only captured — set alone, or to another value, it fails boot",
417
+ "renamed KEY_STORE_LOCAL_PUBLIC_KEY, and only captured — set at any value, beside its new name or not, it fails boot",
411
418
  REFRESH_TOKEN_FAMILY_STORE_REDIS_URL:
412
- "renamed REDIS_CLIENTS_URL, and only captured — set alone, or to another value, it fails boot",
419
+ "renamed REDIS_CLIENTS_URL, and only captured — set at any value, beside its new name or not, it fails boot",
413
420
  REFRESH_TOKEN_FAMILY_STORE_REDIS_PASSWORD:
414
- "renamed REDIS_CLIENTS_PASSWORD, and only captured — set alone, or to another value, it fails boot",
421
+ "renamed REDIS_CLIENTS_PASSWORD, and only captured — set at any value, beside its new name or not, it fails boot",
415
422
  CLIENT_PATH:
416
- "renamed REPOSITORIES_CLIENT_YAML_PATH, and only captured — set alone, or to another value, it fails boot",
423
+ "renamed REPOSITORIES_CLIENT_YAML_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
417
424
  CLIENT_USER_PATH:
418
- "renamed REPOSITORIES_USER_YAML_PATH, and only captured — set alone, or to another value, it fails boot",
425
+ "renamed REPOSITORIES_USER_YAML_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
419
426
  CLIENT_USER_AUTHENTICATE_URL:
420
- "renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_URL, and only captured — set alone, or to another value, it fails boot",
427
+ "renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
421
428
  CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL:
422
- "renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL, and only captured — set alone, or to another value, it fails boot",
429
+ "renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL, and only captured — set at any value, beside its new name or not, it fails boot",
423
430
  CLIENT_USER_LINK_FEDERATED_IDENTITY_URL:
424
- "renamed REPOSITORIES_USER_HTTP_LINK_FEDERATED_IDENTITY_URL, and only captured — set alone, or to another value, it fails boot",
431
+ "renamed REPOSITORIES_USER_HTTP_LINK_FEDERATED_IDENTITY_URL, and only captured — set at any value, beside its new name or not, it fails boot",
425
432
  CLIENT_USER_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL:
426
- "renamed REPOSITORIES_USER_HTTP_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL, and only captured — set alone, or to another value, it fails boot",
433
+ "renamed REPOSITORIES_USER_HTTP_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL, and only captured — set at any value, beside its new name or not, it fails boot",
427
434
  CLIENT_USER_BEARER_TOKEN:
428
- "renamed REPOSITORIES_USER_HTTP_BEARER_TOKEN, and only captured — set alone, or to another value, it fails boot",
435
+ "renamed REPOSITORIES_USER_HTTP_BEARER_TOKEN, and only captured — set at any value, beside its new name or not, it fails boot",
429
436
  CLIENT_USER_TIMEOUT:
430
- "renamed REPOSITORIES_USER_HTTP_TIMEOUT, and only captured — set alone, or to another value, it fails boot",
437
+ "renamed REPOSITORIES_USER_HTTP_TIMEOUT, and only captured — set at any value, beside its new name or not, it fails boot",
431
438
  CLIENT_USER_MAX_RESPONSE_BYTES:
432
- "renamed REPOSITORIES_USER_HTTP_MAX_RESPONSE_BYTES, and only captured — set alone, or to another value, it fails boot",
439
+ "renamed REPOSITORIES_USER_HTTP_MAX_RESPONSE_BYTES, and only captured — set at any value, beside its new name or not, it fails boot",
433
440
  CLIENT_CODE_DEFAULT_EXPIRES_IN:
434
- "renamed REDIS_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the Redis code repository) and STANDALONE_IN_MEMORY_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the in-process one), and only captured — set alone, or to another value, it fails boot",
441
+ "renamed REDIS_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the Redis code repository) and STANDALONE_IN_MEMORY_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the in-process one), and only captured — set at any value, beside its new name or not, it fails boot",
435
442
  CLIENT_CODE_KEY_PREFIX:
436
- "renamed REDIS_CODE_REPOSITORY_KEY_PREFIX, and only captured — set alone, or to another value, it fails boot",
443
+ "renamed REDIS_CODE_REPOSITORY_KEY_PREFIX, and only captured — set at any value, beside its new name or not, it fails boot",
437
444
  CLIENT_CODE_ENDPOINT_URI:
438
445
  "removed: the Redis code repository uses the shared redis-clients connection; only captured — set at all, it fails boot",
439
446
  CLIENT_CODE_PASSWORD:
440
447
  "removed: the Redis code repository uses the shared redis-clients connection; only captured — set at all, it fails boot",
441
448
  ENDPOINTS_MFA_URL:
442
- "renamed MFA_PAGE_URL, and only captured — set alone, or to another value, it fails boot where MFA is installed",
449
+ "renamed MFA_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
443
450
  MFA_TOTP_ENABLED:
444
- "renamed MFA_TOTP_FACTOR_ENABLED, and only captured — set alone, or to another value, it fails boot where MFA is installed",
451
+ "renamed MFA_TOTP_FACTOR_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
445
452
  MFA_TOTP_ISSUER:
446
- "renamed MFA_TOTP_FACTOR_ISSUER, and only captured — set alone, or to another value, it fails boot where MFA is installed",
453
+ "renamed MFA_TOTP_FACTOR_ISSUER, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
447
454
  };
448
455
 
449
456
  /**
@@ -480,11 +487,11 @@ function commentedFederationBindings(): string {
480
487
  }
481
488
 
482
489
  /**
483
- * The provider environment `o3co/auth`'s `tests/docker-compose.yml` sets,
484
- * transcribed: each renamed variable under its old and its new name, the two
485
- * at one value. The umbrella E2E boots the shipped template with exactly this,
486
- * so a parse failure here is a red umbrella build that this repository can see
487
- * first. `SESSION_STORE_SECURE=false` is the one it cannot run without: the
490
+ * The provider environment `o3co/auth`'s `tests/docker-compose.yml` is to
491
+ * set, transcribed: each renamed variable under its new name alone, since an
492
+ * old name set refuses boot beside its new one too. The umbrella E2E boots
493
+ * the shipped template with this, so a parse failure here is a red umbrella
494
+ * build that this repository can see first. `SESSION_STORE_SECURE=false` is the one it cannot run without: the
488
495
  * suite speaks plain HTTP.
489
496
  *
490
497
  * The two `FEDERATION_TOKEN_STORE` lines are required: the federation token
@@ -493,37 +500,23 @@ function commentedFederationBindings(): string {
493
500
  * Redis store is selected with its encryption key.
494
501
  */
495
502
  const UMBRELLA_E2E_ENV: Readonly<Record<string, string>> = {
496
- OAUTH_JWT_ALGORITHM: "HS256",
497
503
  KEY_STORE_LOCAL_ALGORITHM: "HS256",
498
- OAUTH_JWT_SECRET: "e2e-shared-hs256-secret.at-least-32-bytes.ok",
499
504
  KEY_STORE_LOCAL_SECRET: "e2e-shared-hs256-secret.at-least-32-bytes.ok",
500
505
  OAUTH_JWT_ISSUER: "https://auth.e2e.test",
501
- SESSION_SECRET: "lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y=",
502
506
  SESSION_STORE_SECRET: "lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y=",
503
- SESSION_SECURE: "false",
504
507
  SESSION_STORE_SECURE: "false",
505
- SESSION_NAME: "auth.session",
506
508
  SESSION_STORE_NAME: "auth.session",
507
- DEPLOYMENT_MODE: "multi",
508
509
  CORE_DEPLOYMENT_MODE: "multi",
509
- REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: "redis://redis:6379",
510
510
  REDIS_CLIENTS_URL: "redis://redis:6379",
511
- SESSION_STORAGE_REDIS_URL: "redis://redis:6379",
512
511
  SESSION_STORE_STORAGE_REDIS_URL: "redis://redis:6379",
513
- USER_SESSION_STORES_ADAPTER: "redis",
514
512
  ADAPTERS_USER_SESSION_STORES: "redis",
515
- RATE_LIMITER_ADAPTER: "redis",
516
513
  ADAPTERS_RATE_LIMITER: "redis",
517
- OAUTH_CODE_ADAPTER: "redis",
518
514
  ADAPTERS_CODE_REPOSITORY: "redis",
519
- FEDERATION_TOKEN_STORE_TYPE: "redis",
520
515
  ADAPTERS_FEDERATION_TOKEN_STORE: "redis",
521
516
  REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
522
- CLIENT_USER_TYPE: "yaml",
523
517
  ADAPTERS_USER_REPOSITORY: "yaml",
524
518
  OAUTH_RESOURCE_INDICATOR_ENABLED: "true",
525
519
  OAUTH_REQUIRE_EMAIL_VERIFIED: "true",
526
- OAUTH_GRANTS_SESSION_ENABLED: "true",
527
520
  OAUTH_SESSION_ENABLED: "true",
528
521
  MFA_MODE: "off",
529
522
  };
@@ -782,6 +775,11 @@ describe("the shipped config boots with every documented override supplied as a
782
775
  // A leftover string here would be read as "on" by a truthiness check
783
776
  // and as "off" by `=== true`, for a feature whose whole default is off.
784
777
  expect(federationGrantsSection(config).enabled).toBe(true);
778
+ expect(
779
+ standaloneRedisClientsModule.section?.schema.parse(
780
+ (config as Record<string, unknown>)["redis-clients"],
781
+ ),
782
+ ).toMatchObject({ assumeNoEviction: true });
785
783
  });
786
784
 
787
785
  it("turns every non-boolean override into its declared type", async () => {
@@ -1245,12 +1243,18 @@ describe("the shipped config boots with every documented override supplied as a
1245
1243
  ).rejects.toMatchObject({ reason: "environment-variable-renamed" });
1246
1244
  });
1247
1245
 
1248
- it("boots with OAUTH_ACCESS_TOKEN_EXPIRES_IN beside its new name set to the same value, reading the new name", async () => {
1249
- const config = await bootParsed({
1246
+ it("refuses OAUTH_ACCESS_TOKEN_EXPIRES_IN beside its new name set to the same value", async () => {
1247
+ const booting = bootParsed({
1250
1248
  ...DOCUMENTED_ENV,
1251
1249
  OAUTH_ACCESS_TOKEN_EXPIRES_IN:
1252
1250
  DOCUMENTED_ENV.OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN as string,
1253
1251
  });
1252
+ await expect(booting).rejects.toMatchObject({ reason: "environment-variable-renamed" });
1253
+ await expect(booting).rejects.toThrow(/unset OAUTH_ACCESS_TOKEN_EXPIRES_IN/);
1254
+ });
1255
+
1256
+ it("boots with OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN alone, reading it", async () => {
1257
+ const config = await bootParsed(DOCUMENTED_ENV);
1254
1258
  expect(oauthSection(config).accessToken).toEqual({
1255
1259
  defaultExpiresIn: 900,
1256
1260
  maxExpiresIn: 7200,
@@ -85,6 +85,10 @@ vi.mock("ioredis", () => {
85
85
  quit: async () => "OK",
86
86
  disconnect: () => undefined,
87
87
  ping: async () => "PONG",
88
+ // The Redis stores' eviction gate reads the policy from INFO memory: a
89
+ // default server's.
90
+ info: async (section: string) =>
91
+ section === "memory" ? "# Memory\r\nmaxmemory_policy:noeviction\r\n" : null,
88
92
  };
89
93
  const makeMockRedis = (): object =>
90
94
  new Proxy(
@@ -0,0 +1,113 @@
1
+ /*
2
+ * Copyright 2026 1o1 Co. Ltd.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * The template's two logout endpoints and the session cookie: the session
19
+ * store module expires the cookie of a session destroyed during the request,
20
+ * whichever route destroyed it, so `POST /oauth/logout` — whose package knows
21
+ * no cookie name — and `POST /session/logout` both answer with the cookie
22
+ * expired, with the attributes the login set it with.
23
+ */
24
+
25
+ import type { SessionCookiePolicy } from "@o3co/auth-provider-core";
26
+ import request from "supertest";
27
+ import { describe, expect, it } from "vitest";
28
+ import { authorize, codeFrom, compose, login, redeem } from "./all-modules-composition.fixture.mjs";
29
+
30
+ /** The `Set-Cookie` lines among `lines` for the cookie `name`. */
31
+ const linesNamed = (lines: readonly string[], name: string): string[] =>
32
+ lines.filter((line) => line.startsWith(`${name}=`));
33
+
34
+ /** A response's `Set-Cookie` lines. */
35
+ const setCookies = (res: request.Response): string[] =>
36
+ ([] as string[]).concat(res.headers["set-cookie"] ?? []);
37
+
38
+ /** A `Set-Cookie` line's attributes, lower-cased, with its value and expiry left out. */
39
+ const attributesOf = (line: string): string[] =>
40
+ line
41
+ .split(";")
42
+ .slice(1)
43
+ .map((part) => part.trim().toLowerCase())
44
+ .filter((part) => !part.startsWith("expires=") && !part.startsWith("max-age="))
45
+ .sort();
46
+
47
+ /** The CSRF token a login reissued, read off its double-submit cookie. */
48
+ const csrfTokenOf = (cookies: readonly string[]): string => {
49
+ const cookie = cookies.find((c) => /^[^=]*\.csrf=/.test(c));
50
+ if (cookie === undefined) throw new Error("the login reissued no CSRF cookie");
51
+ return decodeURIComponent(cookie.slice(cookie.indexOf("=") + 1).split(";")[0] ?? "");
52
+ };
53
+
54
+ /**
55
+ * The one `Set-Cookie` line of `res` for the session cookie, checked to
56
+ * expire it with the attributes of `setLine`, the line the login set it with.
57
+ */
58
+ function expectExpired(res: request.Response, name: string, setLine: string): void {
59
+ const lines = linesNamed(setCookies(res), name);
60
+ expect(lines).toHaveLength(1);
61
+ const [line] = lines as [string];
62
+ expect(line.startsWith(`${name}=;`)).toBe(true);
63
+ expect(line).toContain("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
64
+ expect(attributesOf(line)).toEqual(attributesOf(setLine));
65
+ }
66
+
67
+ describe("the session cookie at the template's logout endpoints", () => {
68
+ it("POST /oauth/logout expires the session cookie, with the attributes the login set it with", async () => {
69
+ const { app, handle } = await compose();
70
+ try {
71
+ const policy = handle.components.sessionCookiePolicy as SessionCookiePolicy;
72
+ const { res: signedIn, cookies } = await login(app);
73
+ expect(signedIn.status).toBe(200);
74
+ const [setLine] = linesNamed(cookies, policy.name);
75
+ expect(setLine).toBeDefined();
76
+ const tokens = await redeem(app, codeFrom(await authorize(app, cookies)));
77
+ expect(tokens.status).toBe(200);
78
+
79
+ const res = await request(app)
80
+ .post("/oauth/logout")
81
+ .set("Cookie", cookies)
82
+ .type("form")
83
+ .send({ id_token_hint: tokens.body.id_token as string });
84
+
85
+ expect(res.status).toBe(200);
86
+ expect(res.body).toEqual({ logged_out: true });
87
+ expectExpired(res, policy.name, setLine as string);
88
+ } finally {
89
+ await handle.dispose();
90
+ }
91
+ });
92
+
93
+ it("POST /session/logout expires the session cookie, with the attributes the login set it with", async () => {
94
+ const { app, handle } = await compose();
95
+ try {
96
+ const policy = handle.components.sessionCookiePolicy as SessionCookiePolicy;
97
+ const { res: signedIn, cookies } = await login(app);
98
+ expect(signedIn.status).toBe(200);
99
+ const [setLine] = linesNamed(cookies, policy.name);
100
+ expect(setLine).toBeDefined();
101
+
102
+ const res = await request(app)
103
+ .post("/session/logout")
104
+ .set("Cookie", cookies)
105
+ .set("x-csrf-token", csrfTokenOf(cookies));
106
+
107
+ expect(res.status).toBe(200);
108
+ expectExpired(res, policy.name, setLine as string);
109
+ } finally {
110
+ await handle.dispose();
111
+ }
112
+ });
113
+ });