Add Go branch-coverage tests for canonical trust and recovery helpers

Cover pure functions the recent canonical Operational Trust and protection
posture work landed with no unit test:

- recovery/model posture: enum Valid, struct Clone/Validate/Payload and the
  normalize/cloneTime/validOutcome/sortedUniqueStrings/compareProviderStates
  helpers (20 funcs, all 0%->100%)
- operationaltrust contracts: EvidencePayloadRef/Acknowledgement/Suppression/
  LifecycleTransition Validate plus LifecycleTransition/NotificationLink Clone
- cloudcp/proxytrust: ClientIP, ExtractRemoteIP, rightMostUntrustedForwardedIP
  and IsTrustedProxyIP forwarded-header and CIDR parsing
- mockmodel: NormalizeBlendWeight, SeriesForTimestamps, seriesForProfile,
  diskIOValue and flatValue deterministic seeded math
- updatesignature: DecodePrivateKey and HasTrustedPublicKeys
- recovery keys: ProxmoxPBSGuestLooseContinuityKey branch guards
- vmware SourceID and truenas availableAppLogContainers formatting

Test-only, contract-neutral. New *_branchcov0719_test.go files only; no source
or existing test touched.
This commit is contained in:
rcourtman
2026-07-19 06:55:26 +01:00
parent cd75722da3
commit 7791b35ae9
9 changed files with 2508 additions and 0 deletions
@@ -0,0 +1,241 @@
package proxytrust
import (
"net/http"
"testing"
)
// setupTrustEnv resets the package-level sync.Once and trusted CIDRs, sets both
// the primary and fallback env vars for the duration of the subtest, and
// schedules a final reset so global state never leaks to sibling tests.
//
// loadTrustedProxyCIDRs reads env inside trustedProxyOnce.Do, so the reset MUST
// happen before t.Setenv and before the call under test.
func setupTrustEnv(t *testing.T, primary, fallback string) {
t.Helper()
ResetForTesting()
t.Setenv("CP_TRUSTED_PROXY_CIDRS", primary)
t.Setenv("PULSE_TRUSTED_PROXY_CIDRS", fallback)
t.Cleanup(ResetForTesting)
}
func makeReq(t *testing.T, remote string, headers map[string]string) *http.Request {
t.Helper()
h := http.Header{}
for k, v := range headers {
h.Set(k, v)
}
return &http.Request{RemoteAddr: remote, Header: h}
}
func TestExtractRemoteIP(t *testing.T) {
tests := []struct {
name string
remoteAddr string
want string
}{
{"ipv4 host port", "1.2.3.4:5678", "1.2.3.4"},
{"bare ipv4 no port", "1.2.3.4", "1.2.3.4"},
{"empty string", "", ""},
{"ipv6 bracketed with port", "[::1]:80", "::1"},
{"ipv6 bracketed no port", "[::1]", "::1"},
{"ipv6 bare no port", "::1", "::1"},
{"whitespace only", " ", " "},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := ExtractRemoteIP(tc.remoteAddr)
if got != tc.want {
t.Errorf("ExtractRemoteIP(%q) = %q, want %q", tc.remoteAddr, got, tc.want)
}
})
}
}
func TestIsTrustedProxyIP(t *testing.T) {
tests := []struct {
name string
primary string
fallback string
rawIP string
want bool
}{
{"inside v4 cidr", "10.0.0.0/8", "", "10.5.6.7", true},
{"outside v4 cidr", "10.0.0.0/8", "", "192.168.1.1", false},
{"bare v4 exact match", "10.0.0.5", "", "10.0.0.5", true},
{"bare v4 different host", "10.0.0.5", "", "10.0.0.6", false},
{"bare v6 exact match", "::1", "", "::1", true},
{"v6 inside cidr", "fc00::/7", "", "fc00::1234", true},
{"bracketed raw input stripped", "10.0.0.0/8", "", "[10.0.0.5]", true},
{"unparseable input", "10.0.0.0/8", "", "not-an-ip", false},
{"empty input", "10.0.0.0/8", "", "", false},
{"no config returns false", "", "", "10.0.0.5", false},
{"invalid cidr entry ignored valid one used", "not-a-cidr,10.0.0.0/8", "", "10.0.0.5", true},
{"all invalid entries yields false", "not-a-cidr,also-bad", "", "10.0.0.5", false},
{"fallback env used when primary empty", "", "10.0.0.0/8", "10.0.0.5", true},
{"multiple cidrs any match", "10.0.0.0/8,192.168.0.0/16", "", "192.168.50.1", true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
setupTrustEnv(t, tc.primary, tc.fallback)
got := IsTrustedProxyIP(tc.rawIP)
if got != tc.want {
t.Errorf("IsTrustedProxyIP(%q) primary=%q fallback=%q = %v, want %v",
tc.rawIP, tc.primary, tc.fallback, got, tc.want)
}
})
}
}
func TestRightMostUntrustedForwardedIP(t *testing.T) {
tests := []struct {
name string
primary string
fallback string
header string
want string
}{
{"empty header", "10.0.0.0/8", "", "", ""},
{"single untrusted hop", "10.0.0.0/8", "", "203.0.113.5", "203.0.113.5"},
{"single trusted hop returns self", "10.0.0.0/8", "", "10.0.0.1", "10.0.0.1"},
{"rightmost untrusted wins over trusted", "10.0.0.0/8", "", "203.0.113.5, 10.0.0.1", "203.0.113.5"},
{"all trusted returns leftmost valid", "10.0.0.0/8", "", "10.0.0.1, 10.0.0.2", "10.0.0.1"},
{"invalid entries skipped returns leftmost valid", "10.0.0.0/8", "", "10.0.0.1, not-an-ip", "10.0.0.1"},
{"all invalid returns empty", "10.0.0.0/8", "", "garbage, also-bad", ""},
{"multi hop mixed picks rightmost untrusted", "10.0.0.0/8", "", "203.0.113.5, 10.0.0.1, 10.0.0.2", "203.0.113.5"},
{"bracketed entries stripped", "10.0.0.0/8", "", "[203.0.113.5], [10.0.0.1]", "203.0.113.5"},
{"no config treats all as untrusted rightmost wins", "", "", "1.2.3.4, 5.6.7.8", "5.6.7.8"},
{"no config single ip returns it", "", "", "1.2.3.4", "1.2.3.4"},
{"fallback env provides trust set", "", "10.0.0.0/8", "203.0.113.5, 10.0.0.1", "203.0.113.5"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
setupTrustEnv(t, tc.primary, tc.fallback)
got := rightMostUntrustedForwardedIP(tc.header)
if got != tc.want {
t.Errorf("rightMostUntrustedForwardedIP(%q) primary=%q = %q, want %q",
tc.header, tc.primary, got, tc.want)
}
})
}
}
func TestClientIP(t *testing.T) {
const trustCIDR = "10.0.0.0/8"
tests := []struct {
name string
req *http.Request
want string
setupEnv bool // when false, still sets env to trustCIDR for determinism
}{
{
name: "nil request returns empty",
req: nil,
want: "",
setupEnv: true,
},
{
name: "empty remote addr returns empty",
req: makeReq(t, "", nil),
want: "",
setupEnv: true,
},
{
name: "untrusted remote ignores xff",
req: makeReq(t, "192.168.1.1:1234", map[string]string{"X-Forwarded-For": "203.0.113.5"}),
want: "192.168.1.1",
setupEnv: true,
},
{
name: "untrusted remote ignores xreal ip",
req: makeReq(t, "192.168.1.1:1234", map[string]string{"X-Real-IP": "203.0.113.5"}),
want: "192.168.1.1",
setupEnv: true,
},
{
name: "untrusted remote no headers returns remote",
req: makeReq(t, "192.168.1.1:1234", nil),
want: "192.168.1.1",
setupEnv: true,
},
{
name: "trusted remote single untrusted xff hop",
req: makeReq(t, "10.0.0.1:1234", map[string]string{"X-Forwarded-For": "203.0.113.5"}),
want: "203.0.113.5",
setupEnv: true,
},
{
name: "trusted remote xff chain mixed trust",
req: makeReq(t, "10.0.0.1:1234", map[string]string{"X-Forwarded-For": "203.0.113.5, 10.0.0.2"}),
want: "203.0.113.5",
setupEnv: true,
},
{
name: "trusted remote all trusted xff returns leftmost",
req: makeReq(t, "10.0.0.1:1234", map[string]string{"X-Forwarded-For": "10.0.0.7, 10.0.0.8"}),
want: "10.0.0.7",
setupEnv: true,
},
{
name: "trusted remote invalid xff falls back to valid xreal ip",
req: makeReq(t, "10.0.0.1:1234", map[string]string{"X-Forwarded-For": "not-an-ip", "X-Real-IP": "203.0.113.7"}),
want: "203.0.113.7",
setupEnv: true,
},
{
name: "trusted remote empty xff falls back to xreal ip",
req: makeReq(t, "10.0.0.1:1234", map[string]string{"X-Real-IP": "203.0.113.9"}),
want: "203.0.113.9",
setupEnv: true,
},
{
name: "trusted remote invalid xreal ip returns remote",
req: makeReq(t, "10.0.0.1:1234", map[string]string{"X-Real-IP": "not-an-ip"}),
want: "10.0.0.1",
setupEnv: true,
},
{
name: "trusted remote bracketed xreal ip stripped",
req: makeReq(t, "10.0.0.1:1234", map[string]string{"X-Real-IP": "[203.0.113.10]"}),
want: "203.0.113.10",
setupEnv: true,
},
{
name: "trusted remote empty xff and empty xreal ip returns remote",
req: makeReq(t, "10.0.0.1:1234", nil),
want: "10.0.0.1",
setupEnv: true,
},
{
name: "trusted remote unparseable remote addr falls through",
req: makeReq(t, "garbage-no-port", map[string]string{"X-Forwarded-For": "203.0.113.5"}),
want: "garbage-no-port",
setupEnv: true,
},
{
name: "no trust config treats remote as untrusted returns remote",
req: makeReq(t, "10.0.0.1:1234", map[string]string{"X-Forwarded-For": "203.0.113.5"}),
want: "10.0.0.1",
setupEnv: false, // explicitly empty config below
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.setupEnv {
setupTrustEnv(t, trustCIDR, "")
} else {
// Explicit empty config: no CIDRs loaded.
setupTrustEnv(t, "", "")
}
got := ClientIP(tc.req)
if got != tc.want {
t.Errorf("ClientIP() = %q, want %q", got, tc.want)
}
})
}
}
@@ -0,0 +1,279 @@
package mockmodel
import (
"math"
"testing"
"time"
)
// fixedAnchor is the deterministic reference timestamp used across the
// branch-coverage tests in this file. The mock model is fully seeded, so fixing
// the timestamp and seeds makes every assertion below reproducible.
var fixedAnchor = time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC)
// hourlyTimestamps returns n evenly-spaced hourly timestamps ending at end
// inclusive. For n<=0 it returns nil so callers can exercise the empty-input
// branch.
func hourlyTimestamps(end time.Time, n int) []time.Time {
if n <= 0 {
return nil
}
out := make([]time.Time, n)
for i := 0; i < n; i++ {
out[n-1-i] = end.Add(-time.Duration(i) * time.Hour)
}
return out
}
// styleName renders a SeriesStyle for subtest labels.
func styleName(s SeriesStyle) string {
switch s {
case StyleSpiky:
return "spiky"
case StylePlateau:
return "plateau"
case StyleFlat:
return "flat"
default:
return "unknown"
}
}
func TestNormalizeBlendWeight_BranchesAndClamps(t *testing.T) {
const eps = 1e-9
cases := []struct {
name string
weight float64
step time.Duration
reference time.Duration
wantExact bool
want float64 // exact expected value when wantExact is true
lo, hi float64 // inclusive bounds for the in-range scaling cases
}{
{name: "negative weight clamps to floor", weight: -0.5, step: time.Minute, reference: 2 * time.Minute, wantExact: true, want: 0.01},
{name: "zero weight clamps to floor", weight: 0, step: time.Minute, reference: 2 * time.Minute, wantExact: true, want: 0.01},
{name: "at one returns one", weight: 1, step: time.Minute, reference: 2 * time.Minute, wantExact: true, want: 1},
{name: "above one clamps to one", weight: 2.5, step: time.Minute, reference: 2 * time.Minute, wantExact: true, want: 1},
{name: "zero step returns weight unchanged", weight: 0.5, step: 0, reference: 2 * time.Minute, wantExact: true, want: 0.5},
{name: "zero reference returns weight unchanged", weight: 0.5, step: time.Minute, reference: 0, wantExact: true, want: 0.5},
{name: "negative step returns weight unchanged", weight: 0.5, step: -time.Minute, reference: 2 * time.Minute, wantExact: true, want: 0.5},
{name: "negative reference returns weight unchanged", weight: 0.5, step: time.Minute, reference: -2 * time.Minute, wantExact: true, want: 0.5},
{name: "equal step and reference returns weight unchanged", weight: 0.5, step: 2 * time.Minute, reference: 2 * time.Minute, wantExact: true, want: 0.5},
{name: "in-range short step scales below weight", weight: 0.5, step: time.Minute, reference: 4 * time.Minute, wantExact: false, lo: 0.0005, hi: 0.5},
{name: "in-range long step scales above weight", weight: 0.5, step: 4 * time.Minute, reference: time.Minute, wantExact: false, lo: 0.5, hi: 0.999},
{name: "extreme ratio triggers upper clamp", weight: 0.99, step: 24 * time.Hour, reference: time.Millisecond, wantExact: true, want: 0.999},
{name: "tiny ratio triggers lower clamp", weight: 0.01, step: time.Millisecond, reference: time.Hour, wantExact: true, want: 0.0005},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got := NormalizeBlendWeight(tc.weight, tc.step, tc.reference)
if tc.wantExact {
if math.Abs(got-tc.want) > eps {
t.Fatalf("NormalizeBlendWeight(%v,%v,%v) = %v, want %v", tc.weight, tc.step, tc.reference, got, tc.want)
}
return
}
if got < tc.lo || got > tc.hi {
t.Fatalf("NormalizeBlendWeight(%v,%v,%v) = %v, want within [%v,%v]", tc.weight, tc.step, tc.reference, got, tc.lo, tc.hi)
}
})
}
}
func TestNormalizeBlendWeight_Deterministic(t *testing.T) {
args := []struct {
w float64
step time.Duration
ref time.Duration
}{
{0.5, time.Minute, 4 * time.Minute},
{0.3, 2 * time.Minute, 3 * time.Minute},
{0.7, 30 * time.Second, 90 * time.Second},
}
for _, a := range args {
first := NormalizeBlendWeight(a.w, a.step, a.ref)
second := NormalizeBlendWeight(a.w, a.step, a.ref)
third := NormalizeBlendWeight(a.w, a.step, a.ref)
if first != second || second != third {
t.Fatalf("expected deterministic output for (%v,%v,%v), got %v %v %v", a.w, a.step, a.ref, first, second, third)
}
}
}
func TestSeriesForTimestamps_EmptyTimestampsReturnsEmpty(t *testing.T) {
for _, style := range []SeriesStyle{StyleSpiky, StylePlateau, StyleFlat} {
style := style
t.Run(styleName(style), func(t *testing.T) {
got := SeriesForTimestamps(50, nil, 7, 0, 100, style)
if len(got) != 0 {
t.Fatalf("expected len=0 for nil timestamps, got %d", len(got))
}
got2 := SeriesForTimestamps(50, []time.Time{}, 7, 0, 100, style)
if len(got2) != 0 {
t.Fatalf("expected len=0 for zero-length timestamps, got %d", len(got2))
}
})
}
}
func TestSeriesForTimestamps_LengthBoundsAndDeterminism(t *testing.T) {
timestamps := hourlyTimestamps(fixedAnchor, 6)
const min, max = 5.0, 95.0
for _, style := range []SeriesStyle{StyleSpiky, StylePlateau, StyleFlat} {
style := style
t.Run(styleName(style), func(t *testing.T) {
series := SeriesForTimestamps(50, timestamps, 11, min, max, style)
if len(series) != len(timestamps) {
t.Fatalf("expected len=%d, got %d", len(timestamps), len(series))
}
for i, v := range series {
if math.IsNaN(v) || math.IsInf(v, 0) {
t.Fatalf("idx %d: non-finite value %v", i, v)
}
if v < min || v > max {
t.Fatalf("idx %d: value %v outside [%v,%v]", i, v, min, max)
}
}
// In-range current anchors the tail to that exact value.
if series[len(series)-1] != 50.0 {
t.Fatalf("expected tail to equal in-range current 50, got %v", series[len(series)-1])
}
// Determinism: identical args must produce identical output.
again := SeriesForTimestamps(50, timestamps, 11, min, max, style)
for i := range series {
if series[i] != again[i] {
t.Fatalf("non-deterministic at idx %d: %v vs %v", i, series[i], again[i])
}
}
})
}
}
func TestSeriesForTimestamps_ClampsOutOfRangeCurrentToTail(t *testing.T) {
timestamps := hourlyTimestamps(fixedAnchor, 4)
const min, max = 10.0, 90.0
hi := SeriesForTimestamps(500, timestamps, 3, min, max, StyleSpiky)
if hi[len(hi)-1] != max {
t.Fatalf("expected above-range current clamped to max=%v at tail, got %v", max, hi[len(hi)-1])
}
for _, v := range hi {
if v < min || v > max {
t.Fatalf("above-range series value %v outside [%v,%v]", v, min, max)
}
}
lo := SeriesForTimestamps(-50, timestamps, 3, min, max, StyleSpiky)
if lo[len(lo)-1] != min {
t.Fatalf("expected below-range current clamped to min=%v at tail, got %v", min, lo[len(lo)-1])
}
for _, v := range lo {
if v < min || v > max {
t.Fatalf("below-range series value %v outside [%v,%v]", v, min, max)
}
}
}
// TestSeriesForProfile_AllProfilesBoundedAndDeterministic exercises the internal
// seriesForProfile helper directly across every metricProfile so each of its
// style branches receives coverage attribution. The values it produces must be
// finite, within [min,max], and identical across repeated calls.
func TestSeriesForProfile_AllProfilesBoundedAndDeterministic(t *testing.T) {
timestamps := hourlyTimestamps(fixedAnchor, 8)
const min, max = 0.0, 100.0
profiles := []struct {
name string
p metricProfile
}{
{"compute", profileCompute},
{"memory", profileMemory},
{"diskio", profileDiskIO},
{"network", profileNetwork},
{"capacity", profileCapacity},
{"thermal", profileThermal},
{"flat", profileFlat},
}
for _, pc := range profiles {
pc := pc
t.Run(pc.name, func(t *testing.T) {
first := seriesForProfile(50, timestamps, 17, min, max, pc.p)
second := seriesForProfile(50, timestamps, 17, min, max, pc.p)
if len(first) != len(timestamps) {
t.Fatalf("%s: expected len=%d, got %d", pc.name, len(timestamps), len(first))
}
for i, v := range first {
if math.IsNaN(v) || math.IsInf(v, 0) {
t.Fatalf("%s idx %d: non-finite %v", pc.name, i, v)
}
if v < min || v > max {
t.Fatalf("%s idx %d: %v outside [%v,%v]", pc.name, i, v, min, max)
}
if v != second[i] {
t.Fatalf("%s idx %d: non-deterministic %v vs %v", pc.name, i, v, second[i])
}
}
if first[len(first)-1] != 50.0 {
t.Fatalf("%s: expected tail to equal in-range current 50, got %v", pc.name, first[len(first)-1])
}
})
}
}
func TestSeriesForProfile_EmptyTimestampsReturnsNil(t *testing.T) {
for _, p := range []metricProfile{profileCompute, profileMemory, profileDiskIO, profileFlat} {
got := seriesForProfile(50, nil, 1, 0, 100, p)
if got != nil {
t.Fatalf("profile %d: expected nil for empty timestamps, got %v (len=%d)", p, got, len(got))
}
}
}
// TestDiskIOValue_BoundedAndDeterministic asserts the documented behaviour of
// diskIOValue: identical inputs produce identical output, the result is finite,
// and across every realistic role modifier set the value stays inside
// [min, min+span]. The bounds are verified empirically across all role profiles.
func TestDiskIOValue_BoundedAndDeterministic(t *testing.T) {
const min, span = 10.0, 100.0
const hi = min + span
for _, role := range []string{"", "database", "backup", "web", "storage", "ci"} {
mods := metricRoleProfile(role)
for _, seed := range []uint64{0, 1, 7, 42, 99} {
a := diskIOValue(seed, min, span, mods, fixedAnchor)
b := diskIOValue(seed, min, span, mods, fixedAnchor)
if a != b {
t.Fatalf("role=%q seed=%d: non-deterministic %v vs %v", role, seed, a, b)
}
if math.IsNaN(a) || math.IsInf(a, 0) {
t.Fatalf("role=%q seed=%d: non-finite %v", role, seed, a)
}
if a < min || a > hi {
t.Fatalf("role=%q seed=%d: %v outside [%v,%v]", role, seed, a, min, hi)
}
}
}
}
// TestFlatValue_BoundedAndDeterministic asserts the same shape of guarantees
// for flatValue as TestDiskIOValue does for diskIOValue.
func TestFlatValue_BoundedAndDeterministic(t *testing.T) {
const min, span = 10.0, 100.0
const hi = min + span
for _, role := range []string{"", "database", "storage", "cache", "media"} {
mods := metricRoleProfile(role)
for _, seed := range []uint64{0, 1, 5, 23, 77} {
a := flatValue(seed, min, span, mods, fixedAnchor)
b := flatValue(seed, min, span, mods, fixedAnchor)
if a != b {
t.Fatalf("role=%q seed=%d: non-deterministic %v vs %v", role, seed, a, b)
}
if math.IsNaN(a) || math.IsInf(a, 0) {
t.Fatalf("role=%q seed=%d: non-finite %v", role, seed, a)
}
if a < min || a > hi {
t.Fatalf("role=%q seed=%d: %v outside [%v,%v]", role, seed, a, min, hi)
}
}
}
}
@@ -0,0 +1,391 @@
package operationaltrust
import (
"strings"
"testing"
"time"
)
func TestEvidencePayloadRefValidate(t *testing.T) {
valid := EvidencePayloadRef{Kind: "metric", ID: "payload-1"}
tests := []struct {
name string
ref EvidencePayloadRef
wantError string
}{
{
name: "valid",
ref: valid,
wantError: "",
},
{
name: "missing kind",
ref: EvidencePayloadRef{ID: "payload-1"},
wantError: "kind is required",
},
{
name: "whitespace kind",
ref: EvidencePayloadRef{Kind: " ", ID: "payload-1"},
wantError: "kind is required",
},
{
name: "missing id",
ref: EvidencePayloadRef{Kind: "metric"},
wantError: "id is required",
},
{
name: "whitespace id",
ref: EvidencePayloadRef{Kind: "metric", ID: "\t "},
wantError: "id is required",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.ref.Validate()
if tc.wantError == "" {
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("Validate() error = %v, want containing %q", err, tc.wantError)
}
})
}
}
func TestAcknowledgementValidate(t *testing.T) {
at := time.Date(2026, 7, 18, 20, 0, 0, 0, time.UTC)
valid := Acknowledgement{At: at, By: "operator-1", Note: "acknowledged"}
tests := []struct {
name string
ack Acknowledgement
wantError string
}{
{
name: "valid",
ack: valid,
wantError: "",
},
{
name: "zero time",
ack: Acknowledgement{By: "operator-1"},
wantError: "time is required",
},
{
name: "missing actor",
ack: Acknowledgement{At: at, By: ""},
wantError: "actor is required",
},
{
name: "whitespace actor",
ack: Acknowledgement{At: at, By: " "},
wantError: "actor is required",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.ack.Validate()
if tc.wantError == "" {
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("Validate() error = %v, want containing %q", err, tc.wantError)
}
})
}
}
func TestSuppressionValidate(t *testing.T) {
at := time.Date(2026, 7, 18, 20, 0, 0, 0, time.UTC)
later := at.Add(time.Hour)
earlier := at.Add(-time.Minute)
equal := at
tests := []struct {
name string
sup Suppression
wantError string
}{
{
name: "valid without expiry",
sup: Suppression{At: at, By: "operator-1", Reason: "maintenance"},
wantError: "",
},
{
name: "valid with expiry",
sup: Suppression{At: at, By: "operator-1", Reason: "maintenance", ExpiresAt: &later},
wantError: "",
},
{
name: "zero time",
sup: Suppression{By: "operator-1", Reason: "maintenance"},
wantError: "time is required",
},
{
name: "missing actor",
sup: Suppression{At: at, Reason: "maintenance"},
wantError: "actor is required",
},
{
name: "whitespace reason",
sup: Suppression{At: at, By: "operator-1", Reason: " "},
wantError: "reason is required",
},
{
name: "expiry before at",
sup: Suppression{At: at, By: "operator-1", Reason: "maintenance", ExpiresAt: &earlier},
wantError: "expiry must follow",
},
{
name: "expiry equal at",
sup: Suppression{At: at, By: "operator-1", Reason: "maintenance", ExpiresAt: &equal},
wantError: "expiry must follow",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.sup.Validate()
if tc.wantError == "" {
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("Validate() error = %v, want containing %q", err, tc.wantError)
}
})
}
}
func TestLifecycleTransitionClone(t *testing.T) {
t.Run("evidence slice is independent", func(t *testing.T) {
transition := LifecycleTransition{
ID: "transition-1",
OperationalRecordID: "record-1",
From: OperationalObserving,
To: OperationalOpen,
EvidenceIDs: []string{"evidence-a", "evidence-b"},
}
clone := transition.Clone()
clone.ID = "transition-2"
clone.EvidenceIDs[0] = "evidence-mutated"
clone.EvidenceIDs = append(clone.EvidenceIDs, "evidence-new")
if transition.ID != "transition-1" {
t.Fatalf("clone mutated source ID: got %q", transition.ID)
}
if transition.EvidenceIDs[0] != "evidence-a" {
t.Fatalf("clone mutated source EvidenceIDs[0]: got %q", transition.EvidenceIDs[0])
}
if len(transition.EvidenceIDs) != 2 {
t.Fatalf("clone mutated source EvidenceIDs length: got %d, want 2", len(transition.EvidenceIDs))
}
if clone.EvidenceIDs[0] != "evidence-mutated" {
t.Fatalf("clone did not receive mutation: got %q", clone.EvidenceIDs[0])
}
})
t.Run("nil evidence slice stays nil-safe", func(t *testing.T) {
transition := LifecycleTransition{
ID: "transition-1",
OperationalRecordID: "record-1",
From: OperationalObserving,
To: OperationalOpen,
}
clone := transition.Clone()
if clone.ID != "transition-1" {
t.Fatalf("clone lost scalar ID: got %q", clone.ID)
}
if clone.From != OperationalObserving || clone.To != OperationalOpen {
t.Fatalf("clone lost state scalars: from=%q to=%q", clone.From, clone.To)
}
if clone.EvidenceIDs != nil {
t.Fatalf("clone EvidenceIDs = %v, want nil", clone.EvidenceIDs)
}
})
}
func TestLifecycleTransitionValidate(t *testing.T) {
at := time.Date(2026, 7, 18, 20, 0, 0, 0, time.UTC)
base := LifecycleTransition{
ID: "transition-1",
OperationalRecordID: "record-1",
From: OperationalObserving,
To: OperationalOpen,
At: at,
Cause: TransitionDetectorDecision,
CauseKey: "cpu-high",
EvidenceIDs: []string{"evidence-1"},
}
tests := []struct {
name string
mutate func(LifecycleTransition) LifecycleTransition
wantError string
}{
{
name: "valid",
mutate: func(t LifecycleTransition) LifecycleTransition { return t },
wantError: "",
},
{
name: "missing id",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.ID = ""
return t
},
wantError: "transition id is required",
},
{
name: "whitespace id treated as missing",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.ID = " "
return t
},
wantError: "transition id is required",
},
{
name: "delegates missing operational record id",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.OperationalRecordID = ""
return t
},
wantError: "operational record id is required",
},
{
name: "delegates invalid from state",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.From = OperationalState("bogus")
return t
},
wantError: "from state",
},
{
name: "delegates invalid to state",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.To = OperationalState("bogus")
return t
},
wantError: "to state",
},
{
name: "delegates no-op transition",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.To = OperationalObserving
return t
},
wantError: "must change state",
},
{
name: "delegates missing transition time",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.At = time.Time{}
return t
},
wantError: "transition time is required",
},
{
name: "delegates missing cause key",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.CauseKey = ""
return t
},
wantError: "cause key is required",
},
{
name: "delegates detector decision requires evidence",
mutate: func(t LifecycleTransition) LifecycleTransition {
t.EvidenceIDs = nil
return t
},
wantError: "detector decision requires evidence",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
transition := tc.mutate(base)
err := transition.Validate()
if tc.wantError == "" {
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("Validate() error = %v, want containing %q", err, tc.wantError)
}
})
}
}
func TestNotificationLinkClone(t *testing.T) {
t.Run("time pointers are independent", func(t *testing.T) {
attemptedAt := time.Date(2026, 7, 18, 20, 0, 0, 0, time.UTC)
completedAt := attemptedAt.Add(time.Second)
link := NotificationLink{
NotificationID: "notification-1",
OperationalRecordID: "record-1",
TransitionID: "transition-1",
LifecycleState: OperationalOpen,
CauseKey: "cpu-high",
DestinationID: "email-primary",
DeliveryState: NotificationQueued,
AttemptedAt: &attemptedAt,
CompletedAt: &completedAt,
}
originalAttempted := *link.AttemptedAt
originalCompleted := *link.CompletedAt
clone := link.Clone()
*clone.AttemptedAt = attemptedAt.Add(time.Hour)
*clone.CompletedAt = completedAt.Add(time.Hour)
clone.NotificationID = "notification-2"
if link.NotificationID != "notification-1" {
t.Fatalf("clone mutated source NotificationID: got %q", link.NotificationID)
}
if !link.AttemptedAt.Equal(originalAttempted) {
t.Fatalf("clone mutated source AttemptedAt: got %v, want %v", link.AttemptedAt, originalAttempted)
}
if !link.CompletedAt.Equal(originalCompleted) {
t.Fatalf("clone mutated source CompletedAt: got %v, want %v", link.CompletedAt, originalCompleted)
}
if !clone.AttemptedAt.Equal(attemptedAt.Add(time.Hour)) {
t.Fatalf("clone did not receive AttemptedAt mutation: got %v", clone.AttemptedAt)
}
})
t.Run("nil time pointers stay nil", func(t *testing.T) {
link := NotificationLink{
NotificationID: "notification-1",
OperationalRecordID: "record-1",
TransitionID: "transition-1",
LifecycleState: OperationalOpen,
CauseKey: "cpu-high",
DestinationID: "email-primary",
DeliveryState: NotificationQueued,
}
clone := link.Clone()
if clone.NotificationID != "notification-1" {
t.Fatalf("clone lost scalar NotificationID: got %q", clone.NotificationID)
}
if clone.AttemptedAt != nil {
t.Fatalf("clone AttemptedAt = %v, want nil", clone.AttemptedAt)
}
if clone.CompletedAt != nil {
t.Fatalf("clone CompletedAt = %v, want nil", clone.CompletedAt)
}
})
}
@@ -0,0 +1,195 @@
package recovery
import "testing"
// TestProxmoxPBSGuestLooseContinuityKey exercises every branch of
// ProxmoxPBSGuestLooseContinuityKey in keys.go: the itemType guard (default
// arm), the empty entityIDLabel guard, the empty/whitespace subjectLabel guard
// (via normalizeContinuityLabel), the numeric-only subjectLabel guard, and the
// success path for both "vm" and "system-container" (including normalization of
// raw item-type aliases through NormalizeRecoveryItemType).
func TestProxmoxPBSGuestLooseContinuityKey(t *testing.T) {
t.Parallel()
cases := []struct {
name string
subjectLabel string
itemType string
entityIDLabel string
want string
}{
// itemType switch: default arm returns "" for any item type that does
// not normalize to "vm" or "system-container".
{
name: "itemType app-container rejected",
subjectLabel: "web-server",
itemType: "app-container",
entityIDLabel: "100",
want: "",
},
{
name: "itemType pvc rejected",
subjectLabel: "web-server",
itemType: "pvc",
entityIDLabel: "100",
want: "",
},
{
name: "itemType empty rejected",
subjectLabel: "web-server",
itemType: "",
entityIDLabel: "100",
want: "",
},
{
name: "itemType all normalizes to empty and is rejected",
subjectLabel: "web-server",
itemType: "all",
entityIDLabel: "100",
want: "",
},
// entityIDLabel empty guard (both literal empty and whitespace-only,
// since the implementation strings.TrimSpace's the value before checking).
{
name: "vm empty entityIDLabel returns empty",
subjectLabel: "web-server",
itemType: "vm",
entityIDLabel: "",
want: "",
},
{
name: "vm whitespace entityIDLabel returns empty",
subjectLabel: "web-server",
itemType: "vm",
entityIDLabel: " ",
want: "",
},
// subjectLabel that normalizes to empty (normalizeContinuityLabel
// returns "" for empty/whitespace-only input).
{
name: "vm empty subjectLabel returns empty",
subjectLabel: "",
itemType: "vm",
entityIDLabel: "100",
want: "",
},
{
name: "vm whitespace subjectLabel returns empty",
subjectLabel: " \t\n",
itemType: "vm",
entityIDLabel: "100",
want: "",
},
// numeric-only subjectLabel guard (isNumericOnlyLabel matches digits only).
{
name: "vm numeric-only subjectLabel returns empty",
subjectLabel: "100",
itemType: "vm",
entityIDLabel: "100",
want: "",
},
{
name: "system-container numeric-only subjectLabel returns empty",
subjectLabel: "00140",
itemType: "system-container",
entityIDLabel: "140",
want: "",
},
// Success path: vm.
{
name: "vm success builds loose key without namespace",
subjectLabel: "web-server",
itemType: "vm",
entityIDLabel: "100",
want: "proxmox-pbs-guest-loose:vm:100:web-server",
},
// Success path: system-container.
{
name: "system-container success builds loose key without namespace",
subjectLabel: "pulse-v4-prod",
itemType: "system-container",
entityIDLabel: "140",
want: "proxmox-pbs-guest-loose:system-container:140:pulse-v4-prod",
},
// NormalizeRecoveryItemType maps alias item types onto "vm"/"system-container"
// and the loose key uses the normalized value.
{
name: "proxmox-vm alias normalizes to vm in loose key",
subjectLabel: "web-server",
itemType: "proxmox-vm",
entityIDLabel: "100",
want: "proxmox-pbs-guest-loose:vm:100:web-server",
},
{
name: "lxc alias normalizes to system-container in loose key",
subjectLabel: "pulse-v4-prod",
itemType: "lxc",
entityIDLabel: "140",
want: "proxmox-pbs-guest-loose:system-container:140:pulse-v4-prod",
},
// normalizeContinuityLabel lowercases input and collapses internal
// whitespace; the loose key embeds the normalized label.
{
name: "subjectLabel normalized (case folded, whitespace collapsed)",
subjectLabel: " Web Server ",
itemType: "vm",
entityIDLabel: "100",
want: "proxmox-pbs-guest-loose:vm:100:web server",
},
// entityIDLabel is only TrimSpace'd (not lowercased); the loose key
// preserves its case and surrounding whitespace is stripped.
{
name: "entityIDLabel trimmed but case preserved",
subjectLabel: "web-server",
itemType: "vm",
entityIDLabel: " VM-100 ",
want: "proxmox-pbs-guest-loose:vm:VM-100:web-server",
},
// Sanity check that the loose key is structurally distinct from the
// conservative key (no namespace segment between itemType and entityID).
{
name: "loose key omits namespace segment present in conservative key",
subjectLabel: "pulse-v4-prod",
itemType: "system-container",
entityIDLabel: "140",
want: "proxmox-pbs-guest-loose:system-container:140:pulse-v4-prod",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := ProxmoxPBSGuestLooseContinuityKey(tc.subjectLabel, tc.itemType, tc.entityIDLabel)
if got != tc.want {
t.Fatalf("ProxmoxPBSGuestLooseContinuityKey(%q, %q, %q) = %q, want %q",
tc.subjectLabel, tc.itemType, tc.entityIDLabel, got, tc.want)
}
})
}
}
// TestProxmoxPBSGuestLooseContinuityKey_OmitsNamespace guarantees directly that
// for the same (subjectLabel, itemType, entityIDLabel) tuple, the loose key
// never embeds a namespace segment and differs from a hypothetical key that did
// include one. This pins the doc-comment promise at keys.go:228-229.
func TestProxmoxPBSGuestLooseContinuityKey_OmitsNamespace(t *testing.T) {
t.Parallel()
got := ProxmoxPBSGuestLooseContinuityKey("pulse-v4-prod", "system-container", "140")
want := "proxmox-pbs-guest-loose:system-container:140:pulse-v4-prod"
if got != want {
t.Fatalf("loose key = %q, want %q", got, want)
}
// The conservative key for the same identity includes a namespace segment;
// the loose key must be a strict prefix-without-namespace and must not equal
// any key that contains a namespace value.
conservative := ProxmoxPBSGuestContinuityKey("pulse-v4-prod", "system-container", "pimox", "140")
if conservative == "" {
t.Fatalf("conservative key unexpectedly empty; test setup invalid")
}
if got == conservative {
t.Fatalf("loose key (%q) must not equal conservative key with namespace (%q)", got, conservative)
}
}
@@ -0,0 +1,475 @@
package model
import (
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/operationaltrust"
)
// validProviderState returns a ProtectionProviderState that is known to pass
// Validate. Test cases mutate a single field off this baseline to trip one
// validation branch at a time.
func validProviderState() ProtectionProviderState {
return ProtectionProviderState{
Provider: ProviderProxmoxPVE,
Source: "source-1",
Scope: "scope-1",
JobState: OutcomeSuccess,
HistoryCompleteness: ProtectionHistoryComplete,
Permissions: operationaltrust.EvidencePermissionsSufficient,
EvidenceIDs: []string{"evidence-1"},
}
}
// validPosture returns a ProtectionPosture that is known to pass Validate.
func validPosture() ProtectionPosture {
at := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
return ProtectionPosture{
SubjectResourceID: "resource-1",
State: ProtectionStateProtected,
LastAttemptAt: &at,
LastSuccessfulPointAt: &at,
LastVerifiedAt: &at,
Freshness: ProtectionFreshnessCurrent,
Verification: ProtectionVerificationVerified,
Coverage: ProtectionCoverageComplete,
ProviderStates: []ProtectionProviderState{validProviderState()},
RepositoryResourceIDs: []string{"repo-1"},
EvidenceIDs: []string{"evidence-1"},
Explanation: "explanation-1",
EvaluatedAt: at,
}
}
func TestProtectionState_Valid(t *testing.T) {
for _, tc := range []struct {
name string
state ProtectionState
want bool
}{
{"protected", ProtectionStateProtected, true},
{"attention", ProtectionStateAttention, true},
{"unprotected", ProtectionStateUnprotected, true},
{"unknown", ProtectionStateUnknown, true},
{"bogus", ProtectionState("bogus"), false},
{"empty", ProtectionState(""), false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := tc.state.Valid(); got != tc.want {
t.Fatalf("ProtectionState(%q).Valid() = %v, want %v", tc.state, got, tc.want)
}
})
}
}
func TestProtectionFreshness_Valid(t *testing.T) {
for _, tc := range []struct {
name string
freshness ProtectionFreshness
want bool
}{
{"current", ProtectionFreshnessCurrent, true},
{"stale", ProtectionFreshnessStale, true},
{"unknown", ProtectionFreshnessUnknown, true},
{"bogus", ProtectionFreshness("bogus"), false},
{"empty", ProtectionFreshness(""), false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := tc.freshness.Valid(); got != tc.want {
t.Fatalf("ProtectionFreshness(%q).Valid() = %v, want %v", tc.freshness, got, tc.want)
}
})
}
}
func TestProtectionVerification_Valid(t *testing.T) {
for _, tc := range []struct {
name string
verification ProtectionVerification
want bool
}{
{"verified", ProtectionVerificationVerified, true},
{"unverified", ProtectionVerificationUnverified, true},
{"stale", ProtectionVerificationStale, true},
{"unknown", ProtectionVerificationUnknown, true},
{"bogus", ProtectionVerification("bogus"), false},
{"empty", ProtectionVerification(""), false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := tc.verification.Valid(); got != tc.want {
t.Fatalf("ProtectionVerification(%q).Valid() = %v, want %v", tc.verification, got, tc.want)
}
})
}
}
func TestProtectionCoverage_Valid(t *testing.T) {
for _, tc := range []struct {
name string
coverage ProtectionCoverage
want bool
}{
{"complete", ProtectionCoverageComplete, true},
{"partial", ProtectionCoveragePartial, true},
{"none", ProtectionCoverageNone, true},
{"unknown", ProtectionCoverageUnknown, true},
{"bogus", ProtectionCoverage("bogus"), false},
{"empty", ProtectionCoverage(""), false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := tc.coverage.Valid(); got != tc.want {
t.Fatalf("ProtectionCoverage(%q).Valid() = %v, want %v", tc.coverage, got, tc.want)
}
})
}
}
func TestProtectionHistoryCompleteness_Valid(t *testing.T) {
for _, tc := range []struct {
name string
completeness ProtectionHistoryCompleteness
want bool
}{
{"complete", ProtectionHistoryComplete, true},
{"partial", ProtectionHistoryPartial, true},
{"unavailable", ProtectionHistoryUnavailable, true},
{"unknown", ProtectionHistoryUnknown, true},
{"bogus", ProtectionHistoryCompleteness("bogus"), false},
{"empty", ProtectionHistoryCompleteness(""), false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := tc.completeness.Valid(); got != tc.want {
t.Fatalf("ProtectionHistoryCompleteness(%q).Valid() = %v, want %v", tc.completeness, got, tc.want)
}
})
}
}
func TestProtectionProviderState_Clone(t *testing.T) {
at := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
state := ProtectionProviderState{
Provider: ProviderProxmoxPVE,
Source: "source-1",
Scope: "scope-1",
JobState: OutcomeSuccess,
HistoryCompleteness: ProtectionHistoryComplete,
Permissions: operationaltrust.EvidencePermissionsSufficient,
LastAttemptAt: &at,
LastSuccessAt: &at,
LastVerifiedAt: &at,
EvidenceIDs: []string{"evidence-1", "evidence-2"},
VerificationExpected: true,
}
t.Run("evidence slice mutation is isolated", func(t *testing.T) {
clone := state.Clone()
clone.EvidenceIDs[0] = "evidence-mutated"
clone.EvidenceIDs = append(clone.EvidenceIDs, "evidence-3")
if state.EvidenceIDs[0] != "evidence-1" {
t.Fatalf("clone slice element mutation propagated to original: state.EvidenceIDs[0] = %q", state.EvidenceIDs[0])
}
if len(state.EvidenceIDs) != 2 {
t.Fatalf("clone append propagated to original: len(state.EvidenceIDs) = %d, want 2", len(state.EvidenceIDs))
}
})
t.Run("time pointer mutation is isolated", func(t *testing.T) {
clone := state.Clone()
if clone.LastAttemptAt == state.LastAttemptAt {
t.Fatal("Clone aliased LastAttemptAt pointer instead of copying")
}
*clone.LastAttemptAt = at.Add(time.Hour)
if !state.LastAttemptAt.Equal(at) {
t.Fatalf("clone pointer mutation propagated to original: state.LastAttemptAt = %v, want %v", state.LastAttemptAt, at)
}
})
t.Run("nil pointers and slices stay nil", func(t *testing.T) {
empty := ProtectionProviderState{}
clone := empty.Clone()
if clone.LastAttemptAt != nil || clone.LastSuccessAt != nil || clone.LastVerifiedAt != nil {
t.Fatalf("Clone of nil pointers produced non-nil pointers: %+v", clone)
}
if clone.EvidenceIDs != nil {
t.Fatalf("Clone of nil EvidenceIDs produced non-nil: %v", clone.EvidenceIDs)
}
})
}
func TestProtectionProviderState_Validate(t *testing.T) {
for _, tc := range []struct {
name string
modify func(s *ProtectionProviderState)
wantErr string // substring of error message; "" means expect success
}{
{
name: "passes when fully populated",
modify: func(s *ProtectionProviderState) {},
wantErr: "",
},
{
name: "missing provider",
modify: func(s *ProtectionProviderState) { s.Provider = "" },
wantErr: "protection provider is required",
},
{
name: "whitespace-only provider",
modify: func(s *ProtectionProviderState) { s.Provider = Provider(" ") },
wantErr: "protection provider is required",
},
{
name: "missing source",
modify: func(s *ProtectionProviderState) { s.Source = "" },
wantErr: "protection provider source is required",
},
{
name: "missing scope",
modify: func(s *ProtectionProviderState) { s.Scope = "" },
wantErr: "protection provider scope is required",
},
{
name: "invalid job state",
modify: func(s *ProtectionProviderState) { s.JobState = Outcome("bogus") },
wantErr: "is invalid",
},
{
name: "invalid history completeness",
modify: func(s *ProtectionProviderState) { s.HistoryCompleteness = ProtectionHistoryCompleteness("bogus") },
wantErr: "is invalid",
},
{
name: "invalid permissions",
modify: func(s *ProtectionProviderState) { s.Permissions = operationaltrust.EvidencePermissions("bogus") },
wantErr: "are invalid",
},
{
name: "unsorted evidence ids",
modify: func(s *ProtectionProviderState) { s.EvidenceIDs = []string{"b", "a"} },
wantErr: "must be sorted and unique",
},
{
name: "duplicate evidence ids",
modify: func(s *ProtectionProviderState) { s.EvidenceIDs = []string{"a", "a"} },
wantErr: "must be sorted and unique",
},
{
name: "blank evidence id",
modify: func(s *ProtectionProviderState) { s.EvidenceIDs = []string{" "} },
wantErr: "must be sorted and unique",
},
} {
t.Run(tc.name, func(t *testing.T) {
state := validProviderState()
tc.modify(&state)
err := state.Validate()
if tc.wantErr == "" {
if err != nil {
t.Fatalf("Validate() unexpected error = %v", err)
}
return
}
if err == nil {
t.Fatalf("Validate() expected error containing %q, got nil", tc.wantErr)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("Validate() error = %q, want substring %q", err.Error(), tc.wantErr)
}
})
}
}
func TestProtectionPosture_Clone(t *testing.T) {
at := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
posture := ProtectionPosture{
SubjectResourceID: "resource-1",
State: ProtectionStateProtected,
LastAttemptAt: &at,
LastSuccessfulPointAt: &at,
LastVerifiedAt: &at,
Freshness: ProtectionFreshnessCurrent,
Verification: ProtectionVerificationVerified,
Coverage: ProtectionCoverageComplete,
ProviderStates: []ProtectionProviderState{
{
Provider: ProviderProxmoxPVE,
Source: "source-1",
Scope: "scope-1",
JobState: OutcomeSuccess,
HistoryCompleteness: ProtectionHistoryComplete,
Permissions: operationaltrust.EvidencePermissionsSufficient,
EvidenceIDs: []string{"evidence-1"},
},
},
RepositoryResourceIDs: []string{"repo-1"},
EvidenceIDs: []string{"evidence-1"},
Explanation: "explanation-1",
EvaluatedAt: at,
}
t.Run("nested provider state slice is deep-copied", func(t *testing.T) {
clone := posture.Clone()
clone.ProviderStates[0].EvidenceIDs[0] = "evidence-mutated"
clone.ProviderStates[0].Source = "source-mutated"
if posture.ProviderStates[0].EvidenceIDs[0] != "evidence-1" {
t.Fatalf("nested evidence slice mutation propagated to original: %q", posture.ProviderStates[0].EvidenceIDs[0])
}
if posture.ProviderStates[0].Source != "source-1" {
t.Fatalf("nested field mutation propagated to original: %q", posture.ProviderStates[0].Source)
}
})
t.Run("provider slice append is isolated", func(t *testing.T) {
clone := posture.Clone()
clone.ProviderStates = append(clone.ProviderStates, ProtectionProviderState{
Provider: ProviderKubernetes,
})
if len(posture.ProviderStates) != 1 {
t.Fatalf("clone append propagated to original: len(posture.ProviderStates) = %d, want 1", len(posture.ProviderStates))
}
})
t.Run("time pointers are deep-copied", func(t *testing.T) {
clone := posture.Clone()
if clone.LastAttemptAt == posture.LastAttemptAt {
t.Fatal("Clone aliased LastAttemptAt pointer instead of copying")
}
*clone.LastAttemptAt = at.Add(time.Hour)
if !posture.LastAttemptAt.Equal(at) {
t.Fatalf("clone pointer mutation propagated to original: posture.LastAttemptAt = %v, want %v", posture.LastAttemptAt, at)
}
})
t.Run("string slices are deep-copied", func(t *testing.T) {
clone := posture.Clone()
clone.RepositoryResourceIDs[0] = "repo-mutated"
clone.EvidenceIDs[0] = "evidence-mutated"
if posture.RepositoryResourceIDs[0] != "repo-1" {
t.Fatalf("RepositoryResourceIDs mutation propagated to original: %q", posture.RepositoryResourceIDs[0])
}
if posture.EvidenceIDs[0] != "evidence-1" {
t.Fatalf("EvidenceIDs mutation propagated to original: %q", posture.EvidenceIDs[0])
}
})
}
func TestProtectionPosture_Validate(t *testing.T) {
for _, tc := range []struct {
name string
modify func(p *ProtectionPosture)
wantErr string
}{
{
name: "passes when fully populated",
modify: func(p *ProtectionPosture) {},
wantErr: "",
},
{
name: "missing subject resource id",
modify: func(p *ProtectionPosture) { p.SubjectResourceID = "" },
wantErr: "protection posture subject resource id is required",
},
{
name: "invalid state",
modify: func(p *ProtectionPosture) { p.State = ProtectionState("bogus") },
wantErr: "protection posture state",
},
{
name: "invalid freshness",
modify: func(p *ProtectionPosture) { p.Freshness = ProtectionFreshness("bogus") },
wantErr: "protection posture freshness",
},
{
name: "invalid verification",
modify: func(p *ProtectionPosture) { p.Verification = ProtectionVerification("bogus") },
wantErr: "protection posture verification",
},
{
name: "invalid coverage",
modify: func(p *ProtectionPosture) { p.Coverage = ProtectionCoverage("bogus") },
wantErr: "protection posture coverage",
},
{
name: "zero evaluated at",
modify: func(p *ProtectionPosture) { p.EvaluatedAt = time.Time{} },
wantErr: "protection posture evaluation time is required",
},
{
name: "missing explanation",
modify: func(p *ProtectionPosture) { p.Explanation = "" },
wantErr: "protection posture explanation is required",
},
{
name: "unsorted repository resource ids",
modify: func(p *ProtectionPosture) { p.RepositoryResourceIDs = []string{"z", "a"} },
wantErr: "protection repository resource ids must be sorted and unique",
},
{
name: "unsorted evidence ids",
modify: func(p *ProtectionPosture) { p.EvidenceIDs = []string{"z", "a"} },
wantErr: "protection evidence ids must be sorted and unique",
},
{
name: "provider state validation error is wrapped with index",
modify: func(p *ProtectionPosture) {
p.ProviderStates = []ProtectionProviderState{
{
Provider: "",
Source: "source-1",
Scope: "scope-1",
JobState: OutcomeSuccess,
HistoryCompleteness: ProtectionHistoryComplete,
Permissions: operationaltrust.EvidencePermissionsSufficient,
EvidenceIDs: []string{"evidence-1"},
},
}
},
wantErr: "protection provider state 0:",
},
{
name: "provider states must be sorted and unique",
modify: func(p *ProtectionPosture) {
p.ProviderStates = []ProtectionProviderState{
{
Provider: ProviderProxmoxPVE,
Source: "source-1",
Scope: "scope-1",
JobState: OutcomeSuccess,
HistoryCompleteness: ProtectionHistoryComplete,
Permissions: operationaltrust.EvidencePermissionsSufficient,
EvidenceIDs: []string{"evidence-1"},
},
{
Provider: ProviderProxmoxPBS,
Source: "source-1",
Scope: "scope-1",
JobState: OutcomeSuccess,
HistoryCompleteness: ProtectionHistoryComplete,
Permissions: operationaltrust.EvidencePermissionsSufficient,
EvidenceIDs: []string{"evidence-2"},
},
}
},
wantErr: "protection provider states must be sorted and unique",
},
} {
t.Run(tc.name, func(t *testing.T) {
posture := validPosture()
tc.modify(&posture)
err := posture.Validate()
if tc.wantErr == "" {
if err != nil {
t.Fatalf("Validate() unexpected error = %v", err)
}
return
}
if err == nil {
t.Fatalf("Validate() expected error containing %q, got nil", tc.wantErr)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("Validate() error = %q, want substring %q", err.Error(), tc.wantErr)
}
})
}
}
@@ -0,0 +1,580 @@
package model
import (
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/operationaltrust"
)
// validProviderObservation builds a ProtectionProviderObservation that passes
// Validate(). Fields are concrete and deterministic so individual error
// branches can be exercised by mutating one field at a time.
func validProviderObservation(t *testing.T) ProtectionProviderObservation {
t.Helper()
observedAt := time.Date(2026, 7, 18, 20, 0, 0, 0, time.UTC)
ingestedAt := observedAt.Add(2 * time.Second)
source := operationaltrust.EvidenceSource{Provider: "proxmox", Collector: "pulse-core"}
subject := operationaltrust.EvidenceSubject{ResourceID: "resource-123"}
id, err := operationaltrust.NewEvidenceID(source, subject, observedAt, "provider-event-7")
if err != nil {
t.Fatalf("NewEvidenceID() error = %v", err)
}
envelope := operationaltrust.EvidenceEnvelope{
ID: id,
Source: source,
Subject: subject,
ObservedAt: observedAt,
IngestedAt: ingestedAt,
Completeness: operationaltrust.EvidenceComplete,
Confidence: operationaltrust.EvidenceConfirmed,
Permissions: operationaltrust.EvidencePermissionsSufficient,
}
return ProtectionProviderObservation{
ID: id,
Provider: ProviderProxmoxPVE,
Source: "pulse-core",
Scope: "pve-a",
JobState: OutcomeSuccess,
HistoryCompleteness: ProtectionHistoryComplete,
Permissions: operationaltrust.EvidencePermissionsSufficient,
ObservedAt: observedAt,
IngestedAt: ingestedAt,
Evidence: envelope,
}
}
func TestProtectionProviderObservationCloneIsDeep(t *testing.T) {
base := validProviderObservation(t)
// Equip the evidence envelope with mutable pointer/map state so we can
// observe whether Clone() shares it with the original.
validUntil := base.ObservedAt.Add(time.Hour)
base.Evidence.ValidUntil = &validUntil
base.Evidence.Reason = &operationaltrust.EvidenceReason{Code: "inferred"}
base.Evidence.Correlation = &operationaltrust.IdentityCorrelation{
Rule: "normalized_hostname",
MatchedFields: map[string]string{"hostname": "db-01"},
CandidateCount: 1,
}
clone := base.Clone()
// Mutate every mutable subfield reachable through the clone's evidence.
*clone.Evidence.ValidUntil = validUntil.Add(time.Hour)
clone.Evidence.Reason.Code = "mutated"
clone.Evidence.Correlation.MatchedFields["hostname"] = "db-99"
if !base.Evidence.ValidUntil.Equal(validUntil) {
t.Fatalf("Clone() shared ValidUntil pointer: base = %v, want %v",
*base.Evidence.ValidUntil, validUntil)
}
if base.Evidence.Reason == nil || base.Evidence.Reason.Code != "inferred" {
v := "<nil>"
if base.Evidence.Reason != nil {
v = base.Evidence.Reason.Code
}
t.Fatalf("Clone() shared Reason pointer: base.Code = %q, want %q", v, "inferred")
}
if base.Evidence.Correlation.MatchedFields["hostname"] != "db-01" {
t.Fatalf("Clone() shared Correlation map: base = %q, want %q",
base.Evidence.Correlation.MatchedFields["hostname"], "db-01")
}
// Sanity: the clone must reflect the mutations we applied to it.
if !clone.Evidence.ValidUntil.Equal(validUntil.Add(time.Hour)) {
t.Fatalf("clone did not record ValidUntil mutation: = %v", *clone.Evidence.ValidUntil)
}
if clone.Evidence.Reason.Code != "mutated" {
t.Fatalf("clone did not record Reason mutation: = %q", clone.Evidence.Reason.Code)
}
if clone.Evidence.Correlation.MatchedFields["hostname"] != "db-99" {
t.Fatalf("clone did not record Correlation mutation: = %q",
clone.Evidence.Correlation.MatchedFields["hostname"])
}
// Scalar fields copied through.
if clone.ID != base.ID || clone.Provider != base.Provider || clone.Scope != base.Scope {
t.Fatalf("Clone() dropped scalar fields: %+v", clone)
}
}
func TestProtectionProviderObservationValidate(t *testing.T) {
t.Run("valid", func(t *testing.T) {
obs := validProviderObservation(t)
if err := obs.Validate(); err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
})
cases := []struct {
name string
mutate func(*ProtectionProviderObservation)
errSubstr string
}{
{
name: "empty id",
mutate: func(o *ProtectionProviderObservation) { o.ID = " " },
errSubstr: "observation id is required",
},
{
name: "empty provider",
mutate: func(o *ProtectionProviderObservation) { o.Provider = " " },
errSubstr: "observation provider is required",
},
{
name: "empty source",
mutate: func(o *ProtectionProviderObservation) { o.Source = "" },
errSubstr: "observation source is required",
},
{
name: "empty scope",
mutate: func(o *ProtectionProviderObservation) { o.Scope = " " },
errSubstr: "observation scope is required",
},
{
name: "invalid job state",
mutate: func(o *ProtectionProviderObservation) { o.JobState = Outcome("bogus") },
errSubstr: "job state",
},
{
name: "invalid history completeness",
mutate: func(o *ProtectionProviderObservation) {
o.HistoryCompleteness = ProtectionHistoryCompleteness("nope")
},
errSubstr: "history completeness",
},
{
name: "invalid permissions",
mutate: func(o *ProtectionProviderObservation) {
o.Permissions = operationaltrust.EvidencePermissions("nope")
},
errSubstr: "permissions",
},
{
name: "zero observedAt",
mutate: func(o *ProtectionProviderObservation) { o.ObservedAt = time.Time{} },
errSubstr: "times are required",
},
{
name: "zero ingestedAt",
mutate: func(o *ProtectionProviderObservation) { o.IngestedAt = time.Time{} },
errSubstr: "times are required",
},
{
name: "evidence observedAt mismatch",
mutate: func(o *ProtectionProviderObservation) {
o.ObservedAt = o.ObservedAt.Add(time.Minute)
},
errSubstr: "observation times must match",
},
{
name: "evidence ingestedAt mismatch",
mutate: func(o *ProtectionProviderObservation) {
o.IngestedAt = o.IngestedAt.Add(time.Minute)
},
errSubstr: "ingestion times must match",
},
{
name: "evidence id mismatch",
mutate: func(o *ProtectionProviderObservation) { o.ID += "-suffix" },
errSubstr: "id must match its evidence id",
},
{
name: "evidence validate fails",
mutate: func(o *ProtectionProviderObservation) {
o.Evidence.Completeness = operationaltrust.EvidenceCompleteness("bogus")
},
errSubstr: "provider observation evidence:",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
obs := validProviderObservation(t)
tc.mutate(&obs)
err := obs.Validate()
if err == nil {
t.Fatalf("Validate() error = nil, want error containing %q", tc.errSubstr)
}
if !strings.Contains(err.Error(), tc.errSubstr) {
t.Fatalf("Validate() error = %q, want substring %q", err.Error(), tc.errSubstr)
}
})
}
}
func TestProtectionPosturePolicyValidate(t *testing.T) {
cases := []struct {
name string
policy ProtectionPosturePolicy
wantErr bool
substr string
}{
{
name: "valid with requireVerification",
policy: ProtectionPosturePolicy{FreshnessWindow: 5 * time.Minute, VerificationWindow: time.Hour, RequireVerification: true},
wantErr: false,
},
{
name: "freshness zero",
policy: ProtectionPosturePolicy{FreshnessWindow: 0, VerificationWindow: time.Hour},
wantErr: true,
substr: "freshness window must be positive",
},
{
name: "freshness negative",
policy: ProtectionPosturePolicy{FreshnessWindow: -time.Second, VerificationWindow: time.Hour},
wantErr: true,
substr: "freshness window must be positive",
},
{
name: "verification zero",
policy: ProtectionPosturePolicy{FreshnessWindow: time.Minute, VerificationWindow: 0},
wantErr: true,
substr: "verification window must be positive",
},
{
name: "verification negative",
policy: ProtectionPosturePolicy{FreshnessWindow: time.Minute, VerificationWindow: -time.Second},
wantErr: true,
substr: "verification window must be positive",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.policy.Validate()
if tc.wantErr {
if err == nil {
t.Fatalf("Validate() error = nil, want error containing %q", tc.substr)
}
if !strings.Contains(err.Error(), tc.substr) {
t.Fatalf("Validate() error = %q, want substring %q", err.Error(), tc.substr)
}
return
}
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
})
}
}
func TestProtectionPosturePolicyPayload(t *testing.T) {
t.Run("maps whole-second durations and flag", func(t *testing.T) {
policy := ProtectionPosturePolicy{
FreshnessWindow: 90 * time.Second,
VerificationWindow: 3600 * time.Second,
RequireVerification: true,
}
payload := policy.Payload()
if payload.FreshnessWindowSeconds != 90 {
t.Fatalf("FreshnessWindowSeconds = %d, want 90", payload.FreshnessWindowSeconds)
}
if payload.VerificationWindowSeconds != 3600 {
t.Fatalf("VerificationWindowSeconds = %d, want 3600", payload.VerificationWindowSeconds)
}
if payload.RequireVerification != true {
t.Fatalf("RequireVerification = %v, want true", payload.RequireVerification)
}
})
t.Run("truncates sub-second durations toward zero", func(t *testing.T) {
// Payload uses int64(d / time.Second), which truncates fractional
// seconds toward zero for positive durations.
policy := ProtectionPosturePolicy{
FreshnessWindow: 1500 * time.Millisecond,
VerificationWindow: 2*time.Second + 700*time.Millisecond,
RequireVerification: false,
}
payload := policy.Payload()
if payload.FreshnessWindowSeconds != 1 {
t.Fatalf("FreshnessWindowSeconds = %d, want 1 (truncated)", payload.FreshnessWindowSeconds)
}
if payload.VerificationWindowSeconds != 2 {
t.Fatalf("VerificationWindowSeconds = %d, want 2 (truncated)", payload.VerificationWindowSeconds)
}
if payload.RequireVerification != false {
t.Fatalf("RequireVerification = %v, want false", payload.RequireVerification)
}
})
}
func TestProtectionProviderSummaryNormalize(t *testing.T) {
t.Run("trims whitespace and preserves valid enums and pass-through fields", func(t *testing.T) {
attemptAt := time.Date(2026, 7, 18, 20, 0, 0, 0, time.UTC)
successAt := attemptAt.Add(time.Hour)
summary := ProtectionProviderSummary{
Provider: " proxmox-pve ",
Source: " pulse-core\n",
Scope: "\tpve-a ",
JobState: OutcomeWarning,
HistoryCompleteness: ProtectionHistoryPartial,
Permissions: operationaltrust.EvidencePermissionsPartial,
RepositoryResourceIDs: []string{"repo-b", " ", "repo-a", "repo-a"},
EvidenceIDs: []string{"e2", "e1", "e1", ""},
LastAttemptAt: &attemptAt,
LastSuccessAt: &successAt,
BackupPointCount: 7,
SnapshotPointCount: 3,
VerificationExpected: true,
}
got := summary.normalize()
if got.Provider != ProviderProxmoxPVE {
t.Fatalf("Provider = %q, want %q", got.Provider, ProviderProxmoxPVE)
}
if got.Source != "pulse-core" {
t.Fatalf("Source = %q, want %q", got.Source, "pulse-core")
}
if got.Scope != "pve-a" {
t.Fatalf("Scope = %q, want %q", got.Scope, "pve-a")
}
if got.JobState != OutcomeWarning {
t.Fatalf("JobState = %q, want %q (valid value must be preserved)",
got.JobState, OutcomeWarning)
}
if got.HistoryCompleteness != ProtectionHistoryPartial {
t.Fatalf("HistoryCompleteness = %q, want %q",
got.HistoryCompleteness, ProtectionHistoryPartial)
}
if got.Permissions != operationaltrust.EvidencePermissionsPartial {
t.Fatalf("Permissions = %q, want %q",
got.Permissions, operationaltrust.EvidencePermissionsPartial)
}
wantRepos := []string{"repo-a", "repo-b"}
if len(got.RepositoryResourceIDs) != len(wantRepos) {
t.Fatalf("RepositoryResourceIDs = %v, want %v", got.RepositoryResourceIDs, wantRepos)
}
for i := range wantRepos {
if got.RepositoryResourceIDs[i] != wantRepos[i] {
t.Fatalf("RepositoryResourceIDs = %v, want %v", got.RepositoryResourceIDs, wantRepos)
}
}
wantEvidence := []string{"e1", "e2"}
if len(got.EvidenceIDs) != len(wantEvidence) {
t.Fatalf("EvidenceIDs = %v, want %v", got.EvidenceIDs, wantEvidence)
}
for i := range wantEvidence {
if got.EvidenceIDs[i] != wantEvidence[i] {
t.Fatalf("EvidenceIDs = %v, want %v", got.EvidenceIDs, wantEvidence)
}
}
if got.BackupPointCount != 7 || got.SnapshotPointCount != 3 || !got.VerificationExpected {
t.Fatalf("normalize dropped pass-through fields: %+v", got)
}
})
t.Run("replaces invalid enums with unknown sentinels", func(t *testing.T) {
summary := ProtectionProviderSummary{
Provider: ProviderProxmoxPBS,
Source: "src",
Scope: "scope",
JobState: Outcome("bogus"),
HistoryCompleteness: ProtectionHistoryCompleteness("nope"),
Permissions: operationaltrust.EvidencePermissions("nope"),
}
got := summary.normalize()
if got.JobState != OutcomeUnknown {
t.Fatalf("JobState = %q, want %q", got.JobState, OutcomeUnknown)
}
if got.HistoryCompleteness != ProtectionHistoryUnknown {
t.Fatalf("HistoryCompleteness = %q, want %q",
got.HistoryCompleteness, ProtectionHistoryUnknown)
}
if got.Permissions != operationaltrust.EvidencePermissionsUnknown {
t.Fatalf("Permissions = %q, want %q",
got.Permissions, operationaltrust.EvidencePermissionsUnknown)
}
})
t.Run("nil slice fields become empty non-nil slices", func(t *testing.T) {
summary := ProtectionProviderSummary{
Provider: ProviderProxmoxPVE,
Source: "src",
Scope: "scope",
}
got := summary.normalize()
if got.RepositoryResourceIDs == nil {
t.Fatal("RepositoryResourceIDs = nil, want non-nil empty slice")
}
if len(got.RepositoryResourceIDs) != 0 {
t.Fatalf("len(RepositoryResourceIDs) = %d, want 0", len(got.RepositoryResourceIDs))
}
if got.EvidenceIDs == nil {
t.Fatal("EvidenceIDs = nil, want non-nil empty slice")
}
if len(got.EvidenceIDs) != 0 {
t.Fatalf("len(EvidenceIDs) = %d, want 0", len(got.EvidenceIDs))
}
})
}
func TestCloneTime(t *testing.T) {
t.Run("nil returns nil", func(t *testing.T) {
if got := cloneTime(nil); got != nil {
t.Fatalf("cloneTime(nil) = %v, want nil", *got)
}
})
t.Run("non-nil returns distinct pointer with equal UTC value", func(t *testing.T) {
// Construct a time in a non-UTC fixed zone so we can also confirm the
// UTC normalisation performed by cloneTime (uses a fixed offset to
// avoid depending on system tzdata availability).
loc := time.FixedZone("EDT", -4*3600)
original := time.Date(2026, 7, 18, 16, 0, 0, 0, loc) // 16:00 EDT == 20:00 UTC
cloned := cloneTime(&original)
if cloned == nil {
t.Fatal("cloneTime(non-nil) = nil, want non-nil")
}
if cloned == &original {
t.Fatal("cloneTime returned the same pointer as input, want a distinct pointer")
}
if !cloned.Equal(original) {
t.Fatalf("cloned time = %v, want Equal to original %v", *cloned, original)
}
if cloned.Location() != time.UTC {
t.Fatalf("cloned location = %v, want UTC", cloned.Location())
}
// Mutating the clone must not move the original instant.
*cloned = cloned.Add(time.Hour)
if !original.Equal(time.Date(2026, 7, 18, 16, 0, 0, 0, loc)) {
t.Fatalf("original was mutated through clone: = %v", original)
}
})
}
func TestValidOutcome(t *testing.T) {
for _, tc := range []Outcome{
OutcomeSuccess,
OutcomeWarning,
OutcomeFailed,
OutcomeRunning,
OutcomeUnknown,
} {
if !validOutcome(tc) {
t.Errorf("validOutcome(%q) = false, want true", string(tc))
}
}
for _, tc := range []Outcome{"", "success ", "SUCCESS", "pending", "ok"} {
if validOutcome(tc) {
t.Errorf("validOutcome(%q) = true, want false", string(tc))
}
}
}
func TestValidEvidencePermissions(t *testing.T) {
for _, tc := range []operationaltrust.EvidencePermissions{
operationaltrust.EvidencePermissionsSufficient,
operationaltrust.EvidencePermissionsPartial,
operationaltrust.EvidencePermissionsDenied,
operationaltrust.EvidencePermissionsUnknown,
} {
if !validEvidencePermissions(tc) {
t.Errorf("validEvidencePermissions(%q) = false, want true", string(tc))
}
}
for _, tc := range []operationaltrust.EvidencePermissions{"", "FULL", "sufficient "} {
if validEvidencePermissions(tc) {
t.Errorf("validEvidencePermissions(%q) = true, want false", string(tc))
}
}
}
func TestNormalizeSortedStrings(t *testing.T) {
cases := []struct {
name string
in []string
want []string
}{
{name: "nil", in: nil, want: []string{}},
{name: "empty", in: []string{}, want: []string{}},
{name: "all blanks dropped", in: []string{" ", "", "\t"}, want: []string{}},
{name: "trims surrounding whitespace", in: []string{" b ", "a"}, want: []string{"a", "b"}},
{name: "sorts and dedups", in: []string{"c", "a", "b", "a", "c"}, want: []string{"a", "b", "c"}},
{name: "already clean stays clean", in: []string{"a", "b", "c"}, want: []string{"a", "b", "c"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := normalizeSortedStrings(tc.in)
if got == nil {
t.Fatalf("normalizeSortedStrings(%v) returned nil, want non-nil slice", tc.in)
}
if len(got) != len(tc.want) {
t.Fatalf("normalizeSortedStrings(%v) = %v, want %v", tc.in, got, tc.want)
}
for i := range tc.want {
if got[i] != tc.want[i] {
t.Fatalf("normalizeSortedStrings(%v) = %v, want %v", tc.in, got, tc.want)
}
}
})
}
}
func TestSortedUniqueStrings(t *testing.T) {
cases := []struct {
name string
in []string
want bool
}{
{name: "nil is vacuously true", in: nil, want: true},
{name: "empty is vacuously true", in: []string{}, want: true},
{name: "single element", in: []string{"a"}, want: true},
{name: "sorted unique", in: []string{"a", "b", "c"}, want: true},
{name: "whitespace-only rejected", in: []string{"a", " "}, want: false},
{name: "empty string rejected", in: []string{"a", ""}, want: false},
{name: "duplicate rejected", in: []string{"a", "a"}, want: false},
{name: "unsorted rejected", in: []string{"b", "a"}, want: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := sortedUniqueStrings(tc.in); got != tc.want {
t.Fatalf("sortedUniqueStrings(%v) = %v, want %v", tc.in, got, tc.want)
}
})
}
}
func TestCompareProviderStates(t *testing.T) {
mk := func(provider Provider, scope, source string) ProtectionProviderState {
return ProtectionProviderState{Provider: provider, Scope: scope, Source: source}
}
providerPVE := mk(ProviderProxmoxPVE, "scope-1", "src-1")
providerPBS := mk(ProviderProxmoxPBS, "scope-1", "src-1")
sameAsPVE := mk(ProviderProxmoxPVE, "scope-1", "src-1")
higherScope := mk(ProviderProxmoxPVE, "scope-2", "src-1")
higherSource := mk(ProviderProxmoxPVE, "scope-1", "src-2")
t.Run("equal returns zero", func(t *testing.T) {
if got := compareProviderStates(providerPVE, sameAsPVE); got != 0 {
t.Fatalf("compareProviderStates(equal) = %d, want 0", got)
}
})
t.Run("negative when provider ranks lower", func(t *testing.T) {
// "proxmox-pbs" < "proxmox-pve"
if got := compareProviderStates(providerPBS, providerPVE); got >= 0 {
t.Fatalf("compareProviderStates(pbs,pve) = %d, want negative", got)
}
})
t.Run("positive when provider ranks higher", func(t *testing.T) {
if got := compareProviderStates(providerPVE, providerPBS); got <= 0 {
t.Fatalf("compareProviderStates(pve,pbs) = %d, want positive", got)
}
})
t.Run("differentiates by scope when provider equal", func(t *testing.T) {
if got := compareProviderStates(providerPVE, higherScope); got >= 0 {
t.Fatalf("compareProviderStates(scope-1,scope-2) = %d, want negative", got)
}
if got := compareProviderStates(higherScope, providerPVE); got <= 0 {
t.Fatalf("compareProviderStates(scope-2,scope-1) = %d, want positive", got)
}
})
t.Run("differentiates by source when provider and scope equal", func(t *testing.T) {
if got := compareProviderStates(providerPVE, higherSource); got >= 0 {
t.Fatalf("compareProviderStates(src-1,src-2) = %d, want negative", got)
}
if got := compareProviderStates(higherSource, providerPVE); got <= 0 {
t.Fatalf("compareProviderStates(src-2,src-1) = %d, want positive", got)
}
})
}
@@ -0,0 +1,93 @@
package truenas
import "testing"
// TestAvailableAppLogContainersBranchCov exercises every branch of
// availableAppLogContainers: the empty-input short circuit, the
// post-filter empty fallback, the ServiceName→ID fallback, the
// skip-when-both-empty filter, the case-insensitive ID-equality guard
// that suppresses the parenthesised ID, the ID-in-parens formatting,
// whitespace trimming, and the multi-container join.
func TestAvailableAppLogContainersBranchCov(t *testing.T) {
tests := []struct {
name string
containers []AppContainer
want string
}{
{
name: "empty slice returns empty string",
containers: nil,
want: "",
},
{
name: "all containers filtered out returns empty string",
containers: []AppContainer{
{ServiceName: " ", ID: ""},
{ServiceName: "", ID: " "},
},
want: "",
},
{
name: "service name only uses service name without parens",
containers: []AppContainer{
{ServiceName: "svc-a", ID: ""},
},
want: "svc-a",
},
{
name: "id only falls back to id without parens",
containers: []AppContainer{
{ServiceName: "", ID: "id-1"},
},
want: "id-1",
},
{
name: "service name equal to id case-insensitive suppresses parens",
containers: []AppContainer{
{ServiceName: "svc", ID: "SVC"},
},
want: "svc",
},
{
name: "service name with differing id appends id in parens",
containers: []AppContainer{
{ServiceName: "svc-a", ID: "id-1"},
},
want: "svc-a (id-1)",
},
{
name: "leading and trailing whitespace is trimmed before formatting",
containers: []AppContainer{
{ServiceName: " padded-svc ", ID: " padded-id "},
},
want: "padded-svc (padded-id)",
},
{
name: "trimmed service name matching trimmed id suppresses parens",
containers: []AppContainer{
{ServiceName: " Name ", ID: " name "},
},
want: "Name",
},
{
name: "multiple containers are comma joined in order",
containers: []AppContainer{
{ServiceName: "svc-a", ID: "id-a"},
{ServiceName: "", ID: "id-only"},
{ServiceName: "svc-c", ID: ""},
{ServiceName: " ", ID: " "},
{ServiceName: "Same", ID: "same"},
},
want: "svc-a (id-a), id-only, svc-c, Same",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := availableAppLogContainers(tt.containers)
if got != tt.want {
t.Fatalf("availableAppLogContainers(%+v) = %q, want %q", tt.containers, got, tt.want)
}
})
}
}
@@ -0,0 +1,149 @@
package updatesignature
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"errors"
"strings"
"testing"
)
func TestDecodePrivateKey(t *testing.T) {
t.Run("full key roundtrip", func(t *testing.T) {
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
encoded := base64.StdEncoding.EncodeToString(privateKey)
decoded, err := DecodePrivateKey(encoded)
if err != nil {
t.Fatalf("DecodePrivateKey: %v", err)
}
if !bytesEqual(decoded, privateKey) {
t.Fatalf("decoded key = %x, want %x", []byte(decoded), []byte(privateKey))
}
if !decoded.Equal(privateKey) {
t.Fatalf("decoded key does not equal original")
}
})
t.Run("seed roundtrip derives same key", func(t *testing.T) {
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
seed := privateKey.Seed()
encoded := base64.StdEncoding.EncodeToString(seed)
decoded, err := DecodePrivateKey(encoded)
if err != nil {
t.Fatalf("DecodePrivateKey from seed: %v", err)
}
if !decoded.Equal(privateKey) {
t.Fatalf("seed-derived key = %x, want %x", []byte(decoded), []byte(privateKey))
}
})
t.Run("empty returns error and nil key", func(t *testing.T) {
decoded, err := DecodePrivateKey("")
if err == nil {
t.Fatal("expected error for empty input, got nil")
}
if !strings.Contains(err.Error(), "empty signing key") {
t.Fatalf("error = %q, want substring %q", err.Error(), "empty signing key")
}
if decoded != nil {
t.Fatalf("expected nil key, got %x", []byte(decoded))
}
})
t.Run("whitespace only returns empty error", func(t *testing.T) {
decoded, err := DecodePrivateKey(" \t\n ")
if err == nil {
t.Fatal("expected error for whitespace input, got nil")
}
if !strings.Contains(err.Error(), "empty signing key") {
t.Fatalf("error = %q, want substring %q", err.Error(), "empty signing key")
}
if decoded != nil {
t.Fatalf("expected nil key, got %x", []byte(decoded))
}
})
t.Run("invalid base64 returns wrapped error and nil key", func(t *testing.T) {
decoded, err := DecodePrivateKey("not-base64!!!")
if err == nil {
t.Fatal("expected error for invalid base64, got nil")
}
if !strings.Contains(err.Error(), "invalid base64 signing key") {
t.Fatalf("error = %q, want substring %q", err.Error(), "invalid base64 signing key")
}
var inner interface{ Unwrap() error }
if !errors.As(err, &inner) {
t.Fatalf("expected wrapped error, got %T: %v", err, err)
}
if decoded != nil {
t.Fatalf("expected nil key, got %x", []byte(decoded))
}
})
t.Run("valid base64 wrong length returns error and nil key", func(t *testing.T) {
bogus := []byte{1, 2, 3, 4, 5}
encoded := base64.StdEncoding.EncodeToString(bogus)
decoded, err := DecodePrivateKey(encoded)
if err == nil {
t.Fatal("expected error for wrong-length key, got nil")
}
if !strings.Contains(err.Error(), "invalid signing key length") {
t.Fatalf("error = %q, want substring %q", err.Error(), "invalid signing key length")
}
if !strings.Contains(err.Error(), "5") {
t.Fatalf("error = %q, want it to contain the bad length 5", err.Error())
}
if decoded != nil {
t.Fatalf("expected nil key, got %x", []byte(decoded))
}
})
}
func TestHasTrustedPublicKeys(t *testing.T) {
original := EmbeddedTrustedPublicKeys
t.Cleanup(func() { EmbeddedTrustedPublicKeys = original })
t.Run("empty returns false", func(t *testing.T) {
EmbeddedTrustedPublicKeys = ""
if HasTrustedPublicKeys() {
t.Fatal("HasTrustedPublicKeys() = true, want false for empty")
}
})
t.Run("whitespace only returns false", func(t *testing.T) {
EmbeddedTrustedPublicKeys = " \t\n "
if HasTrustedPublicKeys() {
t.Fatal("HasTrustedPublicKeys() = true, want false for whitespace-only")
}
})
t.Run("populated returns true", func(t *testing.T) {
EmbeddedTrustedPublicKeys = "c29tZWtleQ=="
if !HasTrustedPublicKeys() {
t.Fatal("HasTrustedPublicKeys() = false, want true for populated value")
}
})
}
// bytesEqual is a tiny helper to avoid pulling in bytes for a single call.
func bytesEqual(a, b ed25519.PrivateKey) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
@@ -0,0 +1,105 @@
package vmware
import (
"strings"
"testing"
)
// TestSourceID exercises the exported SourceID helper, which delegates to
// vmwareSourceID -> filterNonEmptyStrings -> strings.Join(parts, ":").
//
// filterNonEmptyStrings (a) trims whitespace on each component, (b) drops
// any component that is empty after trimming, and (c) deduplicates the
// remaining components case-insensitively, preserving the first occurrence.
// The cases below drive each of those branches through the public SourceID
// entry point so coverage lands on the named target function.
func TestSourceID(t *testing.T) {
cases := []struct {
name string
connectionID string
entityType string
managedObjectID string
want string
}{
{
name: "all three populated joins with colon",
connectionID: "vc-1",
entityType: "host",
managedObjectID: "host-101",
want: "vc-1:host:host-101",
},
{
name: "empty middle component is dropped",
connectionID: "vc-1",
entityType: "",
managedObjectID: "vm-201",
want: "vc-1:vm-201",
},
{
name: "whitespace-only component is treated as empty",
connectionID: "vc-1",
entityType: " ",
managedObjectID: "vm-201",
want: "vc-1:vm-201",
},
{
name: "all components empty returns empty string",
connectionID: "",
entityType: "",
managedObjectID: "",
want: "",
},
{
name: "leading and trailing whitespace trimmed from each component",
connectionID: " vc-1 ",
entityType: "\thost\n",
managedObjectID: " host-101 ",
want: "vc-1:host:host-101",
},
{
name: "case-insensitive duplicate components collapse to first occurrence",
connectionID: "VC-1",
entityType: "vc-1",
managedObjectID: "VC-1",
want: "VC-1",
},
{
name: "distinct inputs differ only in the third component keep both",
connectionID: "vc-1",
entityType: "host",
managedObjectID: "host-102",
want: "vc-1:host:host-102",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := SourceID(tc.connectionID, tc.entityType, tc.managedObjectID)
if got != tc.want {
t.Fatalf("SourceID(%q, %q, %q) = %q, want %q",
tc.connectionID, tc.entityType, tc.managedObjectID, got, tc.want)
}
})
}
t.Run("different inputs produce different IDs", func(t *testing.T) {
a := SourceID("vc-1", "host", "host-101")
b := SourceID("vc-1", "host", "host-102")
if a == b {
t.Fatalf("expected distinct IDs for distinct managedObjectID, got %q == %q", a, b)
}
})
t.Run("identical inputs are stable across calls", func(t *testing.T) {
first := SourceID("vc-1", "vm", "vm-201")
second := SourceID("vc-1", "vm", "vm-201")
if first != second {
t.Fatalf("SourceID not stable: first=%q second=%q", first, second)
}
// Sanity-check the stable value matches the documented colon-join
// format so this subtest asserts real behaviour, not just equality.
if want := strings.Join([]string{"vc-1", "vm", "vm-201"}, ":"); first != want {
t.Fatalf("stable SourceID = %q, want canonical %q", first, want)
}
})
}