test: strengthen safety-critical coverage

This commit is contained in:
NuklearRabbit
2026-07-29 19:40:27 +02:00
parent 0ed202ec95
commit c826561c77
11 changed files with 613 additions and 5 deletions
+92 -1
View File
@@ -2,9 +2,12 @@ import test from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { createRequire } from "node:module";
import { mkdtemp, writeFile, mkdir } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
const require = createRequire(import.meta.url);
const { SshService, parseCapabilityOutput } = require("../src/main/ssh-service.cjs");
const { SshService, parseCapabilityOutput, shellQuote, fingerprintKey } = require("../src/main/ssh-service.cjs");
test("SSH capability parsing keeps Git optional and reports deployment prerequisites separately", () => {
const b64 = (value) => Buffer.from(value).toString("base64");
@@ -35,3 +38,91 @@ test("SSH execution rejects truncated output instead of using an incomplete inve
(error) => error?.code === "SSH_OUTPUT_TRUNCATED" && /incomplete result/.test(error.message),
);
});
test("SSH helpers quote shell values, fingerprint keys and parse absent capability markers", () => {
assert.equal(shellQuote("it's safe"), "'it'\\''s safe'");
assert.equal(fingerprintKey(Buffer.from('key')), fingerprintKey('key'));
assert.match(fingerprintKey('key'), /^SHA256:/);
assert.deepEqual(parseCapabilityOutput('plain server banner'), {
platform: 'plain server banner', docker: false, dockerReady: false, compose: false, git: false, tar: false, checksum: false
});
});
test("server validation handles password and missing or non-file private keys", async (context) => {
const root = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-ssh-'));
context.after(() => import('node:fs/promises').then(({ rm }) => rm(root, { recursive: true, force: true })));
const service = new SshService({ store: {}, diagnostics: null });
assert.deepEqual(await service.validateServerConfiguration({ authType: 'password' }), { valid: true, method: 'password' });
await assert.rejects(service.validateServerConfiguration({ authType: 'privateKey', privateKeyPath: '' }), /select a private key/i);
await assert.rejects(service.validateServerConfiguration({ authType: 'privateKey', privateKeyPath: path.join(root, 'missing') }), (error) => error.code === 'SSH_PRIVATE_KEY_NOT_FOUND');
await mkdir(path.join(root, 'directory'));
await assert.rejects(service.validateServerConfiguration({ authType: 'privateKey', privateKeyPath: path.join(root, 'directory') }), (error) => error.code === 'SSH_PRIVATE_KEY_NOT_FOUND');
});
test("connection options enforce host identity and support password credentials", async () => {
const key = Buffer.from('server-key');
const fingerprint = fingerprintKey(key);
const store = { getServerCredentials: () => ({ password: 'secret' }) };
const service = new SshService({ store, diagnostics: null });
const trusted = await service.connectionOptions({ id: 'one', host: 'server', port: 2222, username: 'root', authType: 'password', hostFingerprint: fingerprint });
assert.equal(trusted.options.password, 'secret');
assert.equal(trusted.options.port, 2222);
assert.equal(trusted.options.hostVerifier(key), true);
assert.equal(trusted.getObservedFingerprint(), fingerprint);
assert.equal(trusted.options.hostVerifier(Buffer.from('changed')), false);
const firstUse = await service.connectionOptions({ id: 'one', host: 'server', username: 'root', authType: 'password' }, { trustOnFirstUse: true });
assert.equal(firstUse.options.port, 22);
assert.equal(firstUse.options.hostVerifier(key), true);
});
test("connection options report unreadable private keys without leaking credentials", async () => {
const service = new SshService({ store: { getServerCredentials: () => ({ passphrase: 'secret' }) }, diagnostics: null });
await assert.rejects(
service.connectionOptions({ id: 'key', host: 'server', username: 'root', authType: 'privateKey', privateKeyPath: 'Z:/missing/key' }),
(error) => error.code === 'SSH_PRIVATE_KEY_READ_FAILED' && !error.message.includes('secret')
);
});
test("SSH execution distinguishes startup errors, command failures, success and timeout", async () => {
const service = new SshService({ store: {}, diagnostics: null });
const clientFor = (start) => ({ exec(_command, callback) { start(callback); } });
await assert.rejects(service.execClient(clientFor((callback) => callback(new Error('exec unavailable'))), 'x'), /exec unavailable/);
const commandClient = clientFor((callback) => {
const stream = new EventEmitter(); stream.stderr = new EventEmitter(); callback(null, stream);
queueMicrotask(() => { stream.stderr.emit('data', Buffer.from('permission denied')); stream.emit('close', 23, 'TERM'); });
});
await assert.rejects(service.execClient(commandClient, 'x'), (error) => error.code === 'SSH_COMMAND_FAILED' && error.exitCode === 23 && error.signal === 'TERM');
const successClient = clientFor((callback) => {
const stream = new EventEmitter(); stream.stderr = new EventEmitter(); callback(null, stream);
queueMicrotask(() => { stream.emit('data', Buffer.from('ok')); stream.stderr.emit('data', Buffer.from('warning')); stream.emit('close', 0, null); });
});
assert.deepEqual(await service.execClient(successClient, 'x'), { stdout: 'ok', stderr: 'warning', exitCode: 0, truncated: false });
const hangingClient = clientFor((callback) => { const stream = new EventEmitter(); stream.stderr = new EventEmitter(); callback(null, stream); });
await assert.rejects(service.execClient(hangingClient, 'x', { timeout: 5 }), /timed out/i);
});
test("upload and execution reject unsafe paths and untrusted hosts", async () => {
const service = new SshService({ store: { getServer: () => ({ id: 'one' }) }, diagnostics: null });
assert.equal(service.ensureUploadTarget('\\srv\\apps\\file'), '/srv/apps/file');
for (const target of ['', 'relative/file', '/srv/../secret', `/srv/${String.fromCharCode(0)}bad`]) {
assert.throws(() => service.ensureUploadTarget(target), /absolute safe Unix path/i);
}
await assert.rejects(service.withSftp('one', '/srv/file', () => {}), (error) => error.code === 'SSH_HOST_NOT_TRUSTED');
await assert.rejects(service.exec('one', 'true'), (error) => error.code === 'SSH_HOST_NOT_TRUSTED');
});
test("uploadFile rejects directories before connecting", async (context) => {
const root = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-upload-'));
context.after(() => import('node:fs/promises').then(({ rm }) => rm(root, { recursive: true, force: true })));
const service = new SshService({ store: {}, diagnostics: null });
await assert.rejects(service.uploadFile('one', root, '/srv/file'), /not a file/i);
const file = path.join(root, 'file');
await writeFile(file, 'content');
service.withSftp = async (_id, remotePath, action) => action({ fastPut(_local, _target, options, callback) { options.step(7, 7, 7); callback(null); } }, remotePath);
let progress = null;
assert.deepEqual(await service.uploadFile('one', file, '/srv/file', { onProgress: (value) => { progress = value; } }), { remotePath: '/srv/file', size: 7 });
assert.deepEqual(progress, { transferred: 7, total: 7 });
});