basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import { vi, describe, expect, it, afterEach, beforeEach } from 'vitest';8import * as child_process from 'node:child_process';9import {10 isGitHubRepositoryAsync,11 getGitRepoRootAsync,12 getLatestGitHubRelease,13 getGitHubRepoInfoAsync,14} from './gitUtils.js';15 16vi.mock('node:child_process');17 18function mockExecFileStdout(stdout: string): void {19 vi.mocked(child_process.execFile).mockImplementation(((20 _cmd,21 _args,22 _opts,23 cb,24 ) => {25 (cb as (err: Error | null, stdout: string, stderr: string) => void)(26 null,27 stdout,28 '',29 );30 return {} as ReturnType<typeof child_process.execFile>;31 }) as typeof child_process.execFile);32}33 34function mockExecFileError(error: Error): void {35 vi.mocked(child_process.execFile).mockImplementation(((36 _cmd,37 _args,38 _opts,39 cb,40 ) => {41 (cb as (err: Error, stdout: string, stderr: string) => void)(error, '', '');42 return {} as ReturnType<typeof child_process.execFile>;43 }) as typeof child_process.execFile);44}45 46describe('isGitHubRepositoryAsync', async () => {47 beforeEach(() => {48 vi.resetAllMocks();49 });50 51 afterEach(() => {52 vi.restoreAllMocks();53 });54 55 it('returns false if the git command fails', async () => {56 mockExecFileError(new Error('oops'));57 58 await expect(isGitHubRepositoryAsync()).resolves.toBe(false);59 });60 61 it.each([62 [63 'non-GitHub remote',64 `origin https://gitlab.com/owner/repo.git (fetch)65origin https://gitlab.com/owner/repo.git (push)`,66 ],67 [68 'github.com lookalike host',69 `origin https://github.com.evil/owner/repo.git (fetch)70origin https://github.com.evil/owner/repo.git (push)`,71 ],72 [73 'github.com only in path',74 `origin https://gitlab.com/owner/github.com-mirror.git (fetch)75origin https://gitlab.com/owner/github.com-mirror.git (push)`,76 ],77 [78 'GitHub SSH lookalike host',79 `origin git@github.com.evil:owner/repo.git (fetch)80origin git@github.com.evil:owner/repo.git (push)`,81 ],82 ])('returns false for %s', async (_name, remotes) => {83 mockExecFileStdout(remotes);84 85 await expect(isGitHubRepositoryAsync()).resolves.toBe(false);86 });87 88 it.each([89 [90 'HTTPS GitHub remote',91 `origin https://github.com/sethvargo/gemini-cli (fetch)92origin https://github.com/sethvargo/gemini-cli (push)`,93 ],94 [95 'GitHub SSH remote',96 `origin git@github.com:owner/repo.git (fetch)97origin git@github.com:owner/repo.git (push)`,98 ],99 [100 'GitHub SSH URL remote',101 `origin ssh://git@github.com/owner/repo.git (fetch)102origin ssh://git@github.com/owner/repo.git (push)`,103 ],104 [105 'GitHub SSH URL remote with explicit port',106 `origin ssh://git@github.com:22/owner/repo.git (fetch)107origin ssh://git@github.com:22/owner/repo.git (push)`,108 ],109 ])('returns true for %s', async (_name, remotes) => {110 mockExecFileStdout(remotes);111 112 await expect(isGitHubRepositoryAsync()).resolves.toBe(true);113 });114 115 it('returns true for GitHub remotes without blocking execSync', async () => {116 mockExecFileStdout('origin https://github.com/owner/repo.git (fetch)\n');117 118 await expect(isGitHubRepositoryAsync()).resolves.toBe(true);119 expect(child_process.execSync).not.toHaveBeenCalled();120 });121});122 123describe('getGitHubRepoInfoAsync', async () => {124 beforeEach(() => {125 vi.resetAllMocks();126 });127 128 afterEach(() => {129 vi.restoreAllMocks();130 });131 132 it('throws an error if github repo info cannot be determined', async () => {133 mockExecFileError(new Error('oops'));134 135 await expect(getGitHubRepoInfoAsync()).rejects.toThrowError(/oops/);136 });137 138 it.each([139 ['empty remote', ''],140 ['non-GitHub SSH URL', 'git@gitlab.com:owner/repo.git'],141 ['non-GitHub HTTPS URL', 'https://gitlab.com/owner/repo.git'],142 ])('throws if owner/repo cannot be determined for %s', async (_name, url) => {143 mockExecFileStdout(url);144 145 await expect(getGitHubRepoInfoAsync()).rejects.toThrowError(146 /Owner & repo could not be extracted from remote URL/,147 );148 });149 150 it.each([151 ['plain HTTPS URL', 'https://github.com/owner/repo.git'],152 [153 'classic PAT token',154 'https://ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@github.com/owner/repo.git',155 ],156 [157 'fine-grained PAT token',158 'https://github_pat_xxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@github.com/owner/repo.git',159 ],160 [161 'username:password credentials',162 'https://username:password@github.com/owner/repo.git',163 ],164 [165 'OAuth token credentials',166 'https://oauth2:gho_xxxxxxxxxxxx@github.com/owner/repo.git',167 ],168 [169 'GitHub Actions token credentials',170 'https://x-access-token:ghs_xxxxxxxxxxxx@github.com/owner/repo.git',171 ],172 ['uppercase host', 'https://GITHUB.COM/owner/repo.git'],173 ['mixed case host', 'https://GitHub.Com/owner/repo.git'],174 ['SCP-style SSH URL', 'git@github.com:owner/repo.git'],175 ['SSH URL with explicit port', 'ssh://git@github.com:22/owner/repo.git'],176 ['URL without .git suffix', 'https://github.com/owner/repo'],177 ])('returns owner and repo for %s', async (_name, url) => {178 mockExecFileStdout(url);179 180 await expect(getGitHubRepoInfoAsync()).resolves.toEqual({181 owner: 'owner',182 repo: 'repo',183 });184 });185 186 it('handles repo names containing .git substring', async () => {187 mockExecFileStdout('https://github.com/owner/my.git.repo.git');188 189 await expect(getGitHubRepoInfoAsync()).resolves.toEqual({190 owner: 'owner',191 repo: 'my.git.repo',192 });193 });194 195 it('returns the owner and repo without blocking execSync', async () => {196 mockExecFileStdout('git@github.com:owner/repo.git\n');197 198 await expect(getGitHubRepoInfoAsync()).resolves.toEqual({199 owner: 'owner',200 repo: 'repo',201 });202 expect(child_process.execSync).not.toHaveBeenCalled();203 });204});205 206describe('getGitRepoRootAsync', async () => {207 beforeEach(() => {208 vi.resetAllMocks();209 });210 211 afterEach(() => {212 vi.restoreAllMocks();213 });214 215 it('throws an error if git root cannot be determined', async () => {216 mockExecFileError(new Error('oops'));217 218 await expect(getGitRepoRootAsync()).rejects.toThrowError(/oops/);219 });220 221 it('throws an error if git root is empty', async () => {222 mockExecFileStdout('');223 224 await expect(getGitRepoRootAsync()).rejects.toThrowError(225 /Git repo returned empty value/,226 );227 });228 229 it('returns the root without blocking execSync', async () => {230 mockExecFileStdout('/path/to/git/repo\n');231 232 await expect(getGitRepoRootAsync()).resolves.toBe('/path/to/git/repo');233 expect(child_process.execSync).not.toHaveBeenCalled();234 });235});236 237describe('getLatestRelease', async () => {238 beforeEach(() => {239 vi.resetAllMocks();240 });241 242 afterEach(() => {243 vi.restoreAllMocks();244 });245 246 it('throws an error if the fetch fails', async () => {247 global.fetch = vi.fn(() => Promise.reject('nope'));248 await expect(getLatestGitHubRelease()).rejects.toThrowError(249 /Unable to determine the latest/,250 );251 });252 253 it('throws an error if the fetch does not return a json body', async () => {254 global.fetch = vi.fn(() =>255 Promise.resolve({256 ok: true,257 json: () => Promise.resolve({ foo: 'bar' }),258 } as Response),259 );260 await expect(getLatestGitHubRelease()).rejects.toThrowError(261 /Unable to determine the latest/,262 );263 });264 265 it('returns the release version', async () => {266 global.fetch = vi.fn(() =>267 Promise.resolve({268 ok: true,269 json: () => Promise.resolve({ tag_name: 'v1.2.3' }),270 } as Response),271 );272 await expect(getLatestGitHubRelease()).resolves.toBe('v1.2.3');273 });274});275 