97 lines
4.2 KiB
JavaScript
97 lines
4.2 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { mkdtemp, mkdir, copyFile, rm } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import test from 'node:test';
|
|
import shellVerification from '../src/shared/shell-verification.cjs';
|
|
|
|
const { bashSyntaxCheckInvocation, bashSyntaxCheckFromTextInvocation, normalizeRelativePosixPath, validateShellScriptStructure, shouldRunExternalBash } = shellVerification;
|
|
|
|
test('Shell validation refuses absolute and escaping script paths', () => {
|
|
assert.throws(() => normalizeRelativePosixPath('C:\\Projects\\ForgeFlow\\script.sh'), /must be relative/);
|
|
assert.throws(() => normalizeRelativePosixPath('/tmp/script.sh'), /must be relative/);
|
|
assert.throws(() => normalizeRelativePosixPath('../script.sh'), /may not escape/);
|
|
});
|
|
|
|
test('Bash syntax validation works from a project root containing spaces', async (t) => {
|
|
if (spawnSync('bash', ['--version'], { encoding: 'utf8' }).status !== 0) {
|
|
t.skip('Bash is not available in this environment.');
|
|
return;
|
|
}
|
|
const tempBase = await mkdtemp(path.join(os.tmpdir(), 'forge flow verify '));
|
|
try {
|
|
const relativeDirectory = path.join(tempBase, 'examples', 'server');
|
|
await mkdir(relativeDirectory, { recursive: true });
|
|
await copyFile(new URL('../examples/server/forgeflow-deploy', import.meta.url), path.join(relativeDirectory, 'forgeflow-deploy'));
|
|
const invocation = bashSyntaxCheckInvocation(tempBase);
|
|
assert.equal(invocation.options.cwd, tempBase);
|
|
assert.deepEqual(invocation.args, ['-n']);
|
|
assert.equal(invocation.options.input.includes('\r'), false);
|
|
const result = spawnSync(invocation.command, invocation.args, invocation.options);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
} finally {
|
|
try {
|
|
await rm(tempBase, {
|
|
recursive: true,
|
|
force: true,
|
|
maxRetries: 20,
|
|
retryDelay: 100
|
|
});
|
|
} catch (error) {
|
|
// Git Bash on Windows can retain a short-lived working-directory handle
|
|
// after bash -n exits. Do not fail a successful syntax test solely because
|
|
// Windows delayed releasing that temporary directory.
|
|
if (!['EBUSY', 'EPERM', 'ENOTEMPTY'].includes(error?.code)) throw error;
|
|
}
|
|
}
|
|
});
|
|
|
|
test('Bash syntax validation from text does not depend on a Windows working directory', () => {
|
|
const invocation = bashSyntaxCheckFromTextInvocation('#!/usr/bin/env bash\nset -euo pipefail\necho ok\n');
|
|
assert.equal(invocation.command, 'bash');
|
|
assert.deepEqual(invocation.args, ['-n']);
|
|
assert.equal(invocation.options.cwd, undefined);
|
|
assert.match(invocation.options.input, /set -euo pipefail/);
|
|
});
|
|
|
|
test('Bash syntax validation from text detects malformed scripts', (t) => {
|
|
if (spawnSync('bash', ['--version'], { encoding: 'utf8' }).status !== 0) {
|
|
t.skip('Bash is not available in this environment.');
|
|
return;
|
|
}
|
|
const invocation = bashSyntaxCheckFromTextInvocation('if true; then\n echo missing fi\n');
|
|
const result = spawnSync(invocation.command, invocation.args, invocation.options);
|
|
assert.notEqual(result.status, 0);
|
|
});
|
|
|
|
test('portable server-script validation does not require a local Bash executable', () => {
|
|
const script = `#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
readonly CONFIG_FILE="/etc/forgeflow/targets.conf"
|
|
echo "Target configuration must be owned by root"
|
|
APP_DIR=/tmp/app
|
|
SHA=0123456789012345678901234567890123456789
|
|
COMPOSE_FILE=docker-compose.yml
|
|
write_status() { :; }
|
|
exec 9>/tmp/test.lock
|
|
flock -n 9
|
|
git -C "$APP_DIR" fetch origin main
|
|
git -C "$APP_DIR" reset --hard "$SHA"
|
|
docker compose -f "$COMPOSE_FILE" up -d --build --remove-orphans
|
|
write_status "healthy"
|
|
write_status "unhealthy"
|
|
`;
|
|
assert.equal(validateShellScriptStructure(script), true);
|
|
});
|
|
|
|
test('portable server-script validation refuses missing deployment safety markers', () => {
|
|
assert.throws(() => validateShellScriptStructure('#!/usr/bin/env bash\nset -Eeuo pipefail\necho unsafe\n'), /missing required safety marker/);
|
|
});
|
|
|
|
test('Windows publication never depends on an external Bash shim', () => {
|
|
assert.equal(shouldRunExternalBash('win32'), false);
|
|
assert.equal(shouldRunExternalBash('linux'), true);
|
|
assert.equal(shouldRunExternalBash('darwin'), true);
|
|
});
|