mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
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
509 lines
13 KiB
Go
509 lines
13 KiB
Go
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))
|
|
}
|
|
}
|