tests(google): inject oauth credential fs stubs

This commit is contained in:
huntharo 2026-03-17 09:30:01 -04:00 committed by Harold Hunt
parent 4234d9b42c
commit a413da9cca
2 changed files with 54 additions and 26 deletions

View File

@ -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<typeof existsSync>[0]) => ReturnType<typeof existsSync>;
readFileSync: (path: Parameters<typeof readFileSync>[0], encoding: "utf8") => string;
realpathSync: (path: Parameters<typeof realpathSync>[0]) => string;
readdirSync: (
path: Parameters<typeof readdirSync>[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<CredentialFs>): 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;

View File

@ -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<typeof import("node:fs")>();
return {
...actual,
existsSync: (...args: Parameters<typeof actual.existsSync>) => mockExistsSync(...args),
readFileSync: (...args: Parameters<typeof actual.readFileSync>) => mockReadFileSync(...args),
realpathSync: (...args: Parameters<typeof actual.realpathSync>) => mockRealpathSync(...args),
readdirSync: (...args: Parameters<typeof actual.readdirSync>) => 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