package cmd import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSSHCommand_FlagParsing(t *testing.T) { tests := []struct { name string args []string expectedHost string expectedUser string expectedPort int expectedCmd string expectError bool }{ { name: "basic host", args: []string{"hostname"}, expectedHost: "hostname", expectedUser: "", expectedPort: 22, expectedCmd: "", }, { name: "user@host format", args: []string{"user@hostname"}, expectedHost: "hostname", expectedUser: "user", expectedPort: 22, expectedCmd: "", }, { name: "host with command", args: []string{"hostname", "echo", "hello"}, expectedHost: "hostname", expectedUser: "", expectedPort: 22, expectedCmd: "echo hello", }, { name: "command with flags should be preserved", args: []string{"hostname", "ls", "-la", "/tmp"}, expectedHost: "hostname", expectedUser: "", expectedPort: 22, expectedCmd: "ls -la /tmp", }, { name: "double dash separator", args: []string{"hostname", "--", "ls", "-la"}, expectedHost: "hostname", expectedUser: "", expectedPort: 22, expectedCmd: "-- ls -la", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset global variables host = "" username = "" port = 22 command = "" // Mock command for testing cmd := sshCmd cmd.SetArgs(tt.args) err := validateSSHArgsWithoutFlagParsing(cmd, tt.args) if tt.expectError { assert.Error(t, err) return } require.NoError(t, err, "SSH args validation should succeed for valid input") assert.Equal(t, tt.expectedHost, host, "host mismatch") if tt.expectedUser != "" { assert.Equal(t, tt.expectedUser, username, "username mismatch") } assert.Equal(t, tt.expectedPort, port, "port mismatch") assert.Equal(t, tt.expectedCmd, command, "command mismatch") }) } } func TestSSHCommand_FlagConflictPrevention(t *testing.T) { // Test that SSH flags don't conflict with command flags tests := []struct { name string args []string expectedCmd string description string }{ { name: "ls with -la flags", args: []string{"hostname", "ls", "-la"}, expectedCmd: "ls -la", description: "ls flags should be passed to remote command", }, { name: "grep with -r flag", args: []string{"hostname", "grep", "-r", "pattern", "/path"}, expectedCmd: "grep -r pattern /path", description: "grep flags should be passed to remote command", }, { name: "ps with aux flags", args: []string{"hostname", "ps", "aux"}, expectedCmd: "ps aux", description: "ps flags should be passed to remote command", }, { name: "command with double dash", args: []string{"hostname", "--", "ls", "-la"}, expectedCmd: "-- ls -la", description: "double dash should be preserved in command", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset global variables host = "" username = "" port = 22 command = "" cmd := sshCmd err := validateSSHArgsWithoutFlagParsing(cmd, tt.args) require.NoError(t, err, "SSH args validation should succeed for valid input") assert.Equal(t, tt.expectedCmd, command, tt.description) }) } } func TestSSHCommand_NonInteractiveExecution(t *testing.T) { // Test that commands with arguments should execute the command and exit, // not drop to an interactive shell tests := []struct { name string args []string expectedCmd string shouldExit bool description string }{ { name: "ls command should execute and exit", args: []string{"hostname", "ls"}, expectedCmd: "ls", shouldExit: true, description: "ls command should execute and exit, not drop to shell", }, { name: "ls with flags should execute and exit", args: []string{"hostname", "ls", "-la"}, expectedCmd: "ls -la", shouldExit: true, description: "ls with flags should execute and exit, not drop to shell", }, { name: "pwd command should execute and exit", args: []string{"hostname", "pwd"}, expectedCmd: "pwd", shouldExit: true, description: "pwd command should execute and exit, not drop to shell", }, { name: "echo command should execute and exit", args: []string{"hostname", "echo", "hello"}, expectedCmd: "echo hello", shouldExit: true, description: "echo command should execute and exit, not drop to shell", }, { name: "no command should open shell", args: []string{"hostname"}, expectedCmd: "", shouldExit: false, description: "no command should open interactive shell", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset global variables host = "" username = "" port = 22 command = "" cmd := sshCmd err := validateSSHArgsWithoutFlagParsing(cmd, tt.args) require.NoError(t, err, "SSH args validation should succeed for valid input") assert.Equal(t, tt.expectedCmd, command, tt.description) // When command is present, it should execute the command and exit // When command is empty, it should open interactive shell hasCommand := command != "" assert.Equal(t, tt.shouldExit, hasCommand, "Command presence should match expected behavior") }) } } func TestSSHCommand_FlagHandling(t *testing.T) { // Test that flags after hostname are not parsed by netbird but passed to SSH command tests := []struct { name string args []string expectedHost string expectedCmd string expectError bool description string }{ { name: "ls with -la flag should not be parsed by netbird", args: []string{"debian2", "ls", "-la"}, expectedHost: "debian2", expectedCmd: "ls -la", expectError: false, description: "ls -la should be passed as SSH command, not parsed as netbird flags", }, { name: "command with netbird-like flags should be passed through", args: []string{"hostname", "echo", "--help"}, expectedHost: "hostname", expectedCmd: "echo --help", expectError: false, description: "--help should be passed to echo, not parsed by netbird", }, { name: "command with -p flag should not conflict with SSH port flag", args: []string{"hostname", "ps", "-p", "1234"}, expectedHost: "hostname", expectedCmd: "ps -p 1234", expectError: false, description: "ps -p should be passed to ps command, not parsed as port", }, { name: "tar with flags should be passed through", args: []string{"hostname", "tar", "-czf", "backup.tar.gz", "/home"}, expectedHost: "hostname", expectedCmd: "tar -czf backup.tar.gz /home", expectError: false, description: "tar flags should be passed to tar command", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset global variables host = "" username = "" port = 22 command = "" cmd := sshCmd err := validateSSHArgsWithoutFlagParsing(cmd, tt.args) if tt.expectError { assert.Error(t, err) return } require.NoError(t, err, "SSH args validation should succeed for valid input") assert.Equal(t, tt.expectedHost, host, "host mismatch") assert.Equal(t, tt.expectedCmd, command, tt.description) }) } } func TestSSHCommand_RegressionFlagParsing(t *testing.T) { // Regression test for the specific issue: "sudo ./netbird ssh debian2 ls -la" // should not parse -la as netbird flags but pass them to the SSH command tests := []struct { name string args []string expectedHost string expectedCmd string expectError bool description string }{ { name: "original issue: ls -la should be preserved", args: []string{"debian2", "ls", "-la"}, expectedHost: "debian2", expectedCmd: "ls -la", expectError: false, description: "The original failing case should now work", }, { name: "ls -l should be preserved", args: []string{"hostname", "ls", "-l"}, expectedHost: "hostname", expectedCmd: "ls -l", expectError: false, description: "Single letter flags should be preserved", }, { name: "SSH port flag should work", args: []string{"-p", "2222", "hostname", "ls", "-la"}, expectedHost: "hostname", expectedCmd: "ls -la", expectError: false, description: "SSH -p flag should be parsed, command flags preserved", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset global variables host = "" username = "" port = 22 command = "" cmd := sshCmd err := validateSSHArgsWithoutFlagParsing(cmd, tt.args) if tt.expectError { assert.Error(t, err) return } require.NoError(t, err, "SSH args validation should succeed for valid input") assert.Equal(t, tt.expectedHost, host, "host mismatch") assert.Equal(t, tt.expectedCmd, command, tt.description) // Check port for the test case with -p flag if len(tt.args) > 0 && tt.args[0] == "-p" { assert.Equal(t, 2222, port, "port should be parsed from -p flag") } }) } } func TestSSHCommand_PortForwardingFlagParsing(t *testing.T) { tests := []struct { name string args []string expectedHost string expectedLocal []string expectedRemote []string expectError bool description string }{ { name: "local port forwarding -L", args: []string{"-L", "8080:localhost:80", "hostname"}, expectedHost: "hostname", expectedLocal: []string{"8080:localhost:80"}, expectedRemote: []string{}, expectError: false, description: "Single -L flag should be parsed correctly", }, { name: "remote port forwarding -R", args: []string{"-R", "8080:localhost:80", "hostname"}, expectedHost: "hostname", expectedLocal: []string{}, expectedRemote: []string{"8080:localhost:80"}, expectError: false, description: "Single -R flag should be parsed correctly", }, { name: "multiple local port forwards", args: []string{"-L", "8080:localhost:80", "-L", "9090:localhost:443", "hostname"}, expectedHost: "hostname", expectedLocal: []string{"8080:localhost:80", "9090:localhost:443"}, expectedRemote: []string{}, expectError: false, description: "Multiple -L flags should be parsed correctly", }, { name: "multiple remote port forwards", args: []string{"-R", "8080:localhost:80", "-R", "9090:localhost:443", "hostname"}, expectedHost: "hostname", expectedLocal: []string{}, expectedRemote: []string{"8080:localhost:80", "9090:localhost:443"}, expectError: false, description: "Multiple -R flags should be parsed correctly", }, { name: "mixed local and remote forwards", args: []string{"-L", "8080:localhost:80", "-R", "9090:localhost:443", "hostname"}, expectedHost: "hostname", expectedLocal: []string{"8080:localhost:80"}, expectedRemote: []string{"9090:localhost:443"}, expectError: false, description: "Mixed -L and -R flags should be parsed correctly", }, { name: "port forwarding with bind address", args: []string{"-L", "127.0.0.1:8080:localhost:80", "hostname"}, expectedHost: "hostname", expectedLocal: []string{"127.0.0.1:8080:localhost:80"}, expectedRemote: []string{}, expectError: false, description: "Port forwarding with bind address should work", }, { name: "port forwarding with command", args: []string{"-L", "8080:localhost:80", "hostname", "ls", "-la"}, expectedHost: "hostname", expectedLocal: []string{"8080:localhost:80"}, expectedRemote: []string{}, expectError: false, description: "Port forwarding with command should work", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset global variables host = "" username = "" port = 22 command = "" localForwards = nil remoteForwards = nil cmd := sshCmd err := validateSSHArgsWithoutFlagParsing(cmd, tt.args) if tt.expectError { assert.Error(t, err) return } require.NoError(t, err, "SSH args validation should succeed for valid input") assert.Equal(t, tt.expectedHost, host, "host mismatch") // Handle nil vs empty slice comparison if len(tt.expectedLocal) == 0 { assert.True(t, len(localForwards) == 0, tt.description+" - local forwards should be empty") } else { assert.Equal(t, tt.expectedLocal, localForwards, tt.description+" - local forwards") } if len(tt.expectedRemote) == 0 { assert.True(t, len(remoteForwards) == 0, tt.description+" - remote forwards should be empty") } else { assert.Equal(t, tt.expectedRemote, remoteForwards, tt.description+" - remote forwards") } }) } } func TestParsePortForward(t *testing.T) { tests := []struct { name string spec string expectedLocal string expectedRemote string expectError bool description string }{ { name: "simple port forward", spec: "8080:localhost:80", expectedLocal: "localhost:8080", expectedRemote: "localhost:80", expectError: false, description: "Simple port:host:port format should work", }, { name: "port forward with bind address", spec: "127.0.0.1:8080:localhost:80", expectedLocal: "127.0.0.1:8080", expectedRemote: "localhost:80", expectError: false, description: "bind_address:port:host:port format should work", }, { name: "port forward to different host", spec: "8080:example.com:443", expectedLocal: "localhost:8080", expectedRemote: "example.com:443", expectError: false, description: "Forwarding to different host should work", }, { name: "port forward with IPv6 (needs bracket support)", spec: "::1:8080:localhost:80", expectError: true, description: "IPv6 without brackets fails as expected (feature to implement)", }, { name: "invalid format - too few parts", spec: "8080:localhost", expectError: true, description: "Invalid format with too few parts should fail", }, { name: "invalid format - too many parts", spec: "127.0.0.1:8080:localhost:80:extra", expectError: true, description: "Invalid format with too many parts should fail", }, { name: "empty spec", spec: "", expectError: true, description: "Empty spec should fail", }, { name: "unix socket local forward", spec: "8080:/tmp/socket", expectedLocal: "localhost:8080", expectedRemote: "/tmp/socket", expectError: false, description: "Unix socket forwarding should work", }, { name: "unix socket with bind address", spec: "127.0.0.1:8080:/tmp/socket", expectedLocal: "127.0.0.1:8080", expectedRemote: "/tmp/socket", expectError: false, description: "Unix socket with bind address should work", }, { name: "wildcard bind all interfaces", spec: "*:8080:localhost:80", expectedLocal: ":8080", expectedRemote: "localhost:80", expectError: false, description: "Wildcard * should bind to all interfaces (dual-stack)", }, { name: "wildcard for port only", spec: "8080:*:80", expectedLocal: "localhost:8080", expectedRemote: "*:80", expectError: false, description: "Wildcard in remote host should be preserved", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { localAddr, remoteAddr, err := parsePortForwardSpec(tt.spec) if tt.expectError { assert.Error(t, err, tt.description) return } require.NoError(t, err, tt.description) assert.Equal(t, tt.expectedLocal, localAddr, tt.description+" - local address") assert.Equal(t, tt.expectedRemote, remoteAddr, tt.description+" - remote address") }) } } func TestSSHCommand_IntegrationPortForwarding(t *testing.T) { // Integration test for port forwarding with the actual SSH command implementation tests := []struct { name string args []string expectedHost string expectedLocal []string expectedRemote []string expectedCmd string description string }{ { name: "local forward with command", args: []string{"-L", "8080:localhost:80", "hostname", "echo", "test"}, expectedHost: "hostname", expectedLocal: []string{"8080:localhost:80"}, expectedRemote: []string{}, expectedCmd: "echo test", description: "Local forwarding should work with commands", }, { name: "remote forward with command", args: []string{"-R", "8080:localhost:80", "hostname", "ls", "-la"}, expectedHost: "hostname", expectedLocal: []string{}, expectedRemote: []string{"8080:localhost:80"}, expectedCmd: "ls -la", description: "Remote forwarding should work with commands", }, { name: "multiple forwards with user and command", args: []string{"-L", "8080:localhost:80", "-R", "9090:localhost:443", "user@hostname", "ps", "aux"}, expectedHost: "hostname", expectedLocal: []string{"8080:localhost:80"}, expectedRemote: []string{"9090:localhost:443"}, expectedCmd: "ps aux", description: "Complex case with multiple forwards, user, and command", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset global variables host = "" username = "" port = 22 command = "" localForwards = nil remoteForwards = nil cmd := sshCmd err := validateSSHArgsWithoutFlagParsing(cmd, tt.args) require.NoError(t, err, "SSH args validation should succeed for valid input") assert.Equal(t, tt.expectedHost, host, "host mismatch") // Handle nil vs empty slice comparison if len(tt.expectedLocal) == 0 { assert.True(t, len(localForwards) == 0, tt.description+" - local forwards should be empty") } else { assert.Equal(t, tt.expectedLocal, localForwards, tt.description+" - local forwards") } if len(tt.expectedRemote) == 0 { assert.True(t, len(remoteForwards) == 0, tt.description+" - remote forwards should be empty") } else { assert.Equal(t, tt.expectedRemote, remoteForwards, tt.description+" - remote forwards") } assert.Equal(t, tt.expectedCmd, command, tt.description+" - command") }) } } func TestSSHCommand_ParameterIsolation(t *testing.T) { tests := []struct { name string args []string expectedCmd string }{ { name: "cmd flag passed as command", args: []string{"hostname", "--cmd", "echo test"}, expectedCmd: "--cmd echo test", }, { name: "uid flag passed as command", args: []string{"hostname", "--uid", "1000"}, expectedCmd: "--uid 1000", }, { name: "shell flag passed as command", args: []string{"hostname", "--shell", "/bin/bash"}, expectedCmd: "--shell /bin/bash", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { host = "" username = "" port = 22 command = "" err := validateSSHArgsWithoutFlagParsing(sshCmd, tt.args) require.NoError(t, err) assert.Equal(t, "hostname", host) assert.Equal(t, tt.expectedCmd, command) }) } } func TestSSHCommand_InvalidFlagRejection(t *testing.T) { // Test that invalid flags are properly rejected and not misinterpreted as hostnames tests := []struct { name string args []string description string }{ { name: "invalid long flag before hostname", args: []string{"--invalid-flag", "hostname"}, description: "Invalid flag should return parse error, not treat flag as hostname", }, { name: "invalid short flag before hostname", args: []string{"-x", "hostname"}, description: "Invalid short flag should return parse error", }, { name: "invalid flag with value before hostname", args: []string{"--invalid-option=value", "hostname"}, description: "Invalid flag with value should return parse error", }, { name: "typo in known flag", args: []string{"--por", "2222", "hostname"}, description: "Typo in flag name should return parse error (not silently ignored)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset global variables host = "" username = "" port = 22 command = "" err := validateSSHArgsWithoutFlagParsing(sshCmd, tt.args) // Should return an error for invalid flags assert.Error(t, err, tt.description) // Should not have set host to the invalid flag assert.NotEqual(t, tt.args[0], host, "Invalid flag should not be interpreted as hostname") }) } }