diff --git a/internal/discovery/config_override_test.go b/internal/discovery/config_override_test.go new file mode 100644 index 0000000..f362c44 --- /dev/null +++ b/internal/discovery/config_override_test.go @@ -0,0 +1,550 @@ +package discovery + +import ( + "net" + "testing" + + "github.com/rcourtman/pulse-go-rewrite/internal/config" + "github.com/rcourtman/pulse-go-rewrite/pkg/discovery/envdetect" +) + +func TestParseCIDRs(t *testing.T) { + tests := []struct { + name string + values []string + wantCount int + wantWarnings int + }{ + { + name: "empty input", + values: []string{}, + wantCount: 0, + wantWarnings: 0, + }, + { + name: "nil input", + values: nil, + wantCount: 0, + wantWarnings: 0, + }, + { + name: "single valid CIDR", + values: []string{"192.168.1.0/24"}, + wantCount: 1, + wantWarnings: 0, + }, + { + name: "multiple valid CIDRs", + values: []string{"192.168.1.0/24", "10.0.0.0/8", "172.16.0.0/12"}, + wantCount: 3, + wantWarnings: 0, + }, + { + name: "CIDR with whitespace", + values: []string{" 192.168.1.0/24 ", " 10.0.0.0/8"}, + wantCount: 2, + wantWarnings: 0, + }, + { + name: "empty string in list", + values: []string{"192.168.1.0/24", "", "10.0.0.0/8"}, + wantCount: 2, + wantWarnings: 0, + }, + { + name: "whitespace-only string", + values: []string{"192.168.1.0/24", " "}, + wantCount: 1, + wantWarnings: 0, + }, + { + name: "invalid CIDR generates warning", + values: []string{"not-a-cidr"}, + wantCount: 0, + wantWarnings: 1, + }, + { + name: "IP without mask generates warning", + values: []string{"192.168.1.1"}, + wantCount: 0, + wantWarnings: 1, + }, + { + name: "mixed valid and invalid", + values: []string{"192.168.1.0/24", "invalid", "10.0.0.0/8"}, + wantCount: 2, + wantWarnings: 1, + }, + { + name: "IPv6 CIDR", + values: []string{"fe80::/10"}, + wantCount: 1, + wantWarnings: 0, + }, + { + name: "invalid mask", + values: []string{"192.168.1.0/33"}, + wantCount: 0, + wantWarnings: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var warnings []string + result := parseCIDRs(tt.values, &warnings) + + if len(result) != tt.wantCount { + t.Errorf("parseCIDRs() returned %d CIDRs, want %d", len(result), tt.wantCount) + } + if len(warnings) != tt.wantWarnings { + t.Errorf("parseCIDRs() generated %d warnings, want %d", len(warnings), tt.wantWarnings) + } + }) + } +} + +func TestParseCIDRs_NilWarnings(t *testing.T) { + // Test that nil warnings pointer doesn't panic + result := parseCIDRs([]string{"192.168.1.0/24", "invalid"}, nil) + if len(result) != 1 { + t.Errorf("parseCIDRs() with nil warnings returned %d CIDRs, want 1", len(result)) + } +} + +func TestParseCIDRMap(t *testing.T) { + tests := []struct { + name string + values []string + wantCount int + }{ + { + name: "empty input", + values: []string{}, + wantCount: 0, + }, + { + name: "single CIDR", + values: []string{"192.168.1.0/24"}, + wantCount: 1, + }, + { + name: "multiple CIDRs", + values: []string{"192.168.1.0/24", "10.0.0.0/8"}, + wantCount: 2, + }, + { + name: "duplicate CIDRs are deduplicated", + values: []string{"192.168.1.0/24", "192.168.1.0/24"}, + wantCount: 1, + }, + { + name: "invalid CIDR excluded", + values: []string{"192.168.1.0/24", "invalid"}, + wantCount: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var warnings []string + result := parseCIDRMap(tt.values, &warnings) + + if len(result) != tt.wantCount { + t.Errorf("parseCIDRMap() returned %d entries, want %d", len(result), tt.wantCount) + } + }) + } +} + +func TestParseCIDRMap_ContainsExpectedCIDR(t *testing.T) { + values := []string{"192.168.1.0/24", "10.0.0.0/8"} + var warnings []string + result := parseCIDRMap(values, &warnings) + + // The canonical form of the CIDR should be in the map + if _, ok := result["192.168.1.0/24"]; !ok { + t.Error("parseCIDRMap() result should contain 192.168.1.0/24") + } + if _, ok := result["10.0.0.0/8"]; !ok { + t.Error("parseCIDRMap() result should contain 10.0.0.0/8") + } +} + +func TestEnvironmentFromOverride(t *testing.T) { + tests := []struct { + name string + value string + wantEnv envdetect.Environment + wantOK bool + }{ + { + name: "empty string", + value: "", + wantEnv: envdetect.Unknown, + wantOK: false, + }, + { + name: "auto", + value: "auto", + wantEnv: envdetect.Unknown, + wantOK: false, + }, + { + name: "AUTO uppercase", + value: "AUTO", + wantEnv: envdetect.Unknown, + wantOK: false, + }, + { + name: "Auto mixed case", + value: "Auto", + wantEnv: envdetect.Unknown, + wantOK: false, + }, + { + name: "native", + value: "native", + wantEnv: envdetect.Native, + wantOK: true, + }, + { + name: "NATIVE uppercase", + value: "NATIVE", + wantEnv: envdetect.Native, + wantOK: true, + }, + { + name: "docker_host", + value: "docker_host", + wantEnv: envdetect.DockerHost, + wantOK: true, + }, + { + name: "docker_bridge", + value: "docker_bridge", + wantEnv: envdetect.DockerBridge, + wantOK: true, + }, + { + name: "lxc_privileged", + value: "lxc_privileged", + wantEnv: envdetect.LXCPrivileged, + wantOK: true, + }, + { + name: "lxc_unprivileged", + value: "lxc_unprivileged", + wantEnv: envdetect.LXCUnprivileged, + wantOK: true, + }, + { + name: "value with whitespace", + value: " native ", + wantEnv: envdetect.Native, + wantOK: true, + }, + { + name: "unknown value", + value: "unknown_env", + wantEnv: envdetect.Unknown, + wantOK: false, + }, + { + name: "partial match not accepted", + value: "docker", + wantEnv: envdetect.Unknown, + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotEnv, gotOK := environmentFromOverride(tt.value) + if gotEnv != tt.wantEnv { + t.Errorf("environmentFromOverride(%q) environment = %v, want %v", tt.value, gotEnv, tt.wantEnv) + } + if gotOK != tt.wantOK { + t.Errorf("environmentFromOverride(%q) ok = %v, want %v", tt.value, gotOK, tt.wantOK) + } + }) + } +} + +func TestShouldPruneContainerNetworks(t *testing.T) { + tests := []struct { + name string + env envdetect.Environment + want bool + }{ + { + name: "DockerBridge should prune", + env: envdetect.DockerBridge, + want: true, + }, + { + name: "LXCUnprivileged should prune", + env: envdetect.LXCUnprivileged, + want: true, + }, + { + name: "Native should not prune", + env: envdetect.Native, + want: false, + }, + { + name: "DockerHost should not prune", + env: envdetect.DockerHost, + want: false, + }, + { + name: "LXCPrivileged should not prune", + env: envdetect.LXCPrivileged, + want: false, + }, + { + name: "Unknown should not prune", + env: envdetect.Unknown, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shouldPruneContainerNetworks(tt.env); got != tt.want { + t.Errorf("shouldPruneContainerNetworks(%v) = %v, want %v", tt.env, got, tt.want) + } + }) + } +} + +func TestIsLikelyContainerPhase(t *testing.T) { + tests := []struct { + name string + phaseName string + want bool + }{ + { + name: "contains container lowercase", + phaseName: "docker_container_network", + want: true, + }, + { + name: "contains Container mixed case", + phaseName: "DockerContainer", + want: true, + }, + { + name: "contains CONTAINER uppercase", + phaseName: "CONTAINER_NETWORK", + want: true, + }, + { + name: "local_network does not contain container", + phaseName: "local_network", + want: false, + }, + { + name: "host_network does not contain container", + phaseName: "host_network", + want: false, + }, + { + name: "empty string", + phaseName: "", + want: false, + }, + { + name: "whitespace only", + phaseName: " ", + want: false, + }, + { + name: "container with surrounding whitespace", + phaseName: " container_net ", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isLikelyContainerPhase(tt.phaseName); got != tt.want { + t.Errorf("isLikelyContainerPhase(%q) = %v, want %v", tt.phaseName, got, tt.want) + } + }) + } +} + +func TestFilterPhasesForEnvironment(t *testing.T) { + // Helper to create a subnet phase + makePhase := func(name string) envdetect.SubnetPhase { + _, subnet, _ := net.ParseCIDR("192.168.1.0/24") + return envdetect.SubnetPhase{ + Name: name, + Subnets: []net.IPNet{*subnet}, + } + } + + tests := []struct { + name string + phases []envdetect.SubnetPhase + env envdetect.Environment + wantPhases []string // names of phases that should remain + }{ + { + name: "empty phases", + phases: []envdetect.SubnetPhase{}, + env: envdetect.Native, + wantPhases: []string{}, + }, + { + name: "Native keeps local and host phases", + phases: []envdetect.SubnetPhase{ + makePhase("local_network"), + makePhase("host_network"), + makePhase("container_network"), + }, + env: envdetect.Native, + wantPhases: []string{"local_network", "host_network"}, + }, + { + name: "DockerHost keeps local and host phases", + phases: []envdetect.SubnetPhase{ + makePhase("local_network"), + makePhase("container_network"), + }, + env: envdetect.DockerHost, + wantPhases: []string{"local_network"}, + }, + { + name: "LXCPrivileged keeps local and host phases", + phases: []envdetect.SubnetPhase{ + makePhase("host_interface"), + makePhase("container_network"), + }, + env: envdetect.LXCPrivileged, + wantPhases: []string{"host_interface"}, + }, + { + name: "DockerBridge keeps container, inferred, and host phases", + phases: []envdetect.SubnetPhase{ + makePhase("local_network"), + makePhase("container_network"), + makePhase("inferred_subnet"), + makePhase("host_network"), + }, + env: envdetect.DockerBridge, + wantPhases: []string{"container_network", "inferred_subnet", "host_network"}, + }, + { + name: "LXCUnprivileged keeps lxc, container, and parent phases", + phases: []envdetect.SubnetPhase{ + makePhase("lxc_network"), + makePhase("container_network"), + makePhase("parent_host"), + makePhase("local_network"), + }, + env: envdetect.LXCUnprivileged, + wantPhases: []string{"lxc_network", "container_network", "parent_host"}, + }, + { + name: "Unknown keeps all phases", + phases: []envdetect.SubnetPhase{ + makePhase("local_network"), + makePhase("container_network"), + }, + env: envdetect.Unknown, + wantPhases: []string{"local_network", "container_network"}, + }, + { + name: "no matching phases preserves original", + phases: []envdetect.SubnetPhase{ + makePhase("random_network"), + }, + env: envdetect.Native, + wantPhases: []string{"random_network"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: tt.env, + Phases: tt.phases, + } + + filterPhasesForEnvironment(profile, tt.env) + + if len(profile.Phases) != len(tt.wantPhases) { + t.Errorf("filterPhasesForEnvironment() left %d phases, want %d", len(profile.Phases), len(tt.wantPhases)) + return + } + + for i, want := range tt.wantPhases { + if profile.Phases[i].Name != want { + t.Errorf("filterPhasesForEnvironment() phase[%d].Name = %q, want %q", i, profile.Phases[i].Name, want) + } + } + }) + } +} + +func TestApplyConfigToProfile_NilProfile(t *testing.T) { + // Ensure ApplyConfigToProfile handles nil profile without panic + cfg := config.DiscoveryConfig{ + EnvironmentOverride: "native", + } + ApplyConfigToProfile(nil, cfg) // Should not panic +} + +func TestApplyConfigToProfile_InvalidEnvironmentOverride(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + } + cfg := config.DiscoveryConfig{ + EnvironmentOverride: "invalid_environment", + } + + ApplyConfigToProfile(profile, cfg) + + // Should add warning for unknown environment + if len(profile.Warnings) != 1 { + t.Errorf("Expected 1 warning for invalid environment override, got %d", len(profile.Warnings)) + } +} + +func TestApplyConfigToProfile_PolicyOverrides(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Policy: envdetect.ScanPolicy{ + MaxHostsPerScan: 100, + MaxConcurrent: 10, + DialTimeout: 1000, + HTTPTimeout: 2000, + }, + } + + cfg := config.DiscoveryConfig{ + MaxHostsPerScan: 500, + MaxConcurrent: 50, + EnableReverseDNS: true, + ScanGateways: true, + DialTimeout: 5000, + HTTPTimeout: 10000, + } + + ApplyConfigToProfile(profile, cfg) + + if profile.Policy.MaxHostsPerScan != 500 { + t.Errorf("MaxHostsPerScan = %d, want 500", profile.Policy.MaxHostsPerScan) + } + if profile.Policy.MaxConcurrent != 50 { + t.Errorf("MaxConcurrent = %d, want 50", profile.Policy.MaxConcurrent) + } + if !profile.Policy.EnableReverseDNS { + t.Error("EnableReverseDNS should be true") + } + if !profile.Policy.ScanGateways { + t.Error("ScanGateways should be true") + } +}