Files
ForgeFlow-Public/tests/clone-target.test.mjs
T
NuklearRabbit f60b269686
ForgeFlow quality gate / quality (push) Successful in 4m12s
ForgeFlow quality gate / secret-scan (push) Successful in 7s
Publish curated ForgeFlow source from 2ed1787c0b52
2026-09-29 22:50:57 +02:00

121 lines
5.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import cloneTargetModule from '../src/shared/clone-target.cjs';
import gitModule from '../src/main/git-service.cjs';
const exec = promisify(execFile);
const { cloneDirectoryName, resolveCloneTarget } = cloneTargetModule;
const { GitService } = gitModule;
test('derives a safe repository folder name from HTTPS and SSH clone URLs', () => {
assert.equal(cloneDirectoryName('https://gitea.example.test/jens/ForgeFlow.git'), 'ForgeFlow');
assert.equal(cloneDirectoryName('git@gitea.example.test:jens/my-app.git'), 'my-app');
assert.equal(cloneDirectoryName('ssh://git@gitea.example.test/jens/app.git?ref=main'), 'app');
});
test('resolves the automatic clone target inside the configured project root', () => {
const root = path.join(os.tmpdir(), 'forgeflow-projects');
const plan = resolveCloneTarget(root, 'https://gitea.example.test/jens/portfolio.git');
assert.equal(plan.root, path.resolve(root));
assert.equal(plan.target, path.join(path.resolve(root), 'portfolio'));
assert.equal(plan.directoryName, 'portfolio');
});
test('a clone target that would leave the project root is refused', () => {
const root = path.join(os.tmpdir(), 'forgeflow-projects');
const resolved = path.resolve(root);
// The escape guard inside resolveCloneTarget stays as a backstop, but no
// sanitised folder name can reach it any more: the name is a single path
// segment and a dots-only segment falls back to "repository".
for (const remote of ['..', '.', '../escape', '/', '', '....git', 'https://gitea.example.test/jens/....git']) {
const plan = resolveCloneTarget(root, remote);
assert.ok(
plan.target.startsWith(`${resolved}${path.sep}`) && plan.target !== resolved,
`${remote} resolved outside the project root: ${plan.target}`,
);
}
for (const badRoot of ['', ' ', null, undefined]) {
assert.throws(() => resolveCloneTarget(badRoot, 'https://gitea.example.test/jens/app.git'), /project root is required/);
}
});
test('a folder name that sanitises away still produces a usable directory', () => {
// Windows strips trailing dots, so a dots-only name would land on the project
// root itself instead of a subdirectory.
assert.equal(cloneDirectoryName('https://gitea.example.test/jens/....git'), 'repository');
assert.equal(cloneDirectoryName('..'), 'repository');
assert.equal(cloneDirectoryName(''), 'repository');
assert.equal(cloneDirectoryName('https://gitea.example.test/jens/app.git#readme'), 'app');
assert.equal(cloneDirectoryName('https://gitea.example.test/jens/spaced name.git'), 'spaced-name');
});
test('clone target inspection accepts missing and empty destinations', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-target-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const service = new GitService();
const remote = 'https://gitea.example.test/jens/app.git';
const missing = await service.inspectCloneTarget(remote, path.join(root, 'missing-app'));
assert.equal(missing.state, 'missing');
const emptyPath = path.join(root, 'empty-app');
await fs.mkdir(emptyPath);
const empty = await service.inspectCloneTarget(remote, emptyPath);
assert.equal(empty.state, 'empty');
});
test('clone target inspection reuses an existing checkout with the same origin', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-reuse-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const target = path.join(root, 'app');
await fs.mkdir(target);
await exec('git', ['init'], { cwd: target, encoding: 'utf8' });
await exec('git', ['remote', 'add', 'origin', 'git@gitea.example.test:jens/app.git'], { cwd: target, encoding: 'utf8' });
const service = new GitService();
const assessment = await service.inspectCloneTarget('https://gitea.example.test/jens/app.git', target);
assert.equal(assessment.state, 'matching-repository');
});
test('clone target inspection blocks a different repository and ordinary files', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-conflict-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const service = new GitService();
const otherRepository = path.join(root, 'repository');
await fs.mkdir(otherRepository);
await exec('git', ['init'], { cwd: otherRepository, encoding: 'utf8' });
await exec('git', ['remote', 'add', 'origin', 'https://gitea.example.test/jens/other.git'], { cwd: otherRepository, encoding: 'utf8' });
await assert.rejects(
service.inspectCloneTarget('https://gitea.example.test/jens/app.git', otherRepository),
(error) => error.code === 'CLONE_TARGET_DIFFERENT_REPOSITORY'
);
const ordinaryFolder = path.join(root, 'ordinary');
await fs.mkdir(ordinaryFolder);
await fs.writeFile(path.join(ordinaryFolder, 'notes.txt'), 'do not overwrite\n');
await assert.rejects(
service.inspectCloneTarget('https://gitea.example.test/jens/app.git', ordinaryFolder),
(error) => error.code === 'CLONE_TARGET_NOT_EMPTY'
);
});
test('clone target inspection blocks a file at the automatic destination', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-file-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const target = path.join(root, 'app');
await fs.writeFile(target, 'not a directory');
const service = new GitService();
await assert.rejects(
service.inspectCloneTarget('https://gitea.example.test/jens/app.git', target),
(error) => error.code === 'CLONE_TARGET_NOT_DIRECTORY'
);
});