package main import ( "bytes" "strings" "testing" "github.com/spf13/cobra" ) // newTestCmd creates a fresh root command for testing (avoids global state pollution). func newTestCmd() *cobra.Command { var testIface string var testListIfaces bool var testVerbose bool var testFilter string var testRead string var testOutput string rootCmd := &cobra.Command{ Use: "netsynth", Short: "Sonify live network traffic into ambient audio", RunE: run, SilenceUsage: true, } rootCmd.Flags().StringVarP(&testIface, "interface", "i", "", "Network interface to capture on") rootCmd.Flags().BoolVar(&testListIfaces, "list-interfaces", false, "List available network interfaces and exit") rootCmd.Flags().BoolVar(&testVerbose, "verbose", false, "Print per-window protocol activity to stderr") rootCmd.Flags().StringVarP(&testOutput, "output", "o", "", "Output MP3 file path") rootCmd.Flags().StringVar(&testFilter, "filter", "", "BPF filter expression") rootCmd.Flags().StringVar(&testRead, "read", "", "Read packets from pcap file instead of live capture") // Wire test variables to package-level vars used by run() rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { ifaceName = testIface listIfaces = testListIfaces verbose = testVerbose outputPath = testOutput bpfFilter = testFilter readPath = testRead return nil } return rootCmd } // TestListInterfacesFlag verifies --list-interfaces exits without error and prints interface info. func TestListInterfacesFlag(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := &cobra.Command{ Use: "netsynth", RunE: run, } rootCmd.Flags().StringVarP(&ifaceName, "interface", "i", "", "Network interface") rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces", false, "List interfaces") rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Verbose output") rootCmd.Flags().StringVar(&bpfFilter, "filter", "", "BPF filter expression") rootCmd.Flags().StringVar(&readPath, "read", "", "Read from pcap file") var buf bytes.Buffer rootCmd.SetErr(&buf) rootCmd.SetOut(&buf) err := rootCmd.Execute() // Without --list-interfaces and without -i, we expect error about interface required. // This test only checks the command parses correctly (no panic, cobra setup valid). // (The actual list-interfaces path requires running the command with the flag set.) _ = err // may error due to missing -i, that is expected } // TestMissingInterfaceFlag verifies that running without -i returns an error containing "interface required". // Per plan: error message should also mention "--read" (updated behavior). func TestMissingInterfaceFlag(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := &cobra.Command{ Use: "netsynth", RunE: run, SilenceUsage: true, } rootCmd.Flags().StringVarP(&ifaceName, "interface", "i", "", "Network interface") rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces", false, "List interfaces") rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Verbose output") rootCmd.Flags().StringVar(&bpfFilter, "filter", "", "BPF filter expression") rootCmd.Flags().StringVar(&readPath, "read", "", "Read from pcap file") var outBuf, errBuf bytes.Buffer rootCmd.SetOut(&outBuf) rootCmd.SetErr(&errBuf) err := rootCmd.Execute() if err == nil { t.Fatal("expected error when -i flag is missing, got nil") } if !strings.Contains(err.Error(), "interface required") { t.Errorf("expected error to contain 'interface required', got: %q", err.Error()) } // Updated behavior: error should also mention --read if !strings.Contains(err.Error(), "--read") { t.Errorf("expected error to mention '--read', got: %q", err.Error()) } } // TestHelpOutput verifies that --help output contains all flags including new ones. func TestHelpOutput(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := &cobra.Command{ Use: "netsynth", RunE: run, SilenceUsage: true, } rootCmd.Flags().StringVarP(&ifaceName, "interface", "i", "", "Network interface") rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces", false, "List interfaces") rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Verbose output") rootCmd.Flags().StringVar(&bpfFilter, "filter", "", "BPF filter expression") rootCmd.Flags().StringVar(&readPath, "read", "", "Read from pcap file") var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) // Execute with --help rootCmd.SetArgs([]string{"--help"}) _ = rootCmd.Execute() // --help causes cobra to print and return nil helpText := buf.String() if helpText == "" { // cobra may write help to a different writer; use the usage string directly helpText = rootCmd.UsageString() } checks := []struct { flag string present bool }{ {"-i", strings.Contains(helpText, "-i") || strings.Contains(rootCmd.UsageString(), "-i")}, {"--list-interfaces", strings.Contains(helpText, "--list-interfaces") || strings.Contains(rootCmd.UsageString(), "--list-interfaces")}, {"--verbose", strings.Contains(helpText, "--verbose") || strings.Contains(rootCmd.UsageString(), "--verbose")}, } for _, c := range checks { if !c.present { t.Errorf("expected help output to contain %q", c.flag) } } } // TestFlagMutualExclusion verifies that --read and -i together return a "mutually exclusive" error. func TestFlagMutualExclusion(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := newTestCmd() rootCmd.SetArgs([]string{"-i", "eth0", "--read", "capture.pcap"}) var outBuf, errBuf bytes.Buffer rootCmd.SetOut(&outBuf) rootCmd.SetErr(&errBuf) err := rootCmd.Execute() if err == nil { t.Fatal("expected error when both --read and -i are given, got nil") } if !strings.Contains(err.Error(), "mutually exclusive") { t.Errorf("expected error to contain 'mutually exclusive', got: %q", err.Error()) } } // TestMissingSource verifies that running without --read and without -i returns an appropriate error. func TestMissingSource(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := newTestCmd() var outBuf, errBuf bytes.Buffer rootCmd.SetOut(&outBuf) rootCmd.SetErr(&errBuf) err := rootCmd.Execute() if err == nil { t.Fatal("expected error when neither --read nor -i provided, got nil") } if !strings.Contains(err.Error(), "interface required") { t.Errorf("expected error to contain 'interface required', got: %q", err.Error()) } if !strings.Contains(err.Error(), "--read") { t.Errorf("expected error to mention '--read', got: %q", err.Error()) } } // TestDeriveOutputPath verifies output filename derivation from pcap input path. func TestDeriveOutputPath(t *testing.T) { tests := []struct { input string expected string }{ {"capture.pcap", "capture.mp3"}, {"/tmp/net.pcap", "/tmp/net.mp3"}, {"noext", "noext.mp3"}, {"traffic.pcap.gz", "traffic.pcap.mp3"}, // only last extension replaced {"./local.pcap", "./local.mp3"}, } for _, tc := range tests { got := deriveOutputPath(tc.input) if got != tc.expected { t.Errorf("deriveOutputPath(%q) = %q, want %q", tc.input, got, tc.expected) } } } // TestFilterFlagRegistered verifies --filter flag is registered on the root command. func TestFilterFlagRegistered(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := newTestCmd() f := rootCmd.Flags().Lookup("filter") if f == nil { t.Fatal("expected --filter flag to be registered, got nil") } } // TestReadFlagRegistered verifies --read flag is registered on the root command. func TestReadFlagRegistered(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := newTestCmd() f := rootCmd.Flags().Lookup("read") if f == nil { t.Fatal("expected --read flag to be registered, got nil") } } // TestInvalidBPFFilter verifies that an invalid BPF filter returns a clear error before capture. func TestInvalidBPFFilter(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := newTestCmd() rootCmd.SetArgs([]string{"-i", "lo", "--filter", "invalid garbage xyz"}) var outBuf, errBuf bytes.Buffer rootCmd.SetOut(&outBuf) rootCmd.SetErr(&errBuf) err := rootCmd.Execute() if err == nil { t.Fatal("expected error for invalid BPF filter, got nil") } if !strings.Contains(strings.ToLower(err.Error()), "invalid") { t.Errorf("expected error to contain 'invalid', got: %q", err.Error()) } } // TestHelpOutputNewFlags verifies --help output contains --filter and --read flags. func TestHelpOutputNewFlags(t *testing.T) { // Reset global state ifaceName = "" listIfaces = false verbose = false bpfFilter = "" readPath = "" outputPath = "" rootCmd := newTestCmd() usageStr := rootCmd.UsageString() if !strings.Contains(usageStr, "--filter") { t.Errorf("expected help/usage output to contain '--filter', usage: %s", usageStr) } if !strings.Contains(usageStr, "--read") { t.Errorf("expected help/usage output to contain '--read', usage: %s", usageStr) } }