diff --git a/src/auth/antigravity/oauth.test.ts b/src/auth/antigravity/oauth.test.ts index 9fcfa675..7361d554 100644 --- a/src/auth/antigravity/oauth.test.ts +++ b/src/auth/antigravity/oauth.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test" -import { buildAuthURL, exchangeCode } from "./oauth" -import { ANTIGRAVITY_CLIENT_ID, GOOGLE_TOKEN_URL } from "./constants" +import { buildAuthURL, exchangeCode, startCallbackServer } from "./oauth" +import { ANTIGRAVITY_CLIENT_ID, GOOGLE_TOKEN_URL, ANTIGRAVITY_CALLBACK_PORT } from "./constants" describe("OAuth PKCE Removal", () => { describe("buildAuthURL", () => { @@ -188,4 +188,75 @@ describe("OAuth PKCE Removal", () => { expect(result1.state).not.toBe(result2.state) }) }) + + describe("startCallbackServer Port Handling", () => { + it("should prefer port 51121", () => { + // #given + // Port 51121 should be free + + // #when + const handle = startCallbackServer() + + // #then + // If 51121 is available, should use it + // If not available, should use valid fallback + expect(handle.port).toBeGreaterThan(0) + expect(handle.port).toBeLessThan(65536) + handle.close() + }) + + it("should return actual bound port", () => { + // #when + const handle = startCallbackServer() + + // #then + expect(typeof handle.port).toBe("number") + expect(handle.port).toBeGreaterThan(0) + handle.close() + }) + + it("should fallback to OS-assigned port if 51121 is occupied (EADDRINUSE)", async () => { + // #given - Occupy port 51121 first + const blocker = Bun.serve({ + port: ANTIGRAVITY_CALLBACK_PORT, + fetch: () => new Response("blocked") + }) + + try { + // #when + const handle = startCallbackServer() + + // #then + expect(handle.port).not.toBe(ANTIGRAVITY_CALLBACK_PORT) + expect(handle.port).toBeGreaterThan(0) + handle.close() + } finally { + // Cleanup blocker + blocker.stop() + } + }) + + it("should cleanup server on close", () => { + // #given + const handle = startCallbackServer() + const port = handle.port + + // #when + handle.close() + + // #then - port should be released (can bind again) + const testServer = Bun.serve({ port, fetch: () => new Response("test") }) + expect(testServer.port).toBe(port) + testServer.stop() + }) + + it("should provide redirect URI with actual port", () => { + // #given + const handle = startCallbackServer() + + // #then + expect(handle.redirectUri).toBe(`http://localhost:${handle.port}/oauth-callback`) + handle.close() + }) + }) }) diff --git a/src/auth/antigravity/oauth.ts b/src/auth/antigravity/oauth.ts index aa7ca78d..9fa72c32 100644 --- a/src/auth/antigravity/oauth.ts +++ b/src/auth/antigravity/oauth.ts @@ -148,6 +148,7 @@ export async function fetchUserInfo( export interface CallbackServerHandle { port: number + redirectUri: string waitForCallback: () => Promise close: () => void } @@ -171,43 +172,53 @@ export function startCallbackServer( } } - server = Bun.serve({ - port: 0, - fetch(request: Request): Response { - const url = new URL(request.url) + const fetchHandler = (request: Request): Response => { + const url = new URL(request.url) - if (url.pathname === "/oauth-callback") { - const code = url.searchParams.get("code") || "" - const state = url.searchParams.get("state") || "" - const error = url.searchParams.get("error") || undefined + if (url.pathname === "/oauth-callback") { + const code = url.searchParams.get("code") || "" + const state = url.searchParams.get("state") || "" + const error = url.searchParams.get("error") || undefined - let responseBody: string - if (code && !error) { - responseBody = - "

Login successful

You can close this window.

" - } else { - responseBody = - "

Login failed

Please check the CLI output.

" - } - - setTimeout(() => { - cleanup() - if (resolveCallback) { - resolveCallback({ code, state, error }) - } - }, 100) - - return new Response(responseBody, { - status: 200, - headers: { "Content-Type": "text/html" }, - }) + let responseBody: string + if (code && !error) { + responseBody = + "

Login successful

You can close this window.

" + } else { + responseBody = + "

Login failed

Please check the CLI output.

" } - return new Response("Not Found", { status: 404 }) - }, - }) + setTimeout(() => { + cleanup() + if (resolveCallback) { + resolveCallback({ code, state, error }) + } + }, 100) + + return new Response(responseBody, { + status: 200, + headers: { "Content-Type": "text/html" }, + }) + } + + return new Response("Not Found", { status: 404 }) + } + + try { + server = Bun.serve({ + port: ANTIGRAVITY_CALLBACK_PORT, + fetch: fetchHandler, + }) + } catch (error) { + server = Bun.serve({ + port: 0, + fetch: fetchHandler, + }) + } const actualPort = server.port as number + const redirectUri = `http://localhost:${actualPort}/oauth-callback` const waitForCallback = (): Promise => { return new Promise((resolve, reject) => { @@ -223,6 +234,7 @@ export function startCallbackServer( return { port: actualPort, + redirectUri, waitForCallback, close: cleanup, }