diff --git a/internal/discovery/config_override.go b/internal/discovery/config_override.go index 999f872c7..d0b30ac67 100644 --- a/internal/discovery/config_override.go +++ b/internal/discovery/config_override.go @@ -11,11 +11,13 @@ import ( "github.com/rcourtman/pulse-go-rewrite/pkg/discovery/envdetect" ) +var detectEnvironmentFn = envdetect.DetectEnvironment + // BuildScanner creates a discovery scanner configured using the supplied discovery config. func BuildScanner(cfg config.DiscoveryConfig) (*pkgdiscovery.Scanner, error) { cfg = config.NormalizeDiscoveryConfig(cfg) - profile, err := envdetect.DetectEnvironment() + profile, err := detectEnvironmentFn() if err != nil { return nil, err } diff --git a/internal/discovery/config_override_test.go b/internal/discovery/config_override_test.go index f362c4441..eb205a7ba 100644 --- a/internal/discovery/config_override_test.go +++ b/internal/discovery/config_override_test.go @@ -1,13 +1,29 @@ package discovery import ( + "errors" "net" "testing" + "time" "github.com/rcourtman/pulse-go-rewrite/internal/config" + pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery" "github.com/rcourtman/pulse-go-rewrite/pkg/discovery/envdetect" ) +func mustCIDR(t *testing.T, value string) net.IPNet { + t.Helper() + _, cidr, err := net.ParseCIDR(value) + if err != nil { + t.Fatalf("parse CIDR %s: %v", value, err) + } + return *cidr +} + +func resetDetectEnvironment() { + detectEnvironmentFn = envdetect.DetectEnvironment +} + func TestParseCIDRs(t *testing.T) { tests := []struct { name string @@ -271,6 +287,260 @@ func TestEnvironmentFromOverride(t *testing.T) { } } +func TestBuildScanner(t *testing.T) { + t.Cleanup(resetDetectEnvironment) + + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Phases: []envdetect.SubnetPhase{{Name: "local", Subnets: []net.IPNet{mustCIDR(t, "192.168.1.0/24")}}}, + Policy: envdetect.DefaultScanPolicy(), + } + detectEnvironmentFn = func() (*envdetect.EnvironmentProfile, error) { + return profile, nil + } + + scanner, err := BuildScanner(config.DefaultDiscoveryConfig()) + if err != nil { + t.Fatalf("BuildScanner error: %v", err) + } + if scanner == nil { + t.Fatalf("expected scanner") + } +} + +func TestBuildScannerError(t *testing.T) { + t.Cleanup(resetDetectEnvironment) + + detectEnvironmentFn = func() (*envdetect.EnvironmentProfile, error) { + return nil, errors.New("detect failed") + } + + if _, err := BuildScanner(config.DefaultDiscoveryConfig()); err == nil { + t.Fatalf("expected error") + } +} + +func TestApplyConfigToProfileNil(t *testing.T) { + ApplyConfigToProfile(nil, config.DefaultDiscoveryConfig()) +} + +func TestApplyConfigToProfileOverridesAndPolicies(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Unknown, + Phases: []envdetect.SubnetPhase{ + {Name: "container_network", Subnets: []net.IPNet{mustCIDR(t, "10.0.0.0/24"), mustCIDR(t, "192.168.0.0/24")}}, + {Name: "local", Subnets: []net.IPNet{mustCIDR(t, "172.16.0.0/24")}}, + }, + Policy: envdetect.DefaultScanPolicy(), + } + + cfg := config.DiscoveryConfig{ + EnvironmentOverride: "docker_bridge", + SubnetBlocklist: []string{"192.168.0.0/24"}, + SubnetAllowlist: []string{"10.0.0.0/24", "192.168.0.0/24", "invalid"}, + MaxHostsPerScan: 10, + MaxConcurrent: 20, + EnableReverseDNS: true, + ScanGateways: true, + DialTimeout: 1500, + HTTPTimeout: 2500, + IPBlocklist: []string{"192.168.1.10", "invalid"}, + } + + ApplyConfigToProfile(profile, cfg) + + if profile.Type != envdetect.DockerBridge { + t.Fatalf("expected DockerBridge env, got %v", profile.Type) + } + if len(profile.Phases) == 0 || profile.Phases[0].Name != "config_allowlist" { + t.Fatalf("expected allowlist phase first, got %#v", profile.Phases) + } + if len(profile.Phases[0].Subnets) != 1 { + t.Fatalf("expected allowlist to filter blocklisted subnet") + } + if profile.Policy.MaxHostsPerScan != 10 || profile.Policy.MaxConcurrent != 20 { + t.Fatalf("policy not updated: %+v", profile.Policy) + } + if !profile.Policy.EnableReverseDNS || !profile.Policy.ScanGateways { + t.Fatalf("policy flags not updated") + } + if profile.Policy.DialTimeout != 1500*time.Millisecond || profile.Policy.HTTPTimeout != 2500*time.Millisecond { + t.Fatalf("policy timeouts not updated: %+v", profile.Policy) + } + if len(profile.IPBlocklist) != 1 { + t.Fatalf("expected one IP in blocklist, got %d", len(profile.IPBlocklist)) + } +} + +func TestApplyConfigToProfileSkipsEmptyIPBlocklistEntries(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Policy: envdetect.DefaultScanPolicy(), + } + + cfg := config.DiscoveryConfig{ + IPBlocklist: []string{"", " ", "192.168.1.10"}, + } + + ApplyConfigToProfile(profile, cfg) + + if len(profile.IPBlocklist) != 1 { + t.Fatalf("expected 1 IP in blocklist, got %d", len(profile.IPBlocklist)) + } +} + +func TestApplyConfigToProfileInvalidEnvironmentOverride(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Phases: []envdetect.SubnetPhase{}, + Policy: envdetect.DefaultScanPolicy(), + } + cfg := config.DiscoveryConfig{EnvironmentOverride: "invalid_env"} + + ApplyConfigToProfile(profile, cfg) + if len(profile.Warnings) == 0 { + t.Fatalf("expected warning for invalid environment override") + } +} + +func TestApplyConfigToProfilePrunesContainerPhase(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.LXCUnprivileged, + Phases: []envdetect.SubnetPhase{ + {Name: "container_phase", Subnets: []net.IPNet{mustCIDR(t, "10.0.0.0/24")}}, + {Name: "lxc_parent", Subnets: []net.IPNet{mustCIDR(t, "192.168.0.0/24")}}, + }, + Policy: envdetect.DefaultScanPolicy(), + } + + ApplyConfigToProfile(profile, config.DefaultDiscoveryConfig()) + if len(profile.Phases) != 1 || profile.Phases[0].Name != "lxc_parent" { + t.Fatalf("expected container phase pruned, got %#v", profile.Phases) + } +} + +func TestApplyConfigToProfileBlocklistWarnings(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Unknown, + Phases: []envdetect.SubnetPhase{}, + Policy: envdetect.DefaultScanPolicy(), + } + + cfg := config.DiscoveryConfig{ + SubnetBlocklist: []string{"invalid"}, + } + ApplyConfigToProfile(profile, cfg) + if len(profile.Warnings) == 0 { + t.Fatalf("expected warnings for invalid CIDR") + } +} + +func TestApplyConfigToProfileAllowsConfigAllowlist(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Phases: []envdetect.SubnetPhase{ + {Name: "local", Subnets: []net.IPNet{mustCIDR(t, "10.0.0.0/24")}}, + }, + Policy: envdetect.DefaultScanPolicy(), + } + + cfg := config.DiscoveryConfig{ + SubnetAllowlist: []string{"10.0.0.0/24"}, + } + + ApplyConfigToProfile(profile, cfg) + if len(profile.Phases) == 0 || profile.Phases[0].Name != "config_allowlist" { + t.Fatalf("expected allowlist phase, got %#v", profile.Phases) + } +} + +func TestApplyConfigToProfileNoProfileWarnings(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Phases: []envdetect.SubnetPhase{}, + Policy: envdetect.DefaultScanPolicy(), + Warnings: nil, + ExtraTargets: []net.IP{}, + } + cfg := config.DiscoveryConfig{ + IPBlocklist: []string{"bad"}, + } + ApplyConfigToProfile(profile, cfg) + if len(profile.Warnings) == 0 { + t.Fatalf("expected warnings for invalid IP") + } +} + +func TestApplyConfigToProfileBlocksSubnets(t *testing.T) { + subnet := mustCIDR(t, "10.0.0.0/24") + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Phases: []envdetect.SubnetPhase{ + {Name: "local", Subnets: []net.IPNet{subnet}}, + }, + Policy: envdetect.DefaultScanPolicy(), + } + cfg := config.DiscoveryConfig{ + SubnetBlocklist: []string{"10.0.0.0/24"}, + } + + ApplyConfigToProfile(profile, cfg) + if len(profile.Phases) != 0 { + t.Fatalf("expected phases filtered, got %#v", profile.Phases) + } +} + +func TestApplyConfigToProfileAllowsBlockedAllowlist(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Phases: []envdetect.SubnetPhase{}, + Policy: envdetect.DefaultScanPolicy(), + } + cfg := config.DiscoveryConfig{ + SubnetAllowlist: []string{"10.0.0.0/24"}, + SubnetBlocklist: []string{"10.0.0.0/24"}, + } + + ApplyConfigToProfile(profile, cfg) + if len(profile.Phases) != 0 { + t.Fatalf("expected allowlist filtered by blocklist") + } +} + +func TestApplyConfigToProfileKeepsUnknownEnvironmentPhases(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Unknown, + Phases: []envdetect.SubnetPhase{ + {Name: "local", Subnets: []net.IPNet{mustCIDR(t, "10.0.0.0/24")}}, + }, + Policy: envdetect.DefaultScanPolicy(), + } + cfg := config.DiscoveryConfig{ + EnvironmentOverride: "auto", + } + + ApplyConfigToProfile(profile, cfg) + if len(profile.Phases) != 1 { + t.Fatalf("expected phases kept for unknown env") + } +} + +func TestApplyConfigToProfileUsesNewScanner(t *testing.T) { + profile := &envdetect.EnvironmentProfile{ + Type: envdetect.Native, + Phases: []envdetect.SubnetPhase{}, + Policy: envdetect.DefaultScanPolicy(), + } + + cfg := config.DefaultDiscoveryConfig() + ApplyConfigToProfile(profile, cfg) + + scanner, err := pkgdiscovery.NewScannerWithProfile(profile), error(nil) + if err != nil || scanner == nil { + t.Fatalf("expected scanner from profile") + } +} + func TestShouldPruneContainerNetworks(t *testing.T) { tests := []struct { name string diff --git a/internal/discovery/service.go b/internal/discovery/service.go index 0067d125a..745f20c7b 100644 --- a/internal/discovery/service.go +++ b/internal/discovery/service.go @@ -90,6 +90,9 @@ type discoveryScanner interface { type scannerFactory func(config.DiscoveryConfig) (discoveryScanner, error) var ( + newScannerFn = func() discoveryScanner { + return pkgdiscovery.NewScanner() + } discoveryScanResults = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "pulse", @@ -144,7 +147,7 @@ func NewService(wsHub *websocket.Hub, interval time.Duration, subnet string, cfg } return &Service{ - scanner: pkgdiscovery.NewScanner(), + scanner: newScannerFn(), wsHub: wsHub, cache: &DiscoveryCache{}, interval: interval, @@ -366,11 +369,11 @@ func (s *Service) performScan() { } if err != nil { log.Warn().Err(err).Msg("Environment detection failed during discovery; falling back to default scanner configuration") - newScanner = pkgdiscovery.NewScanner() + newScanner = newScannerFn() } if newScanner == nil { log.Warn().Msg("Discovery scanner factory returned nil; using default scanner configuration") - newScanner = pkgdiscovery.NewScanner() + newScanner = newScannerFn() } s.mu.Lock() s.scanner = newScanner diff --git a/internal/discovery/service_test.go b/internal/discovery/service_test.go index f3251f65d..b5078bbd5 100644 --- a/internal/discovery/service_test.go +++ b/internal/discovery/service_test.go @@ -34,6 +34,19 @@ func (f *fakeScanner) DiscoverServersWithCallbacks(ctx context.Context, subnet s return f.result, f.err } +type countingScanner struct { + result *pkgdiscovery.DiscoveryResult + err error + calls chan struct{} +} + +func (c *countingScanner) DiscoverServersWithCallbacks(ctx context.Context, subnet string, serverCallback pkgdiscovery.ServerCallback, progressCallback pkgdiscovery.ProgressCallback) (*pkgdiscovery.DiscoveryResult, error) { + if c.calls != nil { + c.calls <- struct{}{} + } + return c.result, c.err +} + func TestPerformScanRecordsHistoryAndMetrics(t *testing.T) { service := NewService(nil, time.Minute, "192.168.1.0/24", func() config.DiscoveryConfig { cfg := config.DefaultDiscoveryConfig() @@ -149,3 +162,356 @@ func TestPerformScanRecordsPartialFailure(t *testing.T) { t.Fatalf("expected errorCount %d, got %d", len(scanner.result.StructuredErrors), entry.errorCount) } } + +func resetNewScannerFn() { + newScannerFn = func() discoveryScanner { + return pkgdiscovery.NewScanner() + } +} + +func TestHistoryEntryAccessors(t *testing.T) { + started := time.Now().Add(-time.Minute) + completed := time.Now() + entry := historyEntry{ + startedAt: started, + completedAt: completed, + subnet: "10.0.0.0/24", + serverCount: 3, + errorCount: 1, + duration: time.Second, + blocklistLength: 2, + status: "success", + } + + if entry.StartedAt() != started { + t.Fatalf("StartedAt mismatch") + } + if entry.CompletedAt() != completed { + t.Fatalf("CompletedAt mismatch") + } + if entry.Subnet() != "10.0.0.0/24" { + t.Fatalf("Subnet mismatch") + } + if entry.ServerCount() != 3 { + t.Fatalf("ServerCount mismatch") + } + if entry.ErrorCount() != 1 { + t.Fatalf("ErrorCount mismatch") + } + if entry.Duration() != time.Second { + t.Fatalf("Duration mismatch") + } + if entry.BlocklistLength() != 2 { + t.Fatalf("BlocklistLength mismatch") + } + if entry.Status() != "success" { + t.Fatalf("Status mismatch") + } +} + +func TestNewServiceDefaults(t *testing.T) { + service := NewService(nil, 0, "", nil) + if service.interval != 5*time.Minute { + t.Fatalf("expected default interval, got %v", service.interval) + } + if service.subnet != "auto" { + t.Fatalf("expected auto subnet, got %s", service.subnet) + } + if service.cfgProvider == nil { + t.Fatalf("expected default cfgProvider") + } + if service.scannerFactory == nil { + t.Fatalf("expected scannerFactory") + } +} + +func TestAppendHistoryTrim(t *testing.T) { + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.historyLimit = 1 + + service.appendHistory(historyEntry{status: "first"}) + service.appendHistory(historyEntry{status: "second"}) + + history := service.GetHistory(2) + if len(history) != 1 || history[0].status != "second" { + t.Fatalf("expected trimmed history with latest entry") + } +} + +func TestGetHistoryEmpty(t *testing.T) { + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + if history := service.GetHistory(5); history != nil { + t.Fatalf("expected nil history") + } +} + +func TestGetCachedResultEmpty(t *testing.T) { + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + result, updated := service.GetCachedResult() + if result == nil { + t.Fatalf("expected result") + } + if !updated.IsZero() { + t.Fatalf("expected zero updated timestamp") + } +} + +func TestGetCachedResultWithData(t *testing.T) { + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + now := time.Now() + service.cache.mu.Lock() + service.cache.result = &pkgdiscovery.DiscoveryResult{ + Servers: []pkgdiscovery.DiscoveredServer{{IP: "10.0.0.1"}}, + Errors: []string{}, + } + service.cache.updated = now + service.cache.mu.Unlock() + + result, updated := service.GetCachedResult() + if result == nil || len(result.Servers) != 1 { + t.Fatalf("expected cached result") + } + if !updated.Equal(now) { + t.Fatalf("expected updated timestamp") + } +} + +func TestIsScanning(t *testing.T) { + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.mu.Lock() + service.isScanning = true + service.mu.Unlock() + + if !service.IsScanning() { + t.Fatalf("expected scanning to be true") + } +} + +func TestSetInterval(t *testing.T) { + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.SetInterval(2 * time.Minute) + if service.interval != 2*time.Minute { + t.Fatalf("expected interval update") + } +} + +func TestGetStatus(t *testing.T) { + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.mu.Lock() + service.isScanning = true + service.lastScan = time.Unix(10, 0) + service.mu.Unlock() + + status := service.GetStatus() + if status["subnet"] != "auto" { + t.Fatalf("expected subnet in status") + } + if status["interval"] == "" { + t.Fatalf("expected interval in status") + } + if scanning, ok := status["is_scanning"].(bool); !ok || !scanning { + t.Fatalf("expected is_scanning true") + } +} + +func TestForceRefresh(t *testing.T) { + scanner := &countingScanner{ + result: &pkgdiscovery.DiscoveryResult{}, + calls: make(chan struct{}, 1), + } + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.ctx = context.Background() + service.scannerFactory = func(config.DiscoveryConfig) (discoveryScanner, error) { + return scanner, nil + } + + service.ForceRefresh() + + select { + case <-scanner.calls: + case <-time.After(2 * time.Second): + t.Fatalf("expected scan to run") + } +} + +func TestForceRefreshSkippedWhenScanning(t *testing.T) { + scanner := &countingScanner{ + result: &pkgdiscovery.DiscoveryResult{}, + calls: make(chan struct{}, 1), + } + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.ctx = context.Background() + service.scannerFactory = func(config.DiscoveryConfig) (discoveryScanner, error) { + return scanner, nil + } + service.mu.Lock() + service.isScanning = true + service.mu.Unlock() + + service.ForceRefresh() + + select { + case <-scanner.calls: + t.Fatalf("expected scan to be skipped") + case <-time.After(100 * time.Millisecond): + } +} + +func TestSetSubnetTriggersScan(t *testing.T) { + scanner := &countingScanner{ + result: &pkgdiscovery.DiscoveryResult{}, + calls: make(chan struct{}, 1), + } + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.ctx = context.Background() + service.scannerFactory = func(config.DiscoveryConfig) (discoveryScanner, error) { + return scanner, nil + } + + service.SetSubnet("10.0.0.0/24") + + select { + case <-scanner.calls: + case <-time.After(2 * time.Second): + t.Fatalf("expected scan to run") + } +} + +func TestSetSubnetWhileScanning(t *testing.T) { + scanner := &countingScanner{ + result: &pkgdiscovery.DiscoveryResult{}, + calls: make(chan struct{}, 1), + } + service := NewService(nil, time.Minute, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.ctx = context.Background() + service.scannerFactory = func(config.DiscoveryConfig) (discoveryScanner, error) { + return scanner, nil + } + service.mu.Lock() + service.isScanning = true + service.mu.Unlock() + + service.SetSubnet("10.0.0.0/24") + + select { + case <-scanner.calls: + t.Fatalf("expected scan to be skipped") + case <-time.After(100 * time.Millisecond): + } +} + +func TestScanLoopStopsOnStopChan(t *testing.T) { + scanner := &countingScanner{ + result: &pkgdiscovery.DiscoveryResult{}, + calls: make(chan struct{}, 2), + } + service := NewService(nil, 10*time.Millisecond, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.ctx = context.Background() + service.scannerFactory = func(config.DiscoveryConfig) (discoveryScanner, error) { + return scanner, nil + } + + done := make(chan struct{}) + go func() { + service.scanLoop() + close(done) + }() + + select { + case <-scanner.calls: + case <-time.After(2 * time.Second): + t.Fatalf("expected scan") + } + + service.Stop() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatalf("expected scanLoop to stop") + } +} + +func TestScanLoopStopsOnContextCancel(t *testing.T) { + scanner := &countingScanner{ + result: &pkgdiscovery.DiscoveryResult{}, + calls: make(chan struct{}, 2), + } + service := NewService(nil, 10*time.Millisecond, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + ctx, cancel := context.WithCancel(context.Background()) + service.ctx = ctx + service.scannerFactory = func(config.DiscoveryConfig) (discoveryScanner, error) { + return scanner, nil + } + + done := make(chan struct{}) + go func() { + service.scanLoop() + close(done) + }() + + select { + case <-scanner.calls: + case <-time.After(2 * time.Second): + t.Fatalf("expected scan") + } + + cancel() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatalf("expected scanLoop to stop") + } +} + +func TestStartAndStop(t *testing.T) { + scanner := &countingScanner{ + result: &pkgdiscovery.DiscoveryResult{}, + calls: make(chan struct{}, 2), + } + service := NewService(nil, 10*time.Millisecond, "auto", func() config.DiscoveryConfig { + return config.DefaultDiscoveryConfig() + }) + service.scannerFactory = func(config.DiscoveryConfig) (discoveryScanner, error) { + return scanner, nil + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + service.Start(ctx) + + select { + case <-scanner.calls: + case <-time.After(2 * time.Second): + t.Fatalf("expected scan to run") + } + + service.Stop() +} diff --git a/internal/dockeragent/agent_collect_test.go b/internal/dockeragent/agent_collect_test.go index c3e365d25..6fcb55439 100644 --- a/internal/dockeragent/agent_collect_test.go +++ b/internal/dockeragent/agent_collect_test.go @@ -52,6 +52,7 @@ func TestCollectContainer(t *testing.T) { Health: &containertypes.Health{Status: "healthy"}, } inspect.Config.Env = []string{"PASSWORD=secret", "PATH=/bin"} + inspect.Config.Image = "nginx@sha256:abc123" inspect.NetworkSettings.Networks["net1"].IPAddress = "10.0.0.2" inspect.Mounts = []containertypes.MountPoint{ {Type: "bind", Source: "/data", Destination: "/data", RW: true}, @@ -107,8 +108,11 @@ func TestCollectContainer(t *testing.T) { if container.Podman == nil || container.Podman.PodName != "mypod" { t.Fatalf("expected podman metadata") } - if container.UpdateStatus == nil || container.UpdateStatus.Error == "" { - t.Fatalf("expected update status for digest-pinned image") + if container.UpdateStatus == nil { + t.Fatal("expected update status for digest-pinned image, got nil") + } + if container.UpdateStatus.Error == "" { + t.Fatalf("expected update status for digest-pinned image, got empty error. Status: %+v", container.UpdateStatus) } if len(container.Networks) == 0 { t.Fatalf("expected networks to be populated") diff --git a/internal/dockeragent/container_update_test.go b/internal/dockeragent/container_update_test.go index e0dffbd04..83d69ef14 100644 --- a/internal/dockeragent/container_update_test.go +++ b/internal/dockeragent/container_update_test.go @@ -57,7 +57,7 @@ func TestUpdateContainer_Errors(t *testing.T) { logger: logger, } - result := agent.updateContainer(context.Background(), "container1") + result := agent.updateContainerWithProgress(context.Background(), "container1", nil) if result.Error == "" { t.Fatal("expected error for inspect failure") } @@ -76,7 +76,7 @@ func TestUpdateContainer_Errors(t *testing.T) { logger: logger, } - result := agent.updateContainer(context.Background(), "container1") + result := agent.updateContainerWithProgress(context.Background(), "container1", nil) if result.Error == "" { t.Fatal("expected error for pull failure") } @@ -98,7 +98,7 @@ func TestUpdateContainer_Errors(t *testing.T) { logger: logger, } - result := agent.updateContainer(context.Background(), "container1") + result := agent.updateContainerWithProgress(context.Background(), "container1", nil) if result.Error == "" { t.Fatal("expected error for stop failure") } @@ -128,7 +128,7 @@ func TestUpdateContainer_Errors(t *testing.T) { logger: logger, } - result := agent.updateContainer(context.Background(), "container1") + result := agent.updateContainerWithProgress(context.Background(), "container1", nil) if result.Error == "" { t.Fatal("expected error for rename failure") } @@ -166,7 +166,7 @@ func TestUpdateContainer_Errors(t *testing.T) { logger: logger, } - result := agent.updateContainer(context.Background(), "container1") + result := agent.updateContainerWithProgress(context.Background(), "container1", nil) if result.Error == "" { t.Fatal("expected error for create failure") } @@ -212,7 +212,7 @@ func TestUpdateContainer_Errors(t *testing.T) { logger: logger, } - result := agent.updateContainer(context.Background(), "container1") + result := agent.updateContainerWithProgress(context.Background(), "container1", nil) if result.Error == "" { t.Fatal("expected error for start failure") } @@ -276,7 +276,7 @@ func TestUpdateContainer_Success(t *testing.T) { logger: logger, } - result := agent.updateContainer(context.Background(), "container1") + result := agent.updateContainerWithProgress(context.Background(), "container1", nil) if !result.Success { t.Fatalf("expected success, got error %q", result.Error) } @@ -339,7 +339,7 @@ func TestUpdateContainer_CleanupError(t *testing.T) { logger: logger, } - result := agent.updateContainer(context.Background(), "container1") + result := agent.updateContainerWithProgress(context.Background(), "container1", nil) if !result.Success { t.Fatalf("expected success, got error %q", result.Error) } diff --git a/internal/dockeragent/registry_coverage_test.go b/internal/dockeragent/registry_coverage_test.go index a8ea69454..26d8bdecb 100644 --- a/internal/dockeragent/registry_coverage_test.go +++ b/internal/dockeragent/registry_coverage_test.go @@ -200,7 +200,7 @@ func TestRegistryChecker_FetchDigest_StatusErrors(t *testing.T) { }, } - _, err := checker.fetchDigest(context.Background(), "example.test", "repo", "tag", "", "", "") + _, _, err := checker.fetchDigest(context.Background(), "example.test", "repo", "tag", "", "", "") if err == nil || err.Error() != tt.wantErr { t.Fatalf("Expected error %q, got %v", tt.wantErr, err) } @@ -220,7 +220,7 @@ func TestRegistryChecker_FetchDigest_RequestError(t *testing.T) { }, } - _, err := checker.fetchDigest(context.Background(), "example.test", "repo", "tag", "", "", "") + _, _, err := checker.fetchDigest(context.Background(), "example.test", "repo", "tag", "", "", "") if err == nil || !strings.Contains(err.Error(), "request:") { t.Fatalf("Expected request error, got %v", err) } @@ -229,7 +229,7 @@ func TestRegistryChecker_FetchDigest_RequestError(t *testing.T) { func TestRegistryChecker_FetchDigest_RequestCreationError(t *testing.T) { checker := &RegistryChecker{httpClient: &http.Client{}} - _, err := checker.fetchDigest(context.Background(), "bad host", "repo", "tag", "", "", "") + _, _, err := checker.fetchDigest(context.Background(), "bad host", "repo", "tag", "", "", "") if err == nil || !strings.Contains(err.Error(), "create request:") { t.Fatalf("Expected create request error, got %v", err) } @@ -257,7 +257,7 @@ func TestRegistryChecker_FetchDigest_DigestHeaders(t *testing.T) { }, } - digest, err := checker.fetchDigest(context.Background(), "example.test", "repo", "tag", "", "", "") + digest, _, err := checker.fetchDigest(context.Background(), "example.test", "repo", "tag", "", "", "") if err != nil { t.Fatalf("Expected digest, got error %v", err) } @@ -281,7 +281,7 @@ func TestRegistryChecker_FetchDigest_DigestHeaders(t *testing.T) { }, } - digest, err := checker.fetchDigest(context.Background(), "example.test", "repo", "tag", "", "", "") + digest, _, err := checker.fetchDigest(context.Background(), "example.test", "repo", "tag", "", "", "") if err != nil { t.Fatalf("Expected digest, got error %v", err) } @@ -304,7 +304,7 @@ func TestRegistryChecker_FetchDigest_AuthPaths(t *testing.T) { }, } - _, err := checker.fetchDigest(context.Background(), "registry-1.docker.io", "library/nginx", "latest", "", "", "") + _, _, err := checker.fetchDigest(context.Background(), "registry-1.docker.io", "library/nginx", "latest", "", "", "") if err == nil || err.Error() != "auth: token request failed: 500" { t.Fatalf("Expected auth error, got %v", err) } @@ -331,7 +331,7 @@ func TestRegistryChecker_FetchDigest_AuthPaths(t *testing.T) { }, } - digest, err := checker.fetchDigest(context.Background(), "registry-1.docker.io", "library/nginx", "latest", "", "", "") + digest, _, err := checker.fetchDigest(context.Background(), "registry-1.docker.io", "library/nginx", "latest", "", "", "") if err != nil { t.Fatalf("Expected digest, got error %v", err) } diff --git a/internal/dockeragent/registry_test.go b/internal/dockeragent/registry_test.go index e38723e10..17c32354d 100644 --- a/internal/dockeragent/registry_test.go +++ b/internal/dockeragent/registry_test.go @@ -138,6 +138,24 @@ func TestRegistryChecker_DigestsDiffer(t *testing.T) { latest: "abc123", want: false, // Should match after normalization }, + { + name: "match second digest in list", + current: "sha256:abc123", + latest: "def456,abc123", + want: false, // Should match one of them + }, + { + name: "match first digest in list", + current: "sha256:def456", + latest: "def456,abc123", + want: false, // Should match one of them + }, + { + name: "no match in list", + current: "sha256:xyz789", + latest: "def456,abc123", + want: true, // No match found + }, } for _, tt := range tests {