Files
OmniRoute/tests/unit/token-refresh-circuit-breaker.test.ts
MumuTW 533f8051a4 chore(token-refresh): decompose services/tokenRefresh.ts into tokenRefresh/* leaves (999 → 724) (#8547)
* chore(token-refresh): extract rotation/cas/circuit-breaker refresh logic into tokenRefresh/* leaves

* test(oauth): follow isUnrecoverableRefreshError to tokenRefresh/shared.ts

cad2c7285 moved isUnrecoverableRefreshError out of tokenRefresh.ts into
tokenRefresh/shared.ts. This suite asserts on source *text* (it regex-matches
the function body to prove the unrecoverable sentinel is returned), so the
move made it fail to find the definition — the only red test across the 23
tokenRefresh-related suites.

Repoint the read() at the file that now defines the body. The public surface
is unchanged: tokenRefresh.ts still re-exports the symbol, verified by import.

* docs(changelog): add fragment for this PR

* docs(auth): correct the #7338 attribution wording in the tokenRefresh header

The header claimed credit for KooshaPari's #7338 was "preserved via co-authorship on the
extraction commits", but none of the commits carries a Co-authored-by trailer -- and adding
one would be inaccurate, since this is an independent implementation against the current
tip rather than a reuse of that diff. The by-name credit for proposing the split stays;
only the false claim about the mechanism is removed.
2026-07-26 03:52:27 -03:00

177 lines
5.6 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
// Unit tests for the circuit breaker + refreshWithRetry leaf extracted from
// tokenRefresh.ts. refreshWithRetry wraps a refresh attempt with exponential
// backoff, a 30s per-attempt timeout, and a per-provider circuit breaker
// (5 consecutive failures → 30min pause). Unrecoverable refresh errors
// short-circuit retries.
const { isProviderBlocked, getCircuitBreakerStatus, refreshWithRetry } =
await import("../../open-sse/services/tokenRefresh/circuitBreaker.ts");
const silentLog = {
info() {},
warn() {},
error() {},
debug() {},
};
function makeLog() {
const entries = [];
const log = (level) => (scope, message) => entries.push({ level, scope, message });
return {
entries,
debug: log("debug"),
info: log("info"),
warn: log("warn"),
error: log("error"),
};
}
test("isProviderBlocked returns false for an unknown provider", () => {
assert.equal(isProviderBlocked("never-seen"), false);
});
test("getCircuitBreakerStatus returns an empty object when no failures recorded", () => {
assert.deepEqual(getCircuitBreakerStatus(), {});
});
test("refreshWithRetry returns the result on the first success and clears prior failures", async () => {
const provider = "cb-success-" + Math.random().toString(36).slice(2);
// Seed a failure so we can verify success clears it.
await refreshWithRetry(async () => null, 1, silentLog, provider);
assert.equal(getCircuitBreakerStatus()[provider].failures, 1);
const result = await refreshWithRetry(
async () => ({ accessToken: "ok" }),
3,
silentLog,
provider
);
assert.equal(result.accessToken, "ok");
assert.equal(getCircuitBreakerStatus()[provider], undefined, "success resets the breaker");
});
test("refreshWithRetry retries to success within maxRetries", async () => {
const provider = "cb-retry-" + Math.random().toString(36).slice(2);
let attempts = 0;
const result = await refreshWithRetry(
async () => {
attempts++;
if (attempts < 2) return null;
return { accessToken: "ok-after-retry" };
},
3,
silentLog,
provider
);
assert.equal(result.accessToken, "ok-after-retry");
assert.equal(attempts, 2);
assert.equal(getCircuitBreakerStatus()[provider], undefined);
});
test("refreshWithRetry bails immediately on an unrecoverable error without retrying", async () => {
const provider = "cb-unrecoverable-" + Math.random().toString(36).slice(2);
let attempts = 0;
const result = await refreshWithRetry(
async () => {
attempts++;
return { error: "invalid_grant" };
},
3,
silentLog,
provider
);
assert.equal(attempts, 1, "unrecoverable errors must not be retried");
assert.equal(result.error, "invalid_grant");
assert.equal(
getCircuitBreakerStatus()[provider],
undefined,
"no failure recorded for unrecoverable"
);
});
test("refreshWithRetry bails immediately on refresh_token_reused", async () => {
const provider = "cb-reused-" + Math.random().toString(36).slice(2);
let attempts = 0;
const result = await refreshWithRetry(
async () => {
attempts++;
return { error: "refresh_token_reused" };
},
3,
silentLog,
provider
);
assert.equal(attempts, 1);
assert.equal(result.error, "refresh_token_reused");
});
test("refreshWithRetry trips the circuit breaker after repeated failures", async () => {
const provider = "cb-trip-" + Math.random().toString(36).slice(2);
// 5 consecutive single-retry failures trip the breaker.
for (let i = 0; i < 5; i++) {
await refreshWithRetry(async () => null, 1, silentLog, provider);
}
assert.equal(isProviderBlocked(provider), true);
assert.equal(getCircuitBreakerStatus()[provider].blocked, true);
assert.ok(getCircuitBreakerStatus()[provider].blockedUntil);
// A blocked provider short-circuits without calling refreshFn.
let called = false;
const blocked = await refreshWithRetry(
async () => {
called = true;
return { accessToken: "x" };
},
1,
silentLog,
provider
);
assert.equal(called, false, "refreshFn must not run while the breaker is open");
assert.equal(blocked, null);
});
test("refreshWithRetry records a failure when all retries are exhausted", async () => {
const provider = "cb-exhaust-" + Math.random().toString(36).slice(2);
const log = makeLog();
const result = await refreshWithRetry(async () => null, 2, log, provider);
assert.equal(result, null);
assert.equal(getCircuitBreakerStatus()[provider].failures, 1);
assert.ok(
log.entries.some((e) => e.level === "error" && /All 2 retry attempts failed/.test(e.message))
);
});
test("refreshWithRetry propagates thrown errors as retry failures (not crashes)", async () => {
const provider = "cb-throw-" + Math.random().toString(36).slice(2);
const log = makeLog();
let attempts = 0;
const result = await refreshWithRetry(
async () => {
attempts++;
throw new Error("upstream boom");
},
2,
log,
provider
);
assert.equal(result, null);
assert.equal(attempts, 2, "thrown errors are retried, not fatal");
assert.equal(getCircuitBreakerStatus()[provider].failures, 1);
assert.ok(log.entries.some((e) => e.level === "warn" && /failed: upstream boom/.test(e.message)));
});
test("refreshWithRetry defaults: maxRetries=3, provider='unknown'", async () => {
// With defaults, an always-null refresh exhausts 3 attempts and records a
// failure under the "unknown" provider.
let attempts = 0;
await refreshWithRetry(async () => {
attempts++;
return null;
});
assert.equal(attempts, 3);
assert.ok(getCircuitBreakerStatus()["unknown"], "default provider is 'unknown'");
});