110 lines
4.3 KiB
JavaScript
110 lines
4.3 KiB
JavaScript
|
|
const { test, before, after } = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
const { spawnSync, execFileSync } = require('node:child_process');
|
||
|
|
const fs = require('node:fs');
|
||
|
|
const os = require('node:os');
|
||
|
|
const path = require('node:path');
|
||
|
|
|
||
|
|
const CLI = path.join(__dirname, '..', 'src', 'cli.js');
|
||
|
|
const TOKEN = 'stoke-secret-token-for-clone-tests';
|
||
|
|
|
||
|
|
// A local stand-in for the forge: a directory holding bare repositories laid
|
||
|
|
// out as <owner>/<repo>.git, so config.url can point at it with a file:// URL
|
||
|
|
// and `repo clone` exercises real git clones without any network.
|
||
|
|
let root;
|
||
|
|
let remote;
|
||
|
|
let work;
|
||
|
|
let cfg;
|
||
|
|
|
||
|
|
function git(args, cwd) {
|
||
|
|
return execFileSync('git', args, { cwd: cwd || root, encoding: 'utf8' });
|
||
|
|
}
|
||
|
|
|
||
|
|
function run(args, cwd) {
|
||
|
|
return spawnSync(process.execPath, [CLI, ...args], {
|
||
|
|
cwd: cwd || work,
|
||
|
|
encoding: 'utf8',
|
||
|
|
env: { ...process.env, STOKE_CONFIG_FILE: cfg },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
before(() => {
|
||
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-clone-test-'));
|
||
|
|
remote = path.join(root, 'remote');
|
||
|
|
work = path.join(root, 'work');
|
||
|
|
const seed = path.join(root, 'seed');
|
||
|
|
fs.mkdirSync(path.join(remote, 'o'), { recursive: true });
|
||
|
|
fs.mkdirSync(work);
|
||
|
|
|
||
|
|
git(['init', '-b', 'main', seed]);
|
||
|
|
fs.writeFileSync(path.join(seed, 'README.md'), 'hello from seed\n');
|
||
|
|
git(['-C', seed, 'add', 'README.md']);
|
||
|
|
git(['-C', seed, '-c', 'user.name=Tester', '-c', 'user.email=tester@example.com', 'commit', '-m', 'initial']);
|
||
|
|
git(['clone', '--bare', seed, path.join(remote, 'o', 'r.git')]);
|
||
|
|
|
||
|
|
cfg = path.join(root, 'config.json');
|
||
|
|
fs.writeFileSync(cfg, JSON.stringify({ url: `file://${remote}`, token: TOKEN, login: 'tester' }));
|
||
|
|
});
|
||
|
|
|
||
|
|
after(() => {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('repo clone rejects an invalid --depth before running git', () => {
|
||
|
|
const res = run(['repo', 'clone', '-o', 'o', '-r', 'r', '--depth', 'zero']);
|
||
|
|
assert.equal(res.status, 1);
|
||
|
|
assert.match(res.stderr, /Depth must be a positive integer/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('repo clone defaults the destination to the repository name', () => {
|
||
|
|
const res = run(['repo', 'clone', '-o', 'o', '-r', 'r']);
|
||
|
|
assert.equal(res.status, 0, res.stderr);
|
||
|
|
const dest = path.join(work, 'r');
|
||
|
|
assert.ok(fs.existsSync(path.join(dest, '.git')));
|
||
|
|
assert.equal(fs.readFileSync(path.join(dest, 'README.md'), 'utf8'), 'hello from seed\n');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('repo clone honors an explicit destination directory', () => {
|
||
|
|
const res = run(['repo', 'clone', '-o', 'o', '-r', 'r', 'custom-dir']);
|
||
|
|
assert.equal(res.status, 0, res.stderr);
|
||
|
|
assert.ok(fs.existsSync(path.join(work, 'custom-dir', '.git')));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('repo clone fails with git\'s status when the destination is not empty', () => {
|
||
|
|
const dest = path.join(work, 'occupied');
|
||
|
|
fs.mkdirSync(dest);
|
||
|
|
fs.writeFileSync(path.join(dest, 'file.txt'), 'in the way\n');
|
||
|
|
const res = run(['repo', 'clone', '-o', 'o', '-r', 'r', 'occupied']);
|
||
|
|
assert.equal(res.status, 128);
|
||
|
|
assert.match(res.stderr, /already exists and is not an empty directory/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('repo clone propagates git\'s failure for a missing repository', () => {
|
||
|
|
const res = run(['repo', 'clone', '-o', 'o', '-r', 'nonexistent']);
|
||
|
|
assert.equal(res.status, 128);
|
||
|
|
assert.match(res.stderr, /does not appear to be a git repository|repository.*does not exist/i);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('repo clone --origin sets the remote name', () => {
|
||
|
|
const res = run(['repo', 'clone', '-o', 'o', '-r', 'r', '--origin', 'upstream', 'named-origin']);
|
||
|
|
assert.equal(res.status, 0, res.stderr);
|
||
|
|
const url = git(['-C', path.join(work, 'named-origin'), 'config', 'remote.upstream.url']);
|
||
|
|
assert.ok(url.trim().endsWith('/o/r.git'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('repo clone never exposes the token in output or repository config', () => {
|
||
|
|
const ok = run(['repo', 'clone', '-o', 'o', '-r', 'r', 'redacted']);
|
||
|
|
assert.equal(ok.status, 0, ok.stderr);
|
||
|
|
const fail = run(['repo', 'clone', '-o', 'o', '-r', 'nonexistent']);
|
||
|
|
|
||
|
|
for (const output of [ok.stdout, ok.stderr, fail.stdout, fail.stderr]) {
|
||
|
|
assert.ok(!output.includes(TOKEN), 'token leaked into CLI output');
|
||
|
|
}
|
||
|
|
|
||
|
|
const dest = path.join(work, 'redacted');
|
||
|
|
const gitConfig = fs.readFileSync(path.join(dest, '.git', 'config'), 'utf8');
|
||
|
|
assert.ok(!gitConfig.includes(TOKEN), 'token persisted in .git/config');
|
||
|
|
const remoteUrl = git(['-C', dest, 'config', 'remote.origin.url']);
|
||
|
|
assert.ok(!remoteUrl.includes(TOKEN), 'token persisted in the remote URL');
|
||
|
|
});
|