diff --git a/extensions/google/oauth.local-login.test.ts b/extensions/google/oauth.local-login.test.ts new file mode 100644 index 000000000000..626bec4390bb --- /dev/null +++ b/extensions/google/oauth.local-login.test.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth?state=state-123"; + +const exchangeCodeForTokensMock = vi.hoisted(() => + vi.fn(async () => ({ + access: "access-token", + refresh: "refresh-token", + expires: 123, + })), +); +const waitForLocalCallbackMock = vi.hoisted(() => + vi.fn(async () => ({ code: "oauth-code", state: "state-123" })), +); + +vi.mock("./oauth.flow.js", () => ({ + buildAuthUrl: () => AUTH_URL, + generateOAuthState: () => "state-123", + generatePkce: () => ({ challenge: "pkce-challenge", verifier: "pkce-verifier" }), + parseCallbackInput: vi.fn(), + shouldUseManualOAuthFlow: (isRemote: boolean) => isRemote, + waitForLocalCallback: waitForLocalCallbackMock, +})); + +vi.mock("./oauth.token.js", () => ({ + exchangeCodeForTokens: exchangeCodeForTokensMock, +})); + +describe("loginGeminiCliOAuth local browser flow", () => { + beforeEach(() => { + exchangeCodeForTokensMock.mockClear(); + waitForLocalCallbackMock.mockClear(); + }); + + it("prints the auth URL before attempting best-effort browser launch", async () => { + const events: string[] = []; + const { loginGeminiCliOAuth } = await import("./oauth.js"); + const openUrl = vi.fn(async () => { + events.push("open"); + }); + const log = vi.fn((message: string) => { + events.push(`log:${message}`); + }); + + const result = await loginGeminiCliOAuth({ + isRemote: false, + openUrl, + log, + note: async () => {}, + prompt: async () => "", + progress: { update: () => {}, stop: () => {} }, + }); + + expect(result).toEqual({ + access: "access-token", + refresh: "refresh-token", + expires: 123, + }); + expect(log).toHaveBeenCalledWith(expect.stringContaining(AUTH_URL)); + expect(openUrl).toHaveBeenCalledWith(AUTH_URL); + expect(events.findIndex((event) => event.startsWith("log:"))).toBeLessThan( + events.indexOf("open"), + ); + expect(waitForLocalCallbackMock).toHaveBeenCalledWith( + expect.objectContaining({ expectedState: "state-123" }), + ); + expect(exchangeCodeForTokensMock).toHaveBeenCalledWith("oauth-code", "pkce-verifier"); + }); +}); diff --git a/extensions/google/oauth.ts b/extensions/google/oauth.ts index d816067048b4..4461567e60ea 100644 --- a/extensions/google/oauth.ts +++ b/extensions/google/oauth.ts @@ -42,10 +42,11 @@ export async function loginGeminiCliOAuth( } ctx.progress.update("Complete sign-in in browser..."); + ctx.log(`\nOpen this URL in your browser:\n\n${authUrl}\n`); try { await ctx.openUrl(authUrl); } catch { - ctx.log(`\nOpen this URL in your browser:\n\n${authUrl}\n`); + // The URL is already visible; browser launch is best-effort. } try {