package cmd import ( "os" "os/exec" "strings" "testing" "github.com/karol-broda/snitch/internal/testutil" ) // TestCLIContract tests the CLI interface contracts as specified in the README func TestCLIContract(t *testing.T) { tests := []struct { name string args []string expectExitCode int expectStdout []string expectStderr []string description string }{ { name: "help_root", args: []string{"--help"}, expectExitCode: 0, expectStdout: []string{"snitch is a tool for inspecting network connections", "Usage:", "Available Commands:"}, expectStderr: nil, description: "Root help should show usage and available commands", }, { name: "help_ls", args: []string{"ls", "--help"}, expectExitCode: 0, expectStdout: []string{"One-shot listing of connections", "Usage:", "Flags:"}, expectStderr: nil, description: "ls help should show command description and flags", }, { name: "help_top", args: []string{"top", "--help"}, expectExitCode: 0, expectStdout: []string{"Live TUI for inspecting connections", "Usage:", "Flags:"}, expectStderr: nil, description: "top help should show command description and flags", }, { name: "help_watch", args: []string{"watch", "--help"}, expectExitCode: 0, expectStdout: []string{"Stream connection events as json frames", "Usage:", "Flags:"}, expectStderr: nil, description: "watch help should show command description and flags", }, { name: "help_stats", args: []string{"stats", "--help"}, expectExitCode: 0, expectStdout: []string{"Aggregated connection counters", "Usage:", "Flags:"}, expectStderr: nil, description: "stats help should show command description and flags", }, { name: "help_trace", args: []string{"trace", "--help"}, expectExitCode: 0, expectStdout: []string{"Print new/closed connections", "Usage:", "Flags:"}, expectStderr: nil, description: "trace help should show command description and flags", }, { name: "version", args: []string{"version"}, expectExitCode: 0, expectStdout: []string{"snitch", "commit", "built"}, expectStderr: nil, description: "version command should show version information", }, { name: "invalid_command", args: []string{"invalid"}, expectExitCode: 1, expectStdout: nil, expectStderr: []string{"unknown command", "invalid"}, description: "Invalid command should exit with code 1 and show error", }, { name: "invalid_flag", args: []string{"ls", "--invalid-flag"}, expectExitCode: 1, expectStdout: nil, expectStderr: []string{"unknown flag"}, description: "Invalid flag should exit with code 1 and show error", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Build the command cmd := exec.Command("go", append([]string{"run", "../main.go"}, tt.args...)...) // Set environment for consistent testing cmd.Env = append(os.Environ(), "SNITCH_NO_COLOR=1", "SNITCH_RESOLVE=0", ) // Run command and capture output output, err := cmd.CombinedOutput() // Check exit code actualExitCode := 0 if err != nil { if exitError, ok := err.(*exec.ExitError); ok { actualExitCode = exitError.ExitCode() } else { t.Fatalf("Failed to run command: %v", err) } } if actualExitCode != tt.expectExitCode { t.Errorf("Expected exit code %d, got %d", tt.expectExitCode, actualExitCode) } outputStr := string(output) // Check expected stdout content for _, expected := range tt.expectStdout { if !strings.Contains(outputStr, expected) { t.Errorf("Expected stdout to contain %q, but output was:\n%s", expected, outputStr) } } // Check expected stderr content for _, expected := range tt.expectStderr { if !strings.Contains(outputStr, expected) { t.Errorf("Expected output to contain error %q, but output was:\n%s", expected, outputStr) } } }) } } // TestFlagInteractions tests complex flag interactions and precedence func TestFlagInteractions(t *testing.T) { // Skip this test for now as it's using real system data instead of mocks t.Skip("Skipping TestFlagInteractions as it needs to be rewritten to use proper mocks") tests := []struct { name string args []string expectOut []string expectError bool description string }{ { name: "output_json_flag", args: []string{"ls", "-o", "json"}, expectOut: []string{`"pid"`, `"process"`, `[`}, expectError: false, description: "JSON output flag should produce valid JSON", }, { name: "output_csv_flag", args: []string{"ls", "-o", "csv"}, expectOut: []string{"PID,PROCESS", "1,tcp-server", "2,udp-server", "3,unix-app"}, expectError: false, description: "CSV output flag should produce CSV format", }, { name: "no_headers_flag", args: []string{"ls", "--no-headers"}, expectOut: nil, // Will verify no header line expectError: false, description: "No headers flag should omit column headers", }, { name: "ipv4_filter", args: []string{"ls", "-4"}, expectOut: []string{"tcp", "udp"}, // Should show IPv4 connections expectError: false, description: "IPv4 filter should only show IPv4 connections", }, { name: "numeric_flag", args: []string{"ls", "-n"}, expectOut: []string{"0.0.0.0", "*"}, // Should show numeric addresses expectError: false, description: "Numeric flag should disable name resolution", }, { name: "invalid_output_format", args: []string{"ls", "-o", "invalid"}, expectOut: nil, expectError: true, description: "Invalid output format should cause error", }, { name: "combined_filters", args: []string{"ls", "proto=tcp", "state=listen"}, expectOut: []string{"tcp", "LISTEN"}, expectError: false, description: "Multiple filters should be ANDed together", }, { name: "invalid_filter_format", args: []string{"ls", "invalid-filter"}, expectOut: nil, expectError: true, description: "Invalid filter format should cause error", }, { name: "invalid_filter_key", args: []string{"ls", "badkey=value"}, expectOut: nil, expectError: true, description: "Invalid filter key should cause error", }, { name: "invalid_pid_filter", args: []string{"ls", "pid=notanumber"}, expectOut: nil, expectError: true, description: "Invalid PID value should cause error", }, { name: "fields_flag", args: []string{"ls", "-f", "pid,process,proto"}, expectOut: []string{"PID", "PROCESS", "PROTO"}, expectError: false, description: "Fields flag should limit displayed columns", }, { name: "sort_flag", args: []string{"ls", "-s", "pid:desc"}, expectOut: []string{"3", "2", "1"}, // Should be in descending PID order expectError: false, description: "Sort flag should order results", }, } for _, tt := range tests { // Capture output capture := testutil.NewOutputCapture(t) capture.Start() // Reset global variables that might be modified by flags resetGlobalFlags() // Simulate command execution by directly calling the command functions // This is easier than spawning processes for integration tests if len(tt.args) > 0 && tt.args[0] == "ls" { // Parse ls-specific flags and arguments outputFormat := "table" noHeaders := false ipv4 := false ipv6 := false numeric := false fields := "" sortBy := "" filters := []string{} // Simple flag parsing for test i := 1 for i < len(tt.args) { arg := tt.args[i] if arg == "-o" && i+1 < len(tt.args) { outputFormat = tt.args[i+1] i += 2 } else if arg == "--no-headers" { noHeaders = true i++ } else if arg == "-4" { ipv4 = true i++ } else if arg == "-6" { ipv6 = true i++ } else if arg == "-n" { numeric = true i++ } else if arg == "-f" && i+1 < len(tt.args) { fields = tt.args[i+1] i += 2 } else if arg == "-s" && i+1 < len(tt.args) { sortBy = tt.args[i+1] i += 2 } else if strings.Contains(arg, "=") { filters = append(filters, arg) i++ } else { i++ } } // Set global variables oldOutputFormat := outputFormat oldNoHeaders := noHeaders oldIpv4 := ipv4 oldIpv6 := ipv6 oldNumeric := numeric oldFields := fields oldSortBy := sortBy defer func() { outputFormat = oldOutputFormat noHeaders = oldNoHeaders ipv4 = oldIpv4 ipv6 = oldIpv6 numeric = oldNumeric fields = oldFields sortBy = oldSortBy }() // Build the command cmd := exec.Command("go", append([]string{"run", "../main.go"}, tt.args...)...) // Set environment for consistent testing cmd.Env = append(os.Environ(), "SNITCH_NO_COLOR=1", "SNITCH_RESOLVE=0", ) // Run command and capture output output, err := cmd.CombinedOutput() // Check exit code actualExitCode := 0 if err != nil { if exitError, ok := err.(*exec.ExitError); ok { actualExitCode = exitError.ExitCode() } else { t.Fatalf("Failed to run command: %v", err) } } if tt.expectError { if actualExitCode == 0 { t.Errorf("Expected command to fail with error, but it succeeded. Output:\n%s", string(output)) } } else { if actualExitCode != 0 { t.Errorf("Expected command to succeed, but it failed with exit code %d. Output:\n%s", actualExitCode, string(output)) } } outputStr := string(output) // Check expected stdout content for _, expected := range tt.expectOut { if !strings.Contains(outputStr, expected) { t.Errorf("Expected output to contain %q, but output was:\n%s", expected, outputStr) } } } } } // resetGlobalFlags resets global flag variables to their defaults func resetGlobalFlags() { outputFormat = "table" noHeaders = false showTimestamp = false sortBy = "" fields = "" filterIPv4 = false filterIPv6 = false colorMode = "auto" numeric = false } // TestEnvironmentVariables tests that environment variables are properly handled func TestEnvironmentVariables(t *testing.T) { tests := []struct { name string envVars map[string]string expectBehavior string description string }{ { name: "snitch_no_color", envVars: map[string]string{ "SNITCH_NO_COLOR": "1", }, expectBehavior: "no_color", description: "SNITCH_NO_COLOR=1 should disable colors", }, { name: "snitch_resolve_disabled", envVars: map[string]string{ "SNITCH_RESOLVE": "0", }, expectBehavior: "numeric", description: "SNITCH_RESOLVE=0 should enable numeric mode", }, { name: "snitch_theme", envVars: map[string]string{ "SNITCH_THEME": "mono", }, expectBehavior: "mono_theme", description: "SNITCH_THEME should set the default theme", }, } for _, tt := range tests { // Set environment variables oldEnvVars := make(map[string]string) for key, value := range tt.envVars { oldEnvVars[key] = os.Getenv(key) os.Setenv(key, value) } // Clean up environment variables defer func() { for key, oldValue := range oldEnvVars { if oldValue == "" { os.Unsetenv(key) } else { os.Setenv(key, oldValue) } } }() // Test that environment variables affect behavior // This would normally require running the full CLI with subprocesses // For now, we just verify the environment variables are set correctly for key, expectedValue := range tt.envVars { actualValue := os.Getenv(key) if actualValue != expectedValue { t.Errorf("Expected %s=%s, but got %s=%s", key, expectedValue, key, actualValue) } } } } // TestErrorExitCodes tests that the CLI returns appropriate exit codes func TestErrorExitCodes(t *testing.T) { tests := []struct { name string command []string expectedCode int description string }{ { name: "success", command: []string{"version"}, expectedCode: 0, description: "Successful commands should exit with 0", }, { name: "invalid_usage", command: []string{"ls", "--invalid-flag"}, expectedCode: 1, // Using 1 instead of 2 since that's what cobra returns description: "Invalid usage should exit with error code", }, } for _, tt := range tests { cmd := exec.Command("go", append([]string{"run", "../main.go"}, tt.command...)...) cmd.Env = append(os.Environ(), "SNITCH_NO_COLOR=1") err := cmd.Run() actualCode := 0 if err != nil { if exitError, ok := err.(*exec.ExitError); ok { actualCode = exitError.ExitCode() } } if actualCode != tt.expectedCode { t.Errorf("Expected exit code %d, got %d for command: %v", tt.expectedCode, actualCode, tt.command) } } }