import { describe, it, expect } from "vitest"; import { executeRsync } from "../utils/rsync"; import { validateLocalPath, validateRemotePath, validateHostConfig, validatePort, } from "../utils/validation"; import { TransferDirection, TransferOptions, SSHHostConfig, } from "../types/server"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; describe("Error Handling E2E", () => { describe("Validation Errors", () => { it("should handle missing local file", () => { const nonExistentFile = "/path/that/does/not/exist/file.txt"; const validation = validateLocalPath(nonExistentFile); expect(validation.valid).toBe(false); expect(validation.error).toContain("File not found"); }); it("should handle empty local path", () => { const validation = validateLocalPath(""); expect(validation.valid).toBe(false); expect(validation.error).toContain("cannot be empty"); }); it("should handle empty remote path", () => { const validation = validateRemotePath(""); expect(validation.valid).toBe(false); expect(validation.error).toContain("cannot be empty"); }); it("should handle remote path with control characters", () => { const invalidPath = "/path\x00/file.txt"; const validation = validateRemotePath(invalidPath); expect(validation.valid).toBe(false); expect(validation.error).toContain("control characters"); }); it("should handle invalid port numbers", () => { const testCases = [ { port: 0, shouldFail: true }, { port: -1, shouldFail: true }, { port: 65536, shouldFail: true }, { port: 99999, shouldFail: true }, { port: 1, shouldFail: false }, { port: 22, shouldFail: false }, { port: 65535, shouldFail: false }, ]; testCases.forEach(({ port, shouldFail }) => { const validation = validatePort(port); if (shouldFail) { expect(validation.valid).toBe(false); expect(validation.error).toContain("must be between 1 and 65535"); } else { expect(validation.valid).toBe(true); } }); }); it("should handle missing host alias", () => { const invalidHost: SSHHostConfig = { host: "", hostName: "example.com", }; const validation = validateHostConfig(invalidHost); expect(validation.valid).toBe(false); expect(validation.error).toContain("Host alias is required"); }); it("should handle host with invalid port", () => { const invalidHost: SSHHostConfig = { host: "testhost", hostName: "example.com", port: 70000, }; const validation = validateHostConfig(invalidHost); expect(validation.valid).toBe(false); expect(validation.error).toContain("must be between 1 and 65535"); }); }); describe("Rsync Execution Errors", () => { it("should handle connection to unreachable host", async () => { const mockHost: SSHHostConfig = { host: "unreachable-host", hostName: "192.0.2.1", // TEST-NET-1 (non-routable) user: "testuser", }; const testDir = path.join(os.tmpdir(), "rsync-test-" + Date.now()); fs.mkdirSync(testDir, { recursive: true }); const testFile = path.join(testDir, "test.txt"); fs.writeFileSync(testFile, "test content"); const options: TransferOptions = { hostConfig: mockHost, localPath: testFile, remotePath: "/remote/test.txt", direction: TransferDirection.UPLOAD, }; const result = await executeRsync(options); expect(result.success).toBe(false); expect(result.message).toBeDefined(); expect(result.message.length).toBeGreaterThan(0); // Cleanup fs.rmSync(testDir, { recursive: true, force: true }); }); it("should handle invalid hostname", async () => { const mockHost: SSHHostConfig = { host: "invalid-host", hostName: "this-host-does-not-exist-12345.invalid", user: "testuser", }; const testDir = path.join(os.tmpdir(), "rsync-test-" + Date.now()); fs.mkdirSync(testDir, { recursive: true }); const testFile = path.join(testDir, "test.txt"); fs.writeFileSync(testFile, "test content"); const options: TransferOptions = { hostConfig: mockHost, localPath: testFile, remotePath: "/remote/test.txt", direction: TransferDirection.UPLOAD, }; const result = await executeRsync(options); expect(result.success).toBe(false); expect(result.message).toBeDefined(); // Cleanup fs.rmSync(testDir, { recursive: true, force: true }); }); it("should handle missing local file in rsync execution", async () => { const mockHost: SSHHostConfig = { host: "test", hostName: "test.example.com", }; const nonExistentFile = "/tmp/file-that-does-not-exist-" + Date.now() + ".txt"; const options: TransferOptions = { hostConfig: mockHost, localPath: nonExistentFile, remotePath: "/remote/test.txt", direction: TransferDirection.UPLOAD, }; const result = await executeRsync(options); expect(result.success).toBe(false); expect(result.message).toBeDefined(); }); }); describe("Complete Error Workflow", () => { it("should validate inputs before attempting transfer", () => { const mockHost: SSHHostConfig = { host: "testhost", hostName: "test.example.com", port: 99999, // Invalid port }; const nonExistentFile = "/path/does/not/exist.txt"; const emptyRemotePath = ""; // All validations should fail const localValidation = validateLocalPath(nonExistentFile); const remoteValidation = validateRemotePath(emptyRemotePath); const hostValidation = validateHostConfig(mockHost); expect(localValidation.valid).toBe(false); expect(remoteValidation.valid).toBe(false); expect(hostValidation.valid).toBe(false); // Should not proceed to transfer expect(localValidation.error).toBeDefined(); expect(remoteValidation.error).toBeDefined(); expect(hostValidation.error).toBeDefined(); }); it("should pass all validations with correct inputs", () => { const testDir = path.join(os.tmpdir(), "rsync-test-" + Date.now()); fs.mkdirSync(testDir, { recursive: true }); const testFile = path.join(testDir, "test.txt"); fs.writeFileSync(testFile, "test content"); const mockHost: SSHHostConfig = { host: "validhost", hostName: "valid.example.com", user: "validuser", port: 22, }; const remotePath = "/remote/valid/path.txt"; // All validations should pass const localValidation = validateLocalPath(testFile); const remoteValidation = validateRemotePath(remotePath); const hostValidation = validateHostConfig(mockHost); expect(localValidation.valid).toBe(true); expect(remoteValidation.valid).toBe(true); expect(hostValidation.valid).toBe(true); // Cleanup fs.rmSync(testDir, { recursive: true, force: true }); }); }); describe("Edge Cases", () => { it("should handle paths with special characters", () => { const specialPaths = [ "/path/with spaces/file.txt", "/path/with-dashes/file.txt", "/path/with_underscores/file.txt", "/path/with.dots/file.txt", "/path/with(parentheses)/file.txt", "/path/with[brackets]/file.txt", ]; specialPaths.forEach((remotePath) => { const validation = validateRemotePath(remotePath); expect(validation.valid).toBe(true); }); }); it("should handle various port edge cases", () => { const portTests = [ { port: 1, valid: true }, { port: 22, valid: true }, { port: 80, valid: true }, { port: 443, valid: true }, { port: 8080, valid: true }, { port: 65535, valid: true }, { port: 0, valid: false }, { port: -1, valid: false }, { port: 65536, valid: false }, { port: 1.5, valid: false }, // Non-integer ]; portTests.forEach(({ port, valid }) => { const validation = validatePort(port); expect(validation.valid).toBe(valid); }); }); it("should handle host config with minimal properties", () => { const minimalHost: SSHHostConfig = { host: "minimal", }; const validation = validateHostConfig(minimalHost); expect(validation.valid).toBe(true); }); it("should handle host config with all properties", () => { const fullHost: SSHHostConfig = { host: "fullhost", hostName: "full.example.com", user: "fulluser", port: 2222, identityFile: "~/.ssh/id_rsa", proxyJump: "jumphost", }; const validation = validateHostConfig(fullHost); expect(validation.valid).toBe(true); }); }); });