Add Go branch-coverage tests for twelve pure backend helpers

New *_branchcov0718_test.go files raise coverage of previously-uncovered pure
functions across ten packages. Covered areas include securityutil SSRF and URL
validation, truenas path and telemetry parse helpers, storagehealth SMART and
physical-disk risk assessment, vmware inventory sort keys and error classifiers,
servicediscovery token filtering and readiness, telemetry evidence-from-history,
models ToFrontend converters and frontend NormalizeCollections normalizers,
actionplanner type predicates and canonical resource-id sort, config API-token
accessors, and licensing state accessors.

Test-only, with no runtime or subsystem-contract change. Verified in a clean
worktree at HEAD with go vet and package tests green, gofmt clean, and every
named target function moved from 0 percent to covered.
This commit is contained in:
rcourtman
2026-07-18 22:31:08 +01:00
parent ed82d6cd87
commit 278673aa0f
12 changed files with 5167 additions and 0 deletions
@@ -0,0 +1,385 @@
package actionplanner
import (
"encoding/json"
"errors"
"strings"
"testing"
"time"
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
func TestIsIntegerBranchCoverage0718(t *testing.T) {
steps := []struct {
name string
value any
want bool
}{
{"int", int(42), true},
{"int8", int8(42), true},
{"int16", int16(42), true},
{"int32", int32(42), true},
{"int64", int64(42), true},
{"uint", uint(42), true},
{"uint8", uint8(42), true},
{"uint16", uint16(42), true},
{"uint32", uint32(42), true},
{"uint64", uint64(42), true},
{"float32_whole", float32(42), true},
{"float32_fractional", float32(42.5), false},
{"float64_whole", float64(42), true},
{"float64_negative_whole", float64(-7), true},
{"float64_fractional", float64(42.5), false},
{"json_number_whole", json.Number("42"), true},
{"json_number_negative_whole", json.Number("-7"), true},
{"json_number_fractional", json.Number("42.5"), false},
{"json_number_overflow", json.Number("9999999999999999999999"), false},
{"string", "42", false},
{"bool_true", true, false},
{"nil", nil, false},
{"map_string_any", map[string]any{"a": 1}, false},
{"slice_int", []int{1}, false},
{"struct", struct{}{}, false},
}
for _, step := range steps {
t.Run(step.name, func(t *testing.T) {
got := isInteger(step.value)
if got != step.want {
t.Fatalf("isInteger(%T %v) = %v, want %v", step.value, step.value, got, step.want)
}
})
}
}
func TestIsNumberBranchCoverage0718(t *testing.T) {
steps := []struct {
name string
value any
want bool
}{
{"int", int(42), true},
{"int8", int8(42), true},
{"int16", int16(42), true},
{"int32", int32(42), true},
{"int64", int64(42), true},
{"uint", uint(42), true},
{"uint8", uint8(42), true},
{"uint16", uint16(42), true},
{"uint32", uint32(42), true},
{"uint64", uint64(42), true},
{"float32", float32(42.5), true},
{"float64", float64(42.5), true},
{"json_number", json.Number("42.5"), true},
{"string", "42", false},
{"bool_true", true, false},
{"nil", nil, false},
{"map_string_any", map[string]any{"a": 1}, false},
{"slice_int", []int{1}, false},
{"struct", struct{}{}, false},
}
for _, step := range steps {
t.Run(step.name, func(t *testing.T) {
got := isNumber(step.value)
if got != step.want {
t.Fatalf("isNumber(%T %v) = %v, want %v", step.value, step.value, got, step.want)
}
})
}
}
func TestIsMapBranchCoverage0718(t *testing.T) {
steps := []struct {
name string
value any
want bool
}{
{"map_string_any", map[string]any{"a": 1}, true},
{"empty_map_string_any", map[string]any{}, true},
{"map_int_string", map[int]string{1: "a"}, true},
{"map_string_string", map[string]string{"a": "b"}, true},
{"nil", nil, false},
{"slice_int", []int{1}, false},
{"array", [3]int{1, 2, 3}, false},
{"string", "foo", false},
{"int", 42, false},
{"bool_true", true, false},
{"struct", struct{}{}, false},
}
for _, step := range steps {
t.Run(step.name, func(t *testing.T) {
got := isMap(step.value)
if got != step.want {
t.Fatalf("isMap(%T %v) = %v, want %v", step.value, step.value, got, step.want)
}
})
}
}
func TestIsSliceBranchCoverage0718(t *testing.T) {
steps := []struct {
name string
value any
want bool
}{
{"slice_int", []int{1, 2}, true},
{"slice_string", []string{"a"}, true},
{"empty_slice", []int{}, true},
{"nil_slice", []int(nil), true},
{"array", [3]int{1, 2, 3}, true},
{"nil", nil, false},
{"map_string_any", map[string]any{"a": 1}, false},
{"string", "foo", false},
{"int", 42, false},
{"bool_true", true, false},
{"struct", struct{}{}, false},
}
for _, step := range steps {
t.Run(step.name, func(t *testing.T) {
got := isSlice(step.value)
if got != step.want {
t.Fatalf("isSlice(%T %v) = %v, want %v", step.value, step.value, got, step.want)
}
})
}
}
func TestSortedCanonicalResourceIDsBranchCoverage0718(t *testing.T) {
t.Run("nil_input_returns_nonnil_empty", func(t *testing.T) {
got := sortedCanonicalResourceIDs(nil)
if got == nil {
t.Fatalf("sortedCanonicalResourceIDs(nil) = nil, want non-nil empty slice")
}
if len(got) != 0 {
t.Fatalf("sortedCanonicalResourceIDs(nil) = %#v, want empty", got)
}
})
t.Run("empty_input_returns_nonnil_empty", func(t *testing.T) {
got := sortedCanonicalResourceIDs([]string{})
if got == nil {
t.Fatalf("sortedCanonicalResourceIDs([]) = nil, want non-nil empty slice")
}
if len(got) != 0 {
t.Fatalf("sortedCanonicalResourceIDs([]) = %#v, want empty", got)
}
})
t.Run("all_blank_input_returns_nonnil_empty", func(t *testing.T) {
got := sortedCanonicalResourceIDs([]string{"", " ", "\t"})
if got == nil || len(got) != 0 {
t.Fatalf("sortedCanonicalResourceIDs(%#v) = %#v, want non-nil empty slice", []string{"", " ", "\t"}, got)
}
})
t.Run("sorts_dedupes_and_trims", func(t *testing.T) {
input := []string{" vm:3 ", "vm:1", "vm:2", "vm:1", "", " ", "vm:3"}
got := sortedCanonicalResourceIDs(input)
want := []string{"vm:1", "vm:2", "vm:3"}
if len(got) != len(want) {
t.Fatalf("len = %d, want %d (got=%#v)", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("idx %d = %q, want %q (got=%#v)", i, got[i], want[i], got)
}
}
})
t.Run("already_sorted_unchanged", func(t *testing.T) {
got := sortedCanonicalResourceIDs([]string{"vm:1", "vm:2", "vm:3"})
want := []string{"vm:1", "vm:2", "vm:3"}
if len(got) != len(want) {
t.Fatalf("len = %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("idx %d = %q, want %q", i, got[i], want[i])
}
}
})
t.Run("reverse_order_sorted", func(t *testing.T) {
got := sortedCanonicalResourceIDs([]string{"vm:3", "vm:2", "vm:1"})
want := []string{"vm:1", "vm:2", "vm:3"}
if len(got) != len(want) {
t.Fatalf("len = %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("idx %d = %q, want %q", i, got[i], want[i])
}
}
})
}
func TestPlanWithRequirementBranchCoverage0718(t *testing.T) {
now := time.Date(2026, 7, 18, 12, 0, 0, 0, time.UTC)
resource := unified.Resource{
ID: "vm:42",
Type: unified.ResourceTypeVM,
Name: "web-42",
Status: unified.StatusOnline,
Capabilities: []unified.ResourceCapability{{
Name: "restart",
Type: unified.CapabilityTypeCommon,
Description: "Restart the VM",
MinimumApprovalLevel: unified.ApprovalAdmin,
}},
}
actor := unified.ActionActor{
SubjectID: "agent:oncall-helper",
Kind: unified.ActionActorService,
CredentialID: "service:test",
OrgID: "default",
}
t.Run("happy_path_floor_requirement_deterministic", func(t *testing.T) {
req := unified.ActionRequest{
RequestID: "agent-run-req-floor",
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "recover",
Actor: actor,
}
planner := Planner{Now: func() time.Time { return now }}
plan, err := planner.PlanWithRequirement(req, resource, unified.ApprovalRequirement{})
if err != nil {
t.Fatalf("PlanWithRequirement() error = %v", err)
}
if plan.ActionID == "" {
t.Fatal("ActionID is empty")
}
if plan.PlanHash == "" {
t.Fatal("PlanHash is empty")
}
if !plan.Allowed {
t.Fatal("Allowed = false, want true")
}
if !plan.RequiresApproval {
t.Fatal("RequiresApproval = false, want true for ApprovalAdmin floor")
}
if plan.ApprovalPolicy != unified.ApprovalAdmin {
t.Fatalf("ApprovalPolicy = %q, want %q", plan.ApprovalPolicy, unified.ApprovalAdmin)
}
if plan.Preflight == nil {
t.Fatal("Preflight is nil")
}
if plan.Preflight.Target != "vm:42" {
t.Fatalf("Preflight.Target = %q, want vm:42", plan.Preflight.Target)
}
if !plan.PlannedAt.Equal(now) {
t.Fatalf("PlannedAt = %s, want %s", plan.PlannedAt, now)
}
if !plan.ExpiresAt.Equal(now.Add(DefaultPlanTTL)) {
t.Fatalf("ExpiresAt = %s, want %s", plan.ExpiresAt, now.Add(DefaultPlanTTL))
}
again, err := planner.PlanWithRequirement(req, resource, unified.ApprovalRequirement{})
if err != nil {
t.Fatalf("second call error = %v", err)
}
if again.ActionID != plan.ActionID || again.PlanHash != plan.PlanHash {
t.Fatalf("plan not deterministic: first=(%q,%q) second=(%q,%q)",
plan.ActionID, plan.PlanHash, again.ActionID, again.PlanHash)
}
})
t.Run("happy_path_explicit_requirement_version_used", func(t *testing.T) {
req := unified.ActionRequest{
RequestID: "agent-run-req-explicit",
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "recover",
Actor: actor,
}
requested := unified.ApprovalRequirement{
Version: 1,
Floor: unified.ApprovalAdmin,
}
planner := Planner{Now: func() time.Time { return now }}
plan, err := planner.PlanWithRequirement(req, resource, requested)
if err != nil {
t.Fatalf("PlanWithRequirement() error = %v", err)
}
if plan.ApprovalRequirement.Version != requested.Version {
t.Fatalf("plan requirement Version = %d, want %d", plan.ApprovalRequirement.Version, requested.Version)
}
if plan.ActionID == "" || plan.PlanHash == "" {
t.Fatalf("ActionID/PlanHash not populated: %#v", plan)
}
})
t.Run("missing_request_id_returns_validation_error", func(t *testing.T) {
req := unified.ActionRequest{
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "recover",
Actor: actor,
}
planner := Planner{Now: func() time.Time { return now }}
_, err := planner.PlanWithRequirement(req, resource, unified.ApprovalRequirement{})
validationErr, ok := AsValidationError(err)
if !ok {
t.Fatalf("PlanWithRequirement() error = %v, want validation error", err)
}
if validationErr.Field != "requestId" {
t.Fatalf("validation field = %q, want requestId", validationErr.Field)
}
})
t.Run("resource_id_mismatch_returns_validation_error", func(t *testing.T) {
req := unified.ActionRequest{
RequestID: "agent-run-req-mismatch",
ResourceID: "vm:99",
CapabilityName: "restart",
Reason: "recover",
Actor: actor,
}
planner := Planner{Now: func() time.Time { return now }}
_, err := planner.PlanWithRequirement(req, resource, unified.ApprovalRequirement{})
validationErr, ok := AsValidationError(err)
if !ok {
t.Fatalf("error = %v, want validation error", err)
}
if validationErr.Field != "resourceId" {
t.Fatalf("validation field = %q, want resourceId", validationErr.Field)
}
if !strings.Contains(validationErr.Message, "does not match") {
t.Fatalf("validation message = %q, want substring 'does not match'", validationErr.Message)
}
})
t.Run("missing_capability_returns_capability_not_found", func(t *testing.T) {
req := unified.ActionRequest{
RequestID: "agent-run-req-no-cap",
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "recover",
Actor: actor,
}
emptyResource := unified.Resource{ID: "vm:42", Type: unified.ResourceTypeVM}
planner := Planner{Now: func() time.Time { return now }}
_, err := planner.PlanWithRequirement(req, emptyResource, unified.ApprovalRequirement{})
if !errors.Is(err, ErrCapabilityNotFound) {
t.Fatalf("error = %v, want ErrCapabilityNotFound", err)
}
})
t.Run("resource_without_canonical_id_returns_validation_error", func(t *testing.T) {
req := unified.ActionRequest{
RequestID: "agent-run-req-no-canonical",
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "recover",
Actor: actor,
}
blankResource := unified.Resource{Type: unified.ResourceTypeVM}
planner := Planner{Now: func() time.Time { return now }}
_, err := planner.PlanWithRequirement(req, blankResource, unified.ApprovalRequirement{})
validationErr, ok := AsValidationError(err)
if !ok {
t.Fatalf("error = %v, want validation error", err)
}
if validationErr.Field != "resourceId" {
t.Fatalf("validation field = %q, want resourceId", validationErr.Field)
}
if !strings.Contains(validationErr.Message, "no canonical id") {
t.Fatalf("validation message = %q, want substring 'no canonical id'", validationErr.Message)
}
})
}
@@ -0,0 +1,188 @@
package config
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
// TestActiveAPITokenHashes_BranchCov0718 exercises every branch of
// (*Config).ActiveAPITokenHashes: empty config, single token, multiple tokens
// preserving order, records with empty hashes being filtered, all-empty result,
// and expired tokens still being returned (the method filters only on Hash != "").
func TestActiveAPITokenHashes_BranchCov0718(t *testing.T) {
pastExpiry := time.Now().UTC().Add(-time.Hour)
tests := []struct {
name string
config *Config
want []string
}{
{
name: "nil token slice returns non-nil empty result",
config: &Config{},
want: []string{},
},
{
name: "single token hash returned",
config: &Config{
APITokens: []APITokenRecord{{ID: "t1", Hash: "hash-1"}},
},
want: []string{"hash-1"},
},
{
name: "multiple tokens preserve insertion order",
config: &Config{
APITokens: []APITokenRecord{
{ID: "t1", Hash: "hash-1"},
{ID: "t2", Hash: "hash-2"},
{ID: "t3", Hash: "hash-3"},
},
},
want: []string{"hash-1", "hash-2", "hash-3"},
},
{
name: "empty-hash records are filtered out while non-empty are kept in order",
config: &Config{
APITokens: []APITokenRecord{
{ID: "t1", Hash: ""},
{ID: "t2", Hash: "hash-2"},
{ID: "t3", Hash: ""},
{ID: "t4", Hash: "hash-4"},
},
},
want: []string{"hash-2", "hash-4"},
},
{
name: "all empty hashes yields empty non-nil result",
config: &Config{
APITokens: []APITokenRecord{
{ID: "t1", Hash: ""},
{ID: "t2", Hash: ""},
},
},
want: []string{},
},
{
name: "expired tokens are still returned (method does not filter on expiry)",
config: &Config{
APITokens: []APITokenRecord{
{ID: "t1", Hash: "hash-1", ExpiresAt: &pastExpiry},
{ID: "t2", Hash: "hash-2"},
},
},
want: []string{"hash-1", "hash-2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.config.ActiveAPITokenHashes()
assert.NotNil(t, got, "result should be non-nil even when empty (make-allocated)")
assert.Equal(t, tt.want, got)
})
}
}
// TestHasAPITokenHash_BranchCov0718 covers (*Config).HasAPITokenHash across
// present, absent, empty-query, and the empty-query-matches-empty-hash-record
// edge case (a direct == comparison with no empty-string guard).
func TestHasAPITokenHash_BranchCov0718(t *testing.T) {
tests := []struct {
name string
config *Config
hash string
want bool
}{
{
name: "empty config reports absent",
config: &Config{},
hash: "hash-1",
want: false,
},
{
name: "matching hash at first position",
config: &Config{
APITokens: []APITokenRecord{{Hash: "hash-1"}, {Hash: "hash-2"}},
},
hash: "hash-1",
want: true,
},
{
name: "matching hash at non-first position",
config: &Config{
APITokens: []APITokenRecord{{Hash: "hash-1"}, {Hash: "hash-2"}},
},
hash: "hash-2",
want: true,
},
{
name: "non-matching hash reports absent",
config: &Config{
APITokens: []APITokenRecord{{Hash: "hash-1"}, {Hash: "hash-2"}},
},
hash: "not-present",
want: false,
},
{
name: "empty query hash against populated config reports absent",
config: &Config{
APITokens: []APITokenRecord{{Hash: "hash-1"}, {Hash: "hash-2"}},
},
hash: "",
want: false,
},
{
name: "empty query hash matches a stored empty-hash record",
config: &Config{
APITokens: []APITokenRecord{{Hash: ""}, {Hash: "hash-2"}},
},
hash: "",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.config.HasAPITokenHash(tt.hash))
})
}
}
// TestNewAvailabilityTarget_BranchCov0718 asserts every default field set by
// NewAvailabilityTarget, validates the generated ID is a parseable UUID, and
// verifies successive calls produce distinct IDs.
func TestNewAvailabilityTarget_BranchCov0718(t *testing.T) {
t.Run("returns canonical defaults", func(t *testing.T) {
target := NewAvailabilityTarget()
assert.NotEmpty(t, target.ID, "ID should be generated")
parsed, err := uuid.Parse(target.ID)
assert.NoError(t, err, "ID should be a parseable UUID")
assert.Equal(t, target.ID, parsed.String(), "ID should be in canonical UUID form")
assert.Equal(t, AvailabilityTargetService, target.TargetKind)
assert.Equal(t, AvailabilityProbeICMP, target.Protocol)
assert.True(t, target.Enabled, "Enabled should default to true")
assert.Equal(t, DefaultAvailabilityPollIntervalSecs, target.PollIntervalSecs)
assert.Equal(t, DefaultAvailabilityTimeoutMillis, target.TimeoutMillis)
assert.Equal(t, DefaultAvailabilityFailureThreshold, target.FailureThreshold)
// Fields left at their zero values by the constructor.
assert.Empty(t, target.Name)
assert.Empty(t, target.Address)
assert.Equal(t, 0, target.Port)
assert.Empty(t, target.Path)
assert.Empty(t, target.LinkedResourceID)
})
t.Run("each call generates a distinct UUID", func(t *testing.T) {
a := NewAvailabilityTarget()
b := NewAvailabilityTarget()
assert.NotEqual(t, a.ID, b.ID, "successive calls should produce distinct IDs")
assert.NotEmpty(t, a.ID)
assert.NotEmpty(t, b.ID)
})
}
@@ -0,0 +1,463 @@
package models
import (
"testing"
"time"
)
// This file exercises previously-uncovered branches of the ToFrontend converters
// in converters.go. It focuses on nested-collection mapping and conditional arms
// (nil vs populated, zero vs non-zero timestamps), not trivial field copies.
func TestDockerSecretToFrontend_BranchCov0718(t *testing.T) {
now := time.Now()
updatedAt := now.Add(-time.Hour)
t.Run("empty normalizes labels and omits timestamps", func(t *testing.T) {
s := DockerSecret{ID: "sec-1", Name: "empty"}
f := s.ToFrontend()
if f.ID != "sec-1" || f.Name != "empty" {
t.Fatalf("identity fields not mapped: %#v", f)
}
if f.CreatedAt != nil {
t.Fatalf("CreatedAt = %#v, want nil for zero time", f.CreatedAt)
}
if f.UpdatedAt != nil {
t.Fatalf("UpdatedAt = %#v, want nil when source pointer is nil", f.UpdatedAt)
}
if f.Labels == nil {
t.Fatal("Labels should normalize to a non-nil empty map")
}
if len(f.Labels) != 0 {
t.Fatalf("Labels = %#v, want empty map", f.Labels)
}
})
t.Run("populated maps all fields and clones labels", func(t *testing.T) {
s := DockerSecret{
ID: "sec-2",
Name: "tls-cert",
DriverName: "file",
TemplatingDriver: "golang",
Labels: map[string]string{"env": "prod", "team": "infra"},
CreatedAt: now,
UpdatedAt: &updatedAt,
}
f := s.ToFrontend()
if f.DriverName != "file" || f.TemplatingDriver != "golang" {
t.Fatalf("driver fields not mapped: %#v", f)
}
if f.CreatedAt == nil || *f.CreatedAt != now.Unix()*1000 {
t.Fatalf("CreatedAt = %#v, want %d", f.CreatedAt, now.Unix()*1000)
}
if f.UpdatedAt == nil || *f.UpdatedAt != updatedAt.Unix()*1000 {
t.Fatalf("UpdatedAt = %#v, want %d", f.UpdatedAt, updatedAt.Unix()*1000)
}
if len(f.Labels) != 2 || f.Labels["env"] != "prod" || f.Labels["team"] != "infra" {
t.Fatalf("Labels not copied: %#v", f.Labels)
}
// Labels must be a copy, not an alias of the source map.
f.Labels["env"] = "MUTATED"
if s.Labels["env"] != "prod" {
t.Fatal("frontend Labels must not alias the source map")
}
})
t.Run("zero updatedAt pointer is dropped", func(t *testing.T) {
zero := time.Time{}
s := DockerSecret{ID: "sec-3", UpdatedAt: &zero}
f := s.ToFrontend()
if f.UpdatedAt != nil {
t.Fatalf("UpdatedAt = %#v, want nil for zero time", f.UpdatedAt)
}
})
}
func TestDockerConfigToFrontend_BranchCov0718(t *testing.T) {
now := time.Now()
updatedAt := now.Add(-30 * time.Minute)
t.Run("empty normalizes labels and omits timestamps", func(t *testing.T) {
c := DockerConfig{ID: "cfg-1", Name: "empty"}
f := c.ToFrontend()
if f.ID != "cfg-1" || f.Name != "empty" {
t.Fatalf("identity fields not mapped: %#v", f)
}
if f.CreatedAt != nil || f.UpdatedAt != nil {
t.Fatalf("timestamps should be nil for empty config: %#v", f)
}
if f.Labels == nil || len(f.Labels) != 0 {
t.Fatalf("Labels should normalize to non-nil empty map: %#v", f.Labels)
}
})
t.Run("populated maps all fields and clones labels", func(t *testing.T) {
c := DockerConfig{
ID: "cfg-2",
Name: "nginx-conf",
TemplatingDriver: "golang",
Labels: map[string]string{"tier": "web", "owner": "sre"},
CreatedAt: now,
UpdatedAt: &updatedAt,
}
f := c.ToFrontend()
if f.TemplatingDriver != "golang" {
t.Fatalf("TemplatingDriver = %q, want golang", f.TemplatingDriver)
}
if f.CreatedAt == nil || *f.CreatedAt != now.Unix()*1000 {
t.Fatalf("CreatedAt = %#v, want %d", f.CreatedAt, now.Unix()*1000)
}
if f.UpdatedAt == nil || *f.UpdatedAt != updatedAt.Unix()*1000 {
t.Fatalf("UpdatedAt = %#v, want %d", f.UpdatedAt, updatedAt.Unix()*1000)
}
if len(f.Labels) != 2 || f.Labels["tier"] != "web" {
t.Fatalf("Labels not copied: %#v", f.Labels)
}
f.Labels["tier"] = "MUTATED"
if c.Labels["tier"] != "web" {
t.Fatal("frontend Labels must not alias the source map")
}
})
}
func TestNodeToFrontend_TemperatureBranchCov0718(t *testing.T) {
temp := &Temperature{CPUPackage: 72.5, Available: true}
node := Node{
ID: "node-temp",
Name: "pve-temp",
LastSeen: time.Now(),
Temperature: temp,
}
f := node.ToFrontend()
if f.Temperature == nil {
t.Fatal("Temperature should be mapped when Temperature.Available is true")
}
if f.Temperature != temp {
t.Fatalf("Temperature pointer = %p, want %p (should pass through)", f.Temperature, temp)
}
if f.Temperature.CPUPackage != 72.5 {
t.Fatalf("Temperature.CPUPackage = %v, want 72.5", f.Temperature.CPUPackage)
}
// Available=false arm: temperature must NOT be mapped.
nodeUnavailable := Node{
Name: "pve-no-temp",
LastSeen: time.Now(),
Temperature: &Temperature{CPUPackage: 50, Available: false},
}
if fu := nodeUnavailable.ToFrontend(); fu.Temperature != nil {
t.Fatalf("Temperature should be nil when Available=false, got %#v", fu.Temperature)
}
// nil arm: temperature must NOT be mapped.
nodeNil := Node{Name: "pve-nil-temp", LastSeen: time.Now()}
if fn := nodeNil.ToFrontend(); fn.Temperature != nil {
t.Fatalf("Temperature should be nil when source is nil, got %#v", fn.Temperature)
}
}
func TestVMToFrontend_CollectionsBranchCov0718(t *testing.T) {
vm := VM{
ID: "vm-coll",
Name: "vm-a",
LastSeen: time.Now(),
AgentVersion: "agent-9.9",
Disks: []Disk{{Total: 100, Used: 50}, {Total: 200, Used: 80}},
NetworkInterfaces: []GuestNetworkInterface{
{Name: "net0", Addresses: []string{"10.0.0.5"}},
},
}
f := vm.ToFrontend()
if f.AgentVersion != "agent-9.9" {
t.Fatalf("AgentVersion = %q, want agent-9.9", f.AgentVersion)
}
if len(f.Disks) != 2 || f.Disks[1].Total != 200 {
t.Fatalf("Disks array not mapped: %#v", f.Disks)
}
if len(f.NetworkInterfaces) != 1 || f.NetworkInterfaces[0].Name != "net0" {
t.Fatalf("NetworkInterfaces not mapped: %#v", f.NetworkInterfaces)
}
// NetworkInterfaces are copied element-wise; mutating a copy element must not affect source.
f.NetworkInterfaces[0].Name = "mutated"
if vm.NetworkInterfaces[0].Name != "net0" {
t.Fatal("frontend NetworkInterfaces must not alias the source slice elements")
}
}
func TestContainerToFrontend_CollectionsBranchCov0718(t *testing.T) {
ct := Container{
ID: "ct-coll",
Name: "ct-a",
LastSeen: time.Now(),
OSName: "Alpine",
Disks: []Disk{{Total: 300, Used: 100}},
NetworkInterfaces: []GuestNetworkInterface{
{Name: "eth0", Addresses: []string{"172.16.0.2"}},
},
}
f := ct.ToFrontend()
if f.OSName != "Alpine" {
t.Fatalf("OSName = %q, want Alpine", f.OSName)
}
if len(f.Disks) != 1 || f.Disks[0].Total != 300 {
t.Fatalf("Disks array not mapped: %#v", f.Disks)
}
if len(f.NetworkInterfaces) != 1 || f.NetworkInterfaces[0].Name != "eth0" {
t.Fatalf("NetworkInterfaces not mapped: %#v", f.NetworkInterfaces)
}
}
func TestDockerHostToFrontend_SecretsConfigsSecurity_BranchCov0718(t *testing.T) {
now := time.Now()
updated := now.Add(-time.Hour)
host := DockerHost{
ID: "dh-swarm",
Hostname: "swarm-node",
LastSeen: now,
Secrets: []DockerSecret{
{ID: "sec-1", Name: "db-password", Labels: map[string]string{"env": "prod"}, CreatedAt: now, UpdatedAt: &updated},
},
Configs: []DockerConfig{
{ID: "cfg-1", Name: "nginx-conf", Labels: map[string]string{"tier": "web"}, CreatedAt: now, UpdatedAt: &updated},
},
Security: &DockerHostSecurity{
AuthorizationPlugins: []string{"authz", "opa"},
MutatingCommandsBlocked: true,
MutatingCommandsBlockedReason: "policy",
},
}
f := host.ToFrontend()
// Secrets nested-conversion arm.
if len(f.Secrets) != 1 || f.Secrets[0].ID != "sec-1" {
t.Fatalf("Secrets not mapped via nested ToFrontend: %#v", f.Secrets)
}
if f.Secrets[0].CreatedAt == nil || *f.Secrets[0].CreatedAt != now.Unix()*1000 {
t.Fatalf("nested Secret CreatedAt not converted: %#v", f.Secrets[0].CreatedAt)
}
if f.Secrets[0].Labels["env"] != "prod" {
t.Fatalf("nested Secret labels not mapped: %#v", f.Secrets[0].Labels)
}
// Configs nested-conversion arm.
if len(f.Configs) != 1 || f.Configs[0].ID != "cfg-1" {
t.Fatalf("Configs not mapped via nested ToFrontend: %#v", f.Configs)
}
if f.Configs[0].UpdatedAt == nil || *f.Configs[0].UpdatedAt != updated.Unix()*1000 {
t.Fatalf("nested Config UpdatedAt not converted: %#v", f.Configs[0].UpdatedAt)
}
if f.Configs[0].Labels["tier"] != "web" {
t.Fatalf("nested Config labels not mapped: %#v", f.Configs[0].Labels)
}
// Security arm + AuthorizationPlugins clone independence.
if f.Security == nil || !f.Security.MutatingCommandsBlocked || f.Security.MutatingCommandsBlockedReason != "policy" {
t.Fatalf("Security not mapped: %#v", f.Security)
}
if len(f.Security.AuthorizationPlugins) != 2 || f.Security.AuthorizationPlugins[0] != "authz" {
t.Fatalf("AuthorizationPlugins not mapped: %#v", f.Security.AuthorizationPlugins)
}
f.Security.AuthorizationPlugins[0] = "MUTATED"
if host.Security.AuthorizationPlugins[0] != "authz" {
t.Fatal("frontend AuthorizationPlugins must not alias the source slice")
}
}
func TestHostToFrontend_DiskIOAndCloneBranches_BranchCov0718(t *testing.T) {
lastChecked := time.Now().Add(-time.Hour)
lastAttempt := time.Now().Add(-30 * time.Minute)
lastSuccess := time.Now().Add(-15 * time.Minute)
host := Host{
ID: "host-full",
Hostname: "server-full",
LastSeen: time.Now(),
DiskIO: []DiskIO{{Device: "sda", ReadBytes: 1000, WriteBytes: 2000}, {Device: "sdb", ReadBytes: 3000}},
AppliedConfig: &AgentConfigFingerprint{Version: "v1", Hash: "abc123"},
AgentUpdate: &AgentUpdateStatus{
State: "available",
AvailableVersion: "2.0.0",
LastCheckedAt: &lastChecked,
LastAttemptAt: &lastAttempt,
LastSuccessAt: &lastSuccess,
},
AgentModules: []AgentModuleStatus{{Name: "docker", Enabled: true, State: "ok"}},
}
f := host.ToFrontend()
// DiskIO copy arm.
if len(f.DiskIO) != 2 || f.DiskIO[0].Device != "sda" || f.DiskIO[1].ReadBytes != 3000 {
t.Fatalf("DiskIO not mapped: %#v", f.DiskIO)
}
// AppliedConfig deep clone (non-nil arm of cloneAgentConfigFingerprint).
if f.AppliedConfig == nil || f.AppliedConfig.Hash != "abc123" || f.AppliedConfig.Version != "v1" {
t.Fatalf("AppliedConfig not cloned: %#v", f.AppliedConfig)
}
f.AppliedConfig.Hash = "MUTATED"
if host.AppliedConfig.Hash != "abc123" {
t.Fatal("frontend AppliedConfig must not alias the source struct")
}
// AgentUpdate deep clone with nested timestamp pointers.
if f.AgentUpdate == nil || f.AgentUpdate.AvailableVersion != "2.0.0" {
t.Fatalf("AgentUpdate not cloned: %#v", f.AgentUpdate)
}
if f.AgentUpdate.LastCheckedAt == nil || !f.AgentUpdate.LastCheckedAt.Equal(lastChecked) {
t.Fatalf("AgentUpdate.LastCheckedAt not deep-cloned: %#v", f.AgentUpdate.LastCheckedAt)
}
if f.AgentUpdate.LastAttemptAt == nil || !f.AgentUpdate.LastAttemptAt.Equal(lastAttempt) {
t.Fatalf("AgentUpdate.LastAttemptAt not deep-cloned: %#v", f.AgentUpdate.LastAttemptAt)
}
if f.AgentUpdate.LastSuccessAt == nil || !f.AgentUpdate.LastSuccessAt.Equal(lastSuccess) {
t.Fatalf("AgentUpdate.LastSuccessAt not deep-cloned: %#v", f.AgentUpdate.LastSuccessAt)
}
// The cloned timestamp pointers must not alias the source pointers.
*f.AgentUpdate.LastCheckedAt = time.Time{}
if !host.AgentUpdate.LastCheckedAt.Equal(lastChecked) {
t.Fatal("frontend AgentUpdate.LastCheckedAt pointer must not alias the source pointer")
}
// AgentModules clone (non-empty arm of cloneAgentModuleStatuses).
if len(f.AgentModules) != 1 || f.AgentModules[0].Name != "docker" {
t.Fatalf("AgentModules not cloned: %#v", f.AgentModules)
}
f.AgentModules[0].Name = "MUTATED"
if host.AgentModules[0].Name != "docker" {
t.Fatal("frontend AgentModules must not alias the source slice elements")
}
}
func TestDockerContainerToFrontend_FinishedLabelsUpdate_BranchCov0718(t *testing.T) {
now := time.Now()
finished := now.Add(-5 * time.Minute)
lastChecked := now.Add(-1 * time.Minute)
c := DockerContainer{
ID: "ct-branches",
Name: "web",
CreatedAt: now,
FinishedAt: &finished,
Labels: map[string]string{
"com.docker.compose.service": "api",
"app": "shop",
},
UpdateStatus: &DockerContainerUpdateStatus{
UpdateAvailable: true,
CurrentDigest: "sha256:aaa",
LatestDigest: "sha256:bbb",
LastChecked: lastChecked,
Error: "rate limited",
},
}
f := c.ToFrontend()
// FinishedAt arm.
if f.FinishedAt == nil || *f.FinishedAt != finished.Unix()*1000 {
t.Fatalf("FinishedAt = %#v, want %d", f.FinishedAt, finished.Unix()*1000)
}
// Labels copy arm + independence.
if len(f.Labels) != 2 || f.Labels["app"] != "shop" {
t.Fatalf("Labels not copied: %#v", f.Labels)
}
f.Labels["app"] = "MUTATED"
if c.Labels["app"] != "shop" {
t.Fatal("frontend Labels must not alias the source map")
}
// UpdateStatus arm.
if f.UpdateStatus == nil || !f.UpdateStatus.UpdateAvailable || f.UpdateStatus.Error != "rate limited" {
t.Fatalf("UpdateStatus not mapped: %#v", f.UpdateStatus)
}
if f.UpdateStatus.CurrentDigest != "sha256:aaa" || f.UpdateStatus.LatestDigest != "sha256:bbb" {
t.Fatalf("UpdateStatus digests not mapped: %#v", f.UpdateStatus)
}
if f.UpdateStatus.LastChecked != lastChecked.Unix()*1000 {
t.Fatalf("UpdateStatus.LastChecked = %d, want %d", f.UpdateStatus.LastChecked, lastChecked.Unix()*1000)
}
}
func TestDockerServiceToFrontend_UpdateStatusBranchCov0718(t *testing.T) {
completed := time.Now().Add(-time.Hour)
svc := DockerService{
ID: "svc-upd",
Name: "api",
UpdateStatus: &DockerServiceUpdate{
State: "completed",
Message: "done",
CompletedAt: &completed,
},
}
f := svc.ToFrontend()
if f.UpdateStatus == nil {
t.Fatal("UpdateStatus should be mapped when source pointer is non-nil")
}
if f.UpdateStatus.State != "completed" || f.UpdateStatus.Message != "done" {
t.Fatalf("UpdateStatus fields not mapped: %#v", f.UpdateStatus)
}
if f.UpdateStatus.CompletedAt == nil || *f.UpdateStatus.CompletedAt != completed.Unix()*1000 {
t.Fatalf("UpdateStatus.CompletedAt = %#v, want %d", f.UpdateStatus.CompletedAt, completed.Unix()*1000)
}
}
func TestDockerTaskToFrontend_CompletedAtBranchCov0718(t *testing.T) {
now := time.Now()
completed := now.Add(-2 * time.Minute)
task := DockerTask{
ID: "task-done",
ServiceName: "web",
CreatedAt: now,
CompletedAt: &completed,
}
f := task.ToFrontend()
if f.CompletedAt == nil || *f.CompletedAt != completed.Unix()*1000 {
t.Fatalf("CompletedAt = %#v, want %d", f.CompletedAt, completed.Unix()*1000)
}
}
func TestHostSensorSummaryToFrontend_GPUSmartBranchCov0718(t *testing.T) {
gpuTemp := 75.0
gpuUtil := 50.0
src := HostSensorSummary{
GPU: []HostGPUSensor{
{
ID: "gpu0",
Name: "nvidia-3080",
TemperatureCelsius: &gpuTemp,
UtilizationPercent: &gpuUtil,
},
},
SMART: []HostDiskSMART{
{Device: "sda", Model: "Samsung SSD", Temperature: 42, Health: "PASSED"},
},
}
dest := hostSensorSummaryToFrontend(src)
if dest == nil {
t.Fatal("expected non-nil frontend for populated GPU+SMART sensors")
}
// GPU mapping arm.
if len(dest.GPU) != 1 || dest.GPU[0].ID != "gpu0" || dest.GPU[0].Name != "nvidia-3080" {
t.Fatalf("GPU not mapped: %#v", dest.GPU)
}
if dest.GPU[0].TemperatureCelsius == nil || *dest.GPU[0].TemperatureCelsius != 75.0 {
t.Fatalf("GPU.TemperatureCelsius not cloned: %#v", dest.GPU[0].TemperatureCelsius)
}
if dest.GPU[0].UtilizationPercent == nil || *dest.GPU[0].UtilizationPercent != 50.0 {
t.Fatalf("GPU.UtilizationPercent not cloned: %#v", dest.GPU[0].UtilizationPercent)
}
// Cloned metric pointer must not alias the source pointer.
*dest.GPU[0].TemperatureCelsius = 999
if gpuTemp != 75.0 {
t.Fatal("frontend GPU TemperatureCelsius pointer must not alias the source")
}
// SMART mapping arm.
if len(dest.SMART) != 1 || dest.SMART[0].Device != "sda" || dest.SMART[0].Health != "PASSED" {
t.Fatalf("SMART not mapped: %#v", dest.SMART)
}
if dest.SMART[0].Temperature != 42 || dest.SMART[0].Model != "Samsung SSD" {
t.Fatalf("SMART fields not mapped: %#v", dest.SMART[0])
}
}
@@ -0,0 +1,690 @@
package models
import (
"testing"
)
// This file raises BRANCH coverage for the NormalizeCollections methods on
// the Frontend types defined in models_frontend.go.
//
// For every target type we exercise BOTH arms of each nil-collection
// conditional:
//
// - nil arm: the collection field is left nil; after NormalizeCollections
// it MUST be a non-nil empty slice/map of the right type.
// - populated: the collection field is pre-populated; NormalizeCollections
// MUST preserve it (these methods do NOT sort or deduplicate).
// Where the method recurses into nested elements (DockerHost,
// KubernetesCluster, State, Storage.ZFSPool, Resource.Identity,
// Host.Sensors), the populated arm supplies a nested element
// with a nil sub-collection so the observable side-effect of
// the recursion (the sub-collection becoming non-nil) can be
// asserted, exercising the for-loop bodies and the `!= nil`
// guard branches.
//
// No source file or sibling test was modified.
func TestFrontendNormalizeCollections_BranchCov0718(t *testing.T) {
// ---------------- NodeFrontend ----------------
t.Run("NodeFrontend_nil_becomes_empty", func(t *testing.T) {
n := NodeFrontend{ID: "n-1"} // LoadAverage is nil
out := n.NormalizeCollections()
if out.LoadAverage == nil {
t.Fatalf("LoadAverage should be non-nil after normalize, got nil")
}
if len(out.LoadAverage) != 0 {
t.Fatalf("LoadAverage should be empty, got len=%d", len(out.LoadAverage))
}
})
t.Run("NodeFrontend_populated_preserved_no_dedup", func(t *testing.T) {
// Duplicates included on purpose: the method does NOT dedup/sort.
orig := []float64{3.0, 1.0, 3.0, 2.0}
n := NodeFrontend{ID: "n-1", LoadAverage: orig}
out := n.NormalizeCollections()
if len(out.LoadAverage) != len(orig) {
t.Fatalf("LoadAverage length changed: got %d want %d", len(out.LoadAverage), len(orig))
}
for i := range orig {
if out.LoadAverage[i] != orig[i] {
t.Fatalf("LoadAverage[%d] = %v, want %v (no sort/dedup expected)", i, out.LoadAverage[i], orig[i])
}
}
})
// ---------------- VMFrontend ----------------
t.Run("VMFrontend_nil_becomes_empty", func(t *testing.T) {
v := VMFrontend{ID: "v-1"}
out := v.NormalizeCollections()
if v.Disks != nil || v.NetworkInterfaces != nil || v.IPAddresses != nil {
t.Fatalf("precondition: input collections must be nil")
}
if out.Disks == nil || out.NetworkInterfaces == nil || out.IPAddresses == nil {
t.Fatalf("Disks/NetworkInterfaces/IPAddresses must all be non-nil after normalize")
}
if len(out.Disks) != 0 || len(out.NetworkInterfaces) != 0 || len(out.IPAddresses) != 0 {
t.Fatalf("normalized empty collections must have len 0")
}
})
t.Run("VMFrontend_populated_preserved", func(t *testing.T) {
v := VMFrontend{
ID: "v-1",
Disks: []Disk{{Device: "/sda"}},
NetworkInterfaces: []GuestNetworkInterface{{Name: "eth0"}},
IPAddresses: []string{"10.0.0.1", "10.0.0.1"}, // duplicate on purpose
}
out := v.NormalizeCollections()
if len(out.Disks) != 1 || out.Disks[0].Device != "/sda" {
t.Fatalf("Disks not preserved: %+v", out.Disks)
}
if len(out.NetworkInterfaces) != 1 || out.NetworkInterfaces[0].Name != "eth0" {
t.Fatalf("NetworkInterfaces not preserved: %+v", out.NetworkInterfaces)
}
if len(out.IPAddresses) != 2 || out.IPAddresses[0] != "10.0.0.1" || out.IPAddresses[1] != "10.0.0.1" {
t.Fatalf("IPAddresses not preserved (no dedup expected): %+v", out.IPAddresses)
}
})
// ---------------- ContainerFrontend ----------------
t.Run("ContainerFrontend_nil_becomes_empty", func(t *testing.T) {
c := ContainerFrontend{ID: "c-1"}
out := c.NormalizeCollections()
if out.Disks == nil || out.NetworkInterfaces == nil || out.IPAddresses == nil {
t.Fatalf("Disks/NetworkInterfaces/IPAddresses must all be non-nil after normalize")
}
})
t.Run("ContainerFrontend_populated_preserved", func(t *testing.T) {
c := ContainerFrontend{
ID: "c-1",
Disks: []Disk{{Device: "/data"}},
NetworkInterfaces: []GuestNetworkInterface{{Name: "eth0"}},
IPAddresses: []string{"10.0.0.2"},
}
out := c.NormalizeCollections()
if len(out.Disks) != 1 || out.Disks[0].Device != "/data" {
t.Fatalf("Disks not preserved: %+v", out.Disks)
}
if len(out.NetworkInterfaces) != 1 {
t.Fatalf("NetworkInterfaces not preserved")
}
if len(out.IPAddresses) != 1 || out.IPAddresses[0] != "10.0.0.2" {
t.Fatalf("IPAddresses not preserved: %+v", out.IPAddresses)
}
})
// ---------------- DockerHostFrontend ----------------
t.Run("DockerHostFrontend_nil_all_collections", func(t *testing.T) {
h := DockerHostFrontend{ID: "dh-1"}
out := h.NormalizeCollections()
if out.LoadAverage == nil || out.Disks == nil || out.NetworkInterfaces == nil ||
out.Containers == nil || out.Services == nil || out.Tasks == nil ||
out.Nodes == nil || out.Secrets == nil || out.Configs == nil {
t.Fatalf("all nine collection fields must be non-nil after normalize")
}
// Security is nil on input → must remain nil (no fabrication).
if out.Security != nil {
t.Fatalf("Security must remain nil when nil on input")
}
})
t.Run("DockerHostFrontend_populated_recurses_into_nested", func(t *testing.T) {
// Every nested element starts with nil sub-collections so we can
// observe the parent's for-loop recursion turning them non-nil.
h := DockerHostFrontend{
ID: "dh-1",
LoadAverage: []float64{1.5},
Disks: []Disk{{Device: "/dev/sda"}},
NetworkInterfaces: []HostNetworkInterface{{Name: "eno1"}},
Containers: []DockerContainerFrontend{{ID: "ctr-1"}}, // Ports/Labels/Networks/Mounts nil
Services: []DockerServiceFrontend{{ID: "svc-1"}}, // Labels/EndpointPorts nil
Tasks: []DockerTaskFrontend{{ID: "task-1"}},
Nodes: []DockerNodeFrontend{{ID: "node-1"}},
Secrets: []DockerSecretFrontend{{ID: "sec-1"}}, // Labels nil
Configs: []DockerConfigFrontend{{ID: "cfg-1"}}, // Labels nil
Security: &DockerHostSecurityFrontend{}, // AuthorizationPlugins nil
}
out := h.NormalizeCollections()
// Top-level populated collections preserved.
if len(out.LoadAverage) != 1 || out.LoadAverage[0] != 1.5 {
t.Fatalf("LoadAverage not preserved: %+v", out.LoadAverage)
}
if len(out.Disks) != 1 || out.Disks[0].Device != "/dev/sda" {
t.Fatalf("Disks not preserved: %+v", out.Disks)
}
if len(out.NetworkInterfaces) != 1 || out.NetworkInterfaces[0].Name != "eno1" {
t.Fatalf("NetworkInterfaces not preserved")
}
if len(out.Tasks) != 1 || out.Tasks[0].ID != "task-1" {
t.Fatalf("Tasks not preserved")
}
if len(out.Nodes) != 1 || out.Nodes[0].ID != "node-1" {
t.Fatalf("Nodes not preserved")
}
// Recursion into containers/services/secrets/configs observable.
if len(out.Containers) != 1 {
t.Fatalf("Containers length changed")
}
c := out.Containers[0]
if c.Ports == nil || c.Labels == nil || c.Networks == nil || c.Mounts == nil {
t.Fatalf("nested DockerContainerFrontend collections not normalized: %+v", c)
}
if len(out.Services) != 1 || out.Services[0].Labels == nil || out.Services[0].EndpointPorts == nil {
t.Fatalf("nested DockerServiceFrontend collections not normalized")
}
if len(out.Secrets) != 1 || out.Secrets[0].Labels == nil {
t.Fatalf("nested DockerSecretFrontend.Labels not normalized")
}
if len(out.Configs) != 1 || out.Configs[0].Labels == nil {
t.Fatalf("nested DockerConfigFrontend.Labels not normalized")
}
// The `if h.Security != nil` branch must have run and recursed.
if out.Security == nil {
t.Fatalf("Security must be preserved when non-nil on input")
}
if out.Security.AuthorizationPlugins == nil {
t.Fatalf("Security.AuthorizationPlugins should be normalized to non-nil")
}
})
// ---------------- DockerHostSecurityFrontend (was 0%) ----------------
t.Run("DockerHostSecurityFrontend_nil", func(t *testing.T) {
s := DockerHostSecurityFrontend{MutatingCommandsBlocked: true}
out := s.NormalizeCollections()
if out.AuthorizationPlugins == nil {
t.Fatalf("AuthorizationPlugins should be non-nil after normalize")
}
if len(out.AuthorizationPlugins) != 0 {
t.Fatalf("AuthorizationPlugins should be empty, got len=%d", len(out.AuthorizationPlugins))
}
if !out.MutatingCommandsBlocked {
t.Fatalf("scalar MutatingCommandsBlocked must be preserved")
}
})
t.Run("DockerHostSecurityFrontend_populated_preserved", func(t *testing.T) {
s := DockerHostSecurityFrontend{
AuthorizationPlugins: []string{"opa", "opa"}, // duplicate on purpose
}
out := s.NormalizeCollections()
if len(out.AuthorizationPlugins) != 2 || out.AuthorizationPlugins[0] != "opa" || out.AuthorizationPlugins[1] != "opa" {
t.Fatalf("AuthorizationPlugins not preserved (no dedup expected): %+v", out.AuthorizationPlugins)
}
})
// ---------------- ConnectedInfrastructureItemFrontend ----------------
t.Run("ConnectedInfrastructureItemFrontend_nil", func(t *testing.T) {
i := ConnectedInfrastructureItemFrontend{ID: "ci-1"}
out := i.NormalizeCollections()
if out.Surfaces == nil {
t.Fatalf("Surfaces should be non-nil after normalize")
}
})
t.Run("ConnectedInfrastructureItemFrontend_populated_preserved", func(t *testing.T) {
i := ConnectedInfrastructureItemFrontend{
ID: "ci-1",
Surfaces: []ConnectedInfrastructureSurfaceFrontend{{ID: "sf-1", Kind: "agent"}},
}
out := i.NormalizeCollections()
if len(out.Surfaces) != 1 || out.Surfaces[0].ID != "sf-1" || out.Surfaces[0].Kind != "agent" {
t.Fatalf("Surfaces not preserved: %+v", out.Surfaces)
}
})
// ---------------- KubernetesClusterFrontend ----------------
t.Run("KubernetesClusterFrontend_nil", func(t *testing.T) {
c := KubernetesClusterFrontend{ID: "k-1"}
out := c.NormalizeCollections()
if out.Nodes == nil || out.Pods == nil || out.Deployments == nil {
t.Fatalf("Nodes/Pods/Deployments must all be non-nil after normalize")
}
})
t.Run("KubernetesClusterFrontend_populated_recurses_into_nested", func(t *testing.T) {
c := KubernetesClusterFrontend{
ID: "k-1",
Nodes: []KubernetesNodeFrontend{{UID: "n-1"}}, // Roles nil
Pods: []KubernetesPodFrontend{{UID: "p-1"}}, // Labels/Containers nil
Deployments: []KubernetesDeploymentFrontend{{UID: "d-1"}}, // Labels nil
}
out := c.NormalizeCollections()
if len(out.Nodes) != 1 || out.Nodes[0].UID != "n-1" {
t.Fatalf("Nodes not preserved")
}
if out.Nodes[0].Roles == nil {
t.Fatalf("nested KubernetesNodeFrontend.Roles not normalized")
}
if len(out.Pods) != 1 || out.Pods[0].UID != "p-1" {
t.Fatalf("Pods not preserved")
}
if out.Pods[0].Labels == nil || out.Pods[0].Containers == nil {
t.Fatalf("nested KubernetesPodFrontend collections not normalized")
}
if len(out.Deployments) != 1 || out.Deployments[0].UID != "d-1" {
t.Fatalf("Deployments not preserved")
}
if out.Deployments[0].Labels == nil {
t.Fatalf("nested KubernetesDeploymentFrontend.Labels not normalized")
}
})
// ---------------- KubernetesNodeFrontend ----------------
t.Run("KubernetesNodeFrontend_nil", func(t *testing.T) {
n := KubernetesNodeFrontend{UID: "n-1"}
out := n.NormalizeCollections()
if out.Roles == nil {
t.Fatalf("Roles should be non-nil after normalize")
}
})
t.Run("KubernetesNodeFrontend_populated_preserved", func(t *testing.T) {
n := KubernetesNodeFrontend{UID: "n-1", Roles: []string{"control-plane", "worker", "control-plane"}}
out := n.NormalizeCollections()
if len(out.Roles) != 3 || out.Roles[0] != "control-plane" || out.Roles[2] != "control-plane" {
t.Fatalf("Roles not preserved (no dedup expected): %+v", out.Roles)
}
})
// ---------------- KubernetesPodFrontend ----------------
t.Run("KubernetesPodFrontend_nil", func(t *testing.T) {
p := KubernetesPodFrontend{UID: "p-1"}
out := p.NormalizeCollections()
if out.Labels == nil || out.Containers == nil {
t.Fatalf("Labels/Containers should be non-nil after normalize")
}
})
t.Run("KubernetesPodFrontend_populated_preserved", func(t *testing.T) {
p := KubernetesPodFrontend{
UID: "p-1",
Labels: map[string]string{"app": "web"},
Containers: []KubernetesPodContainerFrontend{{Name: "c-1"}},
}
out := p.NormalizeCollections()
if len(out.Labels) != 1 || out.Labels["app"] != "web" {
t.Fatalf("Labels not preserved: %+v", out.Labels)
}
if len(out.Containers) != 1 || out.Containers[0].Name != "c-1" {
t.Fatalf("Containers not preserved: %+v", out.Containers)
}
})
// ---------------- KubernetesDeploymentFrontend ----------------
t.Run("KubernetesDeploymentFrontend_nil", func(t *testing.T) {
d := KubernetesDeploymentFrontend{UID: "d-1"}
out := d.NormalizeCollections()
if out.Labels == nil {
t.Fatalf("Labels should be non-nil after normalize")
}
})
t.Run("KubernetesDeploymentFrontend_populated_preserved", func(t *testing.T) {
d := KubernetesDeploymentFrontend{UID: "d-1", Labels: map[string]string{"app": "api"}}
out := d.NormalizeCollections()
if len(out.Labels) != 1 || out.Labels["app"] != "api" {
t.Fatalf("Labels not preserved: %+v", out.Labels)
}
})
// ---------------- DockerContainerFrontend ----------------
t.Run("DockerContainerFrontend_nil", func(t *testing.T) {
c := DockerContainerFrontend{ID: "ctr-1"}
out := c.NormalizeCollections()
if out.Ports == nil || out.Labels == nil || out.Networks == nil || out.Mounts == nil {
t.Fatalf("Ports/Labels/Networks/Mounts must all be non-nil after normalize")
}
})
t.Run("DockerContainerFrontend_populated_preserved", func(t *testing.T) {
c := DockerContainerFrontend{
ID: "ctr-1",
Ports: []DockerContainerPortFrontend{{PrivatePort: 80, Protocol: "tcp"}},
Labels: map[string]string{"io.docker.compose.service": "web"},
Networks: []DockerContainerNetworkFrontend{{Name: "bridge", IPv4: "172.17.0.2"}},
Mounts: []DockerContainerMountFrontend{{Type: "bind", Source: "/host"}},
}
out := c.NormalizeCollections()
if len(out.Ports) != 1 || out.Ports[0].PrivatePort != 80 || out.Ports[0].Protocol != "tcp" {
t.Fatalf("Ports not preserved: %+v", out.Ports)
}
if len(out.Labels) != 1 || out.Labels["io.docker.compose.service"] != "web" {
t.Fatalf("Labels not preserved: %+v", out.Labels)
}
if len(out.Networks) != 1 || out.Networks[0].IPv4 != "172.17.0.2" {
t.Fatalf("Networks not preserved: %+v", out.Networks)
}
if len(out.Mounts) != 1 || out.Mounts[0].Source != "/host" {
t.Fatalf("Mounts not preserved: %+v", out.Mounts)
}
})
// ---------------- DockerServiceFrontend ----------------
t.Run("DockerServiceFrontend_nil", func(t *testing.T) {
s := DockerServiceFrontend{ID: "svc-1"}
out := s.NormalizeCollections()
if out.Labels == nil || out.EndpointPorts == nil {
t.Fatalf("Labels/EndpointPorts should be non-nil after normalize")
}
})
t.Run("DockerServiceFrontend_populated_preserved", func(t *testing.T) {
s := DockerServiceFrontend{
ID: "svc-1",
Labels: map[string]string{"swarm": "true"},
EndpointPorts: []DockerServicePortFrontend{{PublishedPort: 8080, TargetPort: 80}},
}
out := s.NormalizeCollections()
if len(out.Labels) != 1 || out.Labels["swarm"] != "true" {
t.Fatalf("Labels not preserved: %+v", out.Labels)
}
if len(out.EndpointPorts) != 1 || out.EndpointPorts[0].TargetPort != 80 || out.EndpointPorts[0].PublishedPort != 8080 {
t.Fatalf("EndpointPorts not preserved: %+v", out.EndpointPorts)
}
})
// ---------------- DockerSecretFrontend (was 0%) ----------------
t.Run("DockerSecretFrontend_nil", func(t *testing.T) {
s := DockerSecretFrontend{ID: "sec-1", Name: "tls"}
out := s.NormalizeCollections()
if out.Labels == nil {
t.Fatalf("Labels should be non-nil after normalize")
}
if len(out.Labels) != 0 {
t.Fatalf("Labels should be empty, got len=%d", len(out.Labels))
}
if out.Name != "tls" {
t.Fatalf("Name scalar must be preserved")
}
})
t.Run("DockerSecretFrontend_populated_preserved", func(t *testing.T) {
s := DockerSecretFrontend{
ID: "sec-1",
Name: "tls",
Labels: map[string]string{"managed": "true"},
}
out := s.NormalizeCollections()
if len(out.Labels) != 1 || out.Labels["managed"] != "true" {
t.Fatalf("Labels not preserved: %+v", out.Labels)
}
})
// ---------------- DockerConfigFrontend (was 0%) ----------------
t.Run("DockerConfigFrontend_nil", func(t *testing.T) {
c := DockerConfigFrontend{ID: "cfg-1", Name: "conf"}
out := c.NormalizeCollections()
if out.Labels == nil {
t.Fatalf("Labels should be non-nil after normalize")
}
if len(out.Labels) != 0 {
t.Fatalf("Labels should be empty, got len=%d", len(out.Labels))
}
if out.Name != "conf" {
t.Fatalf("Name scalar must be preserved")
}
})
t.Run("DockerConfigFrontend_populated_preserved", func(t *testing.T) {
c := DockerConfigFrontend{
ID: "cfg-1",
Name: "conf",
Labels: map[string]string{"env": "prod"},
}
out := c.NormalizeCollections()
if len(out.Labels) != 1 || out.Labels["env"] != "prod" {
t.Fatalf("Labels not preserved: %+v", out.Labels)
}
})
// ---------------- HostFrontend ----------------
t.Run("HostFrontend_nil_all_collections_and_nil_sensors", func(t *testing.T) {
h := HostFrontend{ID: "h-1"}
out := h.NormalizeCollections()
if out.LoadAverage == nil || out.Disks == nil || out.DiskIO == nil ||
out.NetworkInterfaces == nil || out.Tags == nil {
t.Fatalf("LoadAverage/Disks/DiskIO/NetworkInterfaces/Tags must all be non-nil after normalize")
}
if out.Sensors != nil {
t.Fatalf("Sensors must remain nil when nil on input")
}
})
t.Run("HostFrontend_populated_with_sensors_recurses", func(t *testing.T) {
h := HostFrontend{
ID: "h-1",
LoadAverage: []float64{0.5, 0.4, 0.3},
Disks: []Disk{{Device: "/"}},
DiskIO: []DiskIO{{Device: "sda"}},
NetworkInterfaces: []HostNetworkInterface{{Name: "eth0"}},
Tags: []string{"prod", "prod"}, // duplicate on purpose
Sensors: &HostSensorSummaryFrontend{}, // all sub-collections nil
}
out := h.NormalizeCollections()
if len(out.LoadAverage) != 3 || out.LoadAverage[2] != 0.3 {
t.Fatalf("LoadAverage not preserved: %+v", out.LoadAverage)
}
if len(out.Disks) != 1 || out.Disks[0].Device != "/" {
t.Fatalf("Disks not preserved")
}
if len(out.DiskIO) != 1 || out.DiskIO[0].Device != "sda" {
t.Fatalf("DiskIO not preserved")
}
if len(out.NetworkInterfaces) != 1 || out.NetworkInterfaces[0].Name != "eth0" {
t.Fatalf("NetworkInterfaces not preserved")
}
if len(out.Tags) != 2 || out.Tags[0] != "prod" || out.Tags[1] != "prod" {
t.Fatalf("Tags not preserved (no dedup expected): %+v", out.Tags)
}
// The `if h.Sensors != nil` branch must have run and recursed.
if out.Sensors == nil {
t.Fatalf("Sensors must be preserved when non-nil on input")
}
s := out.Sensors
if s.TemperatureCelsius == nil || s.FanRPM == nil || s.PowerWatts == nil ||
s.Additional == nil || s.GPU == nil || s.SMART == nil {
t.Fatalf("nested HostSensorSummaryFrontend collections not normalized: %+v", s)
}
})
// ---------------- HostSensorSummaryFrontend ----------------
t.Run("HostSensorSummaryFrontend_nil_all_six", func(t *testing.T) {
s := HostSensorSummaryFrontend{}
out := s.NormalizeCollections()
if out.TemperatureCelsius == nil || out.FanRPM == nil || out.PowerWatts == nil ||
out.Additional == nil || out.GPU == nil || out.SMART == nil {
t.Fatalf("all six collection fields must be non-nil after normalize")
}
})
t.Run("HostSensorSummaryFrontend_populated_preserved", func(t *testing.T) {
temp := 65.0
s := HostSensorSummaryFrontend{
TemperatureCelsius: map[string]float64{"cpu": temp},
FanRPM: map[string]float64{"fan1": 1500},
PowerWatts: map[string]float64{"psu1": 220.5},
Additional: map[string]float64{"voltage": 12.0},
GPU: []HostGPUSensorFrontend{{ID: "gpu0", Name: "nvidia"}},
SMART: []HostDiskSMARTFrontend{{Device: "sda", Temperature: 40}},
}
out := s.NormalizeCollections()
if len(out.TemperatureCelsius) != 1 || out.TemperatureCelsius["cpu"] != temp {
t.Fatalf("TemperatureCelsius not preserved: %+v", out.TemperatureCelsius)
}
if len(out.FanRPM) != 1 || out.FanRPM["fan1"] != 1500 {
t.Fatalf("FanRPM not preserved: %+v", out.FanRPM)
}
if len(out.PowerWatts) != 1 || out.PowerWatts["psu1"] != 220.5 {
t.Fatalf("PowerWatts not preserved: %+v", out.PowerWatts)
}
if len(out.Additional) != 1 || out.Additional["voltage"] != 12.0 {
t.Fatalf("Additional not preserved: %+v", out.Additional)
}
if len(out.GPU) != 1 || out.GPU[0].Name != "nvidia" {
t.Fatalf("GPU not preserved: %+v", out.GPU)
}
if len(out.SMART) != 1 || out.SMART[0].Device != "sda" || out.SMART[0].Temperature != 40 {
t.Fatalf("SMART not preserved: %+v", out.SMART)
}
})
// ---------------- StorageFrontend ----------------
t.Run("StorageFrontend_nil_and_nil_zfs", func(t *testing.T) {
s := StorageFrontend{ID: "s-1"}
out := s.NormalizeCollections()
if out.Nodes == nil || out.NodeIDs == nil {
t.Fatalf("Nodes/NodeIDs should be non-nil after normalize")
}
if out.ZFSPool != nil {
t.Fatalf("ZFSPool must remain nil when nil on input")
}
})
t.Run("StorageFrontend_populated_with_zfs_recurses", func(t *testing.T) {
s := StorageFrontend{
ID: "s-1",
Nodes: []string{"node-a", "node-a"}, // duplicate on purpose
NodeIDs: []string{"nid-1"},
ZFSPool: &ZFSPool{Name: "tank"}, // Devices nil → recursion observable
}
out := s.NormalizeCollections()
if len(out.Nodes) != 2 || out.Nodes[0] != "node-a" || out.Nodes[1] != "node-a" {
t.Fatalf("Nodes not preserved (no dedup expected): %+v", out.Nodes)
}
if len(out.NodeIDs) != 1 || out.NodeIDs[0] != "nid-1" {
t.Fatalf("NodeIDs not preserved: %+v", out.NodeIDs)
}
// The `if s.ZFSPool != nil` branch must have run and recursed.
if out.ZFSPool == nil {
t.Fatalf("ZFSPool must be preserved when non-nil on input")
}
if out.ZFSPool.Name != "tank" {
t.Fatalf("ZFSPool.Name not preserved: %q", out.ZFSPool.Name)
}
if out.ZFSPool.Devices == nil {
t.Fatalf("nested ZFSPool.Devices not normalized (recursion did not run)")
}
})
// ---------------- CephClusterFrontend ----------------
t.Run("CephClusterFrontend_nil", func(t *testing.T) {
c := CephClusterFrontend{ID: "ceph-1"}
out := c.NormalizeCollections()
if out.Pools == nil || out.Services == nil {
t.Fatalf("Pools/Services should be non-nil after normalize")
}
})
t.Run("CephClusterFrontend_populated_preserved", func(t *testing.T) {
c := CephClusterFrontend{
ID: "ceph-1",
Pools: []CephPool{{Name: "replicapool"}},
Services: []CephServiceStatus{{Type: "mon", Running: 1}},
}
out := c.NormalizeCollections()
if len(out.Pools) != 1 || out.Pools[0].Name != "replicapool" {
t.Fatalf("Pools not preserved: %+v", out.Pools)
}
if len(out.Services) != 1 || out.Services[0].Type != "mon" || out.Services[0].Running != 1 {
t.Fatalf("Services not preserved: %+v", out.Services)
}
})
// ---------------- StateFrontend ----------------
t.Run("StateFrontend_nil_all_collections", func(t *testing.T) {
s := StateFrontend{}
out := s.NormalizeCollections()
if out.ActiveAlerts == nil || out.RecentlyResolved == nil || out.Metrics == nil ||
out.ConnectionHealth == nil || out.PVETagColors == nil || out.PVETagStyles == nil ||
out.Resources == nil || out.ConnectedInfrastructure == nil {
t.Fatalf("all top-level collection fields must be non-nil after normalize")
}
// Performance.APICallDuration is a nested nil-map branch.
if out.Performance.APICallDuration == nil {
t.Fatalf("Performance.APICallDuration should be non-nil after normalize")
}
})
t.Run("StateFrontend_populated_recurses_into_nested", func(t *testing.T) {
// Provide a non-nil Performance.APICallDuration to exercise the
// "already populated" arm of that specific branch.
s := StateFrontend{
ActiveAlerts: []Alert{{ID: "a-1"}},
RecentlyResolved: []ResolvedAlert{{Alert: Alert{ID: "r-1"}}},
Metrics: []Metric{{Type: "cpu"}},
ConnectionHealth: map[string]bool{"api": true},
PVETagColors: map[string]string{"prod": "#ff0000"},
PVETagStyles: map[string]PVETagStyle{"pve1": {}},
Performance: Performance{APICallDuration: map[string]float64{"nodes": 12.3}},
// Each nested element has nil sub-collections so recursion is observable.
ConnectedInfrastructure: []ConnectedInfrastructureItemFrontend{{ID: "ci-1"}}, // Surfaces nil
Resources: []ResourceFrontend{{ID: "res-1"}}, // Tags/Labels/Alerts nil
}
out := s.NormalizeCollections()
if len(out.ActiveAlerts) != 1 || out.ActiveAlerts[0].ID != "a-1" {
t.Fatalf("ActiveAlerts not preserved")
}
if len(out.RecentlyResolved) != 1 || out.RecentlyResolved[0].ID != "r-1" {
t.Fatalf("RecentlyResolved not preserved")
}
if len(out.Metrics) != 1 || out.Metrics[0].Type != "cpu" {
t.Fatalf("Metrics not preserved")
}
if !out.ConnectionHealth["api"] {
t.Fatalf("ConnectionHealth not preserved: %+v", out.ConnectionHealth)
}
if out.PVETagColors["prod"] != "#ff0000" {
t.Fatalf("PVETagColors not preserved: %+v", out.PVETagColors)
}
// Performance.APICallDuration must be preserved as-is (non-nil arm).
if got := out.Performance.APICallDuration["nodes"]; got != 12.3 {
t.Fatalf("Performance.APICallDuration[\"nodes\"] = %v, want 12.3", got)
}
// Recursion over ConnectedInfrastructure.
if len(out.ConnectedInfrastructure) != 1 || out.ConnectedInfrastructure[0].ID != "ci-1" {
t.Fatalf("ConnectedInfrastructure not preserved")
}
if out.ConnectedInfrastructure[0].Surfaces == nil {
t.Fatalf("nested ConnectedInfrastructureItemFrontend.Surfaces not normalized")
}
// Recursion over Resources.
if len(out.Resources) != 1 || out.Resources[0].ID != "res-1" {
t.Fatalf("Resources not preserved")
}
r := out.Resources[0]
if r.Tags == nil || r.Labels == nil || r.Alerts == nil {
t.Fatalf("nested ResourceFrontend collections not normalized: %+v", r)
}
})
// ---------------- ResourceFrontend ----------------
t.Run("ResourceFrontend_nil_and_nil_identity", func(t *testing.T) {
r := ResourceFrontend{ID: "res-1"}
out := r.NormalizeCollections()
if out.Tags == nil || out.Labels == nil || out.Alerts == nil {
t.Fatalf("Tags/Labels/Alerts should be non-nil after normalize")
}
if out.Identity != nil {
t.Fatalf("Identity must remain nil when nil on input")
}
})
t.Run("ResourceFrontend_populated_with_identity_recurses", func(t *testing.T) {
r := ResourceFrontend{
ID: "res-1",
Tags: []string{"env:prod", "env:prod"}, // duplicate on purpose
Labels: map[string]string{"team": "infra"},
Alerts: []ResourceAlertFrontend{{ID: "al-1", Level: "warn"}},
Identity: &ResourceIdentityFrontend{Hostname: "host-1"}, // IPs nil → recursion observable
}
out := r.NormalizeCollections()
if len(out.Tags) != 2 || out.Tags[0] != "env:prod" || out.Tags[1] != "env:prod" {
t.Fatalf("Tags not preserved (no dedup expected): %+v", out.Tags)
}
if len(out.Labels) != 1 || out.Labels["team"] != "infra" {
t.Fatalf("Labels not preserved: %+v", out.Labels)
}
if len(out.Alerts) != 1 || out.Alerts[0].ID != "al-1" || out.Alerts[0].Level != "warn" {
t.Fatalf("Alerts not preserved: %+v", out.Alerts)
}
// The `if r.Identity != nil` branch must have run and recursed.
if out.Identity == nil {
t.Fatalf("Identity must be preserved when non-nil on input")
}
if out.Identity.Hostname != "host-1" {
t.Fatalf("Identity.Hostname not preserved: %q", out.Identity.Hostname)
}
if out.Identity.IPs == nil {
t.Fatalf("nested Identity.IPs not normalized (recursion did not run)")
}
})
}
@@ -0,0 +1,465 @@
package servicediscovery
import (
"sort"
"strings"
"testing"
)
// tokenSetFrom runs addResourceIDTokens against a single resource ID and
// returns the resulting set as a sorted slice, so table cases can assert the
// exact token population produced by each branch.
func tokenSetFrom(resourceID string) []string {
tokens := make(map[string]struct{})
addResourceIDTokens(tokens, resourceID)
out := make([]string, 0, len(tokens))
for k := range tokens {
out = append(out, k)
}
sort.Strings(out)
return out
}
func TestAddResourceIDTokens(t *testing.T) {
tests := []struct {
name string
resourceID string
expected []string
}{
{
name: "empty-input-skipped",
resourceID: "",
expected: []string{},
},
{
name: "whitespace-only-skipped",
resourceID: " ",
expected: []string{},
},
{
name: "plain-id",
resourceID: "abc",
expected: []string{"abc"},
},
{
name: "slash-last-segment",
resourceID: "abc/def",
expected: []string{"abc/def", "def"},
},
{
name: "colon-last-segment",
resourceID: "abc:def",
expected: []string{"abc:def", "def"},
},
{
name: "vm-prefix-and-trailing-digits",
resourceID: "VM-101",
expected: []string{"101", "vm-101"},
},
{
name: "ct-prefix-and-trailing-digits",
resourceID: "ct-202",
expected: []string{"202", "ct-202"},
},
{
name: "lxc-prefix-no-trailing-digits-branch",
resourceID: "LXC-303",
expected: []string{"303", "lxc-303"},
},
{
name: "qemu-slash-with-trailing-digits",
resourceID: "qemu/404",
expected: []string{"404", "qemu/404"},
},
{
name: "lxc-slash-with-trailing-digits",
resourceID: "lxc/505",
expected: []string{"505", "lxc/505"},
},
{
name: "docker-host-container-split",
resourceID: "docker:host1/container1",
expected: []string{"container1", "docker:host1/container1", "host1", "host1/container1"},
},
{
name: "colon-without-slash-no-host-container-split",
resourceID: "abc:def",
expected: []string{"abc:def", "def"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tokenSetFrom(tt.resourceID)
if len(got) != len(tt.expected) {
t.Fatalf("token count mismatch: got %v, want %v", got, tt.expected)
}
for i, v := range tt.expected {
if got[i] != v {
t.Fatalf("token[%d]: got %q, want %q (full got=%v)", i, got[i], v, got)
}
}
})
}
}
func TestBuildResourceIDTokenSet(t *testing.T) {
t.Run("empty-input-yields-empty-set", func(t *testing.T) {
if got := buildResourceIDTokenSet(nil); len(got) != 0 {
t.Fatalf("expected empty token set, got %v", got)
}
})
t.Run("all-whitespace-yields-empty-set", func(t *testing.T) {
got := buildResourceIDTokenSet([]string{" ", ""})
if len(got) != 0 {
t.Fatalf("expected empty token set for whitespace-only IDs, got %v", got)
}
})
t.Run("multiple-ids-aggregated", func(t *testing.T) {
got := buildResourceIDTokenSet([]string{"vm-101", "ct-202"})
for _, want := range []string{"vm-101", "101", "ct-202", "202"} {
if _, ok := got[want]; !ok {
t.Fatalf("expected token %q in set, got %v", want, got)
}
}
})
}
func TestDiscoveryMatchesTokens(t *testing.T) {
tokens := map[string]struct{}{"abc": {}, "101": {}}
t.Run("nil-discovery-never-matches", func(t *testing.T) {
if discoveryMatchesTokens(nil, tokens) {
t.Fatalf("nil discovery must not match any token set")
}
})
t.Run("matching-token-returns-true", func(t *testing.T) {
d := &ResourceDiscovery{ResourceID: "ABC"} // lowercased to "abc" by discoveryTokens
if !discoveryMatchesTokens(d, tokens) {
t.Fatalf("expected match for ResourceID ABC against token abc")
}
})
t.Run("no-matching-token-returns-false", func(t *testing.T) {
d := &ResourceDiscovery{ResourceID: "xyz"}
if discoveryMatchesTokens(d, tokens) {
t.Fatalf("expected no match for ResourceID xyz")
}
})
t.Run("empty-token-set-returns-false", func(t *testing.T) {
d := &ResourceDiscovery{ResourceID: "abc"}
if discoveryMatchesTokens(d, map[string]struct{}{}) {
t.Fatalf("empty token set must not match anything")
}
})
}
func TestDiscoveryTokens(t *testing.T) {
tests := []struct {
name string
disc *ResourceDiscovery
mustHave []string // tokens that MUST be present (lowercased)
}{
{
name: "vm-type",
disc: &ResourceDiscovery{ResourceID: "101", TargetID: "node1", ID: "vm:node1:101", ResourceType: ResourceTypeVM},
mustHave: []string{"101", "vm:node1:101", "node1", "qemu/101", "vm/101", "vm-101", "agent:node1"},
},
{
name: "system-container-type",
disc: &ResourceDiscovery{ResourceID: "202", TargetID: "node1", ID: "lxc:node1:202", ResourceType: ResourceTypeSystemContainer},
mustHave: []string{"202", "lxc/202", "ct/202", "ct-202", "system-container/202"},
},
{
name: "docker-type-with-target",
disc: &ResourceDiscovery{ResourceID: "app", TargetID: "host1", ID: "docker:host1:app", ResourceType: ResourceTypeDocker},
mustHave: []string{"app", "host1", "docker:host1", "docker:host1/app"},
},
{
name: "docker-type-without-target",
disc: &ResourceDiscovery{ResourceID: "app", ID: "docker::app", ResourceType: ResourceTypeDocker},
mustHave: []string{"app"},
},
{
name: "agent-type",
disc: &ResourceDiscovery{ResourceID: "ag1", TargetID: "host1", ID: "agent:host1:ag1", ResourceType: ResourceTypeAgent},
mustHave: []string{"agent:ag1", "agent:host1", "ag1"},
},
{
name: "k8s-type",
disc: &ResourceDiscovery{ResourceID: "pod1", TargetID: "cluster1", ID: "k8s:cluster1:pod1", ResourceType: ResourceTypeK8s},
mustHave: []string{"pod1", "k8s/pod1", "kubernetes/pod1"},
},
{
name: "unknown-type-falls-through-switch",
disc: &ResourceDiscovery{ResourceID: "rid", TargetID: "tid", ID: "weird:tid:rid", ResourceType: ResourceType("unknown")},
mustHave: []string{"rid", "tid", "agent:tid"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := discoveryTokens(tt.disc)
set := make(map[string]struct{}, len(got))
for _, g := range got {
set[g] = struct{}{}
}
for _, want := range tt.mustHave {
if _, ok := set[want]; !ok {
t.Fatalf("expected token %q in discoveryTokens output, got %v", want, got)
}
}
// Every returned token must be lowercase — matching relies on it.
for _, g := range got {
if g != strings.ToLower(g) {
t.Fatalf("discoveryTokens returned non-lowercased token %q", g)
}
}
})
}
}
func TestFilterDiscoveriesByResourceIDs(t *testing.T) {
discoveries := []*ResourceDiscovery{
{ID: "vm:node1:101", ResourceType: ResourceTypeVM, ResourceID: "101", TargetID: "node1", ServiceName: "VM One"},
{ID: "docker:host1:app", ResourceType: ResourceTypeDocker, ResourceID: "app", TargetID: "host1", ServiceName: "App"},
{ID: "lxc:node2:202", ResourceType: ResourceTypeSystemContainer, ResourceID: "202", TargetID: "node2", ServiceName: "LXC"},
}
t.Run("empty-discoveries-returns-nil", func(t *testing.T) {
if got := FilterDiscoveriesByResourceIDs(nil, []string{"101"}); got != nil {
t.Fatalf("expected nil for empty discoveries, got %v", got)
}
})
t.Run("empty-resource-ids-returns-all", func(t *testing.T) {
got := FilterDiscoveriesByResourceIDs(discoveries, nil)
if len(got) != len(discoveries) {
t.Fatalf("expected all %d discoveries, got %d (%v)", len(discoveries), len(got), got)
}
})
t.Run("whitespace-only-ids-returns-nil", func(t *testing.T) {
// tokens set ends up empty -> filter returns nil (distinct from the
// empty-resourceIDs arm which returns all).
if got := FilterDiscoveriesByResourceIDs(discoveries, []string{" ", ""}); got != nil {
t.Fatalf("expected nil when token set is empty, got %v", got)
}
})
t.Run("keeps-matching-drops-rest", func(t *testing.T) {
got := FilterDiscoveriesByResourceIDs(discoveries, []string{"101"})
if len(got) != 1 || got[0].ResourceID != "101" {
t.Fatalf("expected only the VM (101), got %v", got)
}
})
t.Run("no-match-returns-empty", func(t *testing.T) {
got := FilterDiscoveriesByResourceIDs(discoveries, []string{"does-not-exist"})
if len(got) != 0 {
t.Fatalf("expected empty result for non-matching ID, got %v", got)
}
})
t.Run("multiple-resource-ids", func(t *testing.T) {
got := FilterDiscoveriesByResourceIDs(discoveries, []string{"101", "app"})
if len(got) != 2 {
t.Fatalf("expected 2 matches, got %d (%v)", len(got), got)
}
seen := map[string]bool{}
for _, d := range got {
seen[d.ResourceID] = true
}
if !seen["101"] || !seen["app"] {
t.Fatalf("expected to keep 101 and app, got %v", seen)
}
})
}
func TestContainerFingerprint_HasChanged(t *testing.T) {
fp := &ContainerFingerprint{Hash: "abc123", SchemaVersion: FingerprintSchemaVersion}
t.Run("nil-other-is-change", func(t *testing.T) {
if !fp.HasChanged(nil) {
t.Fatalf("HasChanged(nil) must be true")
}
})
t.Run("same-hash-no-change", func(t *testing.T) {
other := &ContainerFingerprint{Hash: "abc123", SchemaVersion: FingerprintSchemaVersion}
if fp.HasChanged(other) {
t.Fatalf("HasChanged with identical hash must be false")
}
})
t.Run("different-hash-is-change", func(t *testing.T) {
other := &ContainerFingerprint{Hash: "different", SchemaVersion: FingerprintSchemaVersion}
if !fp.HasChanged(other) {
t.Fatalf("HasChanged with different hash must be true")
}
})
}
func TestContainerFingerprint_String(t *testing.T) {
fp := &ContainerFingerprint{
ResourceID: "rid-1",
TargetID: "tid-1",
Hash: "deadbeef",
ImageName: "nginx:1.2.3",
Ports: []string{"80/tcp", "443/tcp"},
}
got := fp.String()
want := "Fingerprint{id=rid-1, target=tid-1, hash=deadbeef, image=nginx:1.2.3, ports=[80/tcp 443/tcp]}"
if got != want {
t.Fatalf("String() mismatch:\n got: %s\nwant: %s", got, want)
}
}
func TestGenerateK8sPodFingerprint(t *testing.T) {
basePod := &KubernetesPod{
UID: "uid-1",
Name: "web",
Namespace: "prod",
NodeName: "node-a",
OwnerKind: "Deployment",
OwnerName: "web-deploy",
Containers: []KubernetesPodContainer{
{Name: "c1", Image: "img1:v1"},
{Name: "c2", Image: "img2:v2"},
},
Labels: map[string]string{"app": "web", "team": "platform"},
}
t.Run("deterministic-for-same-input", func(t *testing.T) {
fp1 := GenerateK8sPodFingerprint("cluster-1", basePod)
fp2 := GenerateK8sPodFingerprint("cluster-1", basePod)
if fp1.Hash != fp2.Hash {
t.Fatalf("expected deterministic hash; got %q then %q", fp1.Hash, fp2.Hash)
}
if fp1.Hash == "" {
t.Fatalf("expected non-empty hash")
}
})
t.Run("identity-fields-populated", func(t *testing.T) {
fp := GenerateK8sPodFingerprint("cluster-1", basePod)
if fp.ResourceID != "uid-1" {
t.Fatalf("ResourceID: got %q, want uid-1", fp.ResourceID)
}
if fp.TargetID != "cluster-1" {
t.Fatalf("TargetID: got %q, want cluster-1", fp.TargetID)
}
if fp.SchemaVersion != FingerprintSchemaVersion {
t.Fatalf("SchemaVersion: got %d, want %d", fp.SchemaVersion, FingerprintSchemaVersion)
}
// First sorted image is "c1:img1:v1" (c1 < c2).
if fp.ImageName != "c1:img1:v1" {
t.Fatalf("ImageName: got %q, want c1:img1:v1", fp.ImageName)
}
})
t.Run("empty-containers-leaves-image-empty", func(t *testing.T) {
empty := &KubernetesPod{UID: "u", Name: "n", Namespace: "ns"}
fp := GenerateK8sPodFingerprint("c", empty)
if fp.ImageName != "" {
t.Fatalf("ImageName: got %q, want empty", fp.ImageName)
}
if fp.Hash == "" {
t.Fatalf("expected non-empty hash even with no containers")
}
})
// Each mutation below must produce a different hash from the base.
changeCases := []struct {
name string
mutate func(p KubernetesPod) *KubernetesPod
}{
{
name: "changed-uid",
mutate: func(p KubernetesPod) *KubernetesPod {
p.UID = "uid-2"
return &p
},
},
{
name: "changed-name",
mutate: func(p KubernetesPod) *KubernetesPod {
p.Name = "worker"
return &p
},
},
{
name: "changed-namespace",
mutate: func(p KubernetesPod) *KubernetesPod {
p.Namespace = "staging"
return &p
},
},
{
name: "changed-node",
mutate: func(p KubernetesPod) *KubernetesPod {
p.NodeName = "node-b"
return &p
},
},
{
name: "changed-owner-kind",
mutate: func(p KubernetesPod) *KubernetesPod {
p.OwnerKind = "StatefulSet"
return &p
},
},
{
name: "cleared-owner",
mutate: func(p KubernetesPod) *KubernetesPod {
p.OwnerKind = ""
p.OwnerName = ""
return &p
},
},
{
name: "changed-container-image",
mutate: func(p KubernetesPod) *KubernetesPod {
p.Containers = []KubernetesPodContainer{{Name: "c1", Image: "img1:v9"}}
return &p
},
},
{
name: "cleared-containers",
mutate: func(p KubernetesPod) *KubernetesPod {
p.Containers = nil
return &p
},
},
{
name: "changed-label-value",
mutate: func(p KubernetesPod) *KubernetesPod {
p.Labels = map[string]string{"app": "web", "team": "sre"}
return &p
},
},
{
name: "cleared-labels",
mutate: func(p KubernetesPod) *KubernetesPod {
p.Labels = nil
return &p
},
},
}
base := GenerateK8sPodFingerprint("cluster-1", basePod).Hash
for _, tc := range changeCases {
t.Run(tc.name, func(t *testing.T) {
mutated := tc.mutate(*basePod)
fp := GenerateK8sPodFingerprint("cluster-1", mutated)
if fp.Hash == base {
t.Fatalf("expected different hash after %s; both = %q", tc.name, base)
}
})
}
}
@@ -0,0 +1,501 @@
package servicediscovery
import (
"sort"
"strings"
"testing"
"time"
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
// TestDiscoveryReadinessForResource_Branches exercises the pure early-exit
// branches of (*Service).DiscoveryReadinessForResource: nil target, nil
// receiver, nil store, and unsupported resource type. The supported-type
// branch falls through into Store file I/O via GetDiscoveryByResource and is
// covered instead through the dedicated DiscoveryReadinessForTarget sibling
// tests.
func TestDiscoveryReadinessForResource_Branches(t *testing.T) {
now := time.Date(2026, 7, 18, 9, 0, 0, 0, time.UTC)
supportedTarget := &unified.DiscoveryTarget{
ResourceType: "system-container",
AgentID: "node-a",
ResourceID: "101",
}
unsupportedTarget := &unified.DiscoveryTarget{
ResourceType: "ceph",
AgentID: "cluster",
ResourceID: "fsid",
}
t.Run("nil-target-returns-unsupported", func(t *testing.T) {
var s *Service // nil receiver is fine because target short-circuits first
got := s.DiscoveryReadinessForResource(
unified.Resource{DiscoveryTarget: nil}, now,
)
if got.State != unified.ResourceDiscoveryReadinessUnsupported {
t.Fatalf("state = %q, want %q", got.State, unified.ResourceDiscoveryReadinessUnsupported)
}
if got.Source != discoveryReadinessSource {
t.Fatalf("Source = %q, want %q", got.Source, discoveryReadinessSource)
}
if !got.GeneratedAt.Equal(now) {
t.Fatalf("GeneratedAt = %v, want %v", got.GeneratedAt, now)
}
if got.ResourceType != "" || got.TargetID != "" || got.ResourceID != "" {
t.Fatalf("base fields should be empty for nil target, got %+v", got)
}
// DiscoveryReadinessForTarget returns the Unsupported state before
// populating StaleAfterSeconds; confirm the staleness slot is left at
// its zero value (which is the actual observable behaviour here).
if got.StaleAfterSeconds != 0 {
t.Fatalf("StaleAfterSeconds = %d, want 0 (nil-target path returns before staleness is set)",
got.StaleAfterSeconds)
}
// defaultDiscoveryMaxAge is the value passed into the inner call; it
// cannot be observed because the unsupported branch short-circuits, so
// assert the call still completes without populating a discovery id.
if got.DiscoveryID != "" {
t.Fatalf("DiscoveryID should be empty for nil target, got %q", got.DiscoveryID)
}
})
t.Run("nil-receiver-returns-unavailable", func(t *testing.T) {
var s *Service
got := s.DiscoveryReadinessForResource(
unified.Resource{DiscoveryTarget: supportedTarget}, now,
)
if got.State != unified.ResourceDiscoveryReadinessUnavailable {
t.Fatalf("state = %q, want %q", got.State, unified.ResourceDiscoveryReadinessUnavailable)
}
if got.Reason != "Discovery service is not configured." {
t.Fatalf("Reason = %q, want not-configured reason", got.Reason)
}
if got.ResourceType != "system-container" || got.TargetID != "node-a" || got.ResourceID != "101" {
t.Fatalf("target fields not projected: %+v", got)
}
})
t.Run("nil-store-returns-unavailable", func(t *testing.T) {
s := &Service{} // non-nil receiver but nil store
got := s.DiscoveryReadinessForResource(
unified.Resource{DiscoveryTarget: supportedTarget}, now,
)
if got.State != unified.ResourceDiscoveryReadinessUnavailable {
t.Fatalf("state = %q, want %q", got.State, unified.ResourceDiscoveryReadinessUnavailable)
}
if got.Reason != "Discovery service is not configured." {
t.Fatalf("Reason = %q, want not-configured reason", got.Reason)
}
})
t.Run("zero-now-is-normalized-to-utc", func(t *testing.T) {
var s *Service
got := s.DiscoveryReadinessForResource(
unified.Resource{DiscoveryTarget: nil}, time.Time{},
)
if got.GeneratedAt.IsZero() {
t.Fatal("zero now should be replaced with time.Now().UTC()")
}
if got.GeneratedAt.Location() != time.UTC {
t.Fatalf("GeneratedAt location = %v, want UTC", got.GeneratedAt.Location())
}
})
t.Run("unsupported-resource-type-returns-unsupported", func(t *testing.T) {
// Non-nil store with configured maxDiscoveryAge exercises the
// DiscoveryResourceTypeForTarget !ok branch without descending into
// Store file I/O. maxDiscoveryAge is consumed by the inner call but
// not observable on the projection (the unsupported branch returns
// before StaleAfterSeconds is set).
s := &Service{
store: &Store{},
maxDiscoveryAge: 7 * 24 * time.Hour,
}
got := s.DiscoveryReadinessForResource(
unified.Resource{DiscoveryTarget: unsupportedTarget}, now,
)
if got.State != unified.ResourceDiscoveryReadinessUnsupported {
t.Fatalf("state = %q, want %q", got.State, unified.ResourceDiscoveryReadinessUnsupported)
}
if got.Reason != "Service discovery does not support this resource type." {
t.Fatalf("Reason = %q, want unsupported-type reason", got.Reason)
}
// The unsupported-type branch returns from DiscoveryReadinessForTarget
// before StaleAfterSeconds is populated, so the configured maxDiscoveryAge
// is consumed by the call but not observable on the projection.
if got.StaleAfterSeconds != 0 {
t.Fatalf("StaleAfterSeconds = %d, want 0 (unsupported-type path returns before staleness is set)",
got.StaleAfterSeconds)
}
if got.DiscoveryID != "" {
t.Fatalf("DiscoveryID should be empty for unsupported target, got %q", got.DiscoveryID)
}
})
}
// TestDiscoveryReadinessUnavailableForTarget_Branches covers the unavailable
// projection: nil vs non-nil target, custom vs empty reason, and the default
// reason substitution when an empty reason is supplied.
func TestDiscoveryReadinessUnavailableForTarget_Branches(t *testing.T) {
now := time.Date(2026, 7, 18, 9, 0, 0, 0, time.UTC)
t.Run("nil-target-minimal-base", func(t *testing.T) {
got := DiscoveryReadinessUnavailableForTarget(nil, now, "custom reason")
if got.State != unified.ResourceDiscoveryReadinessUnavailable {
t.Fatalf("state = %q, want unavailable", got.State)
}
if got.Reason != "custom reason" {
t.Fatalf("Reason = %q, want custom reason", got.Reason)
}
if got.ResourceType != "" || got.TargetID != "" || got.ResourceID != "" {
t.Fatalf("base fields should be empty for nil target, got %+v", got)
}
if !got.GeneratedAt.Equal(now) {
t.Fatalf("GeneratedAt = %v, want %v", got.GeneratedAt, now)
}
})
t.Run("non-nil-target-with-custom-reason", func(t *testing.T) {
target := &unified.DiscoveryTarget{
ResourceType: "vm",
AgentID: "node-b",
ResourceID: "200",
}
got := DiscoveryReadinessUnavailableForTarget(target, now, " scanner offline ")
if got.State != unified.ResourceDiscoveryReadinessUnavailable {
t.Fatalf("state = %q, want unavailable", got.State)
}
// Reason is trimmed before assignment.
if got.Reason != "scanner offline" {
t.Fatalf("Reason = %q, want trimmed 'scanner offline'", got.Reason)
}
if got.ResourceType != "vm" || got.TargetID != "node-b" || got.ResourceID != "200" {
t.Fatalf("target fields not projected: %+v", got)
}
// Canonical vm target → discoveryID is derived.
if got.DiscoveryID != MakeResourceID(ResourceTypeVM, "node-b", "200") {
t.Fatalf("DiscoveryID = %q, want derived id", got.DiscoveryID)
}
})
t.Run("empty-reason-falls-back-to-default", func(t *testing.T) {
target := &unified.DiscoveryTarget{
ResourceType: "system-container",
AgentID: "node-c",
ResourceID: "303",
}
got := DiscoveryReadinessUnavailableForTarget(target, now, " ")
if got.Reason != "Discovery status is not available." {
t.Fatalf("Reason = %q, want default fallback", got.Reason)
}
})
t.Run("zero-now-normalized", func(t *testing.T) {
got := DiscoveryReadinessUnavailableForTarget(nil, time.Time{}, "")
if got.GeneratedAt.IsZero() || got.GeneratedAt.Location() != time.UTC {
t.Fatalf("GeneratedAt = %v, want non-zero UTC", got.GeneratedAt)
}
})
}
// TestDiscoveryReadinessReadFailureForTarget_Branches covers the read-failure
// projection for both nil and non-nil targets. The state must always be
// "failed" with the fixed read-failure reason.
func TestDiscoveryReadinessReadFailureForTarget_Branches(t *testing.T) {
now := time.Date(2026, 7, 18, 9, 0, 0, 0, time.UTC)
t.Run("nil-target", func(t *testing.T) {
got := DiscoveryReadinessReadFailureForTarget(nil, now)
if got.State != unified.ResourceDiscoveryReadinessFailed {
t.Fatalf("state = %q, want failed", got.State)
}
if got.Reason != "Discovery status could not be read." {
t.Fatalf("Reason = %q, want fixed read-failure reason", got.Reason)
}
if got.Source != discoveryReadinessSource {
t.Fatalf("Source = %q, want %q", got.Source, discoveryReadinessSource)
}
if got.ResourceType != "" || got.TargetID != "" || got.ResourceID != "" {
t.Fatalf("base fields should be empty for nil target, got %+v", got)
}
})
t.Run("non-nil-target-projects-base-fields", func(t *testing.T) {
target := &unified.DiscoveryTarget{
ResourceType: "pod",
AgentID: "cluster-x",
ResourceID: "default/web",
}
got := DiscoveryReadinessReadFailureForTarget(target, now)
if got.State != unified.ResourceDiscoveryReadinessFailed {
t.Fatalf("state = %q, want failed", got.State)
}
if got.Reason != "Discovery status could not be read." {
t.Fatalf("Reason = %q, want fixed read-failure reason", got.Reason)
}
// pod maps to ResourceTypeK8s, so DiscoveryIDForTarget derives an id.
if got.DiscoveryID != MakeResourceID(ResourceTypeK8s, "cluster-x", "default/web") {
t.Fatalf("DiscoveryID = %q, want derived k8s id", got.DiscoveryID)
}
if got.ResourceType != "pod" || got.TargetID != "cluster-x" || got.ResourceID != "default/web" {
t.Fatalf("target fields not projected: %+v", got)
}
})
}
// TestGetCommandCategories_Branches verifies GetCommandCategories returns a
// unique, sorted category set per resource type, including the empty-set case
// for an unknown type.
func TestGetCommandCategories_Branches(t *testing.T) {
// Pre-compute the expected category set per resource type by walking the
// same command list with a de-duping set + sort, independent of the
// production sort order.
expectedFor := func(rt ResourceType) []string {
cmds := GetCommandsForResource(rt)
set := make(map[string]struct{})
for _, c := range cmds {
for _, cat := range c.Categories {
set[cat] = struct{}{}
}
}
out := make([]string, 0, len(set))
for k := range set {
out = append(out, k)
}
sort.Strings(out)
return out
}
cases := []struct {
name string
rt ResourceType
}{
{"system-container", ResourceTypeSystemContainer},
{"vm", ResourceTypeVM},
{"docker", ResourceTypeDocker},
{"docker-vm", ResourceTypeDockerVM},
{"docker-system-container", ResourceTypeDockerSystemContainer},
{"k8s", ResourceTypeK8s},
{"agent", ResourceTypeAgent},
{"unknown", ResourceType("does-not-exist")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := GetCommandCategories(tc.rt)
want := expectedFor(tc.rt)
if len(got) != len(want) {
t.Fatalf("category count = %d, want %d (got=%v)", len(got), len(want), got)
}
// Assert each element matches (sorted order) — catches both
// membership and ordering bugs.
for i := range got {
if got[i] != want[i] {
t.Fatalf("category[%d] = %q, want %q (got=%v want=%v)",
i, got[i], want[i], got, want)
}
}
// Result must be sorted ascending.
if !sort.StringsAreSorted(got) {
t.Fatalf("categories not sorted: %v", got)
}
// Result must be unique.
seen := make(map[string]bool, len(got))
for _, c := range got {
if seen[c] {
t.Fatalf("duplicate category %q in %v", c, got)
}
seen[c] = true
}
})
}
// Explicit invariant: unknown type yields a non-nil empty slice so callers
// can safely iterate.
if cats := GetCommandCategories(ResourceType("nope")); len(cats) != 0 {
t.Fatalf("unknown resource type should yield no categories, got %v", cats)
}
// Cross-check a concrete known category against the agent (host) set so
// the test does not purely mirror production code — "version" must appear
// because getHostCommands includes os_release/proxmox_version commands.
agentCats := GetCommandCategories(ResourceTypeAgent)
if !containsString(agentCats, "version") {
t.Fatalf("agent categories missing expected 'version': %v", agentCats)
}
if !containsString(agentCats, "hardware") {
t.Fatalf("agent categories missing expected 'hardware': %v", agentCats)
}
}
func containsString(haystack []string, needle string) bool {
for _, h := range haystack {
if h == needle {
return true
}
}
return false
}
// TestIsSchemaOutdated_Branches covers the three meaningful arms of the
// schema-version comparison: older (true), current (false), and future
// (false — forward-compatible).
func TestIsSchemaOutdated_Branches(t *testing.T) {
t.Run("zero-schema-outdated", func(t *testing.T) {
fp := &ContainerFingerprint{SchemaVersion: 0}
if !fp.IsSchemaOutdated() {
t.Fatal("SchemaVersion=0 should be outdated")
}
})
t.Run("older-schema-outdated", func(t *testing.T) {
fp := &ContainerFingerprint{SchemaVersion: FingerprintSchemaVersion - 1}
if !fp.IsSchemaOutdated() {
t.Fatalf("SchemaVersion=%d (current-1) should be outdated", FingerprintSchemaVersion-1)
}
})
t.Run("current-schema-not-outdated", func(t *testing.T) {
fp := &ContainerFingerprint{SchemaVersion: FingerprintSchemaVersion}
if fp.IsSchemaOutdated() {
t.Fatalf("SchemaVersion=%d == current should not be outdated", FingerprintSchemaVersion)
}
})
t.Run("future-schema-not-outdated", func(t *testing.T) {
fp := &ContainerFingerprint{SchemaVersion: FingerprintSchemaVersion + 1}
if fp.IsSchemaOutdated() {
t.Fatalf("SchemaVersion=%d (future) should not be outdated", FingerprintSchemaVersion+1)
}
})
}
// TestNormalizeDeepScanTimeout_Branches verifies the clamp/return behavior for
// positive, zero, and negative inputs.
func TestNormalizeDeepScanTimeout_Branches(t *testing.T) {
t.Run("positive-returned-as-is", func(t *testing.T) {
in := 90 * time.Second
if got := normalizeDeepScanTimeout(in); got != in {
t.Fatalf("normalizeDeepScanTimeout(%v) = %v, want %v", in, got, in)
}
})
t.Run("positive-small-value-returned-as-is", func(t *testing.T) {
// Positive but below the default is still honored — only non-positive
// triggers the default.
in := 1 * time.Millisecond
if got := normalizeDeepScanTimeout(in); got != in {
t.Fatalf("normalizeDeepScanTimeout(%v) = %v, want %v", in, got, in)
}
})
t.Run("zero-falls-back-to-default", func(t *testing.T) {
if got := normalizeDeepScanTimeout(0); got != defaultDiscoveryScanTimeout {
t.Fatalf("normalizeDeepScanTimeout(0) = %v, want default %v",
got, defaultDiscoveryScanTimeout)
}
})
t.Run("negative-falls-back-to-default", func(t *testing.T) {
in := -5 * time.Second
if got := normalizeDeepScanTimeout(in); got != defaultDiscoveryScanTimeout {
t.Fatalf("normalizeDeepScanTimeout(%v) = %v, want default %v",
in, got, defaultDiscoveryScanTimeout)
}
})
}
// TestFormatURLSuggestionDiagnostic_Branches covers each combination of
// primary/fallback code/detail, including the all-empty sentinel return.
func TestFormatURLSuggestionDiagnostic_Branches(t *testing.T) {
cases := []struct {
name string
primaryCode string
primaryDetail string
fallbackCode string
fallbackDetail string
want string
wantSubstrings []string
}{
{
name: "all-empty-sentinel",
want: "no suggestion diagnostics available",
},
{
name: "primary-only-with-detail",
primaryCode: "port_match",
primaryDetail: "matched 8123/tcp",
want: "primary=port_match (matched 8123/tcp)",
},
{
name: "primary-only-no-detail",
primaryCode: "port_match",
want: "primary=port_match",
},
{
name: "fallback-only-with-detail",
fallbackCode: "service_type_default",
fallbackDetail: "default for homeassistant",
want: "fallback=service_type_default (default for homeassistant)",
},
{
name: "fallback-only-no-detail",
fallbackCode: "service_type_default",
want: "fallback=service_type_default",
},
{
name: "both-with-details-joined-by-semicolon",
primaryCode: "port_match",
primaryDetail: "8123/tcp",
fallbackCode: "service_type_default",
fallbackDetail: "homeassistant",
want: "primary=port_match (8123/tcp); fallback=service_type_default (homeassistant)",
},
{
name: "both-without-details",
primaryCode: "port_match",
fallbackCode: "service_type_default",
want: "primary=port_match; fallback=service_type_default",
},
{
name: "primary-empty-fallback-present",
fallbackCode: "service_type_default",
want: "fallback=service_type_default",
},
{
name: "primary-empty-fallback-with-detail",
fallbackCode: "service_type_default",
fallbackDetail: "x",
want: "fallback=service_type_default (x)",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := formatURLSuggestionDiagnostic(
tc.primaryCode, tc.primaryDetail,
tc.fallbackCode, tc.fallbackDetail,
)
if tc.want != "" && got != tc.want {
t.Fatalf("formatURLSuggestionDiagnostic(...) = %q, want %q",
got, tc.want)
}
for _, sub := range tc.wantSubstrings {
if !strings.Contains(got, sub) {
t.Fatalf("expected substring %q in %q", sub, got)
}
}
})
}
// Explicit invariant: primary must always precede fallback when both set.
both := formatURLSuggestionDiagnostic("p_code", "p_detail", "f_code", "f_detail")
if !strings.HasPrefix(both, "primary=") {
t.Fatalf("expected primary= prefix, got %q", both)
}
if !strings.Contains(both, "; fallback=") {
t.Fatalf("expected '; fallback=' separator, got %q", both)
}
}
@@ -0,0 +1,570 @@
package storagehealth
import (
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
// Pointer helpers keep malformed-input branches explicit at the call site.
func int64Ptr(v int64) *int64 { return &v }
func intPtr(v int) *int { return &v }
// reasonSeverity returns the severity for the first reason matching code, or
// the zero RiskLevel when no such reason exists. Used to assert both the
// presence of a reason and its severity in a single check.
func reasonSeverity(assessment Assessment, code string) (RiskLevel, bool) {
for _, r := range assessment.Reasons {
if r.Code == code {
return r.Severity, true
}
}
return "", false
}
// --- AssessPhysicalDisk: branch coverage over risk.go:48-88 ---
func TestAssessPhysicalDisk_Branches(t *testing.T) {
tests := []struct {
name string
disk models.PhysicalDisk
wantLevel RiskLevel
wantReasons []string // codes that MUST be present (independent of order)
}{
{
// nil SmartAttributes arm: every *int64 pointer is skipped; only
// Model/Health/Temperature/Wearout are sourced from the disk.
name: "healthy disk with nil smart attributes stays healthy",
disk: models.PhysicalDisk{Model: "Crucial MX500", Health: "PASSED", Temperature: 35, Wearout: 80},
wantLevel: RiskHealthy,
},
{
// non-nil SmartAttributes arm with all pointers set but all benign:
// proves every `attrs.X != nil` branch executes without escalating.
name: "healthy disk with benign smart attributes stays healthy",
disk: models.PhysicalDisk{
Health: "PASSED", Temperature: 35, Wearout: 80,
SmartAttributes: &models.SMARTAttributes{
PowerOnHours: int64Ptr(1000),
PowerCycles: int64Ptr(50),
ReallocatedSectors: int64Ptr(0),
PendingSectors: int64Ptr(0),
OfflineUncorrectable: int64Ptr(0),
UDMACRCErrors: int64Ptr(0),
PercentageUsed: intPtr(10),
AvailableSpare: intPtr(90),
MediaErrors: int64Ptr(0),
UnsafeShutdowns: int64Ptr(0),
},
},
wantLevel: RiskHealthy,
},
{
// FAILED health (no firmware-bug model) -> critical health_status.
name: "failed health escalates to critical",
disk: models.PhysicalDisk{Model: "Crucial MX500", Health: "FAILED"},
wantLevel: RiskCritical,
wantReasons: []string{"health_status"},
},
{
// Pending sectors via SMART attributes -> critical.
name: "pending sectors via smart attributes critical",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
PendingSectors: int64Ptr(5),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"pending_sectors"},
},
{
// Offline uncorrectable via SMART attributes -> critical.
name: "offline uncorrectable via smart attributes critical",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
OfflineUncorrectable: int64Ptr(2),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"offline_uncorrectable"},
},
{
// Media errors via SMART attributes -> critical.
name: "media errors via smart attributes critical",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
MediaErrors: int64Ptr(1),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"media_errors"},
},
{
// Wearout <= 5 (sourced from disk.Wearout directly) -> critical.
name: "wearout at critical threshold",
disk: models.PhysicalDisk{Health: "PASSED", Wearout: 3},
wantLevel: RiskCritical,
wantReasons: []string{"wearout_low"},
},
{
// Wearout 6..9 (warning band) -> warning.
name: "wearout at warning threshold",
disk: models.PhysicalDisk{Health: "PASSED", Wearout: 8},
wantLevel: RiskWarning,
wantReasons: []string{"wearout_low"},
},
{
// Available spare <= 10 via SMART -> critical.
name: "nvme available spare critical",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
AvailableSpare: intPtr(8),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"nvme_available_spare_low"},
},
{
// Available spare 11..19 via SMART -> warning.
name: "nvme available spare warning",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
AvailableSpare: intPtr(15),
},
},
wantLevel: RiskWarning,
wantReasons: []string{"nvme_available_spare_low"},
},
{
// PercentageUsed >= 95 via SMART -> critical.
name: "nvme percentage used critical",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
PercentageUsed: intPtr(97),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"nvme_percentage_used_high"},
},
{
// PercentageUsed 90..94 via SMART -> warning.
name: "nvme percentage used warning",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
PercentageUsed: intPtr(92),
},
},
wantLevel: RiskWarning,
wantReasons: []string{"nvme_percentage_used_high"},
},
{
// Temperature >= 70 -> critical.
name: "temperature critical",
disk: models.PhysicalDisk{Health: "PASSED", Temperature: 72},
wantLevel: RiskCritical,
wantReasons: []string{"temperature_high"},
},
{
// Temperature 60..69 -> warning.
name: "temperature warning",
disk: models.PhysicalDisk{Health: "PASSED", Temperature: 63},
wantLevel: RiskWarning,
wantReasons: []string{"temperature_high"},
},
{
// Reallocated sectors > 0 via SMART -> warning.
name: "reallocated sectors warning",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
ReallocatedSectors: int64Ptr(4),
},
},
wantLevel: RiskWarning,
wantReasons: []string{"reallocated_sectors"},
},
{
// UDMA CRC errors > 0 via SMART -> monitor.
name: "udma crc errors monitor",
disk: models.PhysicalDisk{
Health: "PASSED",
SmartAttributes: &models.SMARTAttributes{
UDMACRCErrors: int64Ptr(10),
},
},
wantLevel: RiskMonitor,
wantReasons: []string{"crc_errors"},
},
{
// Samsung 980 firmware-bug model suppresses FAILED health_status.
name: "samsung 980 firmware bug suppresses failed health",
disk: models.PhysicalDisk{Model: "Samsung SSD 980 Pro", Health: "FAILED"},
wantLevel: RiskHealthy,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := AssessPhysicalDisk(tt.disk)
if got.Level != tt.wantLevel {
t.Fatalf("AssessPhysicalDisk level = %q, want %q; reasons=%+v", got.Level, tt.wantLevel, got.Reasons)
}
for _, code := range tt.wantReasons {
if _, ok := reasonSeverity(got, code); !ok {
t.Errorf("expected reason %q present, got reasons=%+v", code, got.Reasons)
}
}
})
}
}
// TestAssessPhysicalDisk_NilSmartAttributesDoesNotPanic pins the nil-SmartAttributes
// branch in isolation: a disk with zero-valued fields must not deref nil pointers.
func TestAssessPhysicalDisk_NilSmartAttributesDoesNotPanic(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("AssessPhysicalDisk panicked on nil SmartAttributes: %v", r)
}
}()
got := AssessPhysicalDisk(models.PhysicalDisk{})
if got.Level != RiskHealthy {
t.Fatalf("empty disk level = %q, want %q", got.Level, RiskHealthy)
}
if len(got.Reasons) != 0 {
t.Fatalf("empty disk should produce no reasons, got %+v", got.Reasons)
}
}
// TestAssessPhysicalDisk_MultipleReasonsTakeHighest drives several conditions
// at once and asserts both the merged level (critical) and that every reason
// is preserved through the SMART-attribute mapping.
func TestAssessPhysicalDisk_MultipleReasonsTakeHighest(t *testing.T) {
got := AssessPhysicalDisk(models.PhysicalDisk{
Health: "PASSED",
Temperature: 63, // warning
SmartAttributes: &models.SMARTAttributes{
PendingSectors: int64Ptr(1), // critical
UDMACRCErrors: int64Ptr(2), // monitor
},
})
if got.Level != RiskCritical {
t.Fatalf("expected critical (highest), got %q; reasons=%+v", got.Level, got.Reasons)
}
if len(got.Reasons) != 3 {
t.Fatalf("expected 3 reasons (temperature_high, pending_sectors, crc_errors), got %d: %+v", len(got.Reasons), got.Reasons)
}
for _, code := range []string{"pending_sectors", "temperature_high", "crc_errors"} {
if _, ok := reasonSeverity(got, code); !ok {
t.Errorf("expected reason %q present, got %+v", code, got.Reasons)
}
}
}
// --- AssessHostSMARTDisk: branch coverage over risk.go:90-131 ---
//
// AssessHostSMARTDisk differs from AssessPhysicalDisk in two ways:
// - Wearout is initialised to -1 (so the wearout branches only fire when
// PercentageUsed is supplied and derives Wearout into a positive band).
// - When PercentageUsed is set, Wearout is computed as 100 - PercentageUsed.
func TestAssessHostSMARTDisk_Branches(t *testing.T) {
tests := []struct {
name string
disk models.HostDiskSMART
wantLevel RiskLevel
wantReasons []string
}{
{
// nil Attributes arm: only Model/Health/Temperature are sourced;
// Wearout stays -1 so the wearout branch is skipped.
name: "nil attributes stays healthy",
disk: models.HostDiskSMART{Model: "WD Blue", Health: "PASSED", Temperature: 35},
wantLevel: RiskHealthy,
},
{
// non-nil Attributes arm with benign values: every pointer is read
// but none escalate. Proves nil-deref safety for the SMART path.
name: "benign attributes stays healthy",
disk: models.HostDiskSMART{
Health: "PASSED", Temperature: 35,
Attributes: &models.SMARTAttributes{
PowerOnHours: int64Ptr(120),
PowerCycles: int64Ptr(8),
ReallocatedSectors: int64Ptr(0),
PendingSectors: int64Ptr(0),
OfflineUncorrectable: int64Ptr(0),
UDMACRCErrors: int64Ptr(0),
PercentageUsed: intPtr(20), // Wearout = 80, healthy
AvailableSpare: intPtr(99),
MediaErrors: int64Ptr(0),
UnsafeShutdowns: int64Ptr(0),
},
},
wantLevel: RiskHealthy,
},
{
// FAILED health on a host SMART disk -> critical health_status.
name: "failed health critical",
disk: models.HostDiskSMART{Model: "WD Blue", Health: "FAILED"},
wantLevel: RiskCritical,
wantReasons: []string{"health_status"},
},
{
// Pending sectors via host SMART attributes -> critical.
name: "pending sectors critical",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
PendingSectors: int64Ptr(3),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"pending_sectors"},
},
{
// Offline uncorrectable via host SMART attributes -> critical.
name: "offline uncorrectable critical",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
OfflineUncorrectable: int64Ptr(1),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"offline_uncorrectable"},
},
{
// Media errors via host SMART attributes -> critical.
name: "media errors critical",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
MediaErrors: int64Ptr(2),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"media_errors"},
},
{
// PercentageUsed >= 95 derives Wearout <= 5: BOTH the percentage-used
// critical reason AND the wearout_low critical reason should fire.
name: "percentage used critical derives wearout critical",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
PercentageUsed: intPtr(97), // Wearout = 3 -> critical
},
},
wantLevel: RiskCritical,
wantReasons: []string{"nvme_percentage_used_high", "wearout_low"},
},
{
// PercentageUsed 90..94 derives Wearout 6..9: both reasons at warning.
name: "percentage used warning derives wearout warning",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
PercentageUsed: intPtr(92), // Wearout = 8 -> warning
},
},
wantLevel: RiskWarning,
wantReasons: []string{"nvme_percentage_used_high", "wearout_low"},
},
{
// Available spare <= 10 via host SMART -> critical.
name: "nvme available spare critical",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
AvailableSpare: intPtr(7),
},
},
wantLevel: RiskCritical,
wantReasons: []string{"nvme_available_spare_low"},
},
{
// Available spare 11..19 via host SMART -> warning.
name: "nvme available spare warning",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
AvailableSpare: intPtr(15),
},
},
wantLevel: RiskWarning,
wantReasons: []string{"nvme_available_spare_low"},
},
{
// Temperature >= 70 -> critical.
name: "temperature critical",
disk: models.HostDiskSMART{Health: "PASSED", Temperature: 75},
wantLevel: RiskCritical,
wantReasons: []string{"temperature_high"},
},
{
// Temperature 60..69 -> warning.
name: "temperature warning",
disk: models.HostDiskSMART{Health: "PASSED", Temperature: 60},
wantLevel: RiskWarning,
wantReasons: []string{"temperature_high"},
},
{
// Reallocated sectors > 0 via host SMART -> warning.
name: "reallocated sectors warning",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
ReallocatedSectors: int64Ptr(6),
},
},
wantLevel: RiskWarning,
wantReasons: []string{"reallocated_sectors"},
},
{
// UDMA CRC errors > 0 via host SMART -> monitor.
name: "udma crc errors monitor",
disk: models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{
UDMACRCErrors: int64Ptr(11),
},
},
wantLevel: RiskMonitor,
wantReasons: []string{"crc_errors"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := AssessHostSMARTDisk(tt.disk)
if got.Level != tt.wantLevel {
t.Fatalf("AssessHostSMARTDisk level = %q, want %q; reasons=%+v", got.Level, tt.wantLevel, got.Reasons)
}
for _, code := range tt.wantReasons {
if _, ok := reasonSeverity(got, code); !ok {
t.Errorf("expected reason %q present, got %+v", code, got.Reasons)
}
}
})
}
}
// TestAssessHostSMARTDisk_NilAttributesDoesNotPanic pins the nil-Attributes
// branch in isolation; with default Wearout=-1, no wearout reason fires.
func TestAssessHostSMARTDisk_NilAttributesDoesNotPanic(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("AssessHostSMARTDisk panicked on nil Attributes: %v", r)
}
}()
got := AssessHostSMARTDisk(models.HostDiskSMART{})
if got.Level != RiskHealthy {
t.Fatalf("empty disk level = %q, want %q", got.Level, RiskHealthy)
}
if len(got.Reasons) != 0 {
t.Fatalf("empty disk should produce no reasons, got %+v", got.Reasons)
}
// Sanity: when Wearout is computed from a default -1 (no PercentageUsed),
// the wearout_low branch must not fire even with attributes present.
got2 := AssessHostSMARTDisk(models.HostDiskSMART{
Health: "PASSED",
Attributes: &models.SMARTAttributes{PowerOnHours: int64Ptr(100)},
})
if _, ok := reasonSeverity(got2, "wearout_low"); ok {
t.Fatalf("wearout_low must not fire when Wearout is negative")
}
}
// --- SummarizeAssessments: branch coverage over topology.go:335-345 ---
func TestSummarizeAssessments(t *testing.T) {
t.Run("empty returns healthy with no reasons", func(t *testing.T) {
got := SummarizeAssessments()
if got.Level != RiskHealthy {
t.Fatalf("level = %q, want %q", got.Level, RiskHealthy)
}
if len(got.Reasons) != 0 {
t.Fatalf("expected no reasons, got %+v", got.Reasons)
}
})
t.Run("single assessment passes through level", func(t *testing.T) {
in := Assessment{
Level: RiskWarning,
Reasons: []Reason{
{Code: "a", Severity: RiskWarning, Summary: "a summary"},
},
}
got := SummarizeAssessments(in)
if got.Level != RiskWarning {
t.Fatalf("level = %q, want %q", got.Level, RiskWarning)
}
if len(got.Reasons) != 1 || got.Reasons[0].Code != "a" {
t.Fatalf("reasons not preserved, got %+v", got.Reasons)
}
})
t.Run("multiple differing severities picks highest", func(t *testing.T) {
healthy := Assessment{Level: RiskHealthy, Reasons: []Reason{{Code: "h", Severity: RiskHealthy, Summary: "h"}}}
monitor := Assessment{Level: RiskMonitor, Reasons: []Reason{{Code: "m", Severity: RiskMonitor, Summary: "m"}}}
warning := Assessment{Level: RiskWarning, Reasons: []Reason{{Code: "w", Severity: RiskWarning, Summary: "w"}}}
critical := Assessment{Level: RiskCritical, Reasons: []Reason{{Code: "c", Severity: RiskCritical, Summary: "c"}}}
got := SummarizeAssessments(healthy, monitor, warning, critical)
if got.Level != RiskCritical {
t.Fatalf("level = %q, want %q (highest of inputs)", got.Level, RiskCritical)
}
// All reasons must be merged through.
if len(got.Reasons) != 4 {
t.Fatalf("expected 4 merged reasons, got %d: %+v", len(got.Reasons), got.Reasons)
}
gotCodes := map[string]bool{}
for _, r := range got.Reasons {
gotCodes[r.Code] = true
}
for _, want := range []string{"h", "m", "w", "c"} {
if !gotCodes[want] {
t.Errorf("expected merged reason %q present, got %+v", want, got.Reasons)
}
}
})
t.Run("reasons sorted by severity descending then code", func(t *testing.T) {
// Two critical-severity reasons with codes "b" and "a" (so secondary
// sort by code is exercised) plus a warning reason.
in := Assessment{
Level: RiskCritical,
Reasons: []Reason{
{Code: "b", Severity: RiskCritical, Summary: "b"},
{Code: "a", Severity: RiskCritical, Summary: "a"},
{Code: "z", Severity: RiskWarning, Summary: "z"},
},
}
got := SummarizeAssessments(in)
if len(got.Reasons) != 3 {
t.Fatalf("expected 3 reasons, got %d: %+v", len(got.Reasons), got.Reasons)
}
// Criticals first (severity desc), and within equal severity codes ascending.
wantOrder := []string{"a", "b", "z"}
for i, want := range wantOrder {
if got.Reasons[i].Code != want {
t.Errorf("reasons[%d].Code = %q, want %q (full: %+v)", i, got.Reasons[i].Code, want, got.Reasons)
}
}
})
t.Run("healthy inputs dominate only when no higher severity present", func(t *testing.T) {
// All inputs healthy -> summary healthy.
got := SummarizeAssessments(Assessment{Level: RiskHealthy}, Assessment{Level: RiskHealthy})
if got.Level != RiskHealthy {
t.Fatalf("level = %q, want %q", got.Level, RiskHealthy)
}
})
}
@@ -0,0 +1,397 @@
package telemetry
import (
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
// fixedCutoff is the `since` boundary used across these tests; events strictly
// before it must be filtered out, events at-or-after it must be projected.
var fixedCutoff = time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
func TestPulseIntelligenceAIUsageEvidenceFromHistory(t *testing.T) {
// Anchor timestamps relative to the cutoff.
before := fixedCutoff.Add(-24 * time.Hour) // excluded by cutoff
atCutoff := fixedCutoff // included (not Before(since) and not zero)
after := fixedCutoff.Add(24 * time.Hour) // included
tests := []struct {
name string
history *config.AIUsageHistoryData
want PulseIntelligenceAIUsageEvidence
}{
{
name: "nil history returns zero evidence",
history: nil,
want: PulseIntelligenceAIUsageEvidence{},
},
{
name: "empty events returns zero evidence",
history: &config.AIUsageHistoryData{},
want: PulseIntelligenceAIUsageEvidence{},
},
{
name: "event before cutoff is excluded",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: before, UseCase: "chat"},
},
},
want: PulseIntelligenceAIUsageEvidence{},
},
{
name: "zero timestamp event is excluded",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: time.Time{}, UseCase: "chat"},
},
},
want: PulseIntelligenceAIUsageEvidence{},
},
{
name: "event at cutoff is included (Before is strict)",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: atCutoff, UseCase: "patrol"},
},
},
want: PulseIntelligenceAIUsageEvidence{PatrolAICalls: 1},
},
{
name: "plain chat after cutoff increments AssistantAICalls only",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: after, UseCase: "chat"},
},
},
want: PulseIntelligenceAIUsageEvidence{AssistantAICalls: 1},
},
{
name: "chat with governed context scopes increments context counter",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: after, UseCase: "chat", ContextScope: "fleet"},
},
},
want: PulseIntelligenceAIUsageEvidence{AssistantAICalls: 1, AssistantContextAICalls: 1},
},
{
name: "chat with TargetType/TargetID/FindingID all trigger context detection",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: after, UseCase: "chat", TargetType: "vm"},
{Timestamp: after, UseCase: "chat", TargetID: "node-1"},
{Timestamp: after, UseCase: "chat", FindingID: "F-42"},
},
},
want: PulseIntelligenceAIUsageEvidence{AssistantAICalls: 3, AssistantContextAICalls: 3},
},
{
name: "whitespace-only context fields do not count as context",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: after, UseCase: "chat", ContextScope: " "},
},
},
want: PulseIntelligenceAIUsageEvidence{AssistantAICalls: 1},
},
{
name: "chat ToolCallCount is summed across calls",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: after, UseCase: "chat", ToolCallCount: 3},
{Timestamp: after, UseCase: "chat", ToolCallCount: 2},
{Timestamp: after, UseCase: "chat"}, // zero ToolCallCount does not add
},
},
want: PulseIntelligenceAIUsageEvidence{AssistantAICalls: 3, AssistantToolCalls: 5},
},
{
name: "patrol use case increments PatrolAICalls only",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: after, UseCase: "patrol", ToolCallCount: 9, ContextScope: "fleet"},
},
},
want: PulseIntelligenceAIUsageEvidence{PatrolAICalls: 1},
},
{
name: "use case is case-insensitive and trimmed",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: after, UseCase: " CHAT "},
{Timestamp: after, UseCase: "PaTrOl"},
},
},
want: PulseIntelligenceAIUsageEvidence{AssistantAICalls: 1, PatrolAICalls: 1},
},
{
name: "unknown use case is ignored",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: after, UseCase: "summarize"},
},
},
want: PulseIntelligenceAIUsageEvidence{},
},
{
name: "mixed inclusion: before-cutoff filtered, after-cutoff projected",
history: &config.AIUsageHistoryData{
Events: []config.AIUsageEventRecord{
{Timestamp: before, UseCase: "chat", ContextScope: "fleet", ToolCallCount: 100},
{Timestamp: after, UseCase: "chat", ContextScope: "host", ToolCallCount: 2},
{Timestamp: after, UseCase: "patrol"},
},
},
want: PulseIntelligenceAIUsageEvidence{
AssistantAICalls: 1,
AssistantContextAICalls: 1,
AssistantToolCalls: 2,
PatrolAICalls: 1,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PulseIntelligenceAIUsageEvidenceFromHistory(tt.history, fixedCutoff)
if got != tt.want {
t.Fatalf("PulseIntelligenceAIUsageEvidenceFromHistory() = %+v, want %+v", got, tt.want)
}
})
}
}
func TestPulseIntelligenceExternalAgentActivitySurface(t *testing.T) {
tests := []struct {
name string
surface string
want bool
}{
{"agent_api", config.ExternalAgentActivitySurfaceAgentAPI, true},
{"pulse_mcp", config.ExternalAgentActivitySurfacePulseMCP, true},
{"whitespace wrapped agent_api", " " + config.ExternalAgentActivitySurfaceAgentAPI + " ", true},
{"unknown surface", "webhook", false},
{"empty surface", "", false},
{"whitespace only", " ", false},
{"workflow prompt agent_api constant shares the same surface value", config.WorkflowPromptActivitySurfaceAgentAPI, true}, // value is "agent_api"
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := PulseIntelligenceExternalAgentActivitySurface(tt.surface); got != tt.want {
t.Fatalf("PulseIntelligenceExternalAgentActivitySurface(%q) = %v, want %v", tt.surface, got, tt.want)
}
})
}
}
func TestPulseIntelligenceExternalAgentEvidence_CollaborationActive(t *testing.T) {
tests := []struct {
name string
e PulseIntelligenceExternalAgentEvidence
want bool
}{
{name: "zero evidence inactive", want: false},
{name: "Used flag active", e: PulseIntelligenceExternalAgentEvidence{Used: true}, want: true},
{name: "MCPAdapterUsed active", e: PulseIntelligenceExternalAgentEvidence{MCPAdapterUsed: true}, want: true},
{name: "ContextRequests active", e: PulseIntelligenceExternalAgentEvidence{ContextRequests: 1}, want: true},
{name: "EventStreamRequests active", e: PulseIntelligenceExternalAgentEvidence{EventStreamRequests: 1}, want: true},
{name: "ProvisioningRequests active", e: PulseIntelligenceExternalAgentEvidence{ProvisioningRequests: 1}, want: true},
{name: "OperatorStateRequests active", e: PulseIntelligenceExternalAgentEvidence{OperatorStateRequests: 1}, want: true},
{name: "FindingRequests active", e: PulseIntelligenceExternalAgentEvidence{FindingRequests: 1}, want: true},
{name: "ActionRequests active", e: PulseIntelligenceExternalAgentEvidence{ActionRequests: 1}, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.e.CollaborationActive(); got != tt.want {
t.Fatalf("CollaborationActive() = %v, want %v", got, tt.want)
}
})
}
}
func TestPulseIntelligenceExternalAgentEvidence_CollaborationCount(t *testing.T) {
tests := []struct {
name string
e PulseIntelligenceExternalAgentEvidence
want int
}{
{name: "zero evidence count is zero", want: 0},
{name: "request counts sum to total", e: PulseIntelligenceExternalAgentEvidence{
ContextRequests: 2, EventStreamRequests: 3, ProvisioningRequests: 1,
OperatorStateRequests: 1, FindingRequests: 1, ActionRequests: 2,
}, want: 10},
{name: "Used only collapses to coarse count 1", e: PulseIntelligenceExternalAgentEvidence{Used: true}, want: 1},
{name: "MCPAdapterUsed only collapses to coarse count 1", e: PulseIntelligenceExternalAgentEvidence{MCPAdapterUsed: true}, want: 1},
{name: "request counts take precedence over coarse Used flag", e: PulseIntelligenceExternalAgentEvidence{
Used: true, ContextRequests: 4,
}, want: 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.e.CollaborationCount(); got != tt.want {
t.Fatalf("CollaborationCount() = %d, want %d", got, tt.want)
}
})
}
}
func TestPulseIntelligenceExternalAgentEvidence_ApplyActivity(t *testing.T) {
tests := []struct {
name string
activity string
want PulseIntelligenceExternalAgentEvidence
}{
{"resource_context bumps ContextRequests", config.ExternalAgentActivityResourceContext, PulseIntelligenceExternalAgentEvidence{ContextRequests: 1}},
{"fleet_context bumps ContextRequests", config.ExternalAgentActivityFleetContext, PulseIntelligenceExternalAgentEvidence{ContextRequests: 1}},
{"event_stream bumps EventStreamRequests", config.ExternalAgentActivityEventStream, PulseIntelligenceExternalAgentEvidence{EventStreamRequests: 1}},
{"provisioning bumps ProvisioningRequests", config.ExternalAgentActivityProvisioning, PulseIntelligenceExternalAgentEvidence{ProvisioningRequests: 1}},
{"operator_state bumps OperatorStateRequests", config.ExternalAgentActivityOperatorState, PulseIntelligenceExternalAgentEvidence{OperatorStateRequests: 1}},
{"finding_list bumps FindingRequests", config.ExternalAgentActivityFindingList, PulseIntelligenceExternalAgentEvidence{FindingRequests: 1}},
{"finding_decision bumps FindingRequests", config.ExternalAgentActivityFindingDecision, PulseIntelligenceExternalAgentEvidence{FindingRequests: 1}},
{"action_plan bumps ActionRequests", config.ExternalAgentActivityActionPlan, PulseIntelligenceExternalAgentEvidence{ActionRequests: 1}},
{"action_decision bumps ActionRequests", config.ExternalAgentActivityActionDecision, PulseIntelligenceExternalAgentEvidence{ActionRequests: 1}},
{"action_execute bumps ActionRequests", config.ExternalAgentActivityActionExecute, PulseIntelligenceExternalAgentEvidence{ActionRequests: 1}},
{"whitespace wrapped activity still matches", " " + config.ExternalAgentActivityEventStream + " ", PulseIntelligenceExternalAgentEvidence{EventStreamRequests: 1}},
{"unknown activity is a no-op", "nope", PulseIntelligenceExternalAgentEvidence{}},
{"empty activity is a no-op", "", PulseIntelligenceExternalAgentEvidence{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var e PulseIntelligenceExternalAgentEvidence
e.ApplyActivity(tt.activity)
if e != tt.want {
t.Fatalf("ApplyActivity(%q) -> %+v, want %+v", tt.activity, e, tt.want)
}
})
}
t.Run("nil receiver does not panic", func(t *testing.T) {
var e *PulseIntelligenceExternalAgentEvidence
// Must not panic; the guard is the only behaviour observable here.
e.ApplyActivity(config.ExternalAgentActivityEventStream)
})
t.Run("repeated apply accumulates across buckets", func(t *testing.T) {
var e PulseIntelligenceExternalAgentEvidence
e.ApplyActivity(config.ExternalAgentActivityResourceContext)
e.ApplyActivity(config.ExternalAgentActivityFleetContext)
e.ApplyActivity(config.ExternalAgentActivityActionPlan)
e.ApplyActivity(config.ExternalAgentActivityActionExecute)
want := PulseIntelligenceExternalAgentEvidence{ContextRequests: 2, ActionRequests: 2}
if e != want {
t.Fatalf("accumulated evidence = %+v, want %+v", e, want)
}
})
}
func TestPulseIntelligenceExternalAgentEvidenceFromHistory(t *testing.T) {
before := fixedCutoff.Add(-24 * time.Hour)
atCutoff := fixedCutoff
after := fixedCutoff.Add(24 * time.Hour)
tests := []struct {
name string
history *config.ExternalAgentActivityHistoryData
want PulseIntelligenceExternalAgentEvidence
}{
{
name: "nil history returns zero evidence",
history: nil,
want: PulseIntelligenceExternalAgentEvidence{},
},
{
name: "empty events returns zero evidence",
history: &config.ExternalAgentActivityHistoryData{},
want: PulseIntelligenceExternalAgentEvidence{},
},
{
name: "event before cutoff is excluded",
history: &config.ExternalAgentActivityHistoryData{
Events: []config.ExternalAgentActivityRecord{
{Timestamp: before, Surface: config.ExternalAgentActivitySurfaceAgentAPI, Activity: config.ExternalAgentActivityEventStream},
},
},
want: PulseIntelligenceExternalAgentEvidence{},
},
{
name: "zero timestamp event is excluded",
history: &config.ExternalAgentActivityHistoryData{
Events: []config.ExternalAgentActivityRecord{
{Timestamp: time.Time{}, Surface: config.ExternalAgentActivitySurfaceAgentAPI, Activity: config.ExternalAgentActivityEventStream},
},
},
want: PulseIntelligenceExternalAgentEvidence{},
},
{
name: "unknown surface is filtered (Used stays false)",
history: &config.ExternalAgentActivityHistoryData{
Events: []config.ExternalAgentActivityRecord{
{Timestamp: after, Surface: "webhook", Activity: config.ExternalAgentActivityEventStream},
},
},
want: PulseIntelligenceExternalAgentEvidence{},
},
{
name: "agent_api surface marks Used but not MCPAdapterUsed",
history: &config.ExternalAgentActivityHistoryData{
Events: []config.ExternalAgentActivityRecord{
{Timestamp: after, Surface: config.ExternalAgentActivitySurfaceAgentAPI, Activity: config.ExternalAgentActivityEventStream},
},
},
want: PulseIntelligenceExternalAgentEvidence{Used: true, EventStreamRequests: 1},
},
{
name: "pulse_mcp surface marks Used and MCPAdapterUsed",
history: &config.ExternalAgentActivityHistoryData{
Events: []config.ExternalAgentActivityRecord{
{Timestamp: after, Surface: config.ExternalAgentActivitySurfacePulseMCP, Activity: config.ExternalAgentActivityResourceContext},
},
},
want: PulseIntelligenceExternalAgentEvidence{Used: true, MCPAdapterUsed: true, ContextRequests: 1},
},
{
name: "at-cutoff event is included (Before is strict)",
history: &config.ExternalAgentActivityHistoryData{
Events: []config.ExternalAgentActivityRecord{
{Timestamp: atCutoff, Surface: config.ExternalAgentActivitySurfaceAgentAPI, Activity: config.ExternalAgentActivityActionPlan},
},
},
want: PulseIntelligenceExternalAgentEvidence{Used: true, ActionRequests: 1},
},
{
name: "mixed events: before excluded, unknown surface filtered, multiple buckets accumulate",
history: &config.ExternalAgentActivityHistoryData{
Events: []config.ExternalAgentActivityRecord{
{Timestamp: before, Surface: config.ExternalAgentActivitySurfacePulseMCP, Activity: config.ExternalAgentActivityResourceContext},
{Timestamp: after, Surface: "intranet", Activity: config.ExternalAgentActivityEventStream},
{Timestamp: after, Surface: config.ExternalAgentActivitySurfaceAgentAPI, Activity: config.ExternalAgentActivityFindingDecision},
{Timestamp: after, Surface: config.ExternalAgentActivitySurfacePulseMCP, Activity: config.ExternalAgentActivityActionExecute},
{Timestamp: after, Surface: config.ExternalAgentActivitySurfacePulseMCP, Activity: config.ExternalAgentActivityOperatorState},
},
},
want: PulseIntelligenceExternalAgentEvidence{
Used: true,
MCPAdapterUsed: true,
FindingRequests: 1,
ActionRequests: 1,
OperatorStateRequests: 1,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PulseIntelligenceExternalAgentEvidenceFromHistory(tt.history, fixedCutoff)
if got != tt.want {
t.Fatalf("PulseIntelligenceExternalAgentEvidenceFromHistory() = %+v, want %+v", got, tt.want)
}
})
}
}
@@ -0,0 +1,432 @@
package truenas
import (
"fmt"
"net"
"testing"
"time"
)
// timeoutError is a minimal net.Error implementation used to drive the
// Timeout()/Temporary() branches of isTimeoutError deterministically.
type timeoutError struct {
timeout bool
temporary bool
msg string
}
func (e *timeoutError) Error() string { return e.msg }
func (e *timeoutError) Timeout() bool { return e.timeout }
func (e *timeoutError) Temporary() bool {
return e.temporary
}
// wrappedError chains a timeout error so we can verify errors.As walks the
// chain inside isTimeoutError.
type wrappedError struct {
inner error
}
func (e *wrappedError) Error() string { return "wrapped: " + e.inner.Error() }
func (e *wrappedError) Unwrap() error { return e.inner }
func TestIsTimeoutErrorCoversBranches(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{name: "nil error returns false", err: nil, want: false},
{name: "plain non-network error returns false", err: fmt.Errorf("disk full"), want: false},
{name: "network error with timeout flag returns true", err: &timeoutError{timeout: true, temporary: true, msg: "i/o timeout"}, want: true},
{name: "network error without timeout flag returns false", err: &timeoutError{timeout: false, temporary: true, msg: "connection reset"}, want: false},
{name: "wrapped network timeout still detected via errors.As", err: &wrappedError{inner: &timeoutError{timeout: true, msg: "deadline exceeded"}}, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isTimeoutError(tt.err)
if got != tt.want {
t.Fatalf("isTimeoutError(%v) = %v, want %v", tt.err, got, tt.want)
}
})
}
}
func TestSplitSnapshotNameCoversShapes(t *testing.T) {
tests := []struct {
name string
full string
wantDataset string
wantSnap string
}{
{name: "empty string", full: "", wantDataset: "", wantSnap: ""},
{name: "whitespace only collapses to empty", full: " ", wantDataset: "", wantSnap: ""},
{name: "no at sign", full: "tank/media", wantDataset: "", wantSnap: ""},
{name: "simple dataset at snapshot", full: "tank@auto-2024-01-01", wantDataset: "tank", wantSnap: "auto-2024-01-01"},
{name: "nested dataset at snapshot", full: "tank/media/movies@manual-1", wantDataset: "tank/media/movies", wantSnap: "manual-1"},
{name: "only at sign returns empty parts", full: "@", wantDataset: "", wantSnap: ""},
{name: "first at sign wins when multiple present", full: "tank@snap@extra", wantDataset: "tank", wantSnap: "snap@extra"},
{name: "leading and trailing whitespace is trimmed", full: " tank/media @ snap ", wantDataset: "tank/media", wantSnap: "snap"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dataset, snap := splitSnapshotName(tt.full)
if dataset != tt.wantDataset || snap != tt.wantSnap {
t.Fatalf("splitSnapshotName(%q) = (%q, %q), want (%q, %q)", tt.full, dataset, snap, tt.wantDataset, tt.wantSnap)
}
})
}
}
func TestDatasetFromSharePathCoversShapes(t *testing.T) {
tests := []struct {
name string
path string
want string
}{
{name: "empty path", path: "", want: ""},
{name: "whitespace only", path: " ", want: ""},
{name: "EXTERNAL sentinel rejected case-insensitively", path: "external", want: ""},
{name: "EXTERNAL uppercase rejected", path: "EXTERNAL", want: ""},
{name: "bare mnt prefix only", path: "/mnt/", want: ""},
{name: "pool only under mnt", path: "/mnt/tank", want: "tank"},
{name: "pool and dataset under mnt", path: "/mnt/tank/media", want: "tank/media"},
{name: "deep path truncated to pool and dataset", path: "/mnt/tank/media/movies/action", want: "tank/media"},
{name: "trailing slashes trimmed", path: "/mnt/tank/media/", want: "tank/media"},
{name: "no mnt prefix keeps raw value", path: "tank/apps", want: "tank/apps"},
{name: "surrounding whitespace trimmed", path: " /mnt/tank/apps ", want: "tank/apps"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := datasetFromSharePath(tt.path)
if got != tt.want {
t.Fatalf("datasetFromSharePath(%q) = %q, want %q", tt.path, got, tt.want)
}
})
}
}
func TestPoolFromSharePathCoversShapes(t *testing.T) {
tests := []struct {
name string
path string
want string
}{
{name: "empty path returns empty pool", path: "", want: ""},
{name: "mnt only returns empty pool", path: "/mnt/", want: ""},
{name: "EXTERNAL sentinel returns empty pool", path: "EXTERNAL", want: ""},
{name: "pool only yields pool", path: "/mnt/tank", want: "tank"},
{name: "pool and dataset yields pool only", path: "/mnt/tank/media", want: "tank"},
{name: "deep path still yields root pool", path: "/mnt/tank/media/movies", want: "tank"},
{name: "dataset path without slash echoes back as pool", path: "/mnt/tank", want: "tank"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := poolFromSharePath(tt.path)
if got != tt.want {
t.Fatalf("poolFromSharePath(%q) = %q, want %q", tt.path, got, tt.want)
}
})
}
}
func TestExtractFirstReportingFloatValueCoversShapes(t *testing.T) {
tests := []struct {
name string
raw any
wantValue float64
wantOK bool
}{
{name: "nil returns false", raw: nil, wantValue: 0, wantOK: false},
{name: "empty slice returns false", raw: []any{}, wantValue: 0, wantOK: false},
{name: "slice of non-numeric strings returns false", raw: []any{"abc", "def"}, wantValue: 0, wantOK: false},
{name: "slice with first float64 wins", raw: []any{float64(1.5), float64(2.5)}, wantValue: 1.5, wantOK: true},
{name: "slice skips non-numeric leading entries", raw: []any{"junk", float64(7.25)}, wantValue: 7.25, wantOK: true},
{name: "nested slice recurses to first float", raw: []any{[]any{"x", float64(9.5)}}, wantValue: 9.5, wantOK: true},
{name: "deeply nested slice recurses to first float", raw: []any{[]any{[]any{float64(42.0)}}}, wantValue: 42.0, wantOK: true},
{name: "empty map returns false", raw: map[string]any{}, wantValue: 0, wantOK: false},
{name: "single-entry map returns that value", raw: map[string]any{"cpu": float64(3.14)}, wantValue: 3.14, wantOK: true},
{name: "map recurses into nested map for value", raw: map[string]any{"agg": map[string]any{"mean": float64(11.0)}}, wantValue: 11.0, wantOK: true},
{name: "bare float64 default branch returns value", raw: float64(2.5), wantValue: 2.5, wantOK: true},
{name: "bare int default branch returns value as float", raw: int(8), wantValue: 8.0, wantOK: true},
{name: "numeric string default branch parses", raw: "12.5", wantValue: 12.5, wantOK: true},
{name: "non-numeric string default branch returns false", raw: "not-a-number", wantValue: 0, wantOK: false},
{name: "non-numeric unhandled type returns false", raw: struct{ X int }{X: 1}, wantValue: 0, wantOK: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotValue, gotOK := extractFirstReportingFloatValue(tt.raw)
if gotOK != tt.wantOK {
t.Fatalf("extractFirstReportingFloatValue(%v) ok = %v, want %v", tt.raw, gotOK, tt.wantOK)
}
if gotOK && gotValue != tt.wantValue {
t.Fatalf("extractFirstReportingFloatValue(%v) = %v, want %v", tt.raw, gotValue, tt.wantValue)
}
})
}
}
func TestMergeSystemTelemetryCoversAllBranches(t *testing.T) {
t.Run("nil system is a no-op", func(t *testing.T) {
telemetry := &SystemInfo{CPUCount: 8, CPUPercent: 12.5}
mergeSystemTelemetry(nil, telemetry)
})
t.Run("nil telemetry is a no-op preserving system values", func(t *testing.T) {
system := &SystemInfo{Hostname: "nas", CPUCount: 4, MemoryTotalBytes: 1024, CPUPercent: 50}
mergeSystemTelemetry(system, nil)
if system.CPUCount != 4 || system.MemoryTotalBytes != 1024 || system.CPUPercent != 50 {
t.Fatalf("mergeSystemTelemetry with nil telemetry mutated system: %+v", system)
}
})
t.Run("both nil pointers is a no-op", func(t *testing.T) {
mergeSystemTelemetry(nil, nil)
})
t.Run("positive telemetry fields override system fields", func(t *testing.T) {
system := &SystemInfo{CPUCount: 4, MemoryTotalBytes: 1024, MemoryAvailableBytes: 512}
telemetry := &SystemInfo{
CPUCount: 16,
MemoryTotalBytes: 8192,
MemoryAvailableBytes: 4096,
}
mergeSystemTelemetry(system, telemetry)
if system.CPUCount != 16 {
t.Fatalf("CPUCount = %d, want 16", system.CPUCount)
}
if system.MemoryTotalBytes != 8192 {
t.Fatalf("MemoryTotalBytes = %d, want 8192", system.MemoryTotalBytes)
}
if system.MemoryAvailableBytes != 4096 {
t.Fatalf("MemoryAvailableBytes = %d, want 4096", system.MemoryAvailableBytes)
}
})
t.Run("zero or empty telemetry fields do not override system fields", func(t *testing.T) {
system := &SystemInfo{CPUCount: 4, MemoryTotalBytes: 1024, MemoryAvailableBytes: 512, IntervalSeconds: 5}
telemetry := &SystemInfo{
CPUCount: 0,
MemoryTotalBytes: 0,
MemoryAvailableBytes: 0,
IntervalSeconds: 0,
}
mergeSystemTelemetry(system, telemetry)
if system.CPUCount != 4 {
t.Fatalf("CPUCount = %d, want preserved 4", system.CPUCount)
}
if system.MemoryTotalBytes != 1024 {
t.Fatalf("MemoryTotalBytes = %d, want preserved 1024", system.MemoryTotalBytes)
}
if system.MemoryAvailableBytes != 512 {
t.Fatalf("MemoryAvailableBytes = %d, want preserved 512", system.MemoryAvailableBytes)
}
if system.IntervalSeconds != 5 {
t.Fatalf("IntervalSeconds = %d, want preserved 5", system.IntervalSeconds)
}
})
t.Run("rate fields are always copied regardless of zero", func(t *testing.T) {
system := &SystemInfo{CPUPercent: 90, NetInRate: 1, NetOutRate: 2, DiskReadRate: 3, DiskWriteRate: 4}
telemetry := &SystemInfo{CPUPercent: 0, NetInRate: 0, NetOutRate: 0, DiskReadRate: 0, DiskWriteRate: 0}
mergeSystemTelemetry(system, telemetry)
if system.CPUPercent != 0 || system.NetInRate != 0 || system.NetOutRate != 0 || system.DiskReadRate != 0 || system.DiskWriteRate != 0 {
t.Fatalf("rate fields not overwritten with zero: %+v", system)
}
})
t.Run("positive telemetry rate fields are copied through", func(t *testing.T) {
system := &SystemInfo{}
telemetry := &SystemInfo{CPUPercent: 42.5, NetInRate: 100, NetOutRate: 200, DiskReadRate: 300, DiskWriteRate: 400}
mergeSystemTelemetry(system, telemetry)
if system.CPUPercent != 42.5 {
t.Fatalf("CPUPercent = %v, want 42.5", system.CPUPercent)
}
if system.NetInRate != 100 || system.NetOutRate != 200 {
t.Fatalf("Net rates = (%v, %v), want (100, 200)", system.NetInRate, system.NetOutRate)
}
if system.DiskReadRate != 300 || system.DiskWriteRate != 400 {
t.Fatalf("Disk rates = (%v, %v), want (300, 400)", system.DiskReadRate, system.DiskWriteRate)
}
})
t.Run("non-empty temperature map is cloned into system", func(t *testing.T) {
system := &SystemInfo{}
temps := map[string]float64{"cpu_package": 55.0, "core_0": 52.0}
telemetry := &SystemInfo{TemperatureCelsius: temps}
mergeSystemTelemetry(system, telemetry)
if system.TemperatureCelsius == nil {
t.Fatalf("expected cloned temperature map, got nil")
}
if len(system.TemperatureCelsius) != 2 {
t.Fatalf("expected 2 temperature entries, got %d", len(system.TemperatureCelsius))
}
if v, ok := system.TemperatureCelsius["cpu_package"]; !ok || v != 55.0 {
t.Fatalf("cpu_package entry = (%v, %v), want present 55.0", v, ok)
}
system.TemperatureCelsius["cpu_package"] = 999
if temps["cpu_package"] == 999 {
t.Fatal("expected system temperature map to be a clone, not the same reference")
}
})
t.Run("empty temperature map is not copied", func(t *testing.T) {
system := &SystemInfo{TemperatureCelsius: map[string]float64{"existing": 1.0}}
telemetry := &SystemInfo{TemperatureCelsius: map[string]float64{}}
mergeSystemTelemetry(system, telemetry)
if _, ok := system.TemperatureCelsius["existing"]; !ok {
t.Fatalf("expected existing temperature map to be preserved, got %+v", system.TemperatureCelsius)
}
})
t.Run("non-zero collected_at is copied", func(t *testing.T) {
system := &SystemInfo{}
now := time.Date(2026, 7, 18, 12, 0, 0, 0, time.UTC)
telemetry := &SystemInfo{CollectedAt: now}
mergeSystemTelemetry(system, telemetry)
if !system.CollectedAt.Equal(now) {
t.Fatalf("CollectedAt = %v, want %v", system.CollectedAt, now)
}
})
t.Run("zero collected_at is not copied", func(t *testing.T) {
original := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
system := &SystemInfo{CollectedAt: original}
telemetry := &SystemInfo{CollectedAt: time.Time{}}
mergeSystemTelemetry(system, telemetry)
if !system.CollectedAt.Equal(original) {
t.Fatalf("CollectedAt = %v, want preserved %v", system.CollectedAt, original)
}
})
t.Run("positive interval seconds is copied", func(t *testing.T) {
system := &SystemInfo{IntervalSeconds: 0}
telemetry := &SystemInfo{IntervalSeconds: 2}
mergeSystemTelemetry(system, telemetry)
if system.IntervalSeconds != 2 {
t.Fatalf("IntervalSeconds = %d, want 2", system.IntervalSeconds)
}
})
}
func TestAppImagesFromContainersCoversShapes(t *testing.T) {
t.Run("nil containers returns nil", func(t *testing.T) {
if got := appImagesFromContainers(nil); got != nil {
t.Fatalf("appImagesFromContainers(nil) = %#v, want nil", got)
}
})
t.Run("empty containers returns nil", func(t *testing.T) {
if got := appImagesFromContainers([]AppContainer{}); got != nil {
t.Fatalf("appImagesFromContainers([]) = %#v, want nil", got)
}
})
t.Run("containers with blank images are skipped", func(t *testing.T) {
containers := []AppContainer{
{ID: "a", Image: ""},
{ID: "b", Image: " "},
}
got := appImagesFromContainers(containers)
if len(got) != 0 {
t.Fatalf("appImagesFromContainers() = %#v, want empty slice", got)
}
})
t.Run("images are gathered preserving order and trimming whitespace", func(t *testing.T) {
containers := []AppContainer{
{ID: "a", Image: " alpine:3.20 "},
{ID: "b", Image: ""},
{ID: "c", Image: "nginx:latest"},
}
got := appImagesFromContainers(containers)
want := []string{"alpine:3.20", "nginx:latest"}
if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
t.Fatalf("appImagesFromContainers() = %#v, want %#v", got, want)
}
})
t.Run("duplicate images are preserved at this layer", func(t *testing.T) {
containers := []AppContainer{
{ID: "a", Image: "redis:7"},
{ID: "b", Image: "redis:7"},
}
got := appImagesFromContainers(containers)
if len(got) != 2 || got[0] != "redis:7" || got[1] != "redis:7" {
t.Fatalf("appImagesFromContainers() = %#v, want two redis:7 entries", got)
}
})
}
func TestAppVolumesFromContainersCoversShapes(t *testing.T) {
t.Run("nil containers returns nil", func(t *testing.T) {
if got := appVolumesFromContainers(nil); got != nil {
t.Fatalf("appVolumesFromContainers(nil) = %#v, want nil", got)
}
})
t.Run("empty containers returns nil", func(t *testing.T) {
if got := appVolumesFromContainers([]AppContainer{}); got != nil {
t.Fatalf("appVolumesFromContainers([]) = %#v, want nil", got)
}
})
t.Run("containers with no volume mounts produce empty result", func(t *testing.T) {
containers := []AppContainer{
{ID: "a"},
{ID: "b"},
}
got := appVolumesFromContainers(containers)
if len(got) != 0 {
t.Fatalf("appVolumesFromContainers() = %#v, want empty", got)
}
})
t.Run("volumes from multiple containers are concatenated in order", func(t *testing.T) {
containers := []AppContainer{
{ID: "a", VolumeMounts: []AppVolume{{Source: "/host/a", Destination: "/data/a", Mode: "rw", Type: "bind"}}},
{ID: "b", VolumeMounts: []AppVolume{
{Source: "/host/b1", Destination: "/data/b1"},
{Source: "/host/b2", Destination: "/data/b2"},
}},
}
got := appVolumesFromContainers(containers)
if len(got) != 3 {
t.Fatalf("appVolumesFromContainers() len = %d, want 3 (%#v)", len(got), got)
}
if got[0].Source != "/host/a" || got[1].Source != "/host/b1" || got[2].Source != "/host/b2" {
t.Fatalf("appVolumesFromContainers() = %#v, want sources in container order", got)
}
})
t.Run("duplicate volumes across containers are NOT deduped at this layer", func(t *testing.T) {
// This deliberately documents the function's actual behaviour: dedup is
// the caller's responsibility (see dedupeAppVolumes, invoked from
// parseAppsWithStats). Asserting it here keeps the test honest and
// guards against an accidental behaviour change.
dupe := AppVolume{Source: "/host/shared", Destination: "/data/shared", Mode: "rw", Type: "bind"}
containers := []AppContainer{
{ID: "a", VolumeMounts: []AppVolume{dupe}},
{ID: "b", VolumeMounts: []AppVolume{dupe}},
}
got := appVolumesFromContainers(containers)
if len(got) != 2 {
t.Fatalf("appVolumesFromContainers() len = %d, want 2 (no dedup at this layer)", len(got))
}
if got[0] != dupe || got[1] != dupe {
t.Fatalf("appVolumesFromContainers() = %#v, want both volumes preserved", got)
}
})
}
// Compile-time guards: ensure the test helpers we built keep satisfying the
// interfaces the production code targets via errors.As / type switches.
var (
_ net.Error = (*timeoutError)(nil)
_ error = (*wrappedError)(nil)
)
@@ -0,0 +1,403 @@
package vmware
import (
"crypto/x509"
"errors"
"strings"
"testing"
"time"
)
// silentX509Wrap wraps an *x509.UnknownAuthorityError but reports a
// keyword-free Error() string so the substring check in
// classifyTransportError is bypassed and the errors.As branch is the
// only path that can classify the wrapped error as TLS.
type silentX509Wrap struct {
wrapped error
}
func (w *silentX509Wrap) Error() string { return "innocuous transport failure" }
func (w *silentX509Wrap) Unwrap() error { return w.wrapped }
// fakeNetError implements the net.Error interface (Timeout/Temporary) without
// embedding x509/tls/certificate keywords in its message.
type fakeNetError struct{ msg string }
func (e *fakeNetError) Error() string { return e.msg }
func (e *fakeNetError) Timeout() bool { return false }
func (e *fakeNetError) Temporary() bool { return false }
func TestInventoryAlarmSortTime(t *testing.T) {
pst := time.FixedZone("PST", -8*3600)
// 10:30 PST == 18:30 UTC. Using a non-UTC input proves the function
// normalises to UTC rather than returning the raw stored value.
nonUTC := time.Date(2026, time.January, 15, 10, 30, 0, 0, pst)
wantUTC := time.Date(2026, time.January, 15, 18, 30, 0, 0, time.UTC)
cases := []struct {
name string
alarm InventoryAlarm
want time.Time
}{
{
name: "zero triggered_at returns zero time",
alarm: InventoryAlarm{},
want: time.Time{},
},
{
name: "non-zero triggered_at returns same instant in UTC",
alarm: InventoryAlarm{TriggeredAt: nonUTC},
want: wantUTC,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := inventoryAlarmSortTime(tc.alarm)
if tc.want.IsZero() {
if !got.IsZero() {
t.Fatalf("inventoryAlarmSortTime = %v, want zero time", got)
}
return
}
if !got.Equal(tc.want) {
t.Fatalf("inventoryAlarmSortTime = %v, want instant equal to %v", got, tc.want)
}
if got.Location() != time.UTC {
t.Fatalf("inventoryAlarmSortTime location = %v, want UTC", got.Location())
}
})
}
}
func TestInventoryTaskSortTime(t *testing.T) {
pst := time.FixedZone("PST", -8*3600)
started := time.Date(2026, time.February, 1, 9, 0, 0, 0, pst) // 17:00 UTC
completed := time.Date(2026, time.February, 1, 11, 0, 0, 0, pst) // 19:00 UTC
wantStartedUTC := time.Date(2026, time.February, 1, 17, 0, 0, 0, time.UTC)
wantCompletedUTC := time.Date(2026, time.February, 1, 19, 0, 0, 0, time.UTC)
cases := []struct {
name string
task InventoryTask
want time.Time
}{
{
name: "no times returns zero",
task: InventoryTask{},
want: time.Time{},
},
{
name: "only completed returns completed utc",
task: InventoryTask{CompletedAt: completed},
want: wantCompletedUTC,
},
{
name: "only started returns started utc",
task: InventoryTask{StartedAt: started},
want: wantStartedUTC,
},
{
name: "started takes precedence over completed",
task: InventoryTask{StartedAt: started, CompletedAt: completed},
want: wantStartedUTC,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := inventoryTaskSortTime(tc.task)
if tc.want.IsZero() {
if !got.IsZero() {
t.Fatalf("inventoryTaskSortTime = %v, want zero time", got)
}
return
}
if !got.Equal(tc.want) {
t.Fatalf("inventoryTaskSortTime = %v, want instant equal to %v", got, tc.want)
}
if got.Location() != time.UTC {
t.Fatalf("inventoryTaskSortTime location = %v, want UTC", got.Location())
}
})
}
}
func TestInventoryEventSortTime(t *testing.T) {
pst := time.FixedZone("PST", -8*3600)
nonUTC := time.Date(2026, time.March, 12, 5, 15, 0, 0, pst) // 13:15 UTC
wantUTC := time.Date(2026, time.March, 12, 13, 15, 0, 0, time.UTC)
cases := []struct {
name string
event InventoryEvent
want time.Time
}{
{
name: "zero created_at returns zero time",
event: InventoryEvent{},
want: time.Time{},
},
{
name: "non-zero created_at returns same instant in UTC",
event: InventoryEvent{CreatedAt: nonUTC},
want: wantUTC,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := inventoryEventSortTime(tc.event)
if tc.want.IsZero() {
if !got.IsZero() {
t.Fatalf("inventoryEventSortTime = %v, want zero time", got)
}
return
}
if !got.Equal(tc.want) {
t.Fatalf("inventoryEventSortTime = %v, want instant equal to %v", got, tc.want)
}
if got.Location() != time.UTC {
t.Fatalf("inventoryEventSortTime location = %v, want UTC", got.Location())
}
})
}
}
func TestIsAutomationNotFound(t *testing.T) {
cases := []struct {
name string
err error
want bool
}{
{name: "nil error returns false", err: nil, want: false},
{name: "non-connection error returns false", err: errors.New("boom"), want: false},
{name: "not_found category returns true", err: &ConnectionError{Category: "not_found", Message: "missing"}, want: true},
{name: "unavailable category returns false", err: &ConnectionError{Category: "unavailable", Message: "busy"}, want: false},
{name: "endpoint category returns false", err: &ConnectionError{Category: "endpoint", Message: "boom"}, want: false},
{name: "empty category returns false", err: &ConnectionError{}, want: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isAutomationNotFound(tc.err); got != tc.want {
t.Fatalf("isAutomationNotFound(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}
func TestIsAutomationUnavailable(t *testing.T) {
cases := []struct {
name string
err error
want bool
}{
{name: "nil error returns false", err: nil, want: false},
{name: "non-connection error returns false", err: errors.New("boom"), want: false},
{name: "unavailable category returns true", err: &ConnectionError{Category: "unavailable", Message: "busy"}, want: true},
{name: "not_found category returns false", err: &ConnectionError{Category: "not_found", Message: "missing"}, want: false},
{name: "endpoint category returns false", err: &ConnectionError{Category: "endpoint", Message: "boom"}, want: false},
{name: "empty category returns false", err: &ConnectionError{}, want: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isAutomationUnavailable(tc.err); got != tc.want {
t.Fatalf("isAutomationUnavailable(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}
func TestClassifyTransportError(t *testing.T) {
cases := []struct {
name string
stage string
err error
wantCategory string
wantMsgContains string
}{
{
name: "nil error returns nil",
stage: "automation session",
err: nil,
wantCategory: "",
wantMsgContains: "",
},
{
name: "x509 substring classifies as tls",
stage: "automation session",
err: errors.New("Get https://vc/sdk: x509: certificate signed by unknown authority"),
wantCategory: "tls",
wantMsgContains: "VMware TLS validation failed during automation session",
},
{
name: "certificate substring classifies as tls",
stage: "vi-json login",
err: errors.New("certificate verify failed"),
wantCategory: "tls",
wantMsgContains: "VMware TLS validation failed during vi-json login",
},
{
name: "tls substring classifies as tls",
stage: "automation session",
err: errors.New("tls: handshake failure"),
wantCategory: "tls",
wantMsgContains: "VMware TLS validation failed during automation session",
},
{
name: "wrapped unknown authority without keyword hits errors.As tls branch",
stage: "vi-json service content",
err: &silentX509Wrap{wrapped: &x509.UnknownAuthorityError{}},
wantCategory: "tls",
wantMsgContains: "VMware TLS validation failed during vi-json service content",
},
{
name: "net.Error classifies as network error",
stage: "automation session",
err: &fakeNetError{msg: "dial tcp: connection refused"},
wantCategory: "network",
wantMsgContains: "VMware network error during automation session",
},
{
name: "unclassified error falls through to connection failed",
stage: "label",
err: errors.New("unexpected EOF"),
wantCategory: "network",
wantMsgContains: "VMware connection failed during label",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := classifyTransportError(tc.stage, tc.err)
if tc.err == nil {
if got != nil {
t.Fatalf("classifyTransportError(%q, nil) = %v, want nil", tc.stage, got)
}
return
}
connErr, ok := got.(*ConnectionError)
if !ok {
t.Fatalf("classifyTransportError(%q, %v) = %T, want *ConnectionError", tc.stage, tc.err, got)
}
if connErr.Category != tc.wantCategory {
t.Fatalf("classifyTransportError category = %q, want %q (msg=%q)", connErr.Category, tc.wantCategory, connErr.Message)
}
if !strings.Contains(connErr.Message, tc.wantMsgContains) {
t.Fatalf("classifyTransportError message = %q, want substring %q", connErr.Message, tc.wantMsgContains)
}
})
}
}
func TestVMwareSortKey(t *testing.T) {
cases := []struct {
name string
id string
entityName string
want string
}{
{name: "both populated returns lowercased trimmed id", id: "VM-1", entityName: "vm one", want: "vm-1"},
{name: "id wins over name regardless of name case", id: "Host-X", entityName: "lower-priority", want: "host-x"},
{name: "empty id falls back to lowercased trimmed name", id: "", entityName: "VM Two", want: "vm two"},
{name: "whitespace-only id falls back to name", id: " ", entityName: "Spaces", want: "spaces"},
{name: "empty name returns lowercased trimmed id", id: "vm-1", entityName: "", want: "vm-1"},
{name: "both empty returns empty", id: "", entityName: "", want: ""},
{name: "id is trimmed and lowercased", id: " Host-X ", entityName: "ignored", want: "host-x"},
{name: "name is trimmed and lowercased when id empty", id: "", entityName: " Host-Y ", want: "host-y"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := vmwareSortKey(tc.id, tc.entityName); got != tc.want {
t.Fatalf("vmwareSortKey(%q, %q) = %q, want %q", tc.id, tc.entityName, got, tc.want)
}
})
}
}
func TestSortInventorySnapshot(t *testing.T) {
t.Run("nil snapshot is a no-op", func(t *testing.T) {
// Should not panic on a nil receiver-by-argument.
sortInventorySnapshot(nil)
})
t.Run("empty snapshot is a no-op", func(t *testing.T) {
sortInventorySnapshot(&InventorySnapshot{})
})
t.Run("each slice sorted ascending by vmwareSortKey", func(t *testing.T) {
// Each slice is presented in reverse-sorted (z-first, a-second)
// order so that an in-place sort produces a distinct, checkable
// permutation. IDs are populated so the sort key is lowercased id.
snapshot := &InventorySnapshot{
Hosts: []InventoryHost{
{Host: "host-z", Name: "Z Host"},
{Host: "host-a", Name: "A Host"},
},
VMs: []InventoryVM{
{VM: "vm-z", Name: "Z VM"},
{VM: "vm-a", Name: "A VM"},
},
Datastores: []InventoryDatastore{
{Datastore: "ds-z", Name: "Z DS"},
{Datastore: "ds-a", Name: "A DS"},
},
Clusters: []InventoryCluster{
{Cluster: "domain-z", Name: "Z Cluster"},
{Cluster: "domain-a", Name: "A Cluster"},
},
Networks: []InventoryNetwork{
{Network: "net-z", Name: "Z Net"},
{Network: "net-a", Name: "A Net"},
},
EnrichmentIssues: []InventoryEnrichmentIssue{
{Stage: "topology", EntityType: "cluster", EntityID: "domain-z", Category: "unavailable", Message: "z msg"},
{Stage: "signals", EntityType: "vm", EntityID: "vm-a", Category: "not_found", Message: "a msg"},
},
}
sortInventorySnapshot(snapshot)
if got := snapshot.Hosts[0].Host; got != "host-a" {
t.Fatalf("hosts not sorted ascending; first Host = %q, want %q", got, "host-a")
}
if got := snapshot.Hosts[1].Host; got != "host-z" {
t.Fatalf("hosts not sorted ascending; second Host = %q, want %q", got, "host-z")
}
if got := snapshot.VMs[0].VM; got != "vm-a" {
t.Fatalf("vms not sorted ascending; first VM = %q, want %q", got, "vm-a")
}
if got := snapshot.VMs[1].VM; got != "vm-z" {
t.Fatalf("vms not sorted ascending; second VM = %q, want %q", got, "vm-z")
}
if got := snapshot.Datastores[0].Datastore; got != "ds-a" {
t.Fatalf("datastores not sorted ascending; first Datastore = %q, want %q", got, "ds-a")
}
if got := snapshot.Datastores[1].Datastore; got != "ds-z" {
t.Fatalf("datastores not sorted ascending; second Datastore = %q, want %q", got, "ds-z")
}
if got := snapshot.Clusters[0].Cluster; got != "domain-a" {
t.Fatalf("clusters not sorted ascending; first Cluster = %q, want %q", got, "domain-a")
}
if got := snapshot.Clusters[1].Cluster; got != "domain-z" {
t.Fatalf("clusters not sorted ascending; second Cluster = %q, want %q", got, "domain-z")
}
if got := snapshot.Networks[0].Network; got != "net-a" {
t.Fatalf("networks not sorted ascending; first Network = %q, want %q", got, "net-a")
}
if got := snapshot.Networks[1].Network; got != "net-z" {
t.Fatalf("networks not sorted ascending; second Network = %q, want %q", got, "net-z")
}
// Enrichment issue key is "stage\x00entityType\x00..."; "signals"
// sorts before "topology".
if got := snapshot.EnrichmentIssues[0].Stage; got != "signals" {
t.Fatalf("enrichment issues not sorted ascending; first Stage = %q, want %q", got, "signals")
}
if got := snapshot.EnrichmentIssues[1].Stage; got != "topology" {
t.Fatalf("enrichment issues not sorted ascending; second Stage = %q, want %q", got, "topology")
}
// Re-sorting an already-sorted snapshot must leave it stable.
before := snapshot.Hosts[0].Host
sortInventorySnapshot(snapshot)
if snapshot.Hosts[0].Host != before {
t.Fatalf("re-sort changed hosts order; first Host = %q, want %q", snapshot.Hosts[0].Host, before)
}
})
}
@@ -0,0 +1,360 @@
package licensing
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// overflowMockSource is an EntitlementSource whose OverflowGrantedAt can be
// configured. The sibling mockSource (evaluator_test.go) hard-wires
// OverflowGrantedAt to nil, so a dedicated mock is required to exercise the
// "value present" branch of (*Evaluator).OverflowGrantedAt.
type overflowMockSource struct {
overflow *int64
subState SubscriptionState
}
func (m overflowMockSource) Capabilities() []string { return nil }
func (m overflowMockSource) Limits() map[string]int64 { return nil }
func (m overflowMockSource) MetersEnabled() []string { return nil }
func (m overflowMockSource) PlanVersion() string { return "" }
func (m overflowMockSource) SubscriptionState() SubscriptionState { return m.subState }
func (m overflowMockSource) TrialStartedAt() *int64 { return nil }
func (m overflowMockSource) TrialEndsAt() *int64 { return nil }
func (m overflowMockSource) OverflowGrantedAt() *int64 {
if m.overflow == nil {
return nil
}
c := *m.overflow
return &c
}
// TestEvaluatorOverflowGrantedAt0718 covers every branch of
// (*Evaluator).OverflowGrantedAt: nil receiver, non-nil receiver with a nil
// source, source returning nil, and source returning a value (which must be
// handed back as a defensive copy).
func TestEvaluatorOverflowGrantedAt0718(t *testing.T) {
ts := int64(1700000000)
t.Run("nil_receiver_returns_nil", func(t *testing.T) {
var e *Evaluator
assert.Nil(t, e.OverflowGrantedAt())
})
t.Run("nil_source_returns_nil", func(t *testing.T) {
// White-box construction: receiver non-nil, source nil -> second
// short-circuit arm.
e := &Evaluator{source: nil}
assert.Nil(t, e.OverflowGrantedAt())
})
t.Run("source_returns_nil_propagates_nil", func(t *testing.T) {
e := NewEvaluator(overflowMockSource{overflow: nil})
assert.Nil(t, e.OverflowGrantedAt())
})
t.Run("source_returns_value_returns_distinct_copy", func(t *testing.T) {
e := NewEvaluator(overflowMockSource{overflow: &ts})
got := e.OverflowGrantedAt()
require.NotNil(t, got)
assert.Equal(t, ts, *got)
// cloneInt64Ptr must produce a fresh pointer, not alias the input.
assert.NotSame(t, &ts, got)
})
}
// TestDatabaseSourceOverflowGrantedAt0718 covers both arms of
// (*DatabaseSource).OverflowGrantedAt. The active-subscription store path is
// used so no lease-token validation runs and the stored timestamp survives
// normalization; the nil and populated cases exercise the two cloneInt64Ptr
// outcomes.
func TestDatabaseSourceOverflowGrantedAt0718(t *testing.T) {
ts := int64(1700000000)
t.Run("nil_when_state_has_no_timestamp", func(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
PlanVersion: "pro",
SubscriptionState: SubStateActive,
},
}
source := NewDatabaseSource(store, "org-1", time.Hour)
assert.Nil(t, source.OverflowGrantedAt())
})
t.Run("returns_value_when_state_has_timestamp", func(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
PlanVersion: "pro",
SubscriptionState: SubStateActive,
OverflowGrantedAt: &ts,
},
}
source := NewDatabaseSource(store, "org-1", time.Hour)
got := source.OverflowGrantedAt()
require.NotNil(t, got)
assert.Equal(t, ts, *got)
// Must be a defensive copy, not the internal pointer.
assert.NotSame(t, &ts, got)
})
t.Run("returns_distinct_pointers_across_calls", func(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
PlanVersion: "pro",
SubscriptionState: SubStateActive,
OverflowGrantedAt: &ts,
},
}
source := NewDatabaseSource(store, "org-1", time.Hour)
first := source.OverflowGrantedAt()
second := source.OverflowGrantedAt()
require.NotNil(t, first)
require.NotNil(t, second)
assert.Equal(t, ts, *first)
assert.Equal(t, ts, *second)
// Each call must yield its own copy (no shared aliasing).
assert.NotSame(t, first, second)
})
}
// TestServiceGetLicenseState0718 covers every return path of
// (*Service).GetLicenseState: the no-license/no-evaluator None path, every
// evaluator-driven arm (active/trial/grace/expired via suspended), and every
// JWT-driven arm (active/trial/grace/expired-past-grace/suspended-claim).
func TestServiceGetLicenseState0718(t *testing.T) {
// Defensive: ensure no env-var bypass influences the derivation.
t.Setenv("PULSE_DEV", "")
t.Setenv("PULSE_MOCK_MODE", "")
t.Run("no_license_no_evaluator_returns_none", func(t *testing.T) {
s := NewService()
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateNone, state)
assert.Nil(t, lic)
})
// Hosted / evaluator-driven arm (s.license == nil && s.evaluator != nil).
t.Run("evaluator_active_returns_active_nil_license", func(t *testing.T) {
s := NewService()
s.SetEvaluator(NewEvaluator(mockSource{subState: SubStateActive}))
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateActive, state)
assert.Nil(t, lic)
})
t.Run("evaluator_trial_returns_active_nil_license", func(t *testing.T) {
s := NewService()
s.SetEvaluator(NewEvaluator(mockSource{subState: SubStateTrial}))
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateActive, state)
assert.Nil(t, lic)
})
t.Run("evaluator_grace_returns_grace_nil_license", func(t *testing.T) {
s := NewService()
s.SetEvaluator(NewEvaluator(mockSource{subState: SubStateGrace}))
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateGracePeriod, state)
assert.Nil(t, lic)
})
t.Run("evaluator_expired_returns_expired_nil_license", func(t *testing.T) {
s := NewService()
s.SetEvaluator(NewEvaluator(mockSource{subState: SubStateExpired}))
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateExpired, state)
assert.Nil(t, lic)
})
t.Run("evaluator_suspended_falls_through_default_to_expired", func(t *testing.T) {
s := NewService()
s.SetEvaluator(NewEvaluator(mockSource{subState: SubStateSuspended}))
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateExpired, state)
assert.Nil(t, lic)
})
// JWT-driven arm (s.license != nil) via currentJWTSubscriptionStateLocked.
t.Run("jwt_active_no_substate_returns_active_with_clone", func(t *testing.T) {
s := NewService()
original := &License{Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(),
}}
s.SetCurrentForTesting(original)
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateActive, state)
require.NotNil(t, lic)
assert.Equal(t, TierPro, lic.Claims.Tier)
// Returned license must be a clone, not the internal pointer.
assert.NotSame(t, original, lic)
assert.Same(t, original, s.CurrentUnsafeForTesting(), "internal pointer must be unchanged")
})
t.Run("jwt_trial_claim_returns_active", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(),
SubState: SubStateTrial,
}})
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateActive, state)
require.NotNil(t, lic)
})
t.Run("jwt_expired_within_grace_returns_grace", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(-24 * time.Hour).Unix(), // expired yesterday, within 7-day grace
}})
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateGracePeriod, state)
require.NotNil(t, lic)
})
t.Run("jwt_expired_past_grace_returns_expired", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(-365 * 24 * time.Hour).Unix(), // long past grace
}})
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateExpired, state)
require.NotNil(t, lic)
})
t.Run("jwt_suspended_claim_returns_expired", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(),
SubState: SubStateSuspended,
}})
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateExpired, state)
require.NotNil(t, lic)
})
t.Run("jwt_canceled_claim_returns_expired", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(),
SubState: SubStateCanceled,
}})
state, lic := s.GetLicenseState()
assert.Equal(t, LicenseStateExpired, state)
require.NotNil(t, lic)
})
}
// TestServiceGetLicenseStateString0718 covers both arms of the hasFeatures
// computation in (*Service).GetLicenseStateString: true for active/grace,
// false for none/expired.
func TestServiceGetLicenseStateString0718(t *testing.T) {
t.Setenv("PULSE_DEV", "")
t.Setenv("PULSE_MOCK_MODE", "")
tests := []struct {
name string
setup func(*Service)
wantStr string
wantAvail bool
}{
{
name: "none_returns_false",
setup: func(s *Service) {},
wantStr: string(LicenseStateNone),
wantAvail: false,
},
{
name: "active_returns_true",
setup: func(s *Service) {
s.SetEvaluator(NewEvaluator(mockSource{subState: SubStateActive}))
},
wantStr: string(LicenseStateActive),
wantAvail: true,
},
{
name: "grace_returns_true",
setup: func(s *Service) {
s.SetEvaluator(NewEvaluator(mockSource{subState: SubStateGrace}))
},
wantStr: string(LicenseStateGracePeriod),
wantAvail: true,
},
{
name: "expired_returns_false",
setup: func(s *Service) {
s.SetEvaluator(NewEvaluator(mockSource{subState: SubStateExpired}))
},
wantStr: string(LicenseStateExpired),
wantAvail: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
s := NewService()
tt.setup(s)
gotStr, gotAvail := s.GetLicenseStateString()
assert.Equal(t, tt.wantStr, gotStr)
assert.Equal(t, tt.wantAvail, gotAvail)
})
}
}
// TestServiceRequireFeature0718 covers both arms of (*Service).RequireFeature:
// nil when the feature is genuinely entitled via an active license, and a
// wrapped ErrFeatureNotIncluded when it is not. RBAC is a Pro-only feature
// (not free), so it deterministically distinguishes the two arms.
func TestServiceRequireFeature0718(t *testing.T) {
// Force real entitlement evaluation: no dev/demo env bypass.
t.Setenv("PULSE_DEV", "")
t.Setenv("PULSE_MOCK_MODE", "")
t.Setenv("PULSE_LICENSE_DEV_MODE", "")
// Precondition: RBAC is a Pro capability, absent from the free tier, so
// the only path to a nil error is an active license claim.
require.False(t, TierHasFeature(TierFree, FeatureRBAC),
"test fixture requires RBAC to be a non-free feature")
t.Run("licensed_feature_returns_nil", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{Claims: Claims{
Tier: TierPro,
ExpiresAt: 0, // lifetime -> IsExpired=false -> SubStateActive
Capabilities: []string{FeatureRBAC},
}})
assert.NoError(t, s.RequireFeature(FeatureRBAC))
})
t.Run("unlicensed_feature_returns_ErrFeatureNotIncluded", func(t *testing.T) {
s := NewService() // no license, no evaluator -> free-tier fallback
err := s.RequireFeature(FeatureRBAC)
require.Error(t, err)
assert.True(t, errors.Is(err, ErrFeatureNotIncluded),
"want errors.Is ErrFeatureNotIncluded, got %v", err)
// Message format is "<display> requires Pulse <minTier> or above";
// RBAC's min tier is Pro, so the suffix is a stable behavioral signal.
assert.Contains(t, err.Error(), "Pro or above",
"error message should name the required tier")
})
t.Run("free_tier_feature_returns_nil_without_license", func(t *testing.T) {
s := NewService() // no license, no evaluator
// FeatureAIPatrol is in the free tier, so it is granted even without
// any license, exercising the no-error arm via the free fallback.
require.True(t, TierHasFeature(TierFree, FeatureAIPatrol))
assert.NoError(t, s.RequireFeature(FeatureAIPatrol))
})
}
@@ -0,0 +1,313 @@
package securityutil
import (
"context"
"net"
"net/url"
"strings"
"testing"
)
// This file raises branch/function coverage for the pure and pre-DNS error
// paths in outbound_http.go and httpurl.go. It does not exercise any network,
// filesystem, or global-resolver state: every hostname target is either an
// IP literal (which resolvePermittedOutboundIP short-circuits without DNS) or
// a value rejected before DNS resolution runs.
func TestBranchCovAllowedOutboundSchemes(t *testing.T) {
tests := []struct {
name string
opts RestrictedOutboundHTTPOptions
want []string
}{
{name: "empty allowed schemes defaults to http https", opts: RestrictedOutboundHTTPOptions{}, want: []string{"http", "https"}},
{name: "nil slice defaults to http https", opts: RestrictedOutboundHTTPOptions{AllowedSchemes: nil}, want: []string{"http", "https"}},
{name: "explicit schemes returned verbatim", opts: RestrictedOutboundHTTPOptions{AllowedSchemes: []string{"https", "ftp"}}, want: []string{"https", "ftp"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := allowedOutboundSchemes(tt.opts)
if len(got) != len(tt.want) {
t.Fatalf("allowedOutboundSchemes() = %v, want %v", got, tt.want)
}
for i := range got {
if got[i] != tt.want[i] {
t.Fatalf("allowedOutboundSchemes()[%d] = %q, want %q (full: %v)", i, got[i], tt.want[i], got)
}
}
})
}
}
func TestBranchCovIsAllowedOutboundScheme(t *testing.T) {
tests := []struct {
name string
scheme string
allow []string
want bool
}{
{name: "exact match http", scheme: "http", allow: []string{"http", "https"}, want: true},
{name: "match ignores surrounding whitespace in candidate", scheme: "https", allow: []string{" https ", "http"}, want: true},
{name: "match is case insensitive on candidate", scheme: "HTTPS", allow: []string{"https", "http"}, want: true},
{name: "no match when scheme absent", scheme: "ftp", allow: []string{"http", "https"}, want: false},
{name: "no match against empty allow list", scheme: "http", allow: []string{}, want: false},
{name: "exact candidate differs after trim", scheme: "http", allow: []string{"httpx"}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isAllowedOutboundScheme(tt.scheme, tt.allow); got != tt.want {
t.Fatalf("isAllowedOutboundScheme(%q, %v) = %v, want %v", tt.scheme, tt.allow, got, tt.want)
}
})
}
}
func TestBranchCovValidateOutboundIP(t *testing.T) {
allOpts := RestrictedOutboundHTTPOptions{AllowPrivateIPs: true, AllowLoopback: true}
strictOpts := RestrictedOutboundHTTPOptions{}
tests := []struct {
name string
ip net.IP
opts RestrictedOutboundHTTPOptions
wantErr string // empty means expect nil
}{
{name: "nil ip rejected", ip: net.IP(nil), opts: allOpts, wantErr: "invalid IP address"},
{name: "loopback blocked when not allowed", ip: net.ParseIP("127.0.0.1"), opts: strictOpts, wantErr: "loopback addresses are not allowed"},
{name: "loopback permitted when allowed", ip: net.ParseIP("127.0.0.1"), opts: allOpts, wantErr: ""},
{name: "metadata service ip always rejected", ip: net.ParseIP("169.254.169.254"), opts: allOpts, wantErr: "metadata service address is not allowed"},
{name: "link local unicast rejected", ip: net.ParseIP("169.254.10.20"), opts: allOpts, wantErr: "link-local addresses are not allowed"},
{name: "link local multicast rejected as link local", ip: net.ParseIP("224.0.0.1"), opts: allOpts, wantErr: "link-local addresses are not allowed"},
{name: "non-link-local multicast rejected", ip: net.ParseIP("239.0.0.1"), opts: allOpts, wantErr: "multicast addresses are not allowed"},
{name: "unspecified ipv4 rejected", ip: net.ParseIP("0.0.0.0"), opts: allOpts, wantErr: "unspecified addresses are not allowed"},
{name: "unspecified ipv6 rejected", ip: net.ParseIP("::"), opts: allOpts, wantErr: "unspecified addresses are not allowed"},
{name: "private blocked when not allowed", ip: net.ParseIP("192.168.1.1"), opts: strictOpts, wantErr: "private addresses are not allowed"},
{name: "private permitted when allowed", ip: net.ParseIP("10.0.0.1"), opts: allOpts, wantErr: ""},
{name: "public documentation ip accepted", ip: net.ParseIP("203.0.113.1"), opts: strictOpts, wantErr: ""},
{name: "public ipv6 accepted", ip: net.ParseIP("2001:db8::1"), opts: strictOpts, wantErr: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateOutboundIP(tt.ip, tt.opts)
if tt.wantErr == "" {
if err != nil {
t.Fatalf("validateOutboundIP(%v, %+v) unexpected error: %v", tt.ip, tt.opts, err)
}
return
}
if err == nil {
t.Fatalf("validateOutboundIP(%v, %+v) = nil, want error containing %q", tt.ip, tt.opts, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("validateOutboundIP(%v, %+v) err = %q, want substring %q", tt.ip, tt.opts, err.Error(), tt.wantErr)
}
})
}
}
func TestBranchCovCanonicalOriginHost(t *testing.T) {
tests := []struct {
name string
u func(t *testing.T) *url.URL // nil returns nil *url.URL
want string
}{
{name: "nil url returns empty", u: func(t *testing.T) *url.URL { return nil }, want: ""},
{name: "http default port 80", u: func(t *testing.T) *url.URL { return mustParseURL(t, "http://example.com/p") }, want: "example.com:80"},
{name: "https default port 443", u: func(t *testing.T) *url.URL { return mustParseURL(t, "https://example.com/p") }, want: "example.com:443"},
{name: "host uppercased normalized", u: func(t *testing.T) *url.URL { return mustParseURL(t, "http://EXAMPLE.com/p") }, want: "example.com:80"},
{name: "explicit port preserved", u: func(t *testing.T) *url.URL { return mustParseURL(t, "https://example.com:8443/p") }, want: "example.com:8443"},
{name: "ipv6 default http port", u: func(t *testing.T) *url.URL { return mustParseURL(t, "http://[::1]/p") }, want: "[::1]:80"},
{name: "ipv6 explicit port", u: func(t *testing.T) *url.URL { return mustParseURL(t, "http://[::1]:9000/p") }, want: "[::1]:9000"},
{name: "unknown scheme no port returns raw host", u: func(t *testing.T) *url.URL { return mustParseURL(t, "ftp://example.com/p") }, want: "example.com"},
{name: "empty host falls back to raw host", u: func(t *testing.T) *url.URL { return mustParseURL(t, "http://") }, want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := canonicalOriginHost(tt.u(t))
if got != tt.want {
t.Fatalf("canonicalOriginHost() = %q, want %q", got, tt.want)
}
})
}
}
func TestBranchCovValidateOutboundFetchURL(t *testing.T) {
httpsOnly := RestrictedOutboundHTTPOptions{AllowedSchemes: []string{"https"}}
permissive := RestrictedOutboundHTTPOptions{AllowedSchemes: []string{"http", "https"}, AllowPrivateIPs: true, AllowLoopback: true}
tests := []struct {
name string
raw string
opts RestrictedOutboundHTTPOptions
wantErr string
wantStr string
}{
{name: "empty raw rejected", raw: " ", opts: permissive, wantErr: "URL is required"},
{name: "malformed raw rejected", raw: ":not-a-url", opts: permissive, wantErr: "invalid URL"},
{name: "default host required", raw: "https://", opts: permissive, wantErr: "URL host is required"},
{name: "disallowed scheme rejected with configured list", raw: "http://203.0.113.1/x", opts: httpsOnly, wantErr: "URL scheme must be one of: https"},
{name: "fragment rejected before dns", raw: "https://203.0.113.1/x#frag", opts: permissive, wantErr: "URL fragments are not allowed"},
{name: "ip literal metadata blocked without dns", raw: "http://169.254.169.254/x", opts: permissive, wantErr: "metadata service address is not allowed"},
{name: "ip literal private blocked without dns", raw: "http://192.168.1.1/x", opts: RestrictedOutboundHTTPOptions{AllowedSchemes: []string{"http", "https"}}, wantErr: "private addresses are not allowed"},
{name: "ip literal public success without dns", raw: "http://203.0.113.1/path?q=1", opts: permissive, wantStr: "http://203.0.113.1/path?q=1"},
{name: "https ip literal success", raw: "https://203.0.113.1/p", opts: httpsOnly, wantStr: "https://203.0.113.1/p"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ValidateOutboundFetchURL(context.Background(), tt.raw, tt.opts)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("ValidateOutboundFetchURL(%q) = %v, want error containing %q", tt.raw, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("ValidateOutboundFetchURL(%q) err = %q, want substring %q", tt.raw, err.Error(), tt.wantErr)
}
if got != nil {
t.Fatalf("ValidateOutboundFetchURL(%q) returned non-nil URL with error: %v", tt.raw, got)
}
return
}
if err != nil {
t.Fatalf("ValidateOutboundFetchURL(%q) unexpected error: %v", tt.raw, err)
}
if got == nil {
t.Fatalf("ValidateOutboundFetchURL(%q) returned nil URL", tt.raw)
}
if got.String() != tt.wantStr {
t.Fatalf("ValidateOutboundFetchURL(%q) = %q, want %q", tt.raw, got.String(), tt.wantStr)
}
})
}
}
func TestBranchCovNewValidatedRequestWithContext(t *testing.T) {
t.Run("nil target errors", func(t *testing.T) {
req, err := NewValidatedRequestWithContext(context.Background(), "GET", nil, nil)
if err == nil || !strings.Contains(err.Error(), "target URL is required") {
t.Fatalf("err = %v, want target URL is required", err)
}
if req != nil {
t.Fatalf("req = %v, want nil", req)
}
})
t.Run("invalid method surfaces new request error", func(t *testing.T) {
target := mustParseURL(t, "https://example.com/p")
req, err := NewValidatedRequestWithContext(context.Background(), "GET X", target, nil)
if err == nil || !strings.Contains(err.Error(), "invalid method") {
t.Fatalf("err = %v, want invalid method", err)
}
if req != nil {
t.Fatalf("req = %v, want nil", req)
}
})
t.Run("valid target clones url and sets host", func(t *testing.T) {
target := mustParseURL(t, "https://api.example.com:8443/v1?k=v")
req, err := NewValidatedRequestWithContext(context.Background(), "GET", target, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if req == nil {
t.Fatal("req is nil")
}
if req.Method != "GET" {
t.Fatalf("Method = %q, want GET", req.Method)
}
if req.URL == nil || req.URL.String() != "https://api.example.com:8443/v1?k=v" {
t.Fatalf("URL = %v, want the target URL", req.URL)
}
if req.URL == target {
t.Fatal("URL is the same pointer as target; expected a clone")
}
if req.Host != "api.example.com:8443" {
t.Fatalf("Host = %q, want api.example.com:8443", req.Host)
}
if req.RequestURI != "" {
t.Fatalf("RequestURI = %q, want empty", req.RequestURI)
}
})
t.Run("post method preserved with body", func(t *testing.T) {
target := mustParseURL(t, "https://example.com/ingest")
req, err := NewValidatedRequestWithContext(context.Background(), "POST", target, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if req.Method != "POST" {
t.Fatalf("Method = %q, want POST", req.Method)
}
})
}
func TestBranchCovNewRelativeRequestWithContext(t *testing.T) {
t.Run("nil base errors before method validation", func(t *testing.T) {
req, err := NewRelativeRequestWithContext(context.Background(), "GET", nil, "/x", nil)
if err == nil || !strings.Contains(err.Error(), "base URL is required") {
t.Fatalf("err = %v, want base URL is required", err)
}
if req != nil {
t.Fatalf("req = %v, want nil", req)
}
})
t.Run("empty relative path errors", func(t *testing.T) {
base := mustParseURL(t, "https://api.example.com/v1")
_, err := NewRelativeRequestWithContext(context.Background(), "GET", base, " ", nil)
if err == nil || !strings.Contains(err.Error(), "relative path is required") {
t.Fatalf("err = %v, want relative path is required", err)
}
})
t.Run("backslash in relative path errors", func(t *testing.T) {
base := mustParseURL(t, "https://api.example.com/v1")
_, err := NewRelativeRequestWithContext(context.Background(), "GET", base, `/a\b`, nil)
if err == nil || !strings.Contains(err.Error(), "must not contain backslashes") {
t.Fatalf("err = %v, want backslash rejection", err)
}
})
t.Run("absolute url in relative path errors", func(t *testing.T) {
base := mustParseURL(t, "https://api.example.com/v1")
_, err := NewRelativeRequestWithContext(context.Background(), "GET", base, "https://evil.example/x", nil)
if err == nil || !strings.Contains(err.Error(), "must not include scheme or host") {
t.Fatalf("err = %v, want scheme/host rejection", err)
}
})
t.Run("relative path without leading slash errors", func(t *testing.T) {
base := mustParseURL(t, "https://api.example.com/v1")
_, err := NewRelativeRequestWithContext(context.Background(), "GET", base, "users", nil)
if err == nil || !strings.Contains(err.Error(), "must start with '/'") {
t.Fatalf("err = %v, want leading slash rejection", err)
}
})
t.Run("success builds request with joined url", func(t *testing.T) {
base := mustParseURL(t, "https://api.example.com/v1")
req, err := NewRelativeRequestWithContext(context.Background(), "POST", base, "/users?active=1", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if req == nil {
t.Fatal("req is nil")
}
if req.Method != "POST" {
t.Fatalf("Method = %q, want POST", req.Method)
}
if req.URL == nil || req.URL.String() != "https://api.example.com/v1/users?active=1" {
t.Fatalf("URL = %v, want joined relative URL", req.URL)
}
if req.Host != "api.example.com" {
t.Fatalf("Host = %q, want api.example.com", req.Host)
}
if req.RequestURI != "" {
t.Fatalf("RequestURI = %q, want empty", req.RequestURI)
}
})
}