[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
336
electron/bridges/oauthBridge.test.cjs
Normal file
336
electron/bridges/oauthBridge.test.cjs
Normal file
@@ -0,0 +1,336 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user