Files
NetMesh/electron/bridges/oauthBridge.test.cjs

337 lines
10 KiB
JavaScript
Raw Normal View History

const test = require("node:test");
const assert = require("node:assert/strict");
const http = require("node:http");
const BRIDGE_PATH = require.resolve("./oauthBridge.cjs");
function loadBridge() {
// Isolate module state between tests — oauthBridge keeps the bound server
// and pending promise in module-level closures.
delete require.cache[BRIDGE_PATH];
return require("./oauthBridge.cjs");
}
async function freshModule() {
const bridge = loadBridge();
const cleanup = async () => {
try {
bridge.cancelOAuthCallback();
} catch {
// ignore
}
// Give the server a beat to actually close before the next test binds
// the same port.
await new Promise((r) => setTimeout(r, 20));
};
return { bridge, cleanup };
}
function tryBindPort(port) {
return new Promise((resolve) => {
const s = http.createServer(() => {});
s.once("error", () => resolve(null));
s.once("listening", () => resolve(s));
s.listen(port, "127.0.0.1");
});
}
async function closeServer(s) {
if (!s) return;
await new Promise((resolve) => s.close(resolve));
}
function fetchCallback(port, query) {
return new Promise((resolve, reject) => {
const search = new URLSearchParams(query).toString();
const req = http.request(
{
hostname: "127.0.0.1",
port,
path: `/oauth/callback?${search}`,
method: "GET",
},
(res) => {
res.resume();
res.on("end", () => resolve(res.statusCode));
}
);
req.on("error", reject);
req.end();
});
}
test("prepareOAuthCallback binds and reports port + redirectUri", async () => {
const { bridge, cleanup } = await freshModule();
try {
const { port, redirectUri } = await bridge.prepareOAuthCallback();
assert.ok(Number.isInteger(port) && port > 0, `got port=${port}`);
assert.equal(redirectUri, `http://127.0.0.1:${port}/oauth/callback`);
assert.equal(bridge.getActiveOAuthPort(), port);
} finally {
await cleanup();
}
});
test("prepareOAuthCallback prefers port 45678 when it is free", async () => {
// Probe: if 45678 is already held by something external, skip this test —
// the fallback path is exercised in the next test.
const probe = await tryBindPort(45678);
if (!probe) {
return; // treated as pass; environment doesn't permit this assertion
}
await closeServer(probe);
const { bridge, cleanup } = await freshModule();
try {
const { port } = await bridge.prepareOAuthCallback();
assert.equal(port, 45678);
} finally {
await cleanup();
}
});
test("prepareOAuthCallback falls back to an OS-assigned port when 45678 is busy (#823)", async () => {
// Hold port 45678 ourselves so the bridge MUST fall back.
const squatter = await tryBindPort(45678);
if (!squatter) {
// Something else is already holding 45678 — just ensure the bridge
// still produces a working port rather than crashing.
const { bridge, cleanup } = await freshModule();
try {
const { port } = await bridge.prepareOAuthCallback();
assert.ok(port > 0 && port !== 45678);
} finally {
await cleanup();
}
return;
}
const { bridge, cleanup } = await freshModule();
try {
const { port, redirectUri } = await bridge.prepareOAuthCallback();
assert.notEqual(port, 45678, "expected a different port when 45678 is in use");
assert.ok(port > 0);
assert.equal(redirectUri, `http://127.0.0.1:${port}/oauth/callback`);
assert.equal(bridge.getActiveOAuthPort(), port);
} finally {
await cleanup();
await closeServer(squatter);
}
});
test("awaitOAuthCallback resolves with the code when the browser hits the redirect URI", async () => {
const { bridge, cleanup } = await freshModule();
try {
const { port } = await bridge.prepareOAuthCallback();
const pending = bridge.awaitOAuthCallback();
const status = await fetchCallback(port, { code: "test-code", state: "anything" });
assert.equal(status, 200);
const result = await pending;
assert.equal(result.code, "test-code");
} finally {
await cleanup();
}
});
test("awaitOAuthCallback rejects on state mismatch", async () => {
const { bridge, cleanup } = await freshModule();
try {
const { port } = await bridge.prepareOAuthCallback();
const pending = bridge.awaitOAuthCallback("expected-state");
// Attach the assertion BEFORE triggering the callback so the rejection
// is never unhandled.
const rejectsCheck = assert.rejects(pending, /State mismatch/);
await fetchCallback(port, { code: "test-code", state: "different-state" });
await rejectsCheck;
} finally {
await cleanup();
}
});
test("awaitOAuthCallback rejects when the provider returns an error", async () => {
const { bridge, cleanup } = await freshModule();
try {
const { port } = await bridge.prepareOAuthCallback();
const pending = bridge.awaitOAuthCallback();
const rejectsCheck = assert.rejects(pending, /access_denied/);
await fetchCallback(port, { error: "access_denied", error_description: "access_denied" });
await rejectsCheck;
} finally {
await cleanup();
}
});
test("awaitOAuthCallback errors if called before prepareOAuthCallback", async () => {
const { bridge, cleanup } = await freshModule();
try {
await assert.rejects(bridge.awaitOAuthCallback(), /not prepared/);
} finally {
await cleanup();
}
});
test("cancelOAuthCallback releases the bound port", async () => {
const { bridge, cleanup } = await freshModule();
try {
const { port } = await bridge.prepareOAuthCallback();
assert.equal(bridge.getActiveOAuthPort(), port);
bridge.cancelOAuthCallback();
assert.equal(bridge.getActiveOAuthPort(), null);
// Give the OS a beat to release the socket, then confirm re-bindable.
await new Promise((r) => setTimeout(r, 30));
const rebound = await tryBindPort(port);
assert.ok(rebound, `expected ${port} to be re-bindable after cancel`);
await closeServer(rebound);
} finally {
await cleanup();
}
});
test("callbacks that arrive before awaitOAuthCallback do not consume the session", async () => {
const { bridge, cleanup } = await freshModule();
try {
const { port } = await bridge.prepareOAuthCallback();
const earlyStatus = await fetchCallback(port, { code: "stale-code", state: "stale-state" });
assert.equal(earlyStatus, 200);
assert.equal(bridge.getActiveOAuthPort(), port);
const pending = bridge.awaitOAuthCallback("expected-state");
const status = await fetchCallback(port, {
code: "fresh-code",
state: "expected-state",
});
assert.equal(status, 200);
const result = await pending;
assert.equal(result.code, "fresh-code");
} finally {
await cleanup();
}
});
test("cancelOAuthCallback before awaitOAuthCallback is replayed as a cancellation", async () => {
const { bridge, cleanup } = await freshModule();
try {
const { sessionId } = await bridge.prepareOAuthCallback();
bridge.cancelOAuthCallback(sessionId);
await assert.rejects(bridge.awaitOAuthCallback(undefined, sessionId), /cancelled/);
} finally {
await cleanup();
}
});
test("cancelOAuthCallback during prepare rejects the pending prepare", async () => {
const { bridge, cleanup } = await freshModule();
try {
const pendingPrepare = bridge.prepareOAuthCallback();
bridge.cancelOAuthCallback();
await assert.rejects(pendingPrepare, /cancelled/);
assert.equal(bridge.getActiveOAuthPort(), null);
} finally {
await cleanup();
}
});
test("cancelOAuthCallback ignores stale session ids", async () => {
const { bridge, cleanup } = await freshModule();
try {
const stale = await bridge.prepareOAuthCallback();
const fresh = await bridge.prepareOAuthCallback();
bridge.cancelOAuthCallback(stale.sessionId);
assert.equal(bridge.getActiveOAuthPort(), fresh.port);
const pending = bridge.awaitOAuthCallback(undefined, fresh.sessionId);
const status = await fetchCallback(fresh.port, { code: "fresh-code" });
assert.equal(status, 200);
const result = await pending;
assert.equal(result.code, "fresh-code");
} finally {
await cleanup();
}
});
test("awaitOAuthCallback rejects stale session ids instead of attaching to the new flow", async () => {
const { bridge, cleanup } = await freshModule();
try {
const stale = await bridge.prepareOAuthCallback();
const fresh = await bridge.prepareOAuthCallback();
await assert.rejects(
bridge.awaitOAuthCallback(undefined, stale.sessionId),
/cancelled/
);
const pending = bridge.awaitOAuthCallback(undefined, fresh.sessionId);
const status = await fetchCallback(fresh.port, { code: "fresh-code" });
assert.equal(status, 200);
const result = await pending;
assert.equal(result.code, "fresh-code");
} finally {
await cleanup();
}
});
test("concurrent prepareOAuthCallback calls cancel the superseded attempt", async () => {
const { bridge, cleanup } = await freshModule();
try {
const stalePrepare = bridge.prepareOAuthCallback();
const staleRejects = assert.rejects(stalePrepare, /cancelled/);
const freshPrepare = bridge.prepareOAuthCallback();
const fresh = await freshPrepare;
await staleRejects;
const pending = bridge.awaitOAuthCallback(undefined, fresh.sessionId);
const status = await fetchCallback(fresh.port, { code: "fresh-code" });
assert.equal(status, 200);
const result = await pending;
assert.equal(result.code, "fresh-code");
} finally {
await cleanup();
}
});
test("prepareOAuthCallback replaces an in-flight await cleanly", async () => {
const { bridge, cleanup } = await freshModule();
try {
const stale = await bridge.prepareOAuthCallback();
const stalePending = bridge.awaitOAuthCallback(undefined, stale.sessionId);
const staleRejects = assert.rejects(stalePending, /cancelled/);
const fresh = await bridge.prepareOAuthCallback();
await staleRejects;
const nextPending = bridge.awaitOAuthCallback(undefined, fresh.sessionId);
const status = await fetchCallback(fresh.port, { code: "fresh-code" });
assert.equal(status, 200);
const result = await nextPending;
assert.equal(result.code, "fresh-code");
} finally {
await cleanup();
}
});
test("getActiveOAuthPort is null before prepare and after cleanup", async () => {
const { bridge, cleanup } = await freshModule();
try {
assert.equal(bridge.getActiveOAuthPort(), null);
await bridge.prepareOAuthCallback();
assert.notEqual(bridge.getActiveOAuthPort(), null);
} finally {
await cleanup();
}
assert.equal(require("./oauthBridge.cjs").getActiveOAuthPort(), null);
});