feat: refactor commitAndPush to use a runner function and improve token security; add tests for git operations
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { describe, it, before, after, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { commitAndPush } from './git.js';
|
||||
|
||||
// --- helpers ---
|
||||
function makeTmpWorkspace() {
|
||||
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'git-test-'));
|
||||
// Pre-create repo dir so clone branch is skipped
|
||||
fs.mkdirSync(path.join(ws, 'repo'), { recursive: true });
|
||||
// Create a findings.json to copy
|
||||
const findingsDir = path.join(ws, '.gitea/ai-review');
|
||||
fs.mkdirSync(findingsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(findingsDir, 'findings.json'), '[]');
|
||||
return ws;
|
||||
}
|
||||
|
||||
// Default stub: all commands succeed, status returns changes
|
||||
function makeSpawn(overrides = {}) {
|
||||
const calls = [];
|
||||
const spawn = (cmd, args, opts) => {
|
||||
const key = args[0];
|
||||
calls.push({ cmd, args, opts });
|
||||
if (overrides[key]) return overrides[key](args, opts);
|
||||
if (key === 'status') return { status: 0, stdout: 'M .gitea/ai-review/findings.json', stderr: '', error: null };
|
||||
if (key === 'commit') return { status: 0, stdout: '[feature-branch abc1234] chore', stderr: '', error: null };
|
||||
return { status: 0, stdout: '', stderr: '', error: null };
|
||||
};
|
||||
spawn.calls = calls;
|
||||
return spawn;
|
||||
}
|
||||
|
||||
describe('commitAndPush', () => {
|
||||
let workspace;
|
||||
|
||||
before(() => { workspace = makeTmpWorkspace(); });
|
||||
after(() => { fs.rmSync(workspace, { recursive: true, force: true }); });
|
||||
beforeEach(() => {
|
||||
// Remove leftover askpass scripts between tests
|
||||
for (const f of fs.readdirSync(workspace)) {
|
||||
if (f.endsWith('.git-askpass.sh')) fs.unlinkSync(path.join(workspace, f));
|
||||
}
|
||||
});
|
||||
|
||||
it('does not embed token in any git command argument', async () => {
|
||||
const spawn = makeSpawn();
|
||||
await commitAndPush(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 env for network operations (fetch, push, clone)', async () => {
|
||||
const spawn = makeSpawn();
|
||||
await commitAndPush(workspace, spawn);
|
||||
|
||||
const networkOps = ['fetch', 'push', 'clone'];
|
||||
const networkCalls = spawn.calls.filter(c => networkOps.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 successful run', async () => {
|
||||
await commitAndPush(workspace, makeSpawn());
|
||||
const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh'));
|
||||
assert.equal(leftover.length, 0, 'askpass script was not cleaned up');
|
||||
});
|
||||
|
||||
it('cleans up askpass script even when git fails', async () => {
|
||||
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
||||
await commitAndPush(workspace, failSpawn);
|
||||
const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh'));
|
||||
assert.equal(leftover.length, 0, 'askpass script was not cleaned up after failure');
|
||||
});
|
||||
|
||||
it('skips commit when status shows no changes', async () => {
|
||||
const spawn = makeSpawn({ status: () => ({ status: 0, stdout: '', stderr: '', error: null }) });
|
||||
await commitAndPush(workspace, spawn);
|
||||
const commitCalled = spawn.calls.some(c => c.args[0] === 'commit');
|
||||
assert.equal(commitCalled, false, 'commit should not run when there are no changes');
|
||||
});
|
||||
|
||||
it('does not throw when git command fails', async () => {
|
||||
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
||||
await assert.doesNotReject(() => commitAndPush(workspace, failSpawn));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user