159 lines
4.9 KiB
JavaScript
159 lines
4.9 KiB
JavaScript
|
|
"use strict";
|
|||
|
|
|
|||
|
|
const assert = require("node:assert/strict");
|
|||
|
|
const { EventEmitter } = require("node:events");
|
|||
|
|
const test = require("node:test");
|
|||
|
|
|
|||
|
|
const { _execRemoteShellCommandForTests: execRemoteShellCommand } = require("./sftpBridge.cjs");
|
|||
|
|
|
|||
|
|
function createRemoteStream() {
|
|||
|
|
const stream = new EventEmitter();
|
|||
|
|
stream.stderr = new EventEmitter();
|
|||
|
|
stream.closeCalls = 0;
|
|||
|
|
stream.destroyCalls = 0;
|
|||
|
|
stream.close = () => { stream.closeCalls += 1; };
|
|||
|
|
stream.destroy = () => { stream.destroyCalls += 1; };
|
|||
|
|
return stream;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
test("remote delete exec times out while opening and closes a stream that arrives late", async () => {
|
|||
|
|
let callback;
|
|||
|
|
let invalidations = 0;
|
|||
|
|
const sshClient = {
|
|||
|
|
exec(_command, next) { callback = next; },
|
|||
|
|
destroy() { invalidations += 1; },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
await assert.rejects(
|
|||
|
|
() => execRemoteShellCommand(sshClient, "rm -rf -- /tmp/example", {
|
|||
|
|
openingTimeoutMs: 10,
|
|||
|
|
runTimeoutMs: 100,
|
|||
|
|
maxOutputBytes: 64,
|
|||
|
|
}),
|
|||
|
|
/open timed out/i,
|
|||
|
|
);
|
|||
|
|
assert.equal(invalidations, 1);
|
|||
|
|
|
|||
|
|
const lateStream = createRemoteStream();
|
|||
|
|
callback(null, lateStream);
|
|||
|
|
assert.ok(lateStream.closeCalls + lateStream.destroyCalls > 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("remote delete exec has a running deadline and destroys the live stream", async () => {
|
|||
|
|
const stream = createRemoteStream();
|
|||
|
|
const sshClient = {
|
|||
|
|
exec(_command, next) { next(null, stream); },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
await assert.rejects(
|
|||
|
|
() => execRemoteShellCommand(sshClient, "rm -rf -- /tmp/example", {
|
|||
|
|
openingTimeoutMs: 100,
|
|||
|
|
runTimeoutMs: 10,
|
|||
|
|
maxOutputBytes: 64,
|
|||
|
|
}),
|
|||
|
|
/execution timed out/i,
|
|||
|
|
);
|
|||
|
|
assert.ok(stream.closeCalls + stream.destroyCalls > 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("remote delete exec cancellation closes a stream that arrives after cancellation", async () => {
|
|||
|
|
let callback;
|
|||
|
|
let invalidations = 0;
|
|||
|
|
const controller = new AbortController();
|
|||
|
|
const sshClient = {
|
|||
|
|
exec(_command, next) { callback = next; },
|
|||
|
|
destroy() { invalidations += 1; },
|
|||
|
|
};
|
|||
|
|
const pending = execRemoteShellCommand(sshClient, "rm -rf -- /tmp/example", {
|
|||
|
|
signal: controller.signal,
|
|||
|
|
openingTimeoutMs: 100,
|
|||
|
|
runTimeoutMs: 100,
|
|||
|
|
maxOutputBytes: 64,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
controller.abort(new Error("cancelled by test"));
|
|||
|
|
await assert.rejects(() => pending, /cancelled by test/i);
|
|||
|
|
assert.equal(invalidations, 1);
|
|||
|
|
|
|||
|
|
const lateStream = createRemoteStream();
|
|||
|
|
callback(null, lateStream);
|
|||
|
|
assert.ok(lateStream.closeCalls + lateStream.destroyCalls > 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("remote delete exec rejects and closes the stream when output exceeds its hard limit", async () => {
|
|||
|
|
const stream = createRemoteStream();
|
|||
|
|
const sshClient = {
|
|||
|
|
exec(_command, next) {
|
|||
|
|
next(null, stream);
|
|||
|
|
queueMicrotask(() => stream.emit("data", Buffer.alloc(65, "x")));
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
await assert.rejects(
|
|||
|
|
() => execRemoteShellCommand(sshClient, "rm -rf -- /tmp/example", {
|
|||
|
|
openingTimeoutMs: 100,
|
|||
|
|
runTimeoutMs: 100,
|
|||
|
|
maxOutputBytes: 64,
|
|||
|
|
}),
|
|||
|
|
/output exceeded/i,
|
|||
|
|
);
|
|||
|
|
assert.ok(stream.closeCalls + stream.destroyCalls > 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("remote delete exec preserves split UTF-8 independently on stdout and stderr", async () => {
|
|||
|
|
const stream = createRemoteStream();
|
|||
|
|
const sshClient = {
|
|||
|
|
exec(_command, next) { next(null, stream); },
|
|||
|
|
};
|
|||
|
|
const running = execRemoteShellCommand(sshClient, "printf unicode", {
|
|||
|
|
openingTimeoutMs: 100,
|
|||
|
|
runTimeoutMs: 100,
|
|||
|
|
maxOutputBytes: 64,
|
|||
|
|
});
|
|||
|
|
const stdout = Buffer.from("中文", "utf8");
|
|||
|
|
const stderr = Buffer.from("错误", "utf8");
|
|||
|
|
stream.emit("data", stdout.subarray(0, 2));
|
|||
|
|
stream.stderr.emit("data", stderr.subarray(0, 1));
|
|||
|
|
stream.emit("data", stdout.subarray(2));
|
|||
|
|
stream.stderr.emit("data", stderr.subarray(1));
|
|||
|
|
stream.emit("close", 0);
|
|||
|
|
|
|||
|
|
assert.deepEqual(await running, { stdout: "中文", stderr: "错误", code: 0 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("remote delete exec rejects before exposing a character cut by its byte limit", async () => {
|
|||
|
|
const stream = createRemoteStream();
|
|||
|
|
const sshClient = {
|
|||
|
|
exec(_command, next) { next(null, stream); },
|
|||
|
|
};
|
|||
|
|
const running = execRemoteShellCommand(sshClient, "printf unicode", {
|
|||
|
|
openingTimeoutMs: 100,
|
|||
|
|
runTimeoutMs: 100,
|
|||
|
|
maxOutputBytes: 2,
|
|||
|
|
});
|
|||
|
|
stream.emit("data", Buffer.from("中", "utf8"));
|
|||
|
|
|
|||
|
|
await assert.rejects(running, (error) => (
|
|||
|
|
/output exceeded/i.test(error?.message || "") && !/<2F>/u.test(error?.message || "")
|
|||
|
|
));
|
|||
|
|
assert.ok(stream.closeCalls + stream.destroyCalls > 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("remote extract exec drains stdout without treating progress as a hard limit", async () => {
|
|||
|
|
const stream = createRemoteStream();
|
|||
|
|
const sshClient = {
|
|||
|
|
exec(_command, next) { next(null, stream); },
|
|||
|
|
};
|
|||
|
|
const running = execRemoteShellCommand(sshClient, "unzip -qo archive.zip", {
|
|||
|
|
openingTimeoutMs: 100,
|
|||
|
|
runTimeoutMs: 100,
|
|||
|
|
maxOutputBytes: 8,
|
|||
|
|
discardStdout: true,
|
|||
|
|
});
|
|||
|
|
stream.emit("data", Buffer.alloc(64, "x"));
|
|||
|
|
stream.stderr.emit("data", Buffer.from("ok"));
|
|||
|
|
stream.emit("close", 0);
|
|||
|
|
|
|||
|
|
assert.deepEqual(await running, { stdout: "", stderr: "ok", code: 0 });
|
|||
|
|
});
|