Files
diegosouzapw--omniroute/tests/unit/oauth-connection-test-timeout.test.ts
2026-07-13 13:39:12 +08:00

75 lines
3.2 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { testOAuthConnection } from "../../src/app/api/providers/[id]/test/route";
// #1449 (port from 9router) — "Test Connection One-by-One" could hang forever when an
// OAuth provider probe never returned. The OAuth probe path called bare
// fetch(url, {method, headers}) with NO AbortController/signal/timeout, so a hung
// upstream blocked the test queue indefinitely. The fix bounds both the initial probe
// and the post-refresh retry with AbortSignal.timeout(...) and reports a clear
// "timed out" failure in the same shape as other test errors.
//
// This test drives the probe with a fetch() that NEVER resolves but honors the
// AbortSignal. Without the timeout the awaited fetch never settles and the test would
// hang (and time out the runner). With the fix it resolves quickly as a failure.
test("OAuth connection test does not hang when the probe never returns (#1449)", async (t) => {
const originalFetch = globalThis.fetch;
t.after(() => {
globalThis.fetch = originalFetch;
});
// A fetch that never resolves on its own, but rejects with an AbortError as soon as
// the caller's AbortSignal fires — mirroring how the real fetch reacts to a timeout.
globalThis.fetch = ((_url: RequestInfo | URL, init?: RequestInit) => {
return new Promise((_resolve, reject) => {
const signal: AbortSignal | undefined = init?.signal ?? undefined;
if (!signal) return; // no signal => hangs forever (the pre-fix behavior)
if (signal.aborted) {
reject(makeAbortError());
return;
}
signal.addEventListener("abort", () => reject(makeAbortError()), { once: true });
});
}) as typeof fetch;
// github is an OAuth provider with a real test URL (not checkExpiry), so the code
// reaches the bare probe fetch. A future expiry avoids the refresh branch.
const connection = {
provider: "github",
authType: "oauth",
accessToken: "fake-token",
refreshToken: null,
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
};
// Inject a short timeout so the test runs fast and can never hang the runner.
const result = await withDeadline(testOAuthConnection(connection, 50), 5000);
assert.equal(result.valid, false, "a timed-out probe must be reported as a failure");
assert.match(
String(result.error),
/tim(ed )?out/i,
`error should indicate a timeout, got: ${result.error}`
);
// Same failure shape the route returns for every other OAuth test error.
assert.equal(result.refreshed, false);
assert.ok(result.diagnosis, "a failure must carry a diagnosis");
assert.equal(typeof result.diagnosis.type, "string");
});
function makeAbortError() {
// Node's fetch raises a DOMException named "AbortError" when its signal aborts.
return new DOMException("The operation was aborted", "AbortError");
}
// Hard backstop so a regression can never wedge the whole test file.
function withDeadline<T>(p: Promise<T>, ms: number): Promise<T> {
return Promise.race([
p,
new Promise<T>((_resolve, reject) =>
setTimeout(() => reject(new Error(`test deadline exceeded (${ms}ms) — probe hung`)), ms).unref()
),
]);
}