Files
server-deploy/tests/cli-smoke.test.mjs
T

196 lines
9.5 KiB
JavaScript

// Run after building the native executable. No npm packages are required.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { readFileSync, writeFileSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { resolve, join } from 'node:path';
const binary = resolve(process.env.DEPLOYCTL_BIN ?? (process.platform === 'win32' ? 'dist/deployctl.exe' : 'dist/deployctl'));
const intent = JSON.parse(readFileSync(new URL('../protocol/examples/plan-request.json', import.meta.url), 'utf8'));
test('real CLI artifact verification rejects missing metadata without writes', () => {
const directory = mkdtempSync(join(tmpdir(), 'deployctl-artifact-smoke-'));
try {
const result = spawnSync(binary, ['verify-artifacts'], {
encoding:'utf8', timeout:10000,
input:JSON.stringify({directory, artifactDirectory:directory, suite:'resolute', architecture:'amd64', versions:{}}),
});
assert.notEqual(result.status, 0);
assert.equal(result.stdout, '');
assert.equal(result.stderr.includes('unsupported command'), false);
assert.equal(result.stderr.includes(directory), false);
assert.deepEqual(readdirSync(directory), []);
} finally {
rmSync(directory, {recursive:true});
}
});
test('real CLI rejects caller-asserted repository authentication', () => {
const result = spawnSync(binary, ['verify-repository'], {
encoding: 'utf8', timeout: 10000,
input: JSON.stringify({directory:'secret-relative', suite:'resolute', architecture:'amd64',
versions:{}, repositoryAuthenticated:true}),
});
assert.notEqual(result.status, 0);
assert.equal(result.stdout, '');
assert.equal(result.stderr.includes('secret-relative'), false);
});
test('real CLI environment draft rejects malformed locks without writing', () => {
const directory = mkdtempSync(join(tmpdir(), 'deployctl-environment-smoke-'));
try {
const probe = spawnSync(binary, ['preflight'], { encoding: 'utf8', timeout: 10000 });
assert.equal(probe.status, 0, probe.stderr);
const report = JSON.parse(probe.stdout).report;
const suite = report.runtime.os === 'linux' ? report.distribution.codename : 'resolute';
const arch = report.runtime.os === 'linux' ? report.runtime.architecture : 'amd64';
const lock = { protocolVersion: 1, repository: 'https://download.docker.com/linux/ubuntu',
suite, architecture: arch, releaseDigest: `sha256:${'a'.repeat(64)}`,
packages: ['docker-ce', 'docker-ce-cli', 'containerd.io', 'docker-buildx-plugin', 'docker-compose-plugin'].map(name => ({
name, version: '1.2.3-1', filename: `dists/${suite}/pool/stable/${arch}/${name}_1.2.3-1_${arch}.deb`, digest: `sha256:${'b'.repeat(64)}`, size: 123,
})),
};
const run = () => spawnSync(binary, ['plan-environment'], { cwd: directory, encoding: 'utf8', timeout: 10000, input: JSON.stringify({lock}) });
const result = run();
assert.equal(result.status, 0, result.stderr);
const draft = JSON.parse(result.stdout).draft;
assert.equal(draft.executable, false);
assert.equal(draft.repositoryAuthenticated, false);
assert.ok(draft.blockers.includes('dependency_transaction_unresolved'));
assert.equal(draft.requestedPackages.length, 5);
lock.packages[0].version = 'secret;reboot';
const invalid = run();
assert.notEqual(invalid.status, 0);
assert.equal(invalid.stdout, '');
assert.equal(invalid.stderr.includes('secret'), false);
assert.deepEqual(readdirSync(directory), []);
} finally {
rmSync(directory, { recursive: true });
}
});
test('real CLI preflight reports blockers without creating files', () => {
const directory = mkdtempSync(join(tmpdir(), 'deployctl-preflight-smoke-'));
try {
const result = spawnSync(binary, ['preflight'], { cwd: directory, encoding: 'utf8', timeout: 10000 });
assert.equal(result.status, 0, result.stderr);
const response = JSON.parse(result.stdout);
assert.equal(response.mode, 'local-environment-proposal');
assert.equal(response.proposal.executable, false);
assert.ok(response.proposal.blockers.includes('package_versions_unresolved'));
assert.ok(response.proposal.blockers.includes('host_identity_unverified'));
assert.deepEqual(readdirSync(directory), []);
} finally {
rmSync(directory, { recursive: true });
}
});
test('real CLI previews and verifies without writing into its working directory', () => {
const directory = mkdtempSync(join(tmpdir(), 'deployctl-smoke-'));
const run = (command, input) => spawnSync(binary, [command], {
input: JSON.stringify(input), encoding: 'utf8', cwd: directory, timeout: 10000,
});
try {
const preview = run('plan', intent);
assert.equal(preview.error, undefined);
assert.equal(preview.status, 0, preview.stderr);
const response = JSON.parse(preview.stdout);
assert.equal(response.executable, false);
assert.equal(response.mode, 'offline-preview');
assert.equal(response.plan.projectName, 'sd-git-one');
const check = run('verify-plan', { plan: response.plan, current: intent });
assert.equal(check.status, 0, check.stderr);
assert.equal(JSON.parse(check.stdout).valid, true);
const changed = run('verify-plan', { plan: response.plan, current: { ...intent, hostId: 'different-host' } });
assert.notEqual(changed.status, 0);
assert.equal(changed.stdout, '');
const apply = run('apply', response.plan);
assert.notEqual(apply.status, 0);
assert.equal(apply.stdout, '');
assert.deepEqual(readdirSync(directory), []);
} finally {
// Only remove the unique test directory this test just created.
rmSync(directory, { recursive: true });
}
});
test('real CLI inspects locally and verifies packages without claiming execution safety', () => {
const inspection = spawnSync(binary, ['inspect'], { encoding: 'utf8', timeout: 10000 });
assert.equal(inspection.status, 0, inspection.stderr);
assert.equal(JSON.parse(inspection.stdout).deploymentReady, false);
const directory = mkdtempSync(join(tmpdir(), 'deployctl-package-smoke-'));
const digest = bytes => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
try {
const payload = 'services: {}\n';
const manifest = JSON.stringify({
protocolVersion: 1, appId: 'example', version: '1.0.0', runtime: 'compose',
entrypoint: 'compose.yaml', platforms: ['linux/amd64'],
components: [{ name: 'server', image: `example/server@sha256:${'a'.repeat(64)}` }],
files: [{ path: 'compose.yaml', digest: digest(payload) }],
});
writeFileSync(join(directory, 'manifest.json'), manifest);
writeFileSync(join(directory, 'compose.yaml'), payload);
const result = spawnSync(binary, ['verify-package'], {
encoding: 'utf8', timeout: 10000,
input: JSON.stringify({ directory, expectedDigest: digest(manifest) }),
});
assert.equal(result.status, 0, result.stderr);
const verified = JSON.parse(result.stdout);
assert.equal(verified.verified, true);
assert.equal(verified.executable, false);
assert.equal(verified.publisherAuthenticated, false);
assert.deepEqual(readdirSync(directory).sort(), ['compose.yaml', 'manifest.json']);
assert.equal(readFileSync(join(directory, 'compose.yaml'), 'utf8'), payload);
} finally {
rmSync(directory, { recursive: true });
}
});
test('real CLI separates package integrity from restricted policy and rejects tampering', () => {
const directory = mkdtempSync(join(tmpdir(), 'deployctl-policy-smoke-'));
const digest = bytes => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
const image = `example/server@sha256:${'a'.repeat(64)}`;
const document = {
services: { server: { image, user: '1000:1000', read_only: true, cap_drop: ['ALL'],
security_opt: ['no-new-privileges:true'], restart: 'no', networks: ['backend'], volumes: [] } },
networks: { backend: { internal: true } }, volumes: {},
};
const run = (command, expectedDigest) => spawnSync(binary, [command], {
encoding: 'utf8', timeout: 10000, cwd: directory,
input: JSON.stringify({ directory, expectedDigest }),
});
const writePackage = () => {
const payload = JSON.stringify(document);
const manifest = JSON.stringify({ protocolVersion: 1, appId: 'example', version: '1.0.0', runtime: 'compose',
entrypoint: 'compose.json', platforms: ['linux/amd64'], components: [{ name: 'server', image }],
files: [{ path: 'compose.json', digest: digest(payload) }],
});
writeFileSync(join(directory, 'manifest.json'), manifest);
writeFileSync(join(directory, 'compose.json'), payload);
return digest(manifest);
};
try {
const pin = writePackage();
const check = run('check-package', pin);
assert.equal(check.status, 0, check.stderr);
const result = JSON.parse(check.stdout);
assert.equal(result.policyPassed, true);
assert.equal(result.executable, false);
assert.equal(result.publisherAuthenticated, false);
assert.equal(result.digest, pin);
writeFileSync(join(directory, 'compose.json'), '{}');
assert.notEqual(run('check-package', pin).status, 0);
document.services.server.privileged = true;
const unsafePin = writePackage();
assert.equal(run('verify-package', unsafePin).status, 0);
const rejected = run('check-package', unsafePin);
assert.notEqual(rejected.status, 0);
assert.equal(rejected.stdout, '');
assert.deepEqual(readdirSync(directory).sort(), ['compose.json', 'manifest.json']);
} finally {
rmSync(directory, { recursive: true });
}
});