raycast-rsync-extension 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +105 -0
- package/.github/workflows/publish.yml +269 -0
- package/.github/workflows/update-copyright-year.yml +70 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/assets/icon.png +0 -0
- package/eslint.config.js +23 -0
- package/metadata/browse-remote-path.png +0 -0
- package/metadata/browse-remote.png +0 -0
- package/metadata/download-local-path.png +0 -0
- package/metadata/download-remote-path.png +0 -0
- package/metadata/extension.png +0 -0
- package/metadata/upload-local-path.png +0 -0
- package/metadata/upload-remote-path.png +0 -0
- package/metadata/upload-search-host.png +0 -0
- package/package.json +93 -0
- package/src/__mocks__/raycast-api.ts +84 -0
- package/src/browse.tsx +378 -0
- package/src/components/FileList.test.tsx +73 -0
- package/src/components/FileList.tsx +61 -0
- package/src/download.tsx +353 -0
- package/src/e2e/browse.e2e.test.ts +295 -0
- package/src/e2e/download.e2e.test.ts +193 -0
- package/src/e2e/error-handling.e2e.test.ts +292 -0
- package/src/e2e/rsync-options.e2e.test.ts +348 -0
- package/src/e2e/upload.e2e.test.ts +207 -0
- package/src/index.tsx +21 -0
- package/src/test-setup.ts +1 -0
- package/src/types/server.ts +60 -0
- package/src/upload.tsx +404 -0
- package/src/utils/__tests__/sshConfig.test.ts +352 -0
- package/src/utils/__tests__/validation.test.ts +139 -0
- package/src/utils/preferences.ts +24 -0
- package/src/utils/rsync.test.ts +490 -0
- package/src/utils/rsync.ts +517 -0
- package/src/utils/shellEscape.test.ts +98 -0
- package/src/utils/shellEscape.ts +36 -0
- package/src/utils/ssh.test.ts +209 -0
- package/src/utils/ssh.ts +187 -0
- package/src/utils/sshConfig.test.ts +191 -0
- package/src/utils/sshConfig.ts +212 -0
- package/src/utils/validation.test.ts +224 -0
- package/src/utils/validation.ts +115 -0
- package/tsconfig.json +27 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { validateRemotePath, validateHostConfig } from "../utils/validation";
|
|
3
|
+
import { buildRsyncCommand } from "../utils/rsync";
|
|
4
|
+
import {
|
|
5
|
+
TransferDirection,
|
|
6
|
+
TransferOptions,
|
|
7
|
+
SSHHostConfig,
|
|
8
|
+
} from "../types/server";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
|
|
13
|
+
describe("Download E2E Flow", () => {
|
|
14
|
+
let testLocalDir: string;
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
// Create test directory for downloads
|
|
18
|
+
testLocalDir = path.join(os.tmpdir(), "rsync-e2e-download-" + Date.now());
|
|
19
|
+
fs.mkdirSync(testLocalDir, { recursive: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
// Clean up test directory
|
|
24
|
+
if (fs.existsSync(testLocalDir)) {
|
|
25
|
+
fs.rmSync(testLocalDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should complete full download workflow with valid inputs", () => {
|
|
30
|
+
// This test verifies the complete download workflow
|
|
31
|
+
// After successful execution, the UI will call popToRoot() to close the extension
|
|
32
|
+
|
|
33
|
+
// Step 1: Create mock host config
|
|
34
|
+
const downloadHost: SSHHostConfig = {
|
|
35
|
+
host: "downloadserver",
|
|
36
|
+
hostName: "download.example.com",
|
|
37
|
+
user: "downloaduser",
|
|
38
|
+
port: 22,
|
|
39
|
+
identityFile: "~/.ssh/download_key",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Step 2: Validate remote path
|
|
43
|
+
const remotePath = "/remote/source/file.txt";
|
|
44
|
+
const remoteValidation = validateRemotePath(remotePath);
|
|
45
|
+
expect(remoteValidation.valid).toBe(true);
|
|
46
|
+
expect(remoteValidation.error).toBeUndefined();
|
|
47
|
+
|
|
48
|
+
// Step 3: Validate host config
|
|
49
|
+
const hostValidation = validateHostConfig(downloadHost);
|
|
50
|
+
expect(hostValidation.valid).toBe(true);
|
|
51
|
+
expect(hostValidation.error).toBeUndefined();
|
|
52
|
+
|
|
53
|
+
// Step 4: Build rsync command
|
|
54
|
+
const options: TransferOptions = {
|
|
55
|
+
hostConfig: downloadHost,
|
|
56
|
+
localPath: testLocalDir,
|
|
57
|
+
remotePath: remotePath,
|
|
58
|
+
direction: TransferDirection.DOWNLOAD,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const command = buildRsyncCommand(options);
|
|
62
|
+
expect(command).toContain("rsync");
|
|
63
|
+
expect(command).toContain("-a");
|
|
64
|
+
// Host alias is now escaped with single quotes
|
|
65
|
+
expect(command).toContain("'downloadserver':");
|
|
66
|
+
// Paths are now escaped with single quotes
|
|
67
|
+
expect(command).toContain(`'${remotePath}'`);
|
|
68
|
+
// For downloads, local destination should have trailing slash to ensure directory is created
|
|
69
|
+
expect(command).toContain(`'${testLocalDir}/'`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should handle empty remote path in download workflow", () => {
|
|
73
|
+
// Try to validate empty remote path
|
|
74
|
+
const remoteValidation = validateRemotePath("");
|
|
75
|
+
|
|
76
|
+
// Should fail validation
|
|
77
|
+
expect(remoteValidation.valid).toBe(false);
|
|
78
|
+
expect(remoteValidation.error).toBeDefined();
|
|
79
|
+
expect(remoteValidation.error).toContain("cannot be empty");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle invalid remote path with control characters", () => {
|
|
83
|
+
// Try to validate remote path with control characters
|
|
84
|
+
const invalidPath = "/remote/path\x00/file.txt";
|
|
85
|
+
const remoteValidation = validateRemotePath(invalidPath);
|
|
86
|
+
|
|
87
|
+
// Should fail validation
|
|
88
|
+
expect(remoteValidation.valid).toBe(false);
|
|
89
|
+
expect(remoteValidation.error).toBeDefined();
|
|
90
|
+
expect(remoteValidation.error).toContain("control characters");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should validate all inputs before allowing download", () => {
|
|
94
|
+
const downloadHost: SSHHostConfig = {
|
|
95
|
+
host: "downloadserver",
|
|
96
|
+
hostName: "download.example.com",
|
|
97
|
+
user: "downloaduser",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// All validations should pass
|
|
101
|
+
const remoteValidation = validateRemotePath("/remote/file.txt");
|
|
102
|
+
const hostValidation = validateHostConfig(downloadHost);
|
|
103
|
+
|
|
104
|
+
expect(remoteValidation.valid).toBe(true);
|
|
105
|
+
expect(hostValidation.valid).toBe(true);
|
|
106
|
+
|
|
107
|
+
// Should be able to create transfer options
|
|
108
|
+
const options: TransferOptions = {
|
|
109
|
+
hostConfig: downloadHost,
|
|
110
|
+
localPath: testLocalDir,
|
|
111
|
+
remotePath: "/remote/file.txt",
|
|
112
|
+
direction: TransferDirection.DOWNLOAD,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
expect(options).toBeDefined();
|
|
116
|
+
expect(options.direction).toBe(TransferDirection.DOWNLOAD);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should handle directory download", () => {
|
|
120
|
+
const downloadHost: SSHHostConfig = {
|
|
121
|
+
host: "downloadserver",
|
|
122
|
+
hostName: "download.example.com",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const remoteDir = "/remote/directory";
|
|
126
|
+
const remoteValidation = validateRemotePath(remoteDir);
|
|
127
|
+
expect(remoteValidation.valid).toBe(true);
|
|
128
|
+
|
|
129
|
+
const options: TransferOptions = {
|
|
130
|
+
hostConfig: downloadHost,
|
|
131
|
+
localPath: testLocalDir,
|
|
132
|
+
remotePath: remoteDir,
|
|
133
|
+
direction: TransferDirection.DOWNLOAD,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const command = buildRsyncCommand(options);
|
|
137
|
+
expect(command).toContain("-a");
|
|
138
|
+
expect(command).toContain(remoteDir);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should handle host without optional properties", () => {
|
|
142
|
+
const backupServer: SSHHostConfig = {
|
|
143
|
+
host: "backup-server",
|
|
144
|
+
hostName: "backup.example.com",
|
|
145
|
+
user: "backupuser",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Should validate successfully even without port and identityFile
|
|
149
|
+
const hostValidation = validateHostConfig(backupServer);
|
|
150
|
+
expect(hostValidation.valid).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should handle various remote path formats", () => {
|
|
154
|
+
const validPaths = [
|
|
155
|
+
"/absolute/path/file.txt",
|
|
156
|
+
"relative/path/file.txt",
|
|
157
|
+
"/path/with spaces/file.txt",
|
|
158
|
+
"/path/with-dashes/file.txt",
|
|
159
|
+
"/path/with_underscores/file.txt",
|
|
160
|
+
"/path/with.dots/file.txt",
|
|
161
|
+
"~/home/path/file.txt",
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
validPaths.forEach((remotePath) => {
|
|
165
|
+
const validation = validateRemotePath(remotePath);
|
|
166
|
+
expect(validation.valid).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should build correct rsync command for download", () => {
|
|
171
|
+
const downloadHost: SSHHostConfig = {
|
|
172
|
+
host: "backup",
|
|
173
|
+
hostName: "backup.example.com",
|
|
174
|
+
user: "backupuser",
|
|
175
|
+
port: 2222,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const options: TransferOptions = {
|
|
179
|
+
hostConfig: downloadHost,
|
|
180
|
+
localPath: testLocalDir,
|
|
181
|
+
remotePath: "/backup/data.tar.gz",
|
|
182
|
+
direction: TransferDirection.DOWNLOAD,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const command = buildRsyncCommand(options);
|
|
186
|
+
|
|
187
|
+
// Verify command structure (now uses single quotes for escaping)
|
|
188
|
+
expect(command).toMatch(/^rsync -e 'ssh -F .+' -avz 'backup':.+ .+$/);
|
|
189
|
+
expect(command).toContain("'/backup/data.tar.gz'");
|
|
190
|
+
// For downloads, local destination should have trailing slash to ensure directory is created
|
|
191
|
+
expect(command).toContain(`'${testLocalDir}/'`);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { executeRsync } from "../utils/rsync";
|
|
3
|
+
import {
|
|
4
|
+
validateLocalPath,
|
|
5
|
+
validateRemotePath,
|
|
6
|
+
validateHostConfig,
|
|
7
|
+
validatePort,
|
|
8
|
+
} from "../utils/validation";
|
|
9
|
+
import {
|
|
10
|
+
TransferDirection,
|
|
11
|
+
TransferOptions,
|
|
12
|
+
SSHHostConfig,
|
|
13
|
+
} from "../types/server";
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as os from "os";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
|
|
18
|
+
describe("Error Handling E2E", () => {
|
|
19
|
+
describe("Validation Errors", () => {
|
|
20
|
+
it("should handle missing local file", () => {
|
|
21
|
+
const nonExistentFile = "/path/that/does/not/exist/file.txt";
|
|
22
|
+
const validation = validateLocalPath(nonExistentFile);
|
|
23
|
+
|
|
24
|
+
expect(validation.valid).toBe(false);
|
|
25
|
+
expect(validation.error).toContain("File not found");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should handle empty local path", () => {
|
|
29
|
+
const validation = validateLocalPath("");
|
|
30
|
+
|
|
31
|
+
expect(validation.valid).toBe(false);
|
|
32
|
+
expect(validation.error).toContain("cannot be empty");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should handle empty remote path", () => {
|
|
36
|
+
const validation = validateRemotePath("");
|
|
37
|
+
|
|
38
|
+
expect(validation.valid).toBe(false);
|
|
39
|
+
expect(validation.error).toContain("cannot be empty");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should handle remote path with control characters", () => {
|
|
43
|
+
const invalidPath = "/path\x00/file.txt";
|
|
44
|
+
const validation = validateRemotePath(invalidPath);
|
|
45
|
+
|
|
46
|
+
expect(validation.valid).toBe(false);
|
|
47
|
+
expect(validation.error).toContain("control characters");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should handle invalid port numbers", () => {
|
|
51
|
+
const testCases = [
|
|
52
|
+
{ port: 0, shouldFail: true },
|
|
53
|
+
{ port: -1, shouldFail: true },
|
|
54
|
+
{ port: 65536, shouldFail: true },
|
|
55
|
+
{ port: 99999, shouldFail: true },
|
|
56
|
+
{ port: 1, shouldFail: false },
|
|
57
|
+
{ port: 22, shouldFail: false },
|
|
58
|
+
{ port: 65535, shouldFail: false },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
testCases.forEach(({ port, shouldFail }) => {
|
|
62
|
+
const validation = validatePort(port);
|
|
63
|
+
if (shouldFail) {
|
|
64
|
+
expect(validation.valid).toBe(false);
|
|
65
|
+
expect(validation.error).toContain("must be between 1 and 65535");
|
|
66
|
+
} else {
|
|
67
|
+
expect(validation.valid).toBe(true);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should handle missing host alias", () => {
|
|
73
|
+
const invalidHost: SSHHostConfig = {
|
|
74
|
+
host: "",
|
|
75
|
+
hostName: "example.com",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const validation = validateHostConfig(invalidHost);
|
|
79
|
+
expect(validation.valid).toBe(false);
|
|
80
|
+
expect(validation.error).toContain("Host alias is required");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should handle host with invalid port", () => {
|
|
84
|
+
const invalidHost: SSHHostConfig = {
|
|
85
|
+
host: "testhost",
|
|
86
|
+
hostName: "example.com",
|
|
87
|
+
port: 70000,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const validation = validateHostConfig(invalidHost);
|
|
91
|
+
expect(validation.valid).toBe(false);
|
|
92
|
+
expect(validation.error).toContain("must be between 1 and 65535");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("Rsync Execution Errors", () => {
|
|
97
|
+
it("should handle connection to unreachable host", async () => {
|
|
98
|
+
const mockHost: SSHHostConfig = {
|
|
99
|
+
host: "unreachable-host",
|
|
100
|
+
hostName: "192.0.2.1", // TEST-NET-1 (non-routable)
|
|
101
|
+
user: "testuser",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const testDir = path.join(os.tmpdir(), "rsync-test-" + Date.now());
|
|
105
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
106
|
+
const testFile = path.join(testDir, "test.txt");
|
|
107
|
+
fs.writeFileSync(testFile, "test content");
|
|
108
|
+
|
|
109
|
+
const options: TransferOptions = {
|
|
110
|
+
hostConfig: mockHost,
|
|
111
|
+
localPath: testFile,
|
|
112
|
+
remotePath: "/remote/test.txt",
|
|
113
|
+
direction: TransferDirection.UPLOAD,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = await executeRsync(options);
|
|
117
|
+
|
|
118
|
+
expect(result.success).toBe(false);
|
|
119
|
+
expect(result.message).toBeDefined();
|
|
120
|
+
expect(result.message.length).toBeGreaterThan(0);
|
|
121
|
+
|
|
122
|
+
// Cleanup
|
|
123
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should handle invalid hostname", async () => {
|
|
127
|
+
const mockHost: SSHHostConfig = {
|
|
128
|
+
host: "invalid-host",
|
|
129
|
+
hostName: "this-host-does-not-exist-12345.invalid",
|
|
130
|
+
user: "testuser",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const testDir = path.join(os.tmpdir(), "rsync-test-" + Date.now());
|
|
134
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
135
|
+
const testFile = path.join(testDir, "test.txt");
|
|
136
|
+
fs.writeFileSync(testFile, "test content");
|
|
137
|
+
|
|
138
|
+
const options: TransferOptions = {
|
|
139
|
+
hostConfig: mockHost,
|
|
140
|
+
localPath: testFile,
|
|
141
|
+
remotePath: "/remote/test.txt",
|
|
142
|
+
direction: TransferDirection.UPLOAD,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = await executeRsync(options);
|
|
146
|
+
|
|
147
|
+
expect(result.success).toBe(false);
|
|
148
|
+
expect(result.message).toBeDefined();
|
|
149
|
+
|
|
150
|
+
// Cleanup
|
|
151
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should handle missing local file in rsync execution", async () => {
|
|
155
|
+
const mockHost: SSHHostConfig = {
|
|
156
|
+
host: "test",
|
|
157
|
+
hostName: "test.example.com",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const nonExistentFile =
|
|
161
|
+
"/tmp/file-that-does-not-exist-" + Date.now() + ".txt";
|
|
162
|
+
|
|
163
|
+
const options: TransferOptions = {
|
|
164
|
+
hostConfig: mockHost,
|
|
165
|
+
localPath: nonExistentFile,
|
|
166
|
+
remotePath: "/remote/test.txt",
|
|
167
|
+
direction: TransferDirection.UPLOAD,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const result = await executeRsync(options);
|
|
171
|
+
|
|
172
|
+
expect(result.success).toBe(false);
|
|
173
|
+
expect(result.message).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("Complete Error Workflow", () => {
|
|
178
|
+
it("should validate inputs before attempting transfer", () => {
|
|
179
|
+
const mockHost: SSHHostConfig = {
|
|
180
|
+
host: "testhost",
|
|
181
|
+
hostName: "test.example.com",
|
|
182
|
+
port: 99999, // Invalid port
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const nonExistentFile = "/path/does/not/exist.txt";
|
|
186
|
+
const emptyRemotePath = "";
|
|
187
|
+
|
|
188
|
+
// All validations should fail
|
|
189
|
+
const localValidation = validateLocalPath(nonExistentFile);
|
|
190
|
+
const remoteValidation = validateRemotePath(emptyRemotePath);
|
|
191
|
+
const hostValidation = validateHostConfig(mockHost);
|
|
192
|
+
|
|
193
|
+
expect(localValidation.valid).toBe(false);
|
|
194
|
+
expect(remoteValidation.valid).toBe(false);
|
|
195
|
+
expect(hostValidation.valid).toBe(false);
|
|
196
|
+
|
|
197
|
+
// Should not proceed to transfer
|
|
198
|
+
expect(localValidation.error).toBeDefined();
|
|
199
|
+
expect(remoteValidation.error).toBeDefined();
|
|
200
|
+
expect(hostValidation.error).toBeDefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should pass all validations with correct inputs", () => {
|
|
204
|
+
const testDir = path.join(os.tmpdir(), "rsync-test-" + Date.now());
|
|
205
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
206
|
+
const testFile = path.join(testDir, "test.txt");
|
|
207
|
+
fs.writeFileSync(testFile, "test content");
|
|
208
|
+
|
|
209
|
+
const mockHost: SSHHostConfig = {
|
|
210
|
+
host: "validhost",
|
|
211
|
+
hostName: "valid.example.com",
|
|
212
|
+
user: "validuser",
|
|
213
|
+
port: 22,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const remotePath = "/remote/valid/path.txt";
|
|
217
|
+
|
|
218
|
+
// All validations should pass
|
|
219
|
+
const localValidation = validateLocalPath(testFile);
|
|
220
|
+
const remoteValidation = validateRemotePath(remotePath);
|
|
221
|
+
const hostValidation = validateHostConfig(mockHost);
|
|
222
|
+
|
|
223
|
+
expect(localValidation.valid).toBe(true);
|
|
224
|
+
expect(remoteValidation.valid).toBe(true);
|
|
225
|
+
expect(hostValidation.valid).toBe(true);
|
|
226
|
+
|
|
227
|
+
// Cleanup
|
|
228
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("Edge Cases", () => {
|
|
233
|
+
it("should handle paths with special characters", () => {
|
|
234
|
+
const specialPaths = [
|
|
235
|
+
"/path/with spaces/file.txt",
|
|
236
|
+
"/path/with-dashes/file.txt",
|
|
237
|
+
"/path/with_underscores/file.txt",
|
|
238
|
+
"/path/with.dots/file.txt",
|
|
239
|
+
"/path/with(parentheses)/file.txt",
|
|
240
|
+
"/path/with[brackets]/file.txt",
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
specialPaths.forEach((remotePath) => {
|
|
244
|
+
const validation = validateRemotePath(remotePath);
|
|
245
|
+
expect(validation.valid).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should handle various port edge cases", () => {
|
|
250
|
+
const portTests = [
|
|
251
|
+
{ port: 1, valid: true },
|
|
252
|
+
{ port: 22, valid: true },
|
|
253
|
+
{ port: 80, valid: true },
|
|
254
|
+
{ port: 443, valid: true },
|
|
255
|
+
{ port: 8080, valid: true },
|
|
256
|
+
{ port: 65535, valid: true },
|
|
257
|
+
{ port: 0, valid: false },
|
|
258
|
+
{ port: -1, valid: false },
|
|
259
|
+
{ port: 65536, valid: false },
|
|
260
|
+
{ port: 1.5, valid: false }, // Non-integer
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
portTests.forEach(({ port, valid }) => {
|
|
264
|
+
const validation = validatePort(port);
|
|
265
|
+
expect(validation.valid).toBe(valid);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should handle host config with minimal properties", () => {
|
|
270
|
+
const minimalHost: SSHHostConfig = {
|
|
271
|
+
host: "minimal",
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const validation = validateHostConfig(minimalHost);
|
|
275
|
+
expect(validation.valid).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should handle host config with all properties", () => {
|
|
279
|
+
const fullHost: SSHHostConfig = {
|
|
280
|
+
host: "fullhost",
|
|
281
|
+
hostName: "full.example.com",
|
|
282
|
+
user: "fulluser",
|
|
283
|
+
port: 2222,
|
|
284
|
+
identityFile: "~/.ssh/id_rsa",
|
|
285
|
+
proxyJump: "jumphost",
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const validation = validateHostConfig(fullHost);
|
|
289
|
+
expect(validation.valid).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|