Files
ForgeFlow-Public/tests/ssh-connection.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

99 lines
4.0 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
// SshService resolves ssh2 lazily, so replacing the cached module is enough to
// drive a real connection lifecycle without a server.
const ssh2Path = require.resolve("ssh2");
const realSsh2 = require("ssh2");
function withFakeSsh2(Client, run) {
require.cache[ssh2Path] = { id: ssh2Path, filename: ssh2Path, loaded: true, exports: { ...realSsh2, Client } };
try {
return run();
} finally {
require.cache[ssh2Path] = { id: ssh2Path, filename: ssh2Path, loaded: true, exports: realSsh2 };
}
}
const { SshService } = require("../src/main/ssh-service.cjs");
function store(server = {}) {
return {
getServer: () => ({ id: "unraid", host: "tower", port: 22, username: "root", authType: "password", basePath: "/mnt/user/appdata", hostFingerprint: "SHA256:trusted", ...server }),
getServerCredentials: () => ({ password: "secret", passphrase: "" }),
};
}
test("a connection that fails twice rejects once and never terminates the process", async () => {
class DoubleFailingClient extends EventEmitter {
connect() {
setImmediate(() => this.emit("error", Object.assign(new Error("connect ECONNREFUSED"), { code: "ECONNREFUSED" })));
}
end() {
// The socket resets shortly after teardown. An unhandled 'error' event on
// an EventEmitter takes the whole main process down.
setImmediate(() => this.emit("error", new Error("read ECONNRESET")));
}
}
const service = withFakeSsh2(DoubleFailingClient, () => new SshService({ store: store(), diagnostics: null }));
await assert.rejects(
() => withFakeSsh2(DoubleFailingClient, () => service.exec("unraid", "true")),
(error) => {
assert.equal(error.code, "ECONNREFUSED");
assert.match(error.message, /SSH connection failed/);
return true;
},
);
// Give the delayed teardown error time to land while the test is still running.
await new Promise((resolve) => setTimeout(resolve, 50));
});
test("a host key that does not match the trusted fingerprint is reported as an identity change", async () => {
class MismatchingClient extends EventEmitter {
connect(options) {
options.hostVerifier(Buffer.from("a different host key"));
setImmediate(() => this.emit("error", new Error("handshake failed")));
}
end() {}
}
const service = withFakeSsh2(MismatchingClient, () => new SshService({ store: store(), diagnostics: null }));
await assert.rejects(
() => withFakeSsh2(MismatchingClient, () => service.exec("unraid", "true")),
(error) => {
assert.equal(error.code, "SSH_HOST_KEY_MISMATCH");
assert.match(error.message, /SSH host identity changed/);
assert.equal(error.expectedFingerprint, "SHA256:trusted");
assert.ok(error.observedFingerprint.startsWith("SHA256:"));
return true;
},
);
});
test("running a command requires a trusted host fingerprint", async () => {
const service = new SshService({ store: store({ hostFingerprint: "" }), diagnostics: null });
await assert.rejects(() => service.exec("unraid", "true"), (error) => {
assert.equal(error.code, "SSH_HOST_NOT_TRUSTED");
return true;
});
await assert.rejects(() => service.uploadBuffer("unraid", "/mnt/user/appdata/x", "data"), (error) => {
assert.equal(error.code, "SSH_HOST_NOT_TRUSTED");
return true;
});
});
test("a remote upload path may not escape into an arbitrary location", () => {
const service = new SshService({ store: store(), diagnostics: null });
assert.equal(service.ensureUploadTarget("/mnt/user/appdata/app/file.tar"), "/mnt/user/appdata/app/file.tar");
assert.equal(service.ensureUploadTarget("\\mnt\\user\\appdata\\app"), "/mnt/user/appdata/app");
for (const value of ["relative/path", "/mnt/../etc/passwd", "/mnt/user/../../etc", "", null]) {
assert.throws(() => service.ensureUploadTarget(value), /absolute safe Unix path/);
}
});