From d230b5f4458f3ae6c6e0a7e3a6755f49dc74b0b1 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:21:25 +0000 Subject: [PATCH] fix: add Leo/Zara false positive exclusion; add cloneRepo unit tests --- .gitea/ai-review/exclusions.json | 4 +++ app/git.test.js | 58 +++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 3ae3cda..2ca1f18 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -3,5 +3,9 @@ "role": "Rex", "location": "app/git.js", "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數" + }, + { + "location": "app/git.js", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中" } ] diff --git a/app/git.test.js b/app/git.test.js index d96efce..0e7e85b 100644 --- a/app/git.test.js +++ b/app/git.test.js @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { commitAndPush } from './git.js'; +import { commitAndPush, cloneRepo } from './git.js'; // --- helpers --- function makeTmpWorkspace() { @@ -91,3 +91,59 @@ describe('commitAndPush', () => { await assert.doesNotReject(() => commitAndPush(workspace, failSpawn)); }); }); + +describe('cloneRepo', () => { + let workspace; + + before(() => { workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'clone-test-')); }); + after(() => { fs.rmSync(workspace, { recursive: true, force: true }); }); + + it('clones repo when repoDir does not exist', () => { + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + const cloneCalled = spawn.calls.some(c => c.args[0] === 'clone'); + assert.ok(cloneCalled, 'expected git clone to be called'); + }); + + it('fetches and checks out when repoDir already exists', () => { + const repoDir = path.join(workspace, 'repo'); + fs.mkdirSync(repoDir, { recursive: true }); + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + const cloneCalled = spawn.calls.some(c => c.args[0] === 'clone'); + const fetchCalled = spawn.calls.some(c => c.args[0] === 'fetch'); + assert.ok(!cloneCalled, 'clone should not run when repoDir exists'); + assert.ok(fetchCalled, 'fetch should run when repoDir exists'); + }); + + it('does not embed token in any git command argument', () => { + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + for (const { args } of spawn.calls) { + assert.ok(!args.join(' ').includes('test-token'), `Token leaked in git args: ${args.join(' ')}`); + } + }); + + it('uses GIT_ASKPASS for network operations', () => { + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + const networkCalls = spawn.calls.filter(c => ['clone', 'fetch'].includes(c.args[0])); + assert.ok(networkCalls.length > 0, 'expected at least one network git call'); + for (const { args, opts } of networkCalls) { + assert.ok(opts?.env?.GIT_ASKPASS, `GIT_ASKPASS missing for git ${args[0]}`); + } + }); + + it('cleans up askpass script after run', () => { + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh')); + assert.equal(leftover.length, 0, 'askpass script was not cleaned up'); + }); + + it('returns repoDir path', () => { + const spawn = makeSpawn(); + const result = cloneRepo(workspace, spawn); + assert.equal(result, path.join(workspace, 'repo')); + }); +});