@o3co/create-auth-provider 0.14.0 → 0.16.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.
- package/README.ja.md +93 -47
- package/README.md +92 -46
- package/package.json +1 -1
- package/templates/standalone/.env.example +34 -0
- package/templates/standalone/Dockerfile +1 -1
- package/templates/standalone/README.ja.md +565 -130
- package/templates/standalone/README.md +356 -92
- package/templates/standalone/config/application.conf +77 -2
- package/templates/standalone/docker-compose.production.yml +7 -0
- package/templates/standalone/docker-compose.yml +7 -0
- package/templates/standalone/gitignore +3 -2
- package/templates/standalone/package.json +3 -2
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +49 -15
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1103 -0
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +241 -0
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1100 -0
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +40 -5
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +445 -0
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +446 -0
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +133 -0
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +85 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +304 -0
- package/templates/standalone/src/__tests__/host-routes.test.mts +141 -0
- package/templates/standalone/src/__tests__/listen.test.mts +128 -0
- package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +20 -5
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +187 -0
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +139 -0
- package/templates/standalone/src/__tests__/replica-safety.test.mts +13 -3
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +149 -0
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +14 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +253 -22
- package/templates/standalone/src/__tests__/smoke.test.mts +39 -20
- package/templates/standalone/src/__tests__/store-credential.test.mts +276 -0
- package/templates/standalone/src/app.mts +30 -51
- package/templates/standalone/src/buildModules.mts +94 -12
- package/templates/standalone/src/listen.mts +66 -0
- package/templates/standalone/src/logger.mts +7 -4
- package/templates/standalone/src/modules.mts +118 -12
- package/templates/standalone/src/routes.mts +95 -0
- package/templates/standalone/src/shutdown.mts +90 -18
- package/templates/standalone/vitest.supertest-loopback.mts +22 -3
- package/templates/versions.json +15 -14
- package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
- package/templates/standalone/src/terminalError.mts +0 -60
|
@@ -30,10 +30,23 @@
|
|
|
30
30
|
* and these tests are the contract the issue said was missing.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import type
|
|
34
|
-
import
|
|
33
|
+
import { createServer, type Server } from "node:http";
|
|
34
|
+
import {
|
|
35
|
+
type BootstrapMap,
|
|
36
|
+
createApp,
|
|
37
|
+
createMemoryReplaySeenSet,
|
|
38
|
+
defineModule,
|
|
39
|
+
type Logger,
|
|
40
|
+
} from "@o3co/auth-provider-core";
|
|
41
|
+
import { makeValidCoreConfig } from "@o3co/auth-provider-core/testing";
|
|
35
42
|
import { describe, expect, it, vi } from "vitest";
|
|
36
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
cleanupAllowanceFor,
|
|
45
|
+
deferExit,
|
|
46
|
+
FEDERATION_GRANTS_CLEANUP_ALLOWANCE_MS,
|
|
47
|
+
FEDERATION_GRANTS_CLEANUP_MARGIN_MS,
|
|
48
|
+
installGracefulShutdown,
|
|
49
|
+
} from "../shutdown.mjs";
|
|
37
50
|
|
|
38
51
|
/** A `Server` double whose `close` callback fires only when we say so. */
|
|
39
52
|
function makeServer() {
|
|
@@ -79,10 +92,53 @@ const makeLogger = () => {
|
|
|
79
92
|
return spy satisfies Logger;
|
|
80
93
|
};
|
|
81
94
|
|
|
95
|
+
/**
|
|
96
|
+
* A logger that serialises every own property of what it is handed, `cause`
|
|
97
|
+
* and non-enumerable fields included — a deployment is free to install one.
|
|
98
|
+
* `lines` is what it wrote; its levels are spies.
|
|
99
|
+
*/
|
|
100
|
+
const serialiseEverythingLogger = () => {
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
const walk = (value: unknown, seen = new WeakSet<object>()): unknown => {
|
|
103
|
+
if (typeof value !== "object" || value === null) return value;
|
|
104
|
+
if (seen.has(value)) return "[circular]";
|
|
105
|
+
seen.add(value);
|
|
106
|
+
const out: Record<string, unknown> = {};
|
|
107
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
108
|
+
out[key] = walk((value as Record<string, unknown>)[key], seen);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
const record = (level: string) =>
|
|
113
|
+
vi.fn((...args: unknown[]): void => {
|
|
114
|
+
lines.push(JSON.stringify({ level, args: walk(args) }));
|
|
115
|
+
});
|
|
116
|
+
const logger = {
|
|
117
|
+
trace: record("trace"),
|
|
118
|
+
debug: record("debug"),
|
|
119
|
+
info: record("info"),
|
|
120
|
+
warn: record("warn"),
|
|
121
|
+
error: record("error"),
|
|
122
|
+
fatal: record("fatal"),
|
|
123
|
+
child: vi.fn(() => logger as unknown as Logger),
|
|
124
|
+
};
|
|
125
|
+
return { logger: logger satisfies Logger, lines };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** A logged projection's `stack`: frames only, from the first. */
|
|
129
|
+
const FRAMES = expect.stringMatching(/^ {4}at /);
|
|
130
|
+
|
|
82
131
|
/** Drive one shutdown without touching the real `process` or exiting. */
|
|
83
|
-
function install(
|
|
132
|
+
function install(
|
|
133
|
+
opts: {
|
|
134
|
+
cleanup?: () => void | Promise<void>;
|
|
135
|
+
drainTimeoutMs?: number;
|
|
136
|
+
cleanupTimeoutMs?: number;
|
|
137
|
+
logger?: ReturnType<typeof makeLogger>;
|
|
138
|
+
} = {},
|
|
139
|
+
) {
|
|
84
140
|
const { server, spies, finishDraining, failClose } = makeServer();
|
|
85
|
-
const logger = makeLogger();
|
|
141
|
+
const logger = opts.logger ?? makeLogger();
|
|
86
142
|
const exit = vi.fn();
|
|
87
143
|
const signals = new Map<string, () => void>();
|
|
88
144
|
|
|
@@ -90,6 +146,7 @@ function install(opts: { cleanup?: () => void | Promise<void>; drainTimeoutMs?:
|
|
|
90
146
|
logger,
|
|
91
147
|
cleanup: opts.cleanup ?? (() => {}),
|
|
92
148
|
...(opts.drainTimeoutMs === undefined ? {} : { drainTimeoutMs: opts.drainTimeoutMs }),
|
|
149
|
+
...(opts.cleanupTimeoutMs === undefined ? {} : { cleanupTimeoutMs: opts.cleanupTimeoutMs }),
|
|
93
150
|
exit,
|
|
94
151
|
onSignal: (name, handler) => signals.set(name, handler),
|
|
95
152
|
offSignal: (name) => signals.delete(name),
|
|
@@ -159,7 +216,12 @@ describe("installGracefulShutdown (#290)", () => {
|
|
|
159
216
|
signals.get("SIGTERM")?.();
|
|
160
217
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
161
218
|
expect(exit).toHaveBeenCalledWith(1);
|
|
162
|
-
|
|
219
|
+
// Each stage is an event: object-first, a snake_case name.
|
|
220
|
+
expect(logger.info).toHaveBeenCalledWith({ drainTimeoutMs: 5_000 }, "shutdown_draining");
|
|
221
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
222
|
+
{ drainTimeoutMs: 5_000 },
|
|
223
|
+
"shutdown_drain_deadline_exceeded",
|
|
224
|
+
);
|
|
163
225
|
} finally {
|
|
164
226
|
vi.useRealTimers();
|
|
165
227
|
}
|
|
@@ -180,21 +242,91 @@ describe("installGracefulShutdown (#290)", () => {
|
|
|
180
242
|
}
|
|
181
243
|
});
|
|
182
244
|
|
|
183
|
-
it("reports a cleanup failure through the app logger, not console", async () => {
|
|
245
|
+
it("reports a cleanup failure through the app logger, not console, as loggableError's projection", async () => {
|
|
184
246
|
// Every other line this service emits is NDJSON through pino; a bare
|
|
185
247
|
// console.error on the shutdown path is the one a log pipeline drops.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
248
|
+
// What a real `handle.dispose()` rejects with is an AggregateError of
|
|
249
|
+
// every cleanup's own error. The line names each member and its code,
|
|
250
|
+
// and carries nothing of what they hold: an OAuth library's refusal
|
|
251
|
+
// keeps the refresh answer it refused on a non-Error cause, an ioredis
|
|
252
|
+
// reply the write it refused.
|
|
253
|
+
const refusedRefresh = Object.assign(
|
|
254
|
+
new Error("invalid response encountered", {
|
|
255
|
+
cause: { body: { refresh_token: "1//0g-UPSTREAM-S3CRET" } },
|
|
256
|
+
}),
|
|
257
|
+
{ name: "ClientError", code: "OAUTH_INVALID_RESPONSE" },
|
|
258
|
+
);
|
|
259
|
+
const refusedWrite = Object.assign(
|
|
260
|
+
new Error("READONLY You can't write against a read only replica."),
|
|
261
|
+
{
|
|
262
|
+
name: "ReplyError",
|
|
263
|
+
command: { name: "set", args: ["fg:credential:grant-1", "1//0g-ARGS-S3CRET"] },
|
|
189
264
|
},
|
|
265
|
+
);
|
|
266
|
+
const failingCleanups = defineModule({
|
|
267
|
+
name: "test:failing-cleanups",
|
|
268
|
+
provides: {
|
|
269
|
+
replaySeenSet: () => createMemoryReplaySeenSet(),
|
|
270
|
+
challengeStore: () => ({}) as never,
|
|
271
|
+
},
|
|
272
|
+
lifecycle: {
|
|
273
|
+
replaySeenSet: {
|
|
274
|
+
eager: true,
|
|
275
|
+
cleanup: () => {
|
|
276
|
+
throw refusedRefresh;
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
challengeStore: {
|
|
280
|
+
eager: true,
|
|
281
|
+
cleanup: () => {
|
|
282
|
+
throw refusedWrite;
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
const handle = await createApp({
|
|
288
|
+
modules: [failingCleanups],
|
|
289
|
+
bootstrapComponents: {
|
|
290
|
+
config: makeValidCoreConfig(),
|
|
291
|
+
pathResolver: (path: string) => path,
|
|
292
|
+
} as unknown as BootstrapMap,
|
|
293
|
+
});
|
|
294
|
+
const { logger, lines } = serialiseEverythingLogger();
|
|
295
|
+
const { exit, signals, finishDraining } = install({
|
|
296
|
+
logger,
|
|
297
|
+
cleanup: () => handle.dispose(),
|
|
190
298
|
});
|
|
191
299
|
signals.get("SIGTERM")?.();
|
|
192
300
|
finishDraining();
|
|
193
301
|
await vi.waitFor(() => expect(exit).toHaveBeenCalled());
|
|
194
302
|
expect(logger.error).toHaveBeenCalledWith(
|
|
195
|
-
|
|
196
|
-
|
|
303
|
+
{
|
|
304
|
+
err: {
|
|
305
|
+
name: "AggregateError",
|
|
306
|
+
detail: expect.stringMatching(/^AppHandle\.dispose: 2 cleanup errors /),
|
|
307
|
+
stack: FRAMES,
|
|
308
|
+
aggregateErrors: expect.arrayContaining([
|
|
309
|
+
{
|
|
310
|
+
name: "ClientError",
|
|
311
|
+
detail: "invalid response encountered",
|
|
312
|
+
code: "OAUTH_INVALID_RESPONSE",
|
|
313
|
+
stack: FRAMES,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "ReplyError",
|
|
317
|
+
detail: "READONLY You can't write against a read only replica.",
|
|
318
|
+
command: { name: "set" },
|
|
319
|
+
stack: FRAMES,
|
|
320
|
+
},
|
|
321
|
+
]),
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
"shutdown_cleanup_failed",
|
|
197
325
|
);
|
|
326
|
+
for (const line of lines) {
|
|
327
|
+
expect(line).not.toContain("S3CRET");
|
|
328
|
+
expect(line).not.toContain("refresh_token");
|
|
329
|
+
}
|
|
198
330
|
});
|
|
199
331
|
|
|
200
332
|
it("still exits when cleanup throws — a failed dispose must not wedge the process", async () => {
|
|
@@ -208,19 +340,37 @@ describe("installGracefulShutdown (#290)", () => {
|
|
|
208
340
|
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
|
|
209
341
|
});
|
|
210
342
|
|
|
211
|
-
it("does not report a failed close as a clean drain", async () => {
|
|
343
|
+
it("does not report a failed close as a clean drain, and logs the failure as loggableError's projection", async () => {
|
|
212
344
|
// `server.close` reports through its callback -- "Server is not running"
|
|
213
345
|
// is the common one, but any listener teardown failure lands there.
|
|
214
346
|
// Exiting 0 on it would tell an orchestrator the listener came down
|
|
215
|
-
// when it did not.
|
|
216
|
-
|
|
347
|
+
// when it did not. A real server that is not listening gives Node's
|
|
348
|
+
// own error; the line keeps its name, message, code and frames.
|
|
349
|
+
const server = createServer();
|
|
350
|
+
const { logger, lines } = serialiseEverythingLogger();
|
|
351
|
+
const exit = vi.fn();
|
|
352
|
+
const signals = new Map<string, () => void>();
|
|
353
|
+
installGracefulShutdown(server, {
|
|
354
|
+
logger,
|
|
355
|
+
exit,
|
|
356
|
+
onSignal: (name, handler) => signals.set(name, handler),
|
|
357
|
+
offSignal: (name) => signals.delete(name),
|
|
358
|
+
});
|
|
217
359
|
signals.get("SIGTERM")?.();
|
|
218
|
-
failClose(new Error("Server is not running"));
|
|
219
360
|
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
|
|
220
361
|
expect(logger.error).toHaveBeenCalledWith(
|
|
221
|
-
|
|
222
|
-
|
|
362
|
+
{
|
|
363
|
+
err: {
|
|
364
|
+
name: "Error",
|
|
365
|
+
detail: "Server is not running.",
|
|
366
|
+
code: "ERR_SERVER_NOT_RUNNING",
|
|
367
|
+
stack: FRAMES,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
"shutdown_server_close_failed",
|
|
223
371
|
);
|
|
372
|
+
// The frames, never the header line that repeats the message.
|
|
373
|
+
for (const line of lines) expect(line).not.toContain("Error [ERR_SERVER_NOT_RUNNING]");
|
|
224
374
|
});
|
|
225
375
|
|
|
226
376
|
it("still runs cleanup when close reports a failure", async () => {
|
|
@@ -250,8 +400,8 @@ describe("installGracefulShutdown (#290)", () => {
|
|
|
250
400
|
finishDraining();
|
|
251
401
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
252
402
|
expect(logger.error).toHaveBeenCalledWith(
|
|
253
|
-
|
|
254
|
-
|
|
403
|
+
{ cleanupTimeoutMs: 5_000 },
|
|
404
|
+
"shutdown_cleanup_timed_out",
|
|
255
405
|
);
|
|
256
406
|
expect(exit).toHaveBeenCalledWith(1);
|
|
257
407
|
} finally {
|
|
@@ -298,7 +448,7 @@ describe("installGracefulShutdown (#290)", () => {
|
|
|
298
448
|
await new Promise((resolve) => setImmediate(resolve));
|
|
299
449
|
expect(logger.info).toHaveBeenCalledWith(
|
|
300
450
|
{ reason: "cleanup-failed", drain: "drained", exitCode: 1 },
|
|
301
|
-
"
|
|
451
|
+
"shutdown_complete",
|
|
302
452
|
);
|
|
303
453
|
expect(exit).toHaveBeenCalledWith(1);
|
|
304
454
|
});
|
|
@@ -310,7 +460,7 @@ describe("installGracefulShutdown (#290)", () => {
|
|
|
310
460
|
await new Promise((resolve) => setImmediate(resolve));
|
|
311
461
|
expect(logger.info).toHaveBeenCalledWith(
|
|
312
462
|
{ reason: "drained", drain: "drained", exitCode: 0 },
|
|
313
|
-
"
|
|
463
|
+
"shutdown_complete",
|
|
314
464
|
);
|
|
315
465
|
});
|
|
316
466
|
|
|
@@ -322,3 +472,84 @@ describe("installGracefulShutdown (#290)", () => {
|
|
|
322
472
|
expect(signals.size).toBe(0);
|
|
323
473
|
});
|
|
324
474
|
});
|
|
475
|
+
|
|
476
|
+
describe("#593 slice 7: the cleanup allowance federation grants need", () => {
|
|
477
|
+
it("is at least the 45 seconds the package asks for, and only when the feature is on", () => {
|
|
478
|
+
// The package's drain waits for a rotated credential's write; the
|
|
479
|
+
// default cleanup budget is the ten-second drain, which is shorter than
|
|
480
|
+
// the upstream hard timeout and persist budget the feature ships with —
|
|
481
|
+
// a shutdown under it abandons exactly the write the drain exists for.
|
|
482
|
+
expect(FEDERATION_GRANTS_CLEANUP_ALLOWANCE_MS).toBeGreaterThanOrEqual(45_000);
|
|
483
|
+
expect(cleanupAllowanceFor({ federationGrants: { enabled: true } })).toEqual({
|
|
484
|
+
cleanupTimeoutMs: FEDERATION_GRANTS_CLEANUP_ALLOWANCE_MS,
|
|
485
|
+
});
|
|
486
|
+
// Off, nothing: the cleanup budget stays the drain's, as it was.
|
|
487
|
+
expect(cleanupAllowanceFor({ federationGrants: { enabled: false } })).toEqual({});
|
|
488
|
+
expect(cleanupAllowanceFor({})).toEqual({});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("grows with the configured refresh tail, so a raised budget is not cut off by a fixed timer (Copilot, #614)", () => {
|
|
492
|
+
// The longest tail one refresh has: the upstream hard timeout, the
|
|
493
|
+
// persist budget and the wait for the lock, back to back, plus an exit
|
|
494
|
+
// margin. The shipped budgets (25 s + 3 s + 5 s) land exactly on the
|
|
495
|
+
// floor; a deployment that doubles its upstream timeout gets more.
|
|
496
|
+
const shipped = {
|
|
497
|
+
upstreamHardTimeoutMs: 25_000,
|
|
498
|
+
persistRetryBudgetMs: 3_000,
|
|
499
|
+
lockWaitMs: 5_000,
|
|
500
|
+
};
|
|
501
|
+
expect(
|
|
502
|
+
25_000 + 3_000 + 5_000 + FEDERATION_GRANTS_CLEANUP_MARGIN_MS,
|
|
503
|
+
"the margin is what makes the shipped budgets the floor",
|
|
504
|
+
).toBe(FEDERATION_GRANTS_CLEANUP_ALLOWANCE_MS);
|
|
505
|
+
expect(cleanupAllowanceFor({ federationGrants: { enabled: true, ...shipped } })).toEqual({
|
|
506
|
+
cleanupTimeoutMs: FEDERATION_GRANTS_CLEANUP_ALLOWANCE_MS,
|
|
507
|
+
});
|
|
508
|
+
expect(
|
|
509
|
+
cleanupAllowanceFor({
|
|
510
|
+
federationGrants: { enabled: true, ...shipped, upstreamHardTimeoutMs: 60_000 },
|
|
511
|
+
}),
|
|
512
|
+
).toEqual({ cleanupTimeoutMs: 60_000 + 3_000 + 5_000 + FEDERATION_GRANTS_CLEANUP_MARGIN_MS });
|
|
513
|
+
// Lowered budgets never go below the documented minimum, and a config
|
|
514
|
+
// without budgets (built by hand) gets the floor rather than a guess.
|
|
515
|
+
expect(
|
|
516
|
+
cleanupAllowanceFor({
|
|
517
|
+
federationGrants: { enabled: true, ...shipped, upstreamHardTimeoutMs: 1_000 },
|
|
518
|
+
}),
|
|
519
|
+
).toEqual({ cleanupTimeoutMs: FEDERATION_GRANTS_CLEANUP_ALLOWANCE_MS });
|
|
520
|
+
expect(
|
|
521
|
+
cleanupAllowanceFor({ federationGrants: { enabled: true, upstreamHardTimeoutMs: 90_000 } }),
|
|
522
|
+
).toEqual({ cleanupTimeoutMs: FEDERATION_GRANTS_CLEANUP_ALLOWANCE_MS });
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("never asks a timer for more than Node can count: an oversized sum is capped, not overflowed (Copilot, #614)", () => {
|
|
526
|
+
// setTimeout takes a 32-bit signed delay; past it the timer fires after
|
|
527
|
+
// about a millisecond, which would turn a generous allowance into none.
|
|
528
|
+
const oversized = cleanupAllowanceFor({
|
|
529
|
+
federationGrants: {
|
|
530
|
+
enabled: true,
|
|
531
|
+
upstreamHardTimeoutMs: 2_000_000_000,
|
|
532
|
+
persistRetryBudgetMs: 2_000_000_000,
|
|
533
|
+
lockWaitMs: 5_000,
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
expect(oversized).toEqual({ cleanupTimeoutMs: 2_147_483_647 });
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("is honoured by the shutdown: a cleanup that needs longer than the drain is given it", async () => {
|
|
540
|
+
vi.useFakeTimers();
|
|
541
|
+
try {
|
|
542
|
+
const { signals, finishDraining, exit } = install({
|
|
543
|
+
cleanup: () => new Promise<void>((resolve) => setTimeout(resolve, 30_000)),
|
|
544
|
+
drainTimeoutMs: 5_000,
|
|
545
|
+
...cleanupAllowanceFor({ federationGrants: { enabled: true } }),
|
|
546
|
+
});
|
|
547
|
+
signals.get("SIGTERM")?.();
|
|
548
|
+
finishDraining();
|
|
549
|
+
await vi.advanceTimersByTimeAsync(31_000);
|
|
550
|
+
expect(exit).toHaveBeenCalledExactlyOnceWith(0);
|
|
551
|
+
} finally {
|
|
552
|
+
vi.useRealTimers();
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
});
|
|
@@ -618,27 +618,46 @@ describe("standalone smoke test", () => {
|
|
|
618
618
|
expect(names).toContain("standalone:redis-clients");
|
|
619
619
|
});
|
|
620
620
|
|
|
621
|
-
it("buildModules honors legacy repositories.code.type='redis' with deprecation warn when oauth.code.adapter is absent", () => {
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
621
|
+
it("buildModules honors legacy repositories.code.type='redis' with one object-first deprecation warn when oauth.code.adapter is absent", () => {
|
|
622
|
+
const warn = vi.fn();
|
|
623
|
+
const logger = {
|
|
624
|
+
trace: vi.fn(),
|
|
625
|
+
debug: vi.fn(),
|
|
626
|
+
info: vi.fn(),
|
|
627
|
+
warn,
|
|
628
|
+
error: vi.fn(),
|
|
629
|
+
fatal: vi.fn(),
|
|
630
|
+
child: () => logger,
|
|
631
|
+
};
|
|
632
|
+
const legacyConfig = {
|
|
633
|
+
...config,
|
|
634
|
+
oauth: { ...config.oauth, code: undefined },
|
|
635
|
+
repositories: {
|
|
636
|
+
...config.repositories,
|
|
637
|
+
code: { type: "redis" as const, defaultExpiresIn: 600 },
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
const modules = buildModules(legacyConfig, {
|
|
641
|
+
keyStoreModule: testKeyStoreModule,
|
|
642
|
+
repositoriesModule: testRepositoriesModule,
|
|
643
|
+
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
644
|
+
logger,
|
|
645
|
+
});
|
|
646
|
+
const names = modules.map((m) => m.name);
|
|
647
|
+
expect(names).toContain("redis-code-repository");
|
|
648
|
+
expect(warn.mock.calls).toEqual([
|
|
649
|
+
[
|
|
650
|
+
{
|
|
651
|
+
key: "repositories.code.type",
|
|
652
|
+
env: "CLIENT_CODE_TYPE",
|
|
653
|
+
replacement: "oauth.code.adapter",
|
|
654
|
+
replacementEnv: "OAUTH_CODE_ADAPTER",
|
|
630
655
|
},
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
});
|
|
637
|
-
const names = modules.map((m) => m.name);
|
|
638
|
-
expect(names).toContain("redis-code-repository");
|
|
639
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("repositories.code.type"));
|
|
640
|
-
} finally {
|
|
641
|
-
warnSpy.mockRestore();
|
|
656
|
+
"config_key_deprecated",
|
|
657
|
+
],
|
|
658
|
+
]);
|
|
659
|
+
for (const level of ["trace", "debug", "info", "error", "fatal"] as const) {
|
|
660
|
+
expect(logger[level]).not.toHaveBeenCalled();
|
|
642
661
|
}
|
|
643
662
|
});
|
|
644
663
|
|