From fbc86871e623474b081a20c4d722abc6a303bd61 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 26 Nov 2025 14:25:17 +0000 Subject: [PATCH] test: add unit tests for pkg/discovery/envdetect Add 20 tests covering: - Environment enum String() method - DefaultScanPolicy default values - ScanPolicy struct fields - SubnetPhase struct fields - EnvironmentProfile struct fields - parseHexIP little-endian IP parsing - tryCommonSubnets fallback subnet list - profileWithWarning helper - addFallbackSubnets with confidence handling --- pkg/discovery/envdetect/envdetect_test.go | 509 ++++++++++++++++++++++ 1 file changed, 509 insertions(+) create mode 100644 pkg/discovery/envdetect/envdetect_test.go diff --git a/pkg/discovery/envdetect/envdetect_test.go b/pkg/discovery/envdetect/envdetect_test.go new file mode 100644 index 0000000..2e98d6e --- /dev/null +++ b/pkg/discovery/envdetect/envdetect_test.go @@ -0,0 +1,509 @@ +package envdetect + +import ( + "net" + "testing" + "time" +) + +func TestEnvironment_String(t *testing.T) { + tests := []struct { + env Environment + expected string + }{ + {Unknown, "unknown"}, + {Native, "native"}, + {DockerHost, "docker_host"}, + {DockerBridge, "docker_bridge"}, + {LXCPrivileged, "lxc_privileged"}, + {LXCUnprivileged, "lxc_unprivileged"}, + {Environment(99), "unknown"}, // Invalid value + } + + for _, tc := range tests { + t.Run(tc.expected, func(t *testing.T) { + result := tc.env.String() + if result != tc.expected { + t.Errorf("Environment(%d).String() = %q, want %q", tc.env, result, tc.expected) + } + }) + } +} + +func TestDefaultScanPolicy(t *testing.T) { + policy := DefaultScanPolicy() + + if policy.MaxConcurrent != 50 { + t.Errorf("MaxConcurrent = %d, want 50", policy.MaxConcurrent) + } + if policy.DialTimeout != time.Second { + t.Errorf("DialTimeout = %v, want 1s", policy.DialTimeout) + } + if policy.HTTPTimeout != 2*time.Second { + t.Errorf("HTTPTimeout = %v, want 2s", policy.HTTPTimeout) + } + if policy.MaxHostsPerScan != 1024 { + t.Errorf("MaxHostsPerScan = %d, want 1024", policy.MaxHostsPerScan) + } + if !policy.EnableReverseDNS { + t.Error("EnableReverseDNS should be true by default") + } + if !policy.ScanGateways { + t.Error("ScanGateways should be true by default") + } +} + +func TestScanPolicy_Fields(t *testing.T) { + policy := ScanPolicy{ + MaxConcurrent: 100, + DialTimeout: 5 * time.Second, + HTTPTimeout: 10 * time.Second, + MaxHostsPerScan: 2048, + EnableReverseDNS: false, + ScanGateways: false, + } + + if policy.MaxConcurrent != 100 { + t.Errorf("MaxConcurrent = %d, want 100", policy.MaxConcurrent) + } + if policy.DialTimeout != 5*time.Second { + t.Errorf("DialTimeout = %v, want 5s", policy.DialTimeout) + } + if policy.HTTPTimeout != 10*time.Second { + t.Errorf("HTTPTimeout = %v, want 10s", policy.HTTPTimeout) + } + if policy.MaxHostsPerScan != 2048 { + t.Errorf("MaxHostsPerScan = %d, want 2048", policy.MaxHostsPerScan) + } + if policy.EnableReverseDNS { + t.Error("EnableReverseDNS should be false") + } + if policy.ScanGateways { + t.Error("ScanGateways should be false") + } +} + +func TestSubnetPhase_Fields(t *testing.T) { + _, subnet, _ := net.ParseCIDR("192.168.1.0/24") + + phase := SubnetPhase{ + Name: "test_phase", + Subnets: []net.IPNet{*subnet}, + Confidence: 0.85, + Priority: 2, + } + + if phase.Name != "test_phase" { + t.Errorf("Name = %q, want test_phase", phase.Name) + } + if len(phase.Subnets) != 1 { + t.Errorf("Subnets count = %d, want 1", len(phase.Subnets)) + } + if phase.Confidence != 0.85 { + t.Errorf("Confidence = %f, want 0.85", phase.Confidence) + } + if phase.Priority != 2 { + t.Errorf("Priority = %d, want 2", phase.Priority) + } +} + +func TestEnvironmentProfile_Fields(t *testing.T) { + _, subnet, _ := net.ParseCIDR("10.0.0.0/24") + extraIP := net.ParseIP("192.168.1.1") + + profile := EnvironmentProfile{ + Type: Native, + Phases: []SubnetPhase{ + {Name: "local_networks", Subnets: []net.IPNet{*subnet}}, + }, + ExtraTargets: []net.IP{extraIP}, + Policy: DefaultScanPolicy(), + Confidence: 0.95, + Warnings: []string{"test warning"}, + Metadata: map[string]string{"key": "value"}, + } + + if profile.Type != Native { + t.Errorf("Type = %v, want Native", profile.Type) + } + if len(profile.Phases) != 1 { + t.Errorf("Phases count = %d, want 1", len(profile.Phases)) + } + if len(profile.ExtraTargets) != 1 { + t.Errorf("ExtraTargets count = %d, want 1", len(profile.ExtraTargets)) + } + if profile.Confidence != 0.95 { + t.Errorf("Confidence = %f, want 0.95", profile.Confidence) + } + if len(profile.Warnings) != 1 { + t.Errorf("Warnings count = %d, want 1", len(profile.Warnings)) + } + if profile.Metadata["key"] != "value" { + t.Errorf("Metadata[key] = %q, want value", profile.Metadata["key"]) + } +} + +func TestParseHexIP(t *testing.T) { + tests := []struct { + name string + hexIP string + expected net.IP + expectErr bool + }{ + { + name: "valid gateway 192.168.1.1", + hexIP: "0101A8C0", // 192.168.1.1 in little-endian hex + expected: net.IPv4(192, 168, 1, 1), + }, + { + name: "valid gateway 10.0.0.1", + hexIP: "0100000A", // 10.0.0.1 in little-endian hex + expected: net.IPv4(10, 0, 0, 1), + }, + { + name: "all zeros", + hexIP: "00000000", + expected: net.IPv4(0, 0, 0, 0), + }, + { + name: "broadcast", + hexIP: "FFFFFFFF", + expected: net.IPv4(255, 255, 255, 255), + }, + { + name: "too short", + hexIP: "0101A8", + expectErr: true, + }, + { + name: "too long", + hexIP: "0101A8C0FF", + expectErr: true, + }, + { + name: "invalid hex", + hexIP: "ZZZZZZZZ", + expectErr: true, + }, + { + name: "empty", + hexIP: "", + expectErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := parseHexIP(tc.hexIP) + if tc.expectErr { + if err == nil { + t.Errorf("parseHexIP(%q) expected error, got nil", tc.hexIP) + } + return + } + if err != nil { + t.Errorf("parseHexIP(%q) unexpected error: %v", tc.hexIP, err) + return + } + if !result.Equal(tc.expected) { + t.Errorf("parseHexIP(%q) = %v, want %v", tc.hexIP, result, tc.expected) + } + }) + } +} + +func TestTryCommonSubnets(t *testing.T) { + subnets := tryCommonSubnets() + + if len(subnets) == 0 { + t.Error("tryCommonSubnets() returned empty slice") + } + + // Verify we get expected common subnets + expectedCIDRs := map[string]bool{ + "192.168.1.0/24": true, + "192.168.0.0/24": true, + "10.0.0.0/24": true, + "172.16.0.0/24": true, + "192.168.2.0/24": true, + } + + for _, subnet := range subnets { + cidr := subnet.String() + if !expectedCIDRs[cidr] { + t.Errorf("Unexpected subnet %s in fallback list", cidr) + } + delete(expectedCIDRs, cidr) + } + + for cidr := range expectedCIDRs { + t.Errorf("Expected subnet %s not found in fallback list", cidr) + } +} + +func TestProfileWithWarning(t *testing.T) { + profile := &EnvironmentProfile{ + Warnings: []string{"existing warning"}, + } + + result := profileWithWarning(profile, "new warning") + + // Should return same profile + if result != profile { + t.Error("profileWithWarning should return the same profile instance") + } + + // Should have appended warning + if len(result.Warnings) != 2 { + t.Errorf("Warnings count = %d, want 2", len(result.Warnings)) + } + if result.Warnings[1] != "new warning" { + t.Errorf("Warnings[1] = %q, want 'new warning'", result.Warnings[1]) + } +} + +func TestProfileWithWarning_EmptyWarning(t *testing.T) { + profile := &EnvironmentProfile{ + Warnings: []string{"existing"}, + } + + result := profileWithWarning(profile, "") + + // Empty warning should not be appended + if len(result.Warnings) != 1 { + t.Errorf("Warnings count = %d, want 1 (empty warning should not be appended)", len(result.Warnings)) + } +} + +func TestAddFallbackSubnets(t *testing.T) { + profile := &EnvironmentProfile{ + Phases: []SubnetPhase{}, + Confidence: 0.0, + Warnings: []string{}, + } + + result, err := addFallbackSubnets(profile) + if err != nil { + t.Errorf("addFallbackSubnets unexpected error: %v", err) + } + + // Should have added a phase + if len(result.Phases) != 1 { + t.Errorf("Phases count = %d, want 1", len(result.Phases)) + } + + // Phase should have correct properties + phase := result.Phases[0] + if phase.Name != "fallback_common_subnets" { + t.Errorf("Phase name = %q, want fallback_common_subnets", phase.Name) + } + if phase.Confidence != 0.3 { + t.Errorf("Phase confidence = %f, want 0.3", phase.Confidence) + } + if phase.Priority != 10 { + t.Errorf("Phase priority = %d, want 10", phase.Priority) + } + if len(phase.Subnets) == 0 { + t.Error("Phase should have subnets") + } + + // Confidence should be set if it was 0 + if result.Confidence != 0.3 { + t.Errorf("Confidence = %f, want 0.3", result.Confidence) + } + + // Should have warning + foundWarning := false + for _, w := range result.Warnings { + if w == "Using fallback private subnets; consider manual subnet configuration" { + foundWarning = true + break + } + } + if !foundWarning { + t.Error("Expected fallback warning not found") + } +} + +func TestAddFallbackSubnets_PreservesExistingConfidence(t *testing.T) { + profile := &EnvironmentProfile{ + Phases: []SubnetPhase{}, + Confidence: 0.7, + Warnings: []string{}, + } + + result, _ := addFallbackSubnets(profile) + + // Existing confidence should be preserved + if result.Confidence != 0.7 { + t.Errorf("Confidence = %f, want 0.7 (should preserve existing)", result.Confidence) + } +} + +func TestEnvironmentConstants(t *testing.T) { + // Verify enum values are distinct + values := map[Environment]bool{} + envs := []Environment{Unknown, Native, DockerHost, DockerBridge, LXCPrivileged, LXCUnprivileged} + + for _, env := range envs { + if values[env] { + t.Errorf("Duplicate environment value: %d", env) + } + values[env] = true + } + + // Unknown should be 0 (default value) + if Unknown != 0 { + t.Errorf("Unknown = %d, want 0", Unknown) + } +} + +func TestSubnetPhase_MultipleSubnets(t *testing.T) { + _, subnet1, _ := net.ParseCIDR("192.168.1.0/24") + _, subnet2, _ := net.ParseCIDR("10.0.0.0/8") + _, subnet3, _ := net.ParseCIDR("172.16.0.0/16") + + phase := SubnetPhase{ + Name: "multi_subnet_phase", + Subnets: []net.IPNet{*subnet1, *subnet2, *subnet3}, + } + + if len(phase.Subnets) != 3 { + t.Errorf("Subnets count = %d, want 3", len(phase.Subnets)) + } + + // Verify each subnet + if phase.Subnets[0].String() != "192.168.1.0/24" { + t.Errorf("Subnet[0] = %s, want 192.168.1.0/24", phase.Subnets[0].String()) + } + if phase.Subnets[1].String() != "10.0.0.0/8" { + t.Errorf("Subnet[1] = %s, want 10.0.0.0/8", phase.Subnets[1].String()) + } + if phase.Subnets[2].String() != "172.16.0.0/16" { + t.Errorf("Subnet[2] = %s, want 172.16.0.0/16", phase.Subnets[2].String()) + } +} + +func TestEnvironmentProfile_EmptyMetadata(t *testing.T) { + profile := EnvironmentProfile{ + Type: Native, + Metadata: nil, + } + + // Accessing nil map should not panic for reads + if profile.Metadata != nil { + t.Error("Metadata should be nil") + } + + // Verify we can initialize and use metadata + profile.Metadata = make(map[string]string) + profile.Metadata["test"] = "value" + + if profile.Metadata["test"] != "value" { + t.Errorf("Metadata[test] = %q, want value", profile.Metadata["test"]) + } +} + +func TestEnvironmentProfile_MultiplePhases(t *testing.T) { + _, subnet1, _ := net.ParseCIDR("192.168.1.0/24") + _, subnet2, _ := net.ParseCIDR("10.0.0.0/24") + + profile := EnvironmentProfile{ + Type: DockerBridge, + Phases: []SubnetPhase{ + {Name: "container_network", Subnets: []net.IPNet{*subnet1}, Priority: 1, Confidence: 0.95}, + {Name: "inferred_host", Subnets: []net.IPNet{*subnet2}, Priority: 2, Confidence: 0.7}, + }, + Confidence: 0.85, + } + + if len(profile.Phases) != 2 { + t.Errorf("Phases count = %d, want 2", len(profile.Phases)) + } + + // Verify phase ordering by priority + if profile.Phases[0].Priority >= profile.Phases[1].Priority { + t.Error("First phase should have lower priority than second") + } +} + +func TestParseHexIP_LittleEndian(t *testing.T) { + // Test that little-endian parsing is correct + // 0101A8C0 should parse to 192.168.1.1 + // Bytes: 01 01 A8 C0 + // Little-endian: C0 A8 01 01 = 192.168.1.1 + ip, err := parseHexIP("0101A8C0") + if err != nil { + t.Fatalf("parseHexIP unexpected error: %v", err) + } + + expected := net.IPv4(192, 168, 1, 1) + if !ip.Equal(expected) { + t.Errorf("parseHexIP(0101A8C0) = %v, want %v", ip, expected) + } +} + +func TestScanPolicy_ZeroValues(t *testing.T) { + policy := ScanPolicy{} + + // Verify zero values + if policy.MaxConcurrent != 0 { + t.Error("MaxConcurrent should be 0 by default") + } + if policy.DialTimeout != 0 { + t.Error("DialTimeout should be 0 by default") + } + if policy.HTTPTimeout != 0 { + t.Error("HTTPTimeout should be 0 by default") + } + if policy.MaxHostsPerScan != 0 { + t.Error("MaxHostsPerScan should be 0 by default") + } + if policy.EnableReverseDNS { + t.Error("EnableReverseDNS should be false by default") + } + if policy.ScanGateways { + t.Error("ScanGateways should be false by default") + } +} + +func TestEnvironmentProfile_WithExtraTargets(t *testing.T) { + targets := []net.IP{ + net.ParseIP("192.168.1.1"), + net.ParseIP("192.168.1.254"), + net.ParseIP("10.0.0.1"), + } + + profile := EnvironmentProfile{ + ExtraTargets: targets, + } + + if len(profile.ExtraTargets) != 3 { + t.Errorf("ExtraTargets count = %d, want 3", len(profile.ExtraTargets)) + } + + // Verify IP addresses + if !profile.ExtraTargets[0].Equal(net.ParseIP("192.168.1.1")) { + t.Errorf("ExtraTargets[0] = %v, want 192.168.1.1", profile.ExtraTargets[0]) + } +} + +func TestEnvironmentProfile_WarningsSlice(t *testing.T) { + profile := EnvironmentProfile{ + Warnings: []string{ + "Warning 1", + "Warning 2", + "Warning 3", + }, + } + + if len(profile.Warnings) != 3 { + t.Errorf("Warnings count = %d, want 3", len(profile.Warnings)) + } + + // Append more warnings + profile.Warnings = append(profile.Warnings, "Warning 4") + if len(profile.Warnings) != 4 { + t.Errorf("Warnings count after append = %d, want 4", len(profile.Warnings)) + } +}