714 lines
22 KiB
TypeScript
714 lines
22 KiB
TypeScript
|
|
import test from "node:test";
|
||
|
|
import assert from "node:assert/strict";
|
||
|
|
|
||
|
|
import type { DropEntry } from "../../lib/sftpFileUtils";
|
||
|
|
import type { Host } from "../../types";
|
||
|
|
import { TerminalDropNeedsSudoError } from "../../domain/sftpDropElevation";
|
||
|
|
import {
|
||
|
|
ActiveTerminalCwdUnavailableError,
|
||
|
|
DEFAULT_RZ_MISSING_FALLBACK_TIMEOUT_MS,
|
||
|
|
handleTerminalDropEntries,
|
||
|
|
resolveTerminalDropErrorMessage,
|
||
|
|
} from "./hooks/useTerminalDragDrop";
|
||
|
|
import { resolvePreferredTerminalCwd } from "./sftpCwd";
|
||
|
|
|
||
|
|
const host = {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Host",
|
||
|
|
hostname: "example.com",
|
||
|
|
port: 22,
|
||
|
|
username: "alice",
|
||
|
|
protocol: "ssh",
|
||
|
|
} as Host;
|
||
|
|
|
||
|
|
const dropEntries: DropEntry[] = [
|
||
|
|
{
|
||
|
|
file: null,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
test("terminal drag-drop allows the full ZMODEM startup window before falling back", () => {
|
||
|
|
assert.equal(DEFAULT_RZ_MISSING_FALLBACK_TIMEOUT_MS, 15_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("terminal drag-drop shows actionable guidance when the active directory is unknown", () => {
|
||
|
|
const requestedKeys: string[] = [];
|
||
|
|
const message = resolveTerminalDropErrorMessage(
|
||
|
|
new ActiveTerminalCwdUnavailableError(),
|
||
|
|
(key) => {
|
||
|
|
requestedKeys.push(key);
|
||
|
|
return `translated:${key}`;
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(message, "translated:terminal.dragDrop.destinationUnknown");
|
||
|
|
assert.deepEqual(requestedKeys, ["terminal.dragDrop.destinationUnknown"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("terminal drag-drop asks to enable sudo when /root is not writable as the login user", () => {
|
||
|
|
const requestedKeys: string[] = [];
|
||
|
|
const message = resolveTerminalDropErrorMessage(
|
||
|
|
new TerminalDropNeedsSudoError(),
|
||
|
|
(key) => {
|
||
|
|
requestedKeys.push(key);
|
||
|
|
return `translated:${key}`;
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(message, "translated:terminal.dragDrop.needsSudoElevation");
|
||
|
|
assert.deepEqual(requestedKeys, ["terminal.dragDrop.needsSudoElevation"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH terminal drop triggers ZMODEM drag-drop upload", async () => {
|
||
|
|
let uploadedFiles: unknown;
|
||
|
|
let uploadedSessionId: string | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [
|
||
|
|
{
|
||
|
|
file: {
|
||
|
|
name: "report.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
resolveSftpInitialPath: async () => "/srv/app/current",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
startZmodemDragDropUpload: async (sessionId, files) => {
|
||
|
|
uploadedSessionId = sessionId;
|
||
|
|
uploadedFiles = files;
|
||
|
|
return { success: true };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(uploadedSessionId, "session-1");
|
||
|
|
assert.equal(Array.isArray(uploadedFiles), true);
|
||
|
|
const files = uploadedFiles as Array<{ name: string; remoteName: string; data?: ArrayBuffer }>;
|
||
|
|
assert.equal(files.length, 1);
|
||
|
|
assert.equal(files[0].name, "report.txt");
|
||
|
|
assert.equal(files[0].remoteName, "report.txt");
|
||
|
|
assert.ok(files[0].data);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH terminal drop stays on ZMODEM when rz starts", async () => {
|
||
|
|
let openedSftp = false;
|
||
|
|
let zmodemCallback: ((event: { type: string; transferType?: string }) => void) | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [
|
||
|
|
{
|
||
|
|
file: {
|
||
|
|
name: "report.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: () => {
|
||
|
|
openedSftp = true;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/srv/app/current",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
cancelZmodem: () => {},
|
||
|
|
onSessionData: () => () => {},
|
||
|
|
onZmodemEvent: (_sessionId, cb) => {
|
||
|
|
zmodemCallback = cb;
|
||
|
|
return () => {
|
||
|
|
zmodemCallback = undefined;
|
||
|
|
};
|
||
|
|
},
|
||
|
|
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||
|
|
assert.match(uploadCommand ?? "", /NetcattyRzMissing=/);
|
||
|
|
zmodemCallback?.({ type: "detect", transferType: "upload" });
|
||
|
|
return { success: true };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(openedSftp, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH terminal drop still waits for rz after the old 2.5 second deadline", async () => {
|
||
|
|
let openedSftp = false;
|
||
|
|
let zmodemCallback: ((event: { type: string; transferType?: string }) => void) | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [{
|
||
|
|
file: {
|
||
|
|
name: "report.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
}],
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: () => { openedSftp = true; },
|
||
|
|
resolveSftpInitialPath: async () => "/srv/app/current",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
cancelZmodem: () => {},
|
||
|
|
onSessionData: () => () => {},
|
||
|
|
onZmodemEvent: (_sessionId, cb) => {
|
||
|
|
zmodemCallback = cb;
|
||
|
|
return () => { zmodemCallback = undefined; };
|
||
|
|
},
|
||
|
|
startZmodemDragDropUpload: async () => {
|
||
|
|
setTimeout(() => {
|
||
|
|
zmodemCallback?.({ type: "detect", transferType: "upload" });
|
||
|
|
}, 2_600);
|
||
|
|
return { success: true };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(openedSftp, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("serial terminal drop does not wrap rz with an SSH shell fallback", async () => {
|
||
|
|
let uploadCommandSeen: string | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [
|
||
|
|
{
|
||
|
|
file: {
|
||
|
|
name: "report.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
host: { ...host, protocol: "serial" } as Host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: () => {},
|
||
|
|
resolveSftpInitialPath: async () => "/srv/app/current",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
cancelZmodem: () => {},
|
||
|
|
onSessionData: () => () => {},
|
||
|
|
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||
|
|
uploadCommandSeen = uploadCommand;
|
||
|
|
return { success: true };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(uploadCommandSeen, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("telnet terminal drop does not wrap rz with an SSH shell fallback", async () => {
|
||
|
|
let uploadCommandSeen: string | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [
|
||
|
|
{
|
||
|
|
file: {
|
||
|
|
name: "report.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
host: { ...host, protocol: "telnet" } as Host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: () => {},
|
||
|
|
resolveSftpInitialPath: async () => "/srv/app/current",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
cancelZmodem: () => {},
|
||
|
|
onSessionData: () => () => {},
|
||
|
|
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||
|
|
uploadCommandSeen = uploadCommand;
|
||
|
|
return { success: true };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(uploadCommandSeen, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("network device drop falls back to SFTP upload with a freshly resolved cwd", async () => {
|
||
|
|
let receivedOptions: { preferFreshBackend?: boolean } | undefined;
|
||
|
|
let openedPath: string | undefined;
|
||
|
|
let openedEntries: DropEntry[] | undefined;
|
||
|
|
let openedSessionId: string | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries,
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
isNetworkDevice: true,
|
||
|
|
onOpenSftp: (_host, initialPath, pendingUploadEntries, sourceSessionId) => {
|
||
|
|
openedPath = initialPath;
|
||
|
|
openedEntries = pendingUploadEntries;
|
||
|
|
openedSessionId = sourceSessionId;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async (options) => {
|
||
|
|
receivedOptions = options;
|
||
|
|
return "/srv/app/current";
|
||
|
|
},
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.deepEqual(receivedOptions, {
|
||
|
|
preferFreshBackend: true,
|
||
|
|
requireActiveShellCwd: true,
|
||
|
|
});
|
||
|
|
assert.equal(openedPath, "/srv/app/current");
|
||
|
|
assert.equal(openedEntries, dropEntries);
|
||
|
|
assert.equal(openedSessionId, "session-1");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH terminal drop falls back to SFTP when rz is unavailable", async () => {
|
||
|
|
let receivedOptions: { preferFreshBackend?: boolean } | undefined;
|
||
|
|
let openedPath: string | undefined;
|
||
|
|
let openedEntries: DropEntry[] | undefined;
|
||
|
|
let openedSessionId: string | undefined;
|
||
|
|
let dataCallback: ((chunk: string) => void) | undefined;
|
||
|
|
let cancelled: { sessionId: string; interrupt?: boolean } | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [
|
||
|
|
{
|
||
|
|
file: {
|
||
|
|
name: "report.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (_host, initialPath, pendingUploadEntries, sourceSessionId) => {
|
||
|
|
openedPath = initialPath;
|
||
|
|
openedEntries = pendingUploadEntries;
|
||
|
|
openedSessionId = sourceSessionId;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async (options) => {
|
||
|
|
receivedOptions = options;
|
||
|
|
return "/srv/app/current";
|
||
|
|
},
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
onSessionData: (_sessionId: string, cb: (chunk: string) => void) => {
|
||
|
|
dataCallback = cb;
|
||
|
|
return () => {
|
||
|
|
dataCallback = undefined;
|
||
|
|
};
|
||
|
|
},
|
||
|
|
cancelZmodem: (sessionId: string, options?: { interrupt?: boolean }) => {
|
||
|
|
cancelled = { sessionId, interrupt: options?.interrupt };
|
||
|
|
},
|
||
|
|
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||
|
|
assert.match(uploadCommand ?? "", /NetcattyRzMissing=/);
|
||
|
|
assert.equal((uploadCommand ?? "").includes("\u001b]1337;NetcattyRzMissing="), false);
|
||
|
|
const token = uploadCommand?.match(/NetcattyRzMissing=([A-Za-z0-9_-]+)/)?.[1];
|
||
|
|
assert.ok(token);
|
||
|
|
dataCallback?.(`\u001b]1337;NetcattyRzMissing=${token}\u0007`);
|
||
|
|
return { success: true };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.deepEqual(receivedOptions, {
|
||
|
|
preferFreshBackend: true,
|
||
|
|
requireActiveShellCwd: true,
|
||
|
|
});
|
||
|
|
assert.equal(openedPath, "/srv/app/current");
|
||
|
|
assert.equal(openedEntries?.length, 1);
|
||
|
|
assert.equal(openedEntries?.[0].relativePath, "report.txt");
|
||
|
|
assert.equal(openedSessionId, "session-1");
|
||
|
|
assert.deepEqual(cancelled, { sessionId: "session-1", interrupt: false });
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH terminal drop falls back to SFTP when rz never starts", async () => {
|
||
|
|
let receivedOptions: { preferFreshBackend?: boolean } | undefined;
|
||
|
|
let openedPath: string | undefined;
|
||
|
|
let openedEntries: DropEntry[] | undefined;
|
||
|
|
let cancelled: { sessionId: string; interrupt?: boolean } | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [
|
||
|
|
{
|
||
|
|
file: {
|
||
|
|
name: "report.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (_host, initialPath, pendingUploadEntries) => {
|
||
|
|
openedPath = initialPath;
|
||
|
|
openedEntries = pendingUploadEntries;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async (options) => {
|
||
|
|
receivedOptions = options;
|
||
|
|
return "/srv/app/current";
|
||
|
|
},
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
onSessionData: () => () => {},
|
||
|
|
cancelZmodem: (sessionId: string, options?: { interrupt?: boolean }) => {
|
||
|
|
cancelled = { sessionId, interrupt: options?.interrupt };
|
||
|
|
},
|
||
|
|
startZmodemDragDropUpload: async () => ({ success: true }),
|
||
|
|
},
|
||
|
|
rzMissingFallbackTimeoutMs: 1,
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.deepEqual(receivedOptions, {
|
||
|
|
preferFreshBackend: true,
|
||
|
|
requireActiveShellCwd: true,
|
||
|
|
});
|
||
|
|
assert.equal(openedPath, "/srv/app/current");
|
||
|
|
assert.equal(openedEntries?.length, 1);
|
||
|
|
assert.deepEqual(cancelled, { sessionId: "session-1", interrupt: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH folder drop uses SFTP to preserve directory structure", async () => {
|
||
|
|
let openedEntries: DropEntry[] | undefined;
|
||
|
|
let originSessionId: string | undefined;
|
||
|
|
let sourceSessionId: string | undefined;
|
||
|
|
let zmodemStarted = false;
|
||
|
|
|
||
|
|
const folderEntries: DropEntry[] = [
|
||
|
|
{
|
||
|
|
file: null,
|
||
|
|
relativePath: "docs",
|
||
|
|
isDirectory: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
file: {
|
||
|
|
name: "guide.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "docs/guide.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: folderEntries,
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (_host, _initialPath, pendingUploadEntries, origin, source) => {
|
||
|
|
openedEntries = pendingUploadEntries;
|
||
|
|
originSessionId = origin;
|
||
|
|
sourceSessionId = source;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/srv/app/current",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
startZmodemDragDropUpload: async () => {
|
||
|
|
zmodemStarted = true;
|
||
|
|
return { success: true };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(zmodemStarted, false);
|
||
|
|
assert.equal(openedEntries, folderEntries);
|
||
|
|
assert.equal(originSessionId, "session-1");
|
||
|
|
assert.equal(sourceSessionId, "session-1");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("Mosh folder drop keeps its origin but opens a fresh SFTP route", async () => {
|
||
|
|
let originSessionId: string | undefined;
|
||
|
|
let sourceSessionId: string | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||
|
|
host: { ...host, moshEnabled: true },
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (_host, _path, _entries, origin, source) => {
|
||
|
|
originSessionId = origin;
|
||
|
|
sourceSessionId = source;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/srv/app/current",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "mosh-session",
|
||
|
|
sessionRef: { current: "mosh-session" },
|
||
|
|
terminalBackend: { writeToSession: () => {} },
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(originSessionId, "mosh-session");
|
||
|
|
assert.equal(sourceSessionId, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("ET rz fallback keeps its origin but opens a fresh SFTP route", async () => {
|
||
|
|
let originSessionId: string | undefined;
|
||
|
|
let sourceSessionId: string | undefined;
|
||
|
|
let dataCallback: ((chunk: string) => void) | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [{
|
||
|
|
file: {
|
||
|
|
name: "report.txt",
|
||
|
|
arrayBuffer: async () => new Uint8Array([1]).buffer,
|
||
|
|
} as File,
|
||
|
|
relativePath: "report.txt",
|
||
|
|
isDirectory: false,
|
||
|
|
}],
|
||
|
|
host: { ...host, etEnabled: true },
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (_host, _path, _entries, origin, source) => {
|
||
|
|
originSessionId = origin;
|
||
|
|
sourceSessionId = source;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/srv/app/current",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "et-session",
|
||
|
|
sessionRef: { current: "et-session" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
onSessionData: (_sessionId, callback) => {
|
||
|
|
dataCallback = callback;
|
||
|
|
return () => { dataCallback = undefined; };
|
||
|
|
},
|
||
|
|
cancelZmodem: () => {},
|
||
|
|
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||
|
|
const token = uploadCommand?.match(/NetcattyRzMissing=([A-Za-z0-9_-]+)/)?.[1];
|
||
|
|
assert.ok(token);
|
||
|
|
dataCallback?.(`\u001b]1337;NetcattyRzMissing=${token}\u0007`);
|
||
|
|
return { success: true };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(originSessionId, "et-session");
|
||
|
|
assert.equal(sourceSessionId, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH folder drop to /root reuses the saved host password for sudo SFTP", async () => {
|
||
|
|
let openedHost: Host | undefined;
|
||
|
|
let openedPath: string | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||
|
|
host: { ...host, password: "secret" },
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (nextHost, initialPath) => {
|
||
|
|
openedHost = nextHost;
|
||
|
|
openedPath = initialPath;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/root",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: { writeToSession: () => {} },
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(openedPath, "/root");
|
||
|
|
assert.equal(openedHost?.sftpSudo, true);
|
||
|
|
assert.equal(openedHost?.password, "secret");
|
||
|
|
assert.equal(host.sftpSudo, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH folder drop to /root uses a resolved identity username over a stale host username", async () => {
|
||
|
|
let openedHost: Host | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||
|
|
host: { ...host, username: "root", password: "secret" },
|
||
|
|
resolvedLoginUsername: "alice",
|
||
|
|
resolvedSudoPassword: "secret",
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (nextHost) => {
|
||
|
|
openedHost = nextHost;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/root",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: { writeToSession: () => {} },
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(openedHost?.sftpSudo, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH folder drop to /root uses a resolved identity password", async () => {
|
||
|
|
let openedHost: Host | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||
|
|
host: { ...host, identityId: "id-1" },
|
||
|
|
resolvedSudoPassword: "identity-secret",
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (nextHost) => {
|
||
|
|
openedHost = nextHost;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/root",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: { writeToSession: () => {} },
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(openedHost?.sftpSudo, true);
|
||
|
|
assert.equal(openedHost?.password, undefined);
|
||
|
|
assert.equal(openedHost?.identityId, "id-1");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH folder drop to /root fails closed without a saved sudo password", async () => {
|
||
|
|
let openedSftp = false;
|
||
|
|
|
||
|
|
await assert.rejects(
|
||
|
|
handleTerminalDropEntries({
|
||
|
|
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: () => {
|
||
|
|
openedSftp = true;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/root",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: { writeToSession: () => {} },
|
||
|
|
termRef: { current: null },
|
||
|
|
}),
|
||
|
|
TerminalDropNeedsSudoError,
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(openedSftp, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH folder drop to the login home does not enable sudo", async () => {
|
||
|
|
let openedHost: Host | undefined;
|
||
|
|
|
||
|
|
await handleTerminalDropEntries({
|
||
|
|
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||
|
|
host: { ...host, password: "secret" },
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: (nextHost) => {
|
||
|
|
openedHost = nextHost;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async () => "/home/alice",
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: { writeToSession: () => {} },
|
||
|
|
termRef: { current: null },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(openedHost?.sftpSudo, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote SSH folder drop refuses to guess a destination when the active shell cwd is unknown", async () => {
|
||
|
|
let openedSftp = false;
|
||
|
|
let receivedOptions: unknown;
|
||
|
|
|
||
|
|
await assert.rejects(
|
||
|
|
handleTerminalDropEntries({
|
||
|
|
dropEntries: [
|
||
|
|
{
|
||
|
|
file: null,
|
||
|
|
relativePath: "docs",
|
||
|
|
isDirectory: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
host,
|
||
|
|
isLocalConnection: false,
|
||
|
|
onOpenSftp: () => {
|
||
|
|
openedSftp = true;
|
||
|
|
},
|
||
|
|
resolveSftpInitialPath: async (options) => {
|
||
|
|
receivedOptions = options;
|
||
|
|
return undefined;
|
||
|
|
},
|
||
|
|
scrollToBottomAfterProgrammaticInput: () => {},
|
||
|
|
sessionId: "session-1",
|
||
|
|
sessionRef: { current: "session-1" },
|
||
|
|
terminalBackend: {
|
||
|
|
writeToSession: () => {},
|
||
|
|
},
|
||
|
|
termRef: { current: null },
|
||
|
|
}),
|
||
|
|
/Could not determine the active terminal directory/,
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.deepEqual(receivedOptions, {
|
||
|
|
preferFreshBackend: true,
|
||
|
|
requireActiveShellCwd: true,
|
||
|
|
});
|
||
|
|
assert.equal(openedSftp, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("fresh cwd resolution falls back to the renderer cwd when backend probe has no real cwd", async () => {
|
||
|
|
const cwd = await resolvePreferredTerminalCwd({
|
||
|
|
rendererCwd: "/srv/app/current",
|
||
|
|
sessionId: "session-1",
|
||
|
|
preferFreshBackend: true,
|
||
|
|
getSessionPwd: async (_sessionId, options) => {
|
||
|
|
assert.deepEqual(options, {
|
||
|
|
allowHomeFallback: false,
|
||
|
|
allowLoginShellFallback: true,
|
||
|
|
});
|
||
|
|
return { success: false, error: "Could not determine cwd" };
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(cwd, "/srv/app/current");
|
||
|
|
});
|