@o3co/create-auth-provider 0.17.0 → 0.18.0-rc.1
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.
- package/README.md +2 -198
- package/main.js +4 -0
- package/package.json +8 -30
- package/README.ja.md +0 -200
- package/dist/cli.d.mts +0 -3
- package/dist/cli.d.mts.map +0 -1
- package/dist/cli.mjs +0 -18
- package/dist/index.d.mts +0 -48
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -325
- package/dist/internal/template-filter.d.mts +0 -14
- package/dist/internal/template-filter.d.mts.map +0 -1
- package/dist/internal/template-filter.mjs +0 -42
- package/templates/standalone/.dockerignore +0 -23
- package/templates/standalone/.env.example +0 -152
- package/templates/standalone/Dockerfile +0 -125
- package/templates/standalone/Makefile +0 -12
- package/templates/standalone/README.ja.md +0 -912
- package/templates/standalone/README.md +0 -1536
- package/templates/standalone/config/application.conf +0 -351
- package/templates/standalone/config/clients.yaml +0 -4
- package/templates/standalone/config/clients.yaml.example +0 -124
- package/templates/standalone/config/development.conf +0 -17
- package/templates/standalone/config/mailpit.conf +0 -13
- package/templates/standalone/config/production.conf +0 -15
- package/templates/standalone/config/reference.conf +0 -427
- package/templates/standalone/docker-compose.mailpit.yml +0 -39
- package/templates/standalone/docker-compose.production.yml +0 -229
- package/templates/standalone/docker-compose.test.yml +0 -16
- package/templates/standalone/docker-compose.yml +0 -67
- package/templates/standalone/gitignore +0 -23
- package/templates/standalone/package.json +0 -56
- package/templates/standalone/src/__tests__/adapters.test.mts +0 -425
- package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +0 -87
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +0 -1188
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +0 -285
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +0 -1154
- package/templates/standalone/src/__tests__/audit-sink.test.mts +0 -355
- package/templates/standalone/src/__tests__/backchannel-peer.fixture.mts +0 -64
- package/templates/standalone/src/__tests__/config-notices.test.mts +0 -157
- package/templates/standalone/src/__tests__/configPath.test.mts +0 -69
- package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +0 -48
- package/templates/standalone/src/__tests__/core-federations.test.mts +0 -252
- package/templates/standalone/src/__tests__/core-sections.test.mts +0 -192
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +0 -95
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +0 -1291
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +0 -787
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +0 -464
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +0 -141
- package/templates/standalone/src/__tests__/federation-types.test.mts +0 -239
- package/templates/standalone/src/__tests__/frontchannel-logout-csp.test.mts +0 -168
- package/templates/standalone/src/__tests__/google-session.fixture.mts +0 -318
- package/templates/standalone/src/__tests__/host-routes.test.mts +0 -134
- package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +0 -29
- package/templates/standalone/src/__tests__/library-references.fixture.mts +0 -188
- package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +0 -217
- package/templates/standalone/src/__tests__/listen.test.mts +0 -121
- package/templates/standalone/src/__tests__/logger-projection.test.mts +0 -77
- package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +0 -113
- package/templates/standalone/src/__tests__/mail-sender.test.mts +0 -134
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -393
- package/templates/standalone/src/__tests__/metrics.test.mts +0 -253
- package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +0 -122
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +0 -127
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +0 -883
- package/templates/standalone/src/__tests__/own-modules.test.mts +0 -1480
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +0 -183
- package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +0 -283
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +0 -135
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +0 -214
- package/templates/standalone/src/__tests__/redis-stand-in.fixture.mts +0 -98
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +0 -249
- package/templates/standalone/src/__tests__/replica-safety.test.mts +0 -657
- package/templates/standalone/src/__tests__/repositories-logger.test.mts +0 -119
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +0 -146
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +0 -610
- package/templates/standalone/src/__tests__/sections.test.mts +0 -516
- package/templates/standalone/src/__tests__/session-lifecycle.test.mts +0 -213
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +0 -305
- package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +0 -130
- package/templates/standalone/src/__tests__/session-requirements.test.mts +0 -382
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +0 -274
- package/templates/standalone/src/__tests__/shutdown.test.mts +0 -622
- package/templates/standalone/src/__tests__/smoke.test.mts +0 -642
- package/templates/standalone/src/__tests__/startup-log-lines.test.mts +0 -164
- package/templates/standalone/src/__tests__/store-credential.test.mts +0 -314
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +0 -296
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +0 -265
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +0 -605
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +0 -385
- package/templates/standalone/src/adapters.mts +0 -162
- package/templates/standalone/src/app.mts +0 -127
- package/templates/standalone/src/bootRefusal.mts +0 -117
- package/templates/standalone/src/buildModules.mts +0 -358
- package/templates/standalone/src/configPath.mts +0 -487
- package/templates/standalone/src/listen.mts +0 -60
- package/templates/standalone/src/logger.mts +0 -80
- package/templates/standalone/src/metrics.mts +0 -167
- package/templates/standalone/src/mfaSwitch.mts +0 -284
- package/templates/standalone/src/modules.mts +0 -705
- package/templates/standalone/src/rootRenames.mts +0 -95
- package/templates/standalone/src/routes.mts +0 -89
- package/templates/standalone/src/sections.mts +0 -378
- package/templates/standalone/src/securityHeaders.mts +0 -36
- package/templates/standalone/src/shutdown.mts +0 -213
- package/templates/standalone/tests/Makefile +0 -5
- package/templates/standalone/tests/index.test.js +0 -283
- package/templates/standalone/tests/package-lock.json +0 -1617
- package/templates/standalone/tests/package.json +0 -16
- package/templates/standalone/tests/vitest.config.mjs +0 -26
- package/templates/standalone/tsconfig.json +0 -31
- package/templates/standalone/vitest.config.mts +0 -31
- package/templates/standalone/vitest.supertest-loopback.mts +0 -278
- package/templates/versions.json +0 -20
|
@@ -1,657 +0,0 @@
|
|
|
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 standalone's own store modules under `core.deployment.mode = "multi"`,
|
|
19
|
-
* booted the way an operator reaches them: from the shipped HOCON with one
|
|
20
|
-
* environment variable flipped.
|
|
21
|
-
*
|
|
22
|
-
* - Every memory branch must be refused by name, and the all-Redis
|
|
23
|
-
* environment must boot. The replica-safety guard reads each module's
|
|
24
|
-
* manifest declaration, which covers the template's own in-memory modules
|
|
25
|
-
* and express-session's store (`sessionStoreModule`, which declares from
|
|
26
|
-
* its own section).
|
|
27
|
-
* - `federationTokenStore.type = "redis"` mounts
|
|
28
|
-
* `redisFederationTokenStoreModule` off the shared ioredis socket.
|
|
29
|
-
* - The Redis federation store's `allow-plaintext` guard reads the
|
|
30
|
-
* environment the config was selected by (`CONFIG_ENV || NODE_ENV`, passed
|
|
31
|
-
* through `buildModules`) and refuses under `core.deployment.mode = "multi"` in
|
|
32
|
-
* every environment.
|
|
33
|
-
*
|
|
34
|
-
* ioredis, node-redis and connect-redis are mocked, as in
|
|
35
|
-
* `device-code-store-client-module.test.mts`: the point is composition and
|
|
36
|
-
* the boot planner's stage-1 verdict, not Redis. Nothing here issues a
|
|
37
|
-
* command.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
import { fileURLToPath } from "node:url";
|
|
41
|
-
import {
|
|
42
|
-
createApp,
|
|
43
|
-
createKeyStoreFactory,
|
|
44
|
-
defineModule,
|
|
45
|
-
InMemoryClientRepository,
|
|
46
|
-
InMemoryUserRepository,
|
|
47
|
-
type Module,
|
|
48
|
-
memoryRefreshTokenFamilyStoreModule,
|
|
49
|
-
registerBuiltinKeyStores,
|
|
50
|
-
replicaUnsafeReason,
|
|
51
|
-
} from "@o3co/auth-provider-core";
|
|
52
|
-
import { sessionStoreModule } from "@o3co/auth-provider-session";
|
|
53
|
-
import { standardSmtpMailSenderConfigForTests } from "@o3co/auth-provider-standard/testing";
|
|
54
|
-
import { parseFile } from "@o3co/ts.hocon";
|
|
55
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
56
|
-
import { buildModules } from "../buildModules.mjs";
|
|
57
|
-
import { resolveConfigPaths } from "../configPath.mjs";
|
|
58
|
-
import { templateReference } from "../modules.mjs";
|
|
59
|
-
import { keyStoreSectionSchema } from "../sections.mjs";
|
|
60
|
-
import {
|
|
61
|
-
type BothPhases,
|
|
62
|
-
bothPhasesOf,
|
|
63
|
-
capturedRenames,
|
|
64
|
-
libraryLayers,
|
|
65
|
-
} from "./library-references.fixture.mjs";
|
|
66
|
-
|
|
67
|
-
// The redis session-store builder, which the baseline selects, dynamically
|
|
68
|
-
// imports these; mock them so no socket opens.
|
|
69
|
-
vi.mock("redis", () => ({
|
|
70
|
-
createClient: vi.fn(() => ({
|
|
71
|
-
connect: vi.fn().mockResolvedValue(undefined),
|
|
72
|
-
quit: vi.fn().mockResolvedValue(undefined),
|
|
73
|
-
ping: vi.fn().mockResolvedValue("PONG"),
|
|
74
|
-
on: vi.fn(),
|
|
75
|
-
})),
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
vi.mock("connect-redis", async () => {
|
|
79
|
-
const { EventEmitter } = await import("node:events");
|
|
80
|
-
// express-session subscribes to store events, so the fake must be an
|
|
81
|
-
// EventEmitter rather than a plain object.
|
|
82
|
-
return {
|
|
83
|
-
RedisStore: class MockRedisStore extends EventEmitter {
|
|
84
|
-
constructor(_opts: { client: unknown }) {
|
|
85
|
-
super();
|
|
86
|
-
}
|
|
87
|
-
get(): unknown {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
set(): void {}
|
|
91
|
-
destroy(): void {}
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
vi.mock("ioredis", () => {
|
|
97
|
-
// Every command resolves to nothing. A boot that reached Redis would be a
|
|
98
|
-
// boot doing work at stage 2 that belongs in an integration test; the
|
|
99
|
-
// stand-in only has to be constructible, quit cleanly, and answer `ping`
|
|
100
|
-
// for the readiness probe the shared clients module registers.
|
|
101
|
-
const explicit: Record<string, unknown> = {
|
|
102
|
-
on: () => undefined,
|
|
103
|
-
quit: async () => "OK",
|
|
104
|
-
disconnect: () => undefined,
|
|
105
|
-
ping: async () => "PONG",
|
|
106
|
-
// The Redis stores' eviction gate reads the policy from INFO memory: a
|
|
107
|
-
// default server's.
|
|
108
|
-
info: async (section: string) =>
|
|
109
|
-
section === "memory" ? "# Memory\r\nmaxmemory_policy:noeviction\r\n" : null,
|
|
110
|
-
};
|
|
111
|
-
const makeMockRedis = (): object =>
|
|
112
|
-
new Proxy(
|
|
113
|
-
{},
|
|
114
|
-
{
|
|
115
|
-
get(_target, prop) {
|
|
116
|
-
// A `then` that is a function would make the instance a
|
|
117
|
-
// thenable and hang anything that awaits it.
|
|
118
|
-
if (typeof prop !== "string" || prop === "then") return undefined;
|
|
119
|
-
if (prop === "duplicate") return makeMockRedis;
|
|
120
|
-
if (prop in explicit) return explicit[prop];
|
|
121
|
-
return async () => null;
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
// The template does `new Redis(url, options)`. A plain function that
|
|
126
|
-
// returns an object yields that object under `new`, so no class — and no
|
|
127
|
-
// constructor returning a value — is needed for the stand-in.
|
|
128
|
-
function MockRedis(): object {
|
|
129
|
-
return makeMockRedis();
|
|
130
|
-
}
|
|
131
|
-
return { Redis: MockRedis, default: MockRedis };
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// config/ is two levels above this test file: src/__tests__/ → src/ → standalone/
|
|
135
|
-
const configDir = fileURLToPath(new URL("../../config", import.meta.url));
|
|
136
|
-
|
|
137
|
-
/** 32 bytes, base64 — what `REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY` carries. */
|
|
138
|
-
const ENCRYPTION_KEY = Buffer.alloc(32, 7).toString("base64");
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* The umbrella E2E's shape (`o3co/auth` `tests/docker-compose.yml`), with
|
|
142
|
-
* every shared store on Redis, express-session's own included, and MFA off
|
|
143
|
-
* as the umbrella sets it. Each case below flips one variable off this.
|
|
144
|
-
*/
|
|
145
|
-
const ALL_REDIS_ENV: Readonly<Record<string, string>> = {
|
|
146
|
-
MFA_MODE: "off",
|
|
147
|
-
KEY_STORE_LOCAL_ALGORITHM: "HS256",
|
|
148
|
-
KEY_STORE_LOCAL_SECRET: "replica-safety-test-secret.at-least-32-bytes.ok",
|
|
149
|
-
OAUTH_JWT_ISSUER: "https://auth.test",
|
|
150
|
-
SESSION_STORE_SECRET: "replica-safety-session-secret.at-least-32-bytes.ok",
|
|
151
|
-
SESSION_STORE_SECURE: "false",
|
|
152
|
-
SESSION_STORE_NAME: "auth.session",
|
|
153
|
-
SESSION_STORE_STORAGE_TYPE: "redis",
|
|
154
|
-
SESSION_STORE_STORAGE_REDIS_URL: "redis://redis.test:6379",
|
|
155
|
-
ADAPTERS_USER_REPOSITORY: "yaml",
|
|
156
|
-
CORE_DEPLOYMENT_MODE: "multi",
|
|
157
|
-
REDIS_CLIENTS_URL: "redis://redis.test:6379",
|
|
158
|
-
ADAPTERS_USER_SESSION_STORES: "redis",
|
|
159
|
-
ADAPTERS_RATE_LIMITER: "redis",
|
|
160
|
-
// The login's attempt counter, shared: per process, `multi` refuses the login.
|
|
161
|
-
ADAPTERS_ATTEMPT_COUNTER: "redis",
|
|
162
|
-
ADAPTERS_CODE_REPOSITORY: "redis",
|
|
163
|
-
ADAPTERS_ACCESS_TOKEN_DENYLIST: "redis",
|
|
164
|
-
ADAPTERS_REPLAY_SEEN_SET: "redis",
|
|
165
|
-
ADAPTERS_FEDERATION_TOKEN_STORE: "redis",
|
|
166
|
-
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: ENCRYPTION_KEY,
|
|
167
|
-
// The consent step for clients that are not first-party, on the
|
|
168
|
-
// shared store — the one switch value `multi` accepts besides `none`.
|
|
169
|
-
ADAPTERS_CONSENT_STORE: "redis",
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
function resolveConfig(env: Record<string, string>): BothPhases {
|
|
173
|
-
const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "production");
|
|
174
|
-
const layers = parseFile(envConfPath, { env })
|
|
175
|
-
.withFallback(parseFile(applicationConfPath, { env }))
|
|
176
|
-
.withFallback(parseFile(fileURLToPath(templateReference()), { env }))
|
|
177
|
-
.withFallback(libraryLayers(env));
|
|
178
|
-
return {
|
|
179
|
-
...bothPhasesOf(layers, env),
|
|
180
|
-
// What the resolution captured of the renamed variables.
|
|
181
|
-
"renamed-variables": capturedRenames(env),
|
|
182
|
-
} as BothPhases;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Drops a variable, so the HOCON default takes over. */
|
|
186
|
-
function without(env: Record<string, string>, ...names: string[]): Record<string, string> {
|
|
187
|
-
const copy = { ...env };
|
|
188
|
-
for (const name of names) delete copy[name];
|
|
189
|
-
return copy;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// The file-system-backed modules are replaced as in `smoke.test.mts`: the
|
|
193
|
-
// boot under test is the store wiring, not the client registry on disk.
|
|
194
|
-
const testRepositoriesModule = defineModule({
|
|
195
|
-
name: "test:repositories",
|
|
196
|
-
provides: {
|
|
197
|
-
clientRepository: () => new InMemoryClientRepository(new Map()),
|
|
198
|
-
userRepository: () => new InMemoryUserRepository(new Map()),
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// The template's `key-store` section, read by its own schema, without the
|
|
203
|
-
// section's relocation bridge.
|
|
204
|
-
const testKeyStoreModule = defineModule({
|
|
205
|
-
name: "key-store",
|
|
206
|
-
section: { schema: keyStoreSectionSchema },
|
|
207
|
-
provides: {
|
|
208
|
-
keyStore: async ({ section }) => {
|
|
209
|
-
const factory = createKeyStoreFactory();
|
|
210
|
-
registerBuiltinKeyStores(factory);
|
|
211
|
-
return factory.create({
|
|
212
|
-
type: "local",
|
|
213
|
-
...(section.local ?? {}),
|
|
214
|
-
});
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const modulesFor = (config: BothPhases, environment?: string) =>
|
|
220
|
-
buildModules(config, {
|
|
221
|
-
keyStoreModule: testKeyStoreModule,
|
|
222
|
-
repositoriesModule: testRepositoriesModule,
|
|
223
|
-
...(environment === undefined ? {} : { environment }),
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* `module`'s section in `config`, for a declaration made from it: each module
|
|
228
|
-
* here that has one sits at its name.
|
|
229
|
-
*/
|
|
230
|
-
const sectionOf = (config: BothPhases, module: Module): unknown =>
|
|
231
|
-
(config as unknown as Record<string, unknown>)[module.name];
|
|
232
|
-
|
|
233
|
-
const boot = (config: BothPhases, environment?: string) =>
|
|
234
|
-
createApp({
|
|
235
|
-
modules: modulesFor(config, environment),
|
|
236
|
-
bootstrapComponents: { config, pathResolver: (s) => s },
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
/** The message of a boot failure and of the module error it wraps, together. */
|
|
240
|
-
const messageChain = (err: unknown): string => {
|
|
241
|
-
const e = err as { message?: string; cause?: { message?: string } };
|
|
242
|
-
return `${e.message ?? ""} ${e.cause?.message ?? ""}`;
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
describe('the standalone\'s memory modules are refused under core.deployment.mode = "multi"', () => {
|
|
246
|
-
let handleRef: Awaited<ReturnType<typeof boot>> | undefined;
|
|
247
|
-
|
|
248
|
-
afterEach(async () => {
|
|
249
|
-
await handleRef?.dispose();
|
|
250
|
-
handleRef = undefined;
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const cases: ReadonlyArray<readonly [variable: string, module: string]> = [
|
|
254
|
-
// The template's own modules.
|
|
255
|
-
["ADAPTERS_USER_SESSION_STORES", "standalone-in-memory-session-stores"],
|
|
256
|
-
["ADAPTERS_CODE_REPOSITORY", "standalone-in-memory-code-repository"],
|
|
257
|
-
["ADAPTERS_FEDERATION_TOKEN_STORE", "standalone-in-memory-federation-token-store"],
|
|
258
|
-
// express-session's own store. Not a module of this template: it
|
|
259
|
-
// declares from its own section, which boot parses.
|
|
260
|
-
["SESSION_STORE_STORAGE_TYPE", "session-store"],
|
|
261
|
-
// The consent store, wired only when the switch says so.
|
|
262
|
-
["ADAPTERS_CONSENT_STORE", "core-consent-store-memory"],
|
|
263
|
-
// Core's, selected by the same kind of switch.
|
|
264
|
-
["ADAPTERS_RATE_LIMITER", "core-rate-limiter-memory"],
|
|
265
|
-
["ADAPTERS_ACCESS_TOKEN_DENYLIST", "core-access-token-denylist-memory"],
|
|
266
|
-
// The jti single-use record behind private_key_jwt client auth.
|
|
267
|
-
["ADAPTERS_REPLAY_SEEN_SET", "core-replay-seen-set-memory"],
|
|
268
|
-
];
|
|
269
|
-
|
|
270
|
-
for (const [variable, module] of cases) {
|
|
271
|
-
it(`${variable}=memory is refused, naming ${module}`, async () => {
|
|
272
|
-
const config = resolveConfig({ ...ALL_REDIS_ENV, [variable]: "memory" });
|
|
273
|
-
await expect(boot(config)).rejects.toMatchObject({
|
|
274
|
-
name: "BootError",
|
|
275
|
-
reason: "replica-unsafe-adapter",
|
|
276
|
-
details: { modules: [module] },
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
it("names every memory module together when every switch is memory", async () => {
|
|
282
|
-
const config = resolveConfig({
|
|
283
|
-
...ALL_REDIS_ENV,
|
|
284
|
-
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
285
|
-
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
286
|
-
ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
|
|
287
|
-
SESSION_STORE_STORAGE_TYPE: "memory",
|
|
288
|
-
ADAPTERS_RATE_LIMITER: "memory",
|
|
289
|
-
ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
|
|
290
|
-
ADAPTERS_REPLAY_SEEN_SET: "memory",
|
|
291
|
-
ADAPTERS_CONSENT_STORE: "memory",
|
|
292
|
-
});
|
|
293
|
-
await expect(boot(config)).rejects.toMatchObject({
|
|
294
|
-
reason: "replica-unsafe-adapter",
|
|
295
|
-
details: {
|
|
296
|
-
modules: expect.arrayContaining(cases.map(([, module]) => module)),
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("each standalone memory module declares its consequence on its own manifest", () => {
|
|
302
|
-
// The declaration is what the guard reads, so it has to be on
|
|
303
|
-
// the module and it has to say what breaks — the guard quotes it.
|
|
304
|
-
const config = resolveConfig({
|
|
305
|
-
...ALL_REDIS_ENV,
|
|
306
|
-
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
307
|
-
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
308
|
-
ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
|
|
309
|
-
});
|
|
310
|
-
const standaloneMemoryModules = modulesFor(config).filter((m) =>
|
|
311
|
-
m.name.startsWith("standalone-in-memory-"),
|
|
312
|
-
);
|
|
313
|
-
expect(standaloneMemoryModules.map((m) => m.name).sort()).toEqual([
|
|
314
|
-
"standalone-in-memory-code-repository",
|
|
315
|
-
"standalone-in-memory-federation-token-store",
|
|
316
|
-
"standalone-in-memory-session-stores",
|
|
317
|
-
]);
|
|
318
|
-
for (const m of standaloneMemoryModules) {
|
|
319
|
-
expect(replicaUnsafeReason(m), m.name).toBeDefined();
|
|
320
|
-
expect((replicaUnsafeReason(m) ?? "").length, m.name).toBeGreaterThan(40);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("boots the all-Redis environment, with nothing declaring replica-unsafe state", async () => {
|
|
325
|
-
const config = resolveConfig(ALL_REDIS_ENV);
|
|
326
|
-
for (const m of modulesFor(config)) {
|
|
327
|
-
expect(replicaUnsafeReason(m, sectionOf(config, m)), m.name).toBeUndefined();
|
|
328
|
-
}
|
|
329
|
-
handleRef = await boot(config);
|
|
330
|
-
expect(handleRef).toBeDefined();
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
describe("express-session's store declares its replica safety from its own section", () => {
|
|
335
|
-
it("is the session package's sessionStoreModule, listed first", () => {
|
|
336
|
-
expect(modulesFor(resolveConfig(ALL_REDIS_ENV))[0]).toBe(sessionStoreModule);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
/** Each way the storage type is set, laid over the all-Redis environment. */
|
|
340
|
-
const STORAGE: ReadonlyArray<readonly [string, Record<string, string>]> = [
|
|
341
|
-
[
|
|
342
|
-
"SESSION_STORE_STORAGE_TYPE=memory",
|
|
343
|
-
{ ...ALL_REDIS_ENV, SESSION_STORE_STORAGE_TYPE: "memory" },
|
|
344
|
-
],
|
|
345
|
-
["SESSION_STORE_STORAGE_TYPE=redis", ALL_REDIS_ENV],
|
|
346
|
-
[
|
|
347
|
-
"no SESSION_STORE_STORAGE_TYPE, the reference's default",
|
|
348
|
-
without(ALL_REDIS_ENV, "SESSION_STORE_STORAGE_TYPE"),
|
|
349
|
-
],
|
|
350
|
-
[
|
|
351
|
-
"the variable it was renamed from, SESSION_STORAGE_TYPE=memory",
|
|
352
|
-
{ ...without(ALL_REDIS_ENV, "SESSION_STORE_STORAGE_TYPE"), SESSION_STORAGE_TYPE: "memory" },
|
|
353
|
-
],
|
|
354
|
-
];
|
|
355
|
-
const MODES: ReadonlyArray<
|
|
356
|
-
readonly [string, (env: Record<string, string>) => Record<string, string>]
|
|
357
|
-
> = [
|
|
358
|
-
["multi", (env) => ({ ...env, CORE_DEPLOYMENT_MODE: "multi" })],
|
|
359
|
-
["single", (env) => ({ ...env, CORE_DEPLOYMENT_MODE: "single" })],
|
|
360
|
-
["unset", (env) => without(env, "CORE_DEPLOYMENT_MODE")],
|
|
361
|
-
];
|
|
362
|
-
|
|
363
|
-
/** What a boot of `modules` settles as: the refusal, or the replica-safety warnings it logged. */
|
|
364
|
-
async function outcomeOf(config: BothPhases, modules: readonly Module[]): Promise<unknown> {
|
|
365
|
-
const warn = vi.fn();
|
|
366
|
-
const logger = {
|
|
367
|
-
warn,
|
|
368
|
-
info: vi.fn(),
|
|
369
|
-
error: vi.fn(),
|
|
370
|
-
debug: vi.fn(),
|
|
371
|
-
trace: vi.fn(),
|
|
372
|
-
fatal: vi.fn(),
|
|
373
|
-
child: vi.fn(),
|
|
374
|
-
};
|
|
375
|
-
try {
|
|
376
|
-
const handle = await createApp({
|
|
377
|
-
modules: [...modules],
|
|
378
|
-
bootstrapComponents: { config, logger, pathResolver: (s: string) => s } as never,
|
|
379
|
-
});
|
|
380
|
-
await handle.dispose();
|
|
381
|
-
} catch (err) {
|
|
382
|
-
const e = err as { reason?: string; message?: string; details?: unknown };
|
|
383
|
-
return { refused: { reason: e.reason, message: e.message, details: e.details } };
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
warned: warn.mock.calls.filter(([, event]) => event === "replica_unsafe_adapters"),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
for (const [storage, env] of STORAGE) {
|
|
391
|
-
for (const [mode, withMode] of MODES) {
|
|
392
|
-
it(`${storage} under ${mode}`, async () => {
|
|
393
|
-
const config = resolveConfig(withMode(env));
|
|
394
|
-
const modules = modulesFor(config);
|
|
395
|
-
expect(modules[0]).toBe(sessionStoreModule);
|
|
396
|
-
const outcome = await outcomeOf(config, modules);
|
|
397
|
-
if (
|
|
398
|
-
storage === "SESSION_STORE_STORAGE_TYPE=redis" ||
|
|
399
|
-
storage === "no SESSION_STORE_STORAGE_TYPE, the reference's default"
|
|
400
|
-
) {
|
|
401
|
-
// A shared store: nothing is refused or warned about.
|
|
402
|
-
expect(outcome).toEqual({ warned: [] });
|
|
403
|
-
}
|
|
404
|
-
if (storage === "SESSION_STORE_STORAGE_TYPE=memory") {
|
|
405
|
-
const named = { modules: ["session-store"] };
|
|
406
|
-
expect(outcome).toEqual(
|
|
407
|
-
mode === "multi"
|
|
408
|
-
? {
|
|
409
|
-
refused: expect.objectContaining({
|
|
410
|
-
reason: "replica-unsafe-adapter",
|
|
411
|
-
message: expect.stringContaining(
|
|
412
|
-
"session-store: the express-session store forks per replica",
|
|
413
|
-
),
|
|
414
|
-
details: expect.objectContaining(named),
|
|
415
|
-
}),
|
|
416
|
-
}
|
|
417
|
-
: {
|
|
418
|
-
warned:
|
|
419
|
-
mode === "unset"
|
|
420
|
-
? [[expect.objectContaining(named), "replica_unsafe_adapters"]]
|
|
421
|
-
: [],
|
|
422
|
-
},
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
if (storage === "the variable it was renamed from, SESSION_STORAGE_TYPE=memory") {
|
|
426
|
-
// Refused as renamed before the replica-safety guard reads anything.
|
|
427
|
-
expect(outcome).toEqual({
|
|
428
|
-
refused: expect.objectContaining({ reason: "environment-variable-renamed" }),
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
describe('adapters.federationTokenStore = "redis" in the standalone', () => {
|
|
437
|
-
let handleRef: Awaited<ReturnType<typeof boot>> | undefined;
|
|
438
|
-
|
|
439
|
-
afterEach(async () => {
|
|
440
|
-
await handleRef?.dispose();
|
|
441
|
-
handleRef = undefined;
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
it("reaches phase one's adapters from ADAPTERS_FEDERATION_TOKEN_STORE", () => {
|
|
445
|
-
expect(resolveConfig(ALL_REDIS_ENV).adapters.federationTokenStore).toBe("redis");
|
|
446
|
-
expect(
|
|
447
|
-
resolveConfig(without(ALL_REDIS_ENV, "ADAPTERS_FEDERATION_TOKEN_STORE")).adapters
|
|
448
|
-
.federationTokenStore,
|
|
449
|
-
).toBe("memory");
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it("selects the Redis module and the shared clients module, not the memory one", () => {
|
|
453
|
-
const modules = modulesFor(resolveConfig(ALL_REDIS_ENV));
|
|
454
|
-
const names = modules.map((m) => m.name);
|
|
455
|
-
expect(names).toContain("redis-federation-token-store");
|
|
456
|
-
expect(names).toContain("redis-clients");
|
|
457
|
-
expect(names).not.toContain("standalone-in-memory-federation-token-store");
|
|
458
|
-
// Exactly one provider for the slot: both modules provide it, so
|
|
459
|
-
// selecting both would be a boot-time slot collision.
|
|
460
|
-
const providers = modules.filter((m) =>
|
|
461
|
-
Object.keys(m.provides ?? {}).includes("federationTokenStore"),
|
|
462
|
-
);
|
|
463
|
-
expect(providers.map((m) => m.name)).toEqual(["redis-federation-token-store"]);
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it("pulls the shared clients module in for the federation store alone", () => {
|
|
467
|
-
// Every other store on memory, single replica: the Redis federation
|
|
468
|
-
// store still needs `federationTokenStoreClient`, which only the shared
|
|
469
|
-
// clients module provides. Without this the branch fails stage 1 with
|
|
470
|
-
// `missing-required-component`.
|
|
471
|
-
const config = resolveConfig({
|
|
472
|
-
...ALL_REDIS_ENV,
|
|
473
|
-
CORE_DEPLOYMENT_MODE: "single",
|
|
474
|
-
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
475
|
-
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
476
|
-
ADAPTERS_RATE_LIMITER: "memory",
|
|
477
|
-
ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
|
|
478
|
-
});
|
|
479
|
-
const names = modulesFor(config).map((m) => m.name);
|
|
480
|
-
expect(names).toContain("redis-federation-token-store");
|
|
481
|
-
expect(names).toContain("redis-clients");
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
it("boots, and the resolved store is the Redis adapter", async () => {
|
|
485
|
-
handleRef = await boot(resolveConfig(ALL_REDIS_ENV));
|
|
486
|
-
const store = handleRef.components.federationTokenStore;
|
|
487
|
-
expect(store?.kind).toBe("redis");
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
it("fails at boot, naming the key, when the encryption key is missing", async () => {
|
|
491
|
-
// The store encrypts long-lived IdP refresh tokens at rest; a Redis
|
|
492
|
-
// branch with no key is a misconfiguration to refuse at boot, not a
|
|
493
|
-
// store that works until the first federation login.
|
|
494
|
-
const config = resolveConfig(
|
|
495
|
-
without(ALL_REDIS_ENV, "REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY"),
|
|
496
|
-
);
|
|
497
|
-
await expect(boot(config)).rejects.toSatisfy((err: unknown) => {
|
|
498
|
-
const e = err as { message?: string; cause?: { message?: string } };
|
|
499
|
-
return /encryption\.key/.test(`${e.message ?? ""} ${e.cause?.message ?? ""}`);
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it("keeps the memory branch: the default, in single mode, resolves the memory adapter", async () => {
|
|
504
|
-
const config = resolveConfig({
|
|
505
|
-
...without(ALL_REDIS_ENV, "ADAPTERS_FEDERATION_TOKEN_STORE"),
|
|
506
|
-
CORE_DEPLOYMENT_MODE: "single",
|
|
507
|
-
});
|
|
508
|
-
const names = modulesFor(config).map((m) => m.name);
|
|
509
|
-
expect(names).toContain("standalone-in-memory-federation-token-store");
|
|
510
|
-
expect(names).not.toContain("redis-federation-token-store");
|
|
511
|
-
handleRef = await boot(config);
|
|
512
|
-
expect(handleRef.components.federationTokenStore?.kind).toBe("memory");
|
|
513
|
-
});
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
describe('consentStore.adapter = "redis" in the standalone', () => {
|
|
517
|
-
let handleRef: Awaited<ReturnType<typeof boot>> | undefined;
|
|
518
|
-
|
|
519
|
-
afterEach(async () => {
|
|
520
|
-
await handleRef?.dispose();
|
|
521
|
-
handleRef = undefined;
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
it("selects the Redis consent module, providing both slots, and not the memory one", () => {
|
|
525
|
-
const modules = modulesFor(resolveConfig(ALL_REDIS_ENV));
|
|
526
|
-
const names = modules.map((m) => m.name);
|
|
527
|
-
expect(names).toContain("redis-consent-store");
|
|
528
|
-
expect(names).not.toContain("core-consent-store-memory");
|
|
529
|
-
// Exactly one provider for each slot: the consent step refuses a
|
|
530
|
-
// composition with one and not the other, and two would collide.
|
|
531
|
-
for (const slot of ["consentStore", "pendingConsentStore"]) {
|
|
532
|
-
const providers = modules.filter((m) => Object.keys(m.provides ?? {}).includes(slot));
|
|
533
|
-
expect(
|
|
534
|
-
providers.map((m) => m.name),
|
|
535
|
-
slot,
|
|
536
|
-
).toEqual(["redis-consent-store"]);
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
it("pulls the shared clients module in for the consent stores alone", () => {
|
|
541
|
-
// Every other store on memory, single replica: the consent module still
|
|
542
|
-
// needs its two client slots, which only the shared clients module
|
|
543
|
-
// provides.
|
|
544
|
-
const config = resolveConfig({
|
|
545
|
-
...ALL_REDIS_ENV,
|
|
546
|
-
CORE_DEPLOYMENT_MODE: "single",
|
|
547
|
-
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
548
|
-
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
549
|
-
ADAPTERS_RATE_LIMITER: "memory",
|
|
550
|
-
ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
|
|
551
|
-
ADAPTERS_REPLAY_SEEN_SET: "memory",
|
|
552
|
-
ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
|
|
553
|
-
});
|
|
554
|
-
const names = buildModules(config, {
|
|
555
|
-
keyStoreModule: testKeyStoreModule,
|
|
556
|
-
repositoriesModule: testRepositoriesModule,
|
|
557
|
-
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
558
|
-
}).map((m) => m.name);
|
|
559
|
-
expect(names).toContain("redis-consent-store");
|
|
560
|
-
expect(names).toContain("redis-clients");
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
it("boots under CORE_DEPLOYMENT_MODE=multi with both slots resolved to the Redis adapters", async () => {
|
|
564
|
-
handleRef = await boot(resolveConfig(ALL_REDIS_ENV));
|
|
565
|
-
expect(handleRef.components.consentStore?.kind).toBe("redis");
|
|
566
|
-
expect(handleRef.components.pendingConsentStore?.kind).toBe("redis");
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
describe("the Redis federation store's plaintext guard, booted from the shipped config", () => {
|
|
571
|
-
let handleRef: Awaited<ReturnType<typeof boot>> | undefined;
|
|
572
|
-
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
573
|
-
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
574
|
-
let origInsecure: string | undefined;
|
|
575
|
-
|
|
576
|
-
beforeEach(() => {
|
|
577
|
-
// vitest runs with NODE_ENV=test: not a production name, so every
|
|
578
|
-
// refusal below comes from what the standalone passes, not from NODE_ENV.
|
|
579
|
-
expect(process.env.NODE_ENV).not.toMatch(/^(production|staging)$/);
|
|
580
|
-
origInsecure = process.env.FEDERATION_TOKENS_ALLOW_INSECURE;
|
|
581
|
-
delete process.env.FEDERATION_TOKENS_ALLOW_INSECURE;
|
|
582
|
-
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
583
|
-
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
afterEach(async () => {
|
|
587
|
-
await handleRef?.dispose();
|
|
588
|
-
handleRef = undefined;
|
|
589
|
-
if (origInsecure === undefined) delete process.env.FEDERATION_TOKENS_ALLOW_INSECURE;
|
|
590
|
-
else process.env.FEDERATION_TOKENS_ALLOW_INSECURE = origInsecure;
|
|
591
|
-
warnSpy.mockRestore();
|
|
592
|
-
errorSpy.mockRestore();
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
/** All-Redis, with the federation store told to skip encryption. */
|
|
596
|
-
const PLAINTEXT_ENV: Readonly<Record<string, string>> = {
|
|
597
|
-
...without(ALL_REDIS_ENV, "REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY"),
|
|
598
|
-
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_MODE: "allow-plaintext",
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
it('is refused under core.deployment.mode = "multi" in an environment that is not production', async () => {
|
|
602
|
-
// The umbrella shape with plaintext. A multi-replica deployment is
|
|
603
|
-
// never a development box. A development configuration under multi is
|
|
604
|
-
// refused by the development mail sender first, so the environment here
|
|
605
|
-
// is another name that is not production, with the SMTP sender's section.
|
|
606
|
-
await expect(
|
|
607
|
-
boot({ ...resolveConfig(PLAINTEXT_ENV), ...standardSmtpMailSenderConfigForTests() }, "test"),
|
|
608
|
-
).rejects.toSatisfy((err: unknown) =>
|
|
609
|
-
/allow-plaintext[\s\S]*core\.deployment\.mode is "multi"/.test(messageChain(err)),
|
|
610
|
-
);
|
|
611
|
-
expect(errorSpy).not.toHaveBeenCalled();
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it("is refused when the config was selected by CONFIG_ENV=production, whatever NODE_ENV says", async () => {
|
|
615
|
-
// `app.mts` selects `production.conf` by `CONFIG_ENV || NODE_ENV` and
|
|
616
|
-
// passes that name through `buildModules`; the guard reads that name,
|
|
617
|
-
// not NODE_ENV alone.
|
|
618
|
-
// Under production the template installs the SMTP sender's module, whose
|
|
619
|
-
// section the package's builder carries.
|
|
620
|
-
const config = {
|
|
621
|
-
...resolveConfig({ ...PLAINTEXT_ENV, CORE_DEPLOYMENT_MODE: "single" }),
|
|
622
|
-
...standardSmtpMailSenderConfigForTests(),
|
|
623
|
-
};
|
|
624
|
-
await expect(boot(config, "production")).rejects.toSatisfy((err: unknown) =>
|
|
625
|
-
/allow-plaintext[\s\S]*the environment is "production"/.test(messageChain(err)),
|
|
626
|
-
);
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it("boots with the plaintext warning in a development environment on a single replica", async () => {
|
|
630
|
-
const config = resolveConfig({ ...PLAINTEXT_ENV, CORE_DEPLOYMENT_MODE: "single" });
|
|
631
|
-
handleRef = await boot(config, "development");
|
|
632
|
-
expect(handleRef.components.federationTokenStore?.kind).toBe("redis");
|
|
633
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
634
|
-
{ store: "federation-tokens", mode: "allow-plaintext" },
|
|
635
|
-
"federation_store_plaintext",
|
|
636
|
-
);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
it("keeps the FEDERATION_TOKENS_ALLOW_INSECURE=1 escape hatch under multi, logged at error", async () => {
|
|
640
|
-
process.env.FEDERATION_TOKENS_ALLOW_INSECURE = "1";
|
|
641
|
-
// Under multi the development mail sender refuses a development
|
|
642
|
-
// configuration, so the environment here is another name that is not
|
|
643
|
-
// production, with the SMTP sender's section its module reads.
|
|
644
|
-
handleRef = await boot(
|
|
645
|
-
{ ...resolveConfig(PLAINTEXT_ENV), ...standardSmtpMailSenderConfigForTests() },
|
|
646
|
-
"test",
|
|
647
|
-
);
|
|
648
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
649
|
-
expect.objectContaining({
|
|
650
|
-
store: "federation-tokens",
|
|
651
|
-
deploymentMode: "multi",
|
|
652
|
-
override: "FEDERATION_TOKENS_ALLOW_INSECURE",
|
|
653
|
-
}),
|
|
654
|
-
"federation_store_plaintext_override",
|
|
655
|
-
);
|
|
656
|
-
});
|
|
657
|
-
});
|