From a413da9ccaaccd6665390f0a748259811e89386b Mon Sep 17 00:00:00 2001 From: huntharo Date: Tue, 17 Mar 2026 09:30:01 -0400 Subject: [PATCH] tests(google): inject oauth credential fs stubs --- extensions/google/oauth.credentials.ts | 37 +++++++++++++++++----- extensions/google/oauth.test.ts | 43 ++++++++++++++------------ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/extensions/google/oauth.credentials.ts b/extensions/google/oauth.credentials.ts index 1c1e88db042..670ae4de943 100644 --- a/extensions/google/oauth.credentials.ts +++ b/extensions/google/oauth.credentials.ts @@ -1,7 +1,27 @@ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs"; +import type { Dirent } from "node:fs"; import { delimiter, dirname, join } from "node:path"; import { CLIENT_ID_KEYS, CLIENT_SECRET_KEYS } from "./oauth.shared.js"; +type CredentialFs = { + existsSync: (path: Parameters[0]) => ReturnType; + readFileSync: (path: Parameters[0], encoding: "utf8") => string; + realpathSync: (path: Parameters[0]) => string; + readdirSync: ( + path: Parameters[0], + options: { withFileTypes: true }, + ) => Dirent[]; +}; + +const defaultFs: CredentialFs = { + existsSync, + readFileSync, + realpathSync, + readdirSync, +}; + +let credentialFs: CredentialFs = defaultFs; + function resolveEnv(keys: string[]): string | undefined { for (const key of keys) { const value = process.env[key]?.trim(); @@ -18,6 +38,10 @@ export function clearCredentialsCache(): void { cachedGeminiCliCredentials = null; } +export function setOAuthCredentialsFsForTest(overrides?: Partial): void { + credentialFs = overrides ? { ...defaultFs, ...overrides } : defaultFs; +} + export function extractGeminiCliCredentials(): { clientId: string; clientSecret: string } | null { if (cachedGeminiCliCredentials) { return cachedGeminiCliCredentials; @@ -29,7 +53,7 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret: return null; } - const resolvedPath = realpathSync(geminiPath); + const resolvedPath = credentialFs.realpathSync(geminiPath); const geminiCliDirs = resolveGeminiCliDirs(geminiPath, resolvedPath); let content: string | null = null; @@ -55,10 +79,9 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret: "oauth2.js", ), ]; - for (const path of searchPaths) { - if (existsSync(path)) { - content = readFileSync(path, "utf8"); + if (credentialFs.existsSync(path)) { + content = credentialFs.readFileSync(path, "utf8"); break; } } @@ -67,7 +90,7 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret: } const found = findFile(geminiCliDir, "oauth2.js", 10); if (found) { - content = readFileSync(found, "utf8"); + content = credentialFs.readFileSync(found, "utf8"); break; } } @@ -116,7 +139,7 @@ function findInPath(name: string): string | null { for (const dir of (process.env.PATH ?? "").split(delimiter)) { for (const ext of exts) { const path = join(dir, name + ext); - if (existsSync(path)) { + if (credentialFs.existsSync(path)) { return path; } } @@ -129,7 +152,7 @@ function findFile(dir: string, name: string, depth: number): string | null { return null; } try { - for (const entry of readdirSync(dir, { withFileTypes: true })) { + for (const entry of credentialFs.readdirSync(dir, { withFileTypes: true })) { const path = join(dir, entry.name); if (entry.isFile() && entry.name === name) { return path; diff --git a/extensions/google/oauth.test.ts b/extensions/google/oauth.test.ts index 8aec64d528d..d37f0751dbe 100644 --- a/extensions/google/oauth.test.ts +++ b/extensions/google/oauth.test.ts @@ -21,23 +21,11 @@ vi.mock("../../src/infra/net/fetch-guard.js", () => ({ }, })); -// Mock fs module before importing the module under test const mockExistsSync = vi.fn(); const mockReadFileSync = vi.fn(); const mockRealpathSync = vi.fn(); const mockReaddirSync = vi.fn(); -vi.mock("node:fs", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - existsSync: (...args: Parameters) => mockExistsSync(...args), - readFileSync: (...args: Parameters) => mockReadFileSync(...args), - realpathSync: (...args: Parameters) => mockRealpathSync(...args), - readdirSync: (...args: Parameters) => mockReaddirSync(...args), - }; -}); - describe("extractGeminiCliCredentials", () => { const normalizePath = (value: string) => value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase(); @@ -51,6 +39,20 @@ describe("extractGeminiCliCredentials", () => { let originalPath: string | undefined; + async function loadCredentialsModule() { + return await import("./oauth.credentials.js"); + } + + async function installMockFs() { + const { setOAuthCredentialsFsForTest } = await loadCredentialsModule(); + setOAuthCredentialsFsForTest({ + existsSync: (...args) => mockExistsSync(...args), + readFileSync: (...args) => mockReadFileSync(...args), + realpathSync: (...args) => mockRealpathSync(...args), + readdirSync: (...args) => mockReaddirSync(...args), + }); + } + function makeFakeLayout() { const binDir = join(rootDir, "fake", "bin"); const geminiPath = join(binDir, "gemini"); @@ -157,17 +159,20 @@ describe("extractGeminiCliCredentials", () => { beforeEach(async () => { vi.clearAllMocks(); originalPath = process.env.PATH; + await installMockFs(); }); - afterEach(() => { + afterEach(async () => { process.env.PATH = originalPath; + const { setOAuthCredentialsFsForTest } = await loadCredentialsModule(); + setOAuthCredentialsFsForTest(); }); it("returns null when gemini binary is not in PATH", async () => { process.env.PATH = "/nonexistent"; mockExistsSync.mockReturnValue(false); - const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js"); + const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule(); clearCredentialsCache(); expect(extractGeminiCliCredentials()).toBeNull(); }); @@ -175,7 +180,7 @@ describe("extractGeminiCliCredentials", () => { it("extracts credentials from oauth2.js in known path", async () => { installGeminiLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT }); - const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js"); + const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule(); clearCredentialsCache(); const result = extractGeminiCliCredentials(); @@ -185,7 +190,7 @@ describe("extractGeminiCliCredentials", () => { it("extracts credentials when PATH entry is an npm global shim", async () => { installNpmShimLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT }); - const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js"); + const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule(); clearCredentialsCache(); const result = extractGeminiCliCredentials(); @@ -195,7 +200,7 @@ describe("extractGeminiCliCredentials", () => { it("returns null when oauth2.js cannot be found", async () => { installGeminiLayout({ oauth2Exists: false, readdir: [] }); - const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js"); + const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule(); clearCredentialsCache(); expect(extractGeminiCliCredentials()).toBeNull(); }); @@ -203,7 +208,7 @@ describe("extractGeminiCliCredentials", () => { it("returns null when oauth2.js lacks credentials", async () => { installGeminiLayout({ oauth2Exists: true, oauth2Content: "// no credentials here" }); - const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js"); + const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule(); clearCredentialsCache(); expect(extractGeminiCliCredentials()).toBeNull(); }); @@ -211,7 +216,7 @@ describe("extractGeminiCliCredentials", () => { it("caches credentials after first extraction", async () => { installGeminiLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT }); - const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js"); + const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule(); clearCredentialsCache(); // First call