mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Cover telemetry classifiers, lifecycle records and install snapshot counts
Five new branch-coverage tests over the packages the telemetry lifecycle drop touched, taking twenty-two functions to full coverage without touching any source or existing test. internal/telemetry: the deployment method precedence chain, the duration bucket asserted one nanosecond either side of every boundary so the strict comparison is pinned, every activation stage in its precedence order with the rank ordering proved strictly increasing, the estate size bucket at each inclusive boundary, the monitored resource count proved to draw on each counted field individually, and the update failure classifier through every category arm including the status short-circuit that wins over error text. The lifecycle and install-id records are exercised as write-then-read round trips under a temp directory and a frozen clock: a first-ever ping with nothing on disk, a later ping against an existing record, an unreadable record, a directory that cannot be written, and a reset proved to change the identifier and persist the new one. internal/monitoring: the alert outcome accumulation across the cutoff boundary and each classification, and the install outcome accumulation over the arms reachable without a live monitor. internal/api: the read state snapshot mapping asserted field by field with collection independence, both diagnostics collection normalizers, the in-memory zip entry extraction across a missing entry, a non-zip payload and a zero-length entry, and the trusted proxy check over IPv4, IPv6 and malformed input. Contract-Neutral: test-only branch coverage, no contract surface touched
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
)
|
||||
|
||||
// branchcov0723amPopulatedSnapshot builds a snapshot that carries exactly one
|
||||
// entry of every top-level collection SnapshotReadState routes into the
|
||||
// unified registry. Values are chosen to be distinctive so a subtest can
|
||||
// assert the source value lands in the matching ReadState accessor.
|
||||
func branchcov0723amPopulatedSnapshot() models.StateSnapshot {
|
||||
return models.StateSnapshot{
|
||||
Nodes: []models.Node{{
|
||||
ID: "node-branchcov",
|
||||
Name: "node-branchcov",
|
||||
Instance: "instance-branchcov",
|
||||
Status: "online",
|
||||
}},
|
||||
VMs: []models.VM{{
|
||||
ID: "vm-branchcov",
|
||||
VMID: 4242,
|
||||
Name: "vm-branchcov",
|
||||
Status: "running",
|
||||
}},
|
||||
Containers: []models.Container{{
|
||||
ID: "ct-branchcov",
|
||||
Name: "ct-branchcov",
|
||||
Status: "running",
|
||||
}},
|
||||
DockerHosts: []models.DockerHost{{
|
||||
ID: "docker-host-branchcov",
|
||||
Hostname: "docker-host-branchcov",
|
||||
Status: "online",
|
||||
Containers: []models.DockerContainer{{
|
||||
ID: "app-container-branchcov",
|
||||
Name: "app-container-branchcov",
|
||||
State: "running",
|
||||
}},
|
||||
}},
|
||||
Hosts: []models.Host{{
|
||||
ID: "host-branchcov",
|
||||
Hostname: "host-branchcov",
|
||||
Status: "online",
|
||||
}},
|
||||
Storage: []models.Storage{{
|
||||
ID: "storage-branchcov",
|
||||
Name: "storage-branchcov",
|
||||
Type: "lvm",
|
||||
}},
|
||||
PhysicalDisks: []models.PhysicalDisk{{
|
||||
ID: "disk-branchcov",
|
||||
DevPath: "/dev/sda",
|
||||
Type: "sata",
|
||||
}},
|
||||
PBSInstances: []models.PBSInstance{{
|
||||
ID: "pbs-branchcov",
|
||||
Name: "pbs-branchcov",
|
||||
}},
|
||||
PMGInstances: []models.PMGInstance{{
|
||||
ID: "pmg-branchcov",
|
||||
Name: "pmg-branchcov",
|
||||
}},
|
||||
KubernetesClusters: []models.KubernetesCluster{{
|
||||
ID: "k8s-branchcov",
|
||||
Name: "k8s-branchcov",
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// findHostViewByHostname returns the first host view whose Hostname matches,
|
||||
// or nil. Hosts() may synthesise linked-agent siblings, so callers locate the
|
||||
// specific source host by hostname rather than by position.
|
||||
func findHostViewByHostname(views []*unifiedresources.HostView, hostname string) *unifiedresources.HostView {
|
||||
for _, v := range views {
|
||||
if v != nil && v.Hostname() == hostname {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findPhysicalDiskViewByDevPath(views []*unifiedresources.PhysicalDiskView, devPath string) *unifiedresources.PhysicalDiskView {
|
||||
for _, v := range views {
|
||||
if v != nil && v.DevPath() == devPath {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_SnapshotReadState_Zero drives SnapshotReadState with a
|
||||
// zero-value snapshot and asserts every ReadState accessor returns an empty
|
||||
// (length 0) result rather than nil-panicking.
|
||||
func TestBranchcov0723Am_SnapshotReadState_Zero(t *testing.T) {
|
||||
rs := SnapshotReadState(models.StateSnapshot{})
|
||||
if rs == nil {
|
||||
t.Fatal("SnapshotReadState returned nil ReadState for zero snapshot")
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
got int
|
||||
}{
|
||||
{"VMs", len(rs.VMs())},
|
||||
{"Containers", len(rs.Containers())},
|
||||
{"Nodes", len(rs.Nodes())},
|
||||
{"Hosts", len(rs.Hosts())},
|
||||
{"DockerHosts", len(rs.DockerHosts())},
|
||||
{"DockerContainers", len(rs.DockerContainers())},
|
||||
{"StoragePools", len(rs.StoragePools())},
|
||||
{"PhysicalDisks", len(rs.PhysicalDisks())},
|
||||
{"PBSInstances", len(rs.PBSInstances())},
|
||||
{"PMGInstances", len(rs.PMGInstances())},
|
||||
{"K8sClusters", len(rs.K8sClusters())},
|
||||
{"K8sNodes", len(rs.K8sNodes())},
|
||||
{"Pods", len(rs.Pods())},
|
||||
{"K8sDeployments", len(rs.K8sDeployments())},
|
||||
{"Workloads", len(rs.Workloads())},
|
||||
{"Infrastructure", len(rs.Infrastructure())},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if c.got != 0 {
|
||||
t.Errorf("%s() = %d items for zero snapshot, want 0", c.name, c.got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_SnapshotReadState_Populated drives SnapshotReadState
|
||||
// with one entry per top-level collection and asserts each lands in the
|
||||
// matching accessor with its source identity intact. Canonical registry IDs
|
||||
// are generated, so identity is asserted through the source carriers the
|
||||
// registry preserves verbatim (Name / Hostname / VMID / DevPath) plus the
|
||||
// exact accessor count, which proves a 1:1 map with no cross-collection
|
||||
// leakage.
|
||||
func TestBranchcov0723Am_SnapshotReadState_Populated(t *testing.T) {
|
||||
rs := SnapshotReadState(branchcov0723amPopulatedSnapshot())
|
||||
if rs == nil {
|
||||
t.Fatal("SnapshotReadState returned nil ReadState")
|
||||
}
|
||||
|
||||
t.Run("VM", func(t *testing.T) {
|
||||
vms := rs.VMs()
|
||||
if len(vms) != 1 {
|
||||
t.Fatalf("VMs() = %d views, want 1", len(vms))
|
||||
}
|
||||
v := vms[0]
|
||||
if v == nil {
|
||||
t.Fatal("VM view is nil")
|
||||
}
|
||||
if v.Name() != "vm-branchcov" {
|
||||
t.Errorf("VM Name = %q, want vm-branchcov", v.Name())
|
||||
}
|
||||
if v.VMID() != 4242 {
|
||||
t.Errorf("VM VMID = %d, want 4242", v.VMID())
|
||||
}
|
||||
if v.Status() == "" {
|
||||
t.Error("VM Status empty; source snapshot Status was \"running\"")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
nodes := rs.Nodes()
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("Nodes() = %d views, want 1", len(nodes))
|
||||
}
|
||||
n := nodes[0]
|
||||
if n == nil {
|
||||
t.Fatal("Node view is nil")
|
||||
}
|
||||
if n.Name() != "node-branchcov" {
|
||||
t.Errorf("Node Name = %q, want node-branchcov", n.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Container", func(t *testing.T) {
|
||||
cts := rs.Containers()
|
||||
if len(cts) != 1 {
|
||||
t.Fatalf("Containers() = %d views, want 1", len(cts))
|
||||
}
|
||||
c := cts[0]
|
||||
if c == nil {
|
||||
t.Fatal("Container view is nil")
|
||||
}
|
||||
if c.Name() != "ct-branchcov" {
|
||||
t.Errorf("Container Name = %q, want ct-branchcov", c.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DockerHost", func(t *testing.T) {
|
||||
dhs := rs.DockerHosts()
|
||||
if len(dhs) != 1 {
|
||||
t.Fatalf("DockerHosts() = %d views, want 1", len(dhs))
|
||||
}
|
||||
dh := dhs[0]
|
||||
if dh == nil {
|
||||
t.Fatal("DockerHost view is nil")
|
||||
}
|
||||
if dh.Hostname() != "docker-host-branchcov" {
|
||||
t.Errorf("DockerHost Hostname = %q, want docker-host-branchcov", dh.Hostname())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DockerContainer", func(t *testing.T) {
|
||||
dcs := rs.DockerContainers()
|
||||
if len(dcs) != 1 {
|
||||
t.Fatalf("DockerContainers() = %d views, want 1", len(dcs))
|
||||
}
|
||||
dc := dcs[0]
|
||||
if dc == nil {
|
||||
t.Fatal("DockerContainer view is nil")
|
||||
}
|
||||
if dc.Name() != "app-container-branchcov" {
|
||||
t.Errorf("DockerContainer Name = %q, want app-container-branchcov", dc.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Host", func(t *testing.T) {
|
||||
h := findHostViewByHostname(rs.Hosts(), "host-branchcov")
|
||||
if h == nil {
|
||||
t.Fatalf("Hosts() did not surface host-branchcov (got %d views)", len(rs.Hosts()))
|
||||
}
|
||||
if h.ID() == "" {
|
||||
t.Error("Host canonical ID empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("StoragePool", func(t *testing.T) {
|
||||
sps := rs.StoragePools()
|
||||
if len(sps) != 1 {
|
||||
t.Fatalf("StoragePools() = %d views, want 1", len(sps))
|
||||
}
|
||||
s := sps[0]
|
||||
if s == nil {
|
||||
t.Fatal("StoragePool view is nil")
|
||||
}
|
||||
if s.Name() != "storage-branchcov" {
|
||||
t.Errorf("StoragePool Name = %q, want storage-branchcov", s.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PhysicalDisk", func(t *testing.T) {
|
||||
pds := rs.PhysicalDisks()
|
||||
if len(pds) != 1 {
|
||||
t.Fatalf("PhysicalDisks() = %d views, want 1", len(pds))
|
||||
}
|
||||
d := findPhysicalDiskViewByDevPath(pds, "/dev/sda")
|
||||
if d == nil {
|
||||
t.Fatalf("PhysicalDisks() did not surface DevPath /dev/sda")
|
||||
}
|
||||
if d.DevPath() != "/dev/sda" {
|
||||
t.Errorf("PhysicalDisk DevPath = %q, want /dev/sda", d.DevPath())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PBSInstance", func(t *testing.T) {
|
||||
pbs := rs.PBSInstances()
|
||||
if len(pbs) != 1 {
|
||||
t.Fatalf("PBSInstances() = %d views, want 1", len(pbs))
|
||||
}
|
||||
p := pbs[0]
|
||||
if p == nil {
|
||||
t.Fatal("PBSInstance view is nil")
|
||||
}
|
||||
if p.Name() != "pbs-branchcov" {
|
||||
t.Errorf("PBSInstance Name = %q, want pbs-branchcov", p.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PMGInstance", func(t *testing.T) {
|
||||
pmgs := rs.PMGInstances()
|
||||
if len(pmgs) != 1 {
|
||||
t.Fatalf("PMGInstances() = %d views, want 1", len(pmgs))
|
||||
}
|
||||
p := pmgs[0]
|
||||
if p == nil {
|
||||
t.Fatal("PMGInstance view is nil")
|
||||
}
|
||||
if p.Name() != "pmg-branchcov" {
|
||||
t.Errorf("PMGInstance Name = %q, want pmg-branchcov", p.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("K8sCluster", func(t *testing.T) {
|
||||
clusters := rs.K8sClusters()
|
||||
if len(clusters) != 1 {
|
||||
t.Fatalf("K8sClusters() = %d views, want 1", len(clusters))
|
||||
}
|
||||
k := clusters[0]
|
||||
if k == nil {
|
||||
t.Fatal("K8sCluster view is nil")
|
||||
}
|
||||
if k.Name() != "k8s-branchcov" {
|
||||
t.Errorf("K8sCluster Name = %q, want k8s-branchcov", k.Name())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_SnapshotReadState_SourceIndependence proves the registry
|
||||
// built by SnapshotReadState copies snapshot data rather than aliasing the
|
||||
// caller's slice elements: after the read state is built, mutating the source
|
||||
// snapshot must not change the already-materialized views. (The reverse
|
||||
// direction is structurally impossible: views reference registry-owned
|
||||
// Resources, never the caller's snapshot, so no view mutation can reach the
|
||||
// source.)
|
||||
func TestBranchcov0723Am_SnapshotReadState_SourceIndependence(t *testing.T) {
|
||||
snap := branchcov0723amPopulatedSnapshot()
|
||||
rs := SnapshotReadState(snap)
|
||||
|
||||
vms := rs.VMs()
|
||||
if len(vms) != 1 || vms[0] == nil {
|
||||
t.Fatalf("expected one VM view before mutation; got %d", len(vms))
|
||||
}
|
||||
beforeName := vms[0].Name()
|
||||
if beforeName != "vm-branchcov" {
|
||||
t.Fatalf("VM Name before mutation = %q, want vm-branchcov", beforeName)
|
||||
}
|
||||
|
||||
// Mutate the caller's snapshot in place. SnapshotReadState received (and
|
||||
// IngestSnapshot received) the snapshot by value and the registry copied
|
||||
// field values into its own Resources, so this must not leak through.
|
||||
snap.VMs[0].Name = "mutated-after-read"
|
||||
snap.VMs[0].VMID = 9999
|
||||
|
||||
// The caller's own snapshot reflects the mutation (sanity), but the
|
||||
// already-built ReadState must be isolated from it.
|
||||
if snap.VMs[0].Name != "mutated-after-read" {
|
||||
t.Fatalf("test harness sanity check failed: caller snapshot Name = %q", snap.VMs[0].Name)
|
||||
}
|
||||
after := rs.VMs()
|
||||
if len(after) != 1 || after[0] == nil {
|
||||
t.Fatalf("after mutating source snapshot, ReadState has %d VMs, want 1", len(after))
|
||||
}
|
||||
if after[0].Name() != beforeName {
|
||||
t.Errorf("ReadState VM Name = %q after source mutation, want %q (source aliasing)", after[0].Name(), beforeName)
|
||||
}
|
||||
if after[0].VMID() != 4242 {
|
||||
t.Errorf("ReadState VM VMID = %d after source mutation, want 4242 (source aliasing)", after[0].VMID())
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_APITokenUsage_NormalizeCollections covers both arms of
|
||||
// (u APITokenUsage).NormalizeCollections: nil Agents normalised to a non-nil
|
||||
// empty slice, and a populated Agents slice preserved. Because the method has
|
||||
// a value receiver, the caller's struct must be unchanged regardless of input.
|
||||
func TestBranchcov0723Am_APITokenUsage_NormalizeCollections(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input APITokenUsage
|
||||
wantAgents int
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "nil_agents_normalized_to_empty",
|
||||
input: APITokenUsage{TokenID: "tok-1", AgentCount: 0, Agents: nil},
|
||||
wantAgents: 0,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "empty_slice_preserved_non_nil",
|
||||
input: APITokenUsage{TokenID: "tok-2", Agents: []string{}},
|
||||
wantAgents: 0,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "populated_agents_preserved",
|
||||
input: APITokenUsage{TokenID: "tok-3", AgentCount: 2, Agents: []string{"agent-a", "agent-b"}},
|
||||
wantAgents: 2,
|
||||
wantNil: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
original := tc.input
|
||||
got := tc.input.NormalizeCollections()
|
||||
|
||||
if (got.Agents == nil) != tc.wantNil {
|
||||
t.Errorf("Agents nil = %v, want %v", got.Agents == nil, tc.wantNil)
|
||||
}
|
||||
if len(got.Agents) != tc.wantAgents {
|
||||
t.Errorf("len(Agents) = %d, want %d", len(got.Agents), tc.wantAgents)
|
||||
}
|
||||
if got.TokenID != original.TokenID {
|
||||
t.Errorf("TokenID = %q, want %q", got.TokenID, original.TokenID)
|
||||
}
|
||||
if got.AgentCount != original.AgentCount {
|
||||
t.Errorf("AgentCount = %d, want %d", got.AgentCount, original.AgentCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_DockerAgentAttention_NormalizeCollections covers both
|
||||
// arms of (a DockerAgentAttention).NormalizeCollections: nil Issues normalised
|
||||
// to a non-nil empty slice, and a populated Issues slice preserved. Value
|
||||
// receiver => caller's struct is unchanged.
|
||||
func TestBranchcov0723Am_DockerAgentAttention_NormalizeCollections(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input DockerAgentAttention
|
||||
wantIssues int
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "nil_issues_normalized_to_empty",
|
||||
input: DockerAgentAttention{AgentID: "agent-1", Issues: nil},
|
||||
wantIssues: 0,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "empty_slice_preserved_non_nil",
|
||||
input: DockerAgentAttention{AgentID: "agent-2", Issues: []string{}},
|
||||
wantIssues: 0,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "populated_issues_preserved",
|
||||
input: DockerAgentAttention{AgentID: "agent-3", Issues: []string{"stale", "no-token"}},
|
||||
wantIssues: 2,
|
||||
wantNil: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
original := tc.input
|
||||
got := tc.input.NormalizeCollections()
|
||||
|
||||
if (got.Issues == nil) != tc.wantNil {
|
||||
t.Errorf("Issues nil = %v, want %v", got.Issues == nil, tc.wantNil)
|
||||
}
|
||||
if len(got.Issues) != tc.wantIssues {
|
||||
t.Errorf("len(Issues) = %d, want %d", len(got.Issues), tc.wantIssues)
|
||||
}
|
||||
if got.AgentID != original.AgentID {
|
||||
t.Errorf("AgentID = %q, want %q", got.AgentID, original.AgentID)
|
||||
}
|
||||
if got.Name != original.Name || got.Status != original.Status {
|
||||
t.Errorf("scalar fields changed: Name=%q Status=%q", got.Name, got.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// zipEntry0723Am is a single in-memory zip member used by buildZipBytes0723Am.
|
||||
type zipEntry0723Am struct {
|
||||
name string
|
||||
content []byte
|
||||
}
|
||||
|
||||
// buildZipBytes0723Am builds an in-memory zip archive containing entries in the
|
||||
// given order and returns its raw bytes. Test-only fixture builder for
|
||||
// extractFromZip coverage.
|
||||
func buildZipBytes0723Am(t *testing.T, entries ...zipEntry0723Am) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
for _, e := range entries {
|
||||
w, err := zw.Create(e.name)
|
||||
if err != nil {
|
||||
t.Fatalf("create zip entry %q: %v", e.name, err)
|
||||
}
|
||||
if _, err := w.Write(e.content); err != nil {
|
||||
t.Fatalf("write zip entry %q: %v", e.name, err)
|
||||
}
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatalf("close zip: %v", err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// zipLocalHeaderDataOffset0723Am parses the first local file header of archive
|
||||
// and returns the byte offset where that entry's compressed data begins.
|
||||
func zipLocalHeaderDataOffset0723Am(t *testing.T, archive []byte) int {
|
||||
t.Helper()
|
||||
const localHeaderSig = 0x04034b50
|
||||
if len(archive) < 30 {
|
||||
t.Fatal("archive too small to contain a local file header")
|
||||
}
|
||||
if binary.LittleEndian.Uint32(archive[0:4]) != localHeaderSig {
|
||||
t.Fatal("archive does not start with a local file header signature")
|
||||
}
|
||||
nameLen := int(binary.LittleEndian.Uint16(archive[26:28]))
|
||||
extraLen := int(binary.LittleEndian.Uint16(archive[28:30]))
|
||||
return 30 + nameLen + extraLen
|
||||
}
|
||||
|
||||
// zipCentralDirOffset0723Am returns the byte offset of the first central
|
||||
// directory entry, read from the trailing EOCD record (archive/zip writes no
|
||||
// comment, so the EOCD is the last 22 bytes).
|
||||
func zipCentralDirOffset0723Am(t *testing.T, archive []byte) int {
|
||||
t.Helper()
|
||||
const eocdSig = 0x06054b50
|
||||
if len(archive) < 22 {
|
||||
t.Fatal("archive too small to contain an EOCD record")
|
||||
}
|
||||
eocd := len(archive) - 22
|
||||
if binary.LittleEndian.Uint32(archive[eocd:eocd+4]) != eocdSig {
|
||||
t.Fatal("EOCD signature not found at end of archive")
|
||||
}
|
||||
cdOff := int(binary.LittleEndian.Uint32(archive[eocd+16 : eocd+20]))
|
||||
if cdOff+12 > len(archive) || binary.LittleEndian.Uint32(archive[cdOff:cdOff+4]) != 0x02014b50 {
|
||||
t.Fatalf("central directory signature not found at offset %d", cdOff)
|
||||
}
|
||||
return cdOff
|
||||
}
|
||||
|
||||
// patchZipMethodUnsupported0723Am returns a copy of archive with both the local
|
||||
// and central-directory compression methods of the first entry set to 99 (an
|
||||
// unsupported method), so that file.Open() fails while zip.NewReader still
|
||||
// parses the structure.
|
||||
func patchZipMethodUnsupported0723Am(t *testing.T, archive []byte) []byte {
|
||||
t.Helper()
|
||||
const unsupported = uint16(99)
|
||||
out := append([]byte(nil), archive...)
|
||||
binary.LittleEndian.PutUint16(out[8:10], unsupported)
|
||||
cdOff := zipCentralDirOffset0723Am(t, out)
|
||||
binary.LittleEndian.PutUint16(out[cdOff+10:cdOff+12], unsupported)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestBranchcov0723AmExtractFromZip(t *testing.T) {
|
||||
want := []byte("hello-agent-binary")
|
||||
|
||||
t.Run("valid_zip_returns_exact_entry_bytes", func(t *testing.T) {
|
||||
archive := buildZipBytes0723Am(t, zipEntry0723Am{name: "pulse-agent.exe", content: want})
|
||||
got, err := extractFromZip(archive, "pulse-agent.exe")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("extractFromZip returned %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing_entry_returns_not_found_error", func(t *testing.T) {
|
||||
archive := buildZipBytes0723Am(t, zipEntry0723Am{name: "other.bin", content: []byte("not-the-one")})
|
||||
got, err := extractFromZip(archive, "pulse-agent.exe")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for missing entry, got content %q", got)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not found in zip") {
|
||||
t.Fatalf("expected 'not found in zip' error, got %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil content on error, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("entry_found_when_not_first", func(t *testing.T) {
|
||||
archive := buildZipBytes0723Am(t,
|
||||
zipEntry0723Am{name: "README.txt", content: []byte("readme")},
|
||||
zipEntry0723Am{name: "LICENSE", content: []byte("license")},
|
||||
zipEntry0723Am{name: "pulse-agent.exe", content: want},
|
||||
zipEntry0723Am{name: "tail.bin", content: []byte("tail")},
|
||||
)
|
||||
got, err := extractFromZip(archive, "pulse-agent.exe")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("extractFromZip returned %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty_archive_returns_open_error", func(t *testing.T) {
|
||||
got, err := extractFromZip([]byte{}, "pulse-agent.exe")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for empty archive, got content %q", got)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to open zip") {
|
||||
t.Fatalf("expected 'failed to open zip' error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non_zip_bytes_returns_open_error", func(t *testing.T) {
|
||||
archive := []byte("this is definitely not a zip file")
|
||||
got, err := extractFromZip(archive, "pulse-agent.exe")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for non-zip bytes, got content %q", got)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to open zip") {
|
||||
t.Fatalf("expected 'failed to open zip' error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("name_match_is_case_sensitive", func(t *testing.T) {
|
||||
// extractFromZip compares filepath.Base(file.Name) to entryName using a
|
||||
// verbatim byte comparison; differing case does not match.
|
||||
archive := buildZipBytes0723Am(t, zipEntry0723Am{name: "Pulse-Agent.EXE", content: want})
|
||||
if _, err := extractFromZip(archive, "pulse-agent.exe"); err == nil {
|
||||
t.Fatal("expected not-found for case-mismatched entry name, got no error")
|
||||
}
|
||||
got, err := extractFromZip(archive, "Pulse-Agent.EXE")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for exact-case name: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("extractFromZip returned %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("name_matched_on_filepath_base", func(t *testing.T) {
|
||||
// The match is on filepath.Base(file.Name), so a leading directory
|
||||
// segment is stripped and a nested entry still resolves.
|
||||
archive := buildZipBytes0723Am(t, zipEntry0723Am{name: "dist/pulse-agent.exe", content: want})
|
||||
got, err := extractFromZip(archive, "pulse-agent.exe")
|
||||
if err != nil {
|
||||
t.Fatalf("expected base-name match for nested entry, got error: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("extractFromZip returned %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero_length_entry_returns_empty_bytes", func(t *testing.T) {
|
||||
archive := buildZipBytes0723Am(t, zipEntry0723Am{name: "pulse-agent.exe", content: nil})
|
||||
got, err := extractFromZip(archive, "pulse-agent.exe")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for zero-length entry: %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("expected empty byte slice, got %d bytes", len(got))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("oversized_entry_returns_size_limit_error", func(t *testing.T) {
|
||||
// Deflate compresses all-zero content to a few KB, so the archive stays
|
||||
// small while the decompressed entry exceeds maxAgentBinarySize.
|
||||
big := bytes.Repeat([]byte{0}, maxAgentBinarySize+1)
|
||||
archive := buildZipBytes0723Am(t, zipEntry0723Am{name: "pulse-agent.exe", content: big})
|
||||
got, err := extractFromZip(archive, "pulse-agent.exe")
|
||||
if err == nil {
|
||||
t.Fatalf("expected size-limit error, got %d bytes", len(got))
|
||||
}
|
||||
if !strings.Contains(err.Error(), "exceeded size limit") {
|
||||
t.Fatalf("expected 'exceeded size limit' error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unsupported_compression_method_returns_open_error", func(t *testing.T) {
|
||||
// Rewrite the method to an unsupported value in both headers; the
|
||||
// structure still parses but file.Open() rejects the method.
|
||||
archive := patchZipMethodUnsupported0723Am(t,
|
||||
buildZipBytes0723Am(t, zipEntry0723Am{name: "pulse-agent.exe", content: want}))
|
||||
got, err := extractFromZip(archive, "pulse-agent.exe")
|
||||
if err == nil {
|
||||
t.Fatalf("expected open error for unsupported method, got %d bytes", len(got))
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed opening binary in zip") {
|
||||
t.Fatalf("expected 'failed opening binary in zip' error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("corrupt_compressed_data_returns_read_error", func(t *testing.T) {
|
||||
// Flip bits inside the compressed-data region; file.Open() still
|
||||
// succeeds but decompression fails mid-read.
|
||||
base := buildZipBytes0723Am(t,
|
||||
zipEntry0723Am{name: "pulse-agent.exe", content: bytes.Repeat([]byte("payload-"), 32)})
|
||||
out := append([]byte(nil), base...)
|
||||
dataOff := zipLocalHeaderDataOffset0723Am(t, out)
|
||||
cdOff := zipCentralDirOffset0723Am(t, out)
|
||||
n := 8
|
||||
if max := cdOff - dataOff - 1; max < n {
|
||||
n = max
|
||||
}
|
||||
if n <= 0 {
|
||||
t.Fatal("no compressed-data bytes available to corrupt")
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
out[dataOff+i] ^= 0xFF
|
||||
}
|
||||
got, err := extractFromZip(out, "pulse-agent.exe")
|
||||
if err == nil {
|
||||
t.Fatalf("expected read error for corrupt data, got %d bytes", len(got))
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed reading binary from zip") {
|
||||
t.Fatalf("expected 'failed reading binary from zip' error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBranchcov0723AmIsTrustedProxyIP(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
envCIDR string
|
||||
ipStr string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty string returns false",
|
||||
envCIDR: "10.0.0.0/8",
|
||||
ipStr: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "syntactically invalid IP returns false",
|
||||
envCIDR: "10.0.0.0/8",
|
||||
ipStr: "not-an-ip",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "IPv4 inside trusted range returns true",
|
||||
envCIDR: "10.0.0.0/8",
|
||||
ipStr: "10.1.2.3",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "IPv4 outside trusted range returns false",
|
||||
envCIDR: "10.0.0.0/8",
|
||||
ipStr: "192.168.1.1",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "IPv6 inside trusted range returns true",
|
||||
envCIDR: "2001:db8::/32",
|
||||
ipStr: "2001:db8::1",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 outside trusted range returns false",
|
||||
envCIDR: "2001:db8::/32",
|
||||
ipStr: "2001:dead::1",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "surrounding whitespace is trimmed then matched",
|
||||
envCIDR: "10.0.0.0/8",
|
||||
ipStr: " 10.1.2.3 ",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "bracketed IPv6 is unbracketed then matched",
|
||||
envCIDR: "2001:db8::/32",
|
||||
ipStr: "[2001:db8::1]",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("PULSE_TRUSTED_PROXY_CIDRS", tt.envCIDR)
|
||||
resetTrustedProxyConfig()
|
||||
|
||||
if got := IsTrustedProxyIP(tt.ipStr); got != tt.want {
|
||||
t.Errorf("IsTrustedProxyIP(%q) = %v, want %v", tt.ipStr, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/operationaltrust"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestBranchcov0723Am_AccumulateAlertOutcomeCounts drives every branch of
|
||||
// accumulateAlertOutcomeCounts: the nil-counts guard, the empty-history path
|
||||
// (which must not perturb pre-existing counts), and every alert classification
|
||||
// the loop distinguishes, including the precise cutoff boundary semantics of
|
||||
// `!AckTime.Before(cutoff)`.
|
||||
func TestBranchcov0723Am_AccumulateAlertOutcomeCounts(t *testing.T) {
|
||||
cutoff := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
t.Run("nil_counts_is_noop", func(t *testing.T) {
|
||||
// The guard at the top of the function is the only thing that prevents
|
||||
// a nil-pointer dereference once the loop begins mutating fields, so it
|
||||
// has no observable side effect beyond returning safely. Pass a rich
|
||||
// history so the guard is provably the gate.
|
||||
richHistory := []alerts.Alert{
|
||||
{AckTime: &cutoff, OperationalRecord: &operationaltrust.OperationalRecord{State: operationaltrust.OperationalResolved}},
|
||||
}
|
||||
assert.NotPanics(t, func() {
|
||||
accumulateAlertOutcomeCounts(nil, richHistory, cutoff)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("empty_history_preserves_existing_counts", func(t *testing.T) {
|
||||
// Pre-existing totals (resource counts and prior alert totals) must be
|
||||
// preserved untouched when there is no history to fold in, and the three
|
||||
// alert-outcome fields must not be incremented.
|
||||
counts := InstallSnapshotCounts{
|
||||
PVENodes: 3,
|
||||
AlertsFired30d: 7,
|
||||
AlertsAcknowledged30d: 2,
|
||||
AlertsResolved30d: 1,
|
||||
}
|
||||
accumulateAlertOutcomeCounts(&counts, nil, cutoff)
|
||||
assert.Equal(t, InstallSnapshotCounts{
|
||||
PVENodes: 3,
|
||||
AlertsFired30d: 7,
|
||||
AlertsAcknowledged30d: 2,
|
||||
AlertsResolved30d: 1,
|
||||
}, counts)
|
||||
|
||||
// Same behaviour with an explicit empty (non-nil) slice.
|
||||
counts2 := InstallSnapshotCounts{
|
||||
PVENodes: 3,
|
||||
AlertsFired30d: 7,
|
||||
AlertsAcknowledged30d: 2,
|
||||
AlertsResolved30d: 1,
|
||||
}
|
||||
accumulateAlertOutcomeCounts(&counts2, []alerts.Alert{}, cutoff)
|
||||
assert.Equal(t, counts, counts2)
|
||||
})
|
||||
|
||||
t.Run("classifications_and_cutoff_boundary", func(t *testing.T) {
|
||||
// AckTime exactly at the cutoff must be counted (Before is strict), an
|
||||
// AckTime strictly before must not, and each OperationalRecord
|
||||
// classification must increment only the resolved field.
|
||||
atCutoff := cutoff
|
||||
beforeCutoff := cutoff.Add(-time.Second)
|
||||
afterCutoff := cutoff.Add(time.Second)
|
||||
|
||||
counts := InstallSnapshotCounts{}
|
||||
accumulateAlertOutcomeCounts(&counts, []alerts.Alert{
|
||||
// 1) Acknowledged strictly before cutoff: fired, NOT acked, no OR.
|
||||
{AckTime: &beforeCutoff},
|
||||
// 2) Acknowledged exactly at cutoff: fired, acked.
|
||||
{AckTime: &atCutoff},
|
||||
// 3) Acknowledged after cutoff: fired, acked.
|
||||
{AckTime: &afterCutoff},
|
||||
// 4) Nil AckTime: fired, NOT acked, nil OR.
|
||||
{},
|
||||
// 5) Resolved operational record, acknowledged: fired, acked, resolved.
|
||||
{
|
||||
AckTime: &afterCutoff,
|
||||
OperationalRecord: &operationaltrust.OperationalRecord{State: operationaltrust.OperationalResolved},
|
||||
},
|
||||
// 6) Non-resolved operational record, nil ack: fired, NOT acked, NOT resolved.
|
||||
{
|
||||
OperationalRecord: &operationaltrust.OperationalRecord{State: operationaltrust.OperationalOpen},
|
||||
},
|
||||
}, cutoff)
|
||||
|
||||
assert.Equal(t, 6, counts.AlertsFired30d, "every history entry is fired")
|
||||
assert.Equal(t, 3, counts.AlertsAcknowledged30d, "acked: at-cutoff, after-cutoff, resolved+after")
|
||||
assert.Equal(t, 1, counts.AlertsResolved30d, "only the resolved operational record counts")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_AccumulateInstallOutcomeCounts covers the guard arms and
|
||||
// every arm reachable with a zero-value or minimally-wired Monitor. The
|
||||
// notification-manager arm (and its error arm) are intentionally skipped: they
|
||||
// require a sqlite-backed NotificationManager with seeded audit rows to assert
|
||||
// meaningfully, which is fully-wired-live-Monitor territory.
|
||||
func TestBranchcov0723Am_AccumulateInstallOutcomeCounts(t *testing.T) {
|
||||
now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
// sentryCounts is a baseline that must survive any no-op accumulation call
|
||||
// so a regression that spuriously mutates counts is caught.
|
||||
sentryCounts := func() InstallSnapshotCounts {
|
||||
return InstallSnapshotCounts{
|
||||
PVENodes: 4,
|
||||
ActiveAlerts: 9,
|
||||
AlertsFired30d: 5,
|
||||
NotificationAttempts7d: 11,
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("nil_counts_is_noop", func(t *testing.T) {
|
||||
// Guard fires before any manager access; only observable effect is safe
|
||||
// return. Use a Monitor with a real alert manager so the guard is
|
||||
// provably the only gate in front of the nil pointer.
|
||||
alertMgr := alerts.NewManagerWithDataDir(t.TempDir())
|
||||
defer alertMgr.Stop()
|
||||
mon := &Monitor{alertManager: alertMgr}
|
||||
assert.NotPanics(t, func() {
|
||||
accumulateInstallOutcomeCounts(nil, mon, now)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("nil_monitor_leaves_counts_untouched", func(t *testing.T) {
|
||||
counts := sentryCounts()
|
||||
accumulateInstallOutcomeCounts(&counts, nil, now)
|
||||
assert.Equal(t, sentryCounts(), counts)
|
||||
})
|
||||
|
||||
t.Run("zero_monitor_zero_now_uses_now_recompute", func(t *testing.T) {
|
||||
// now.IsZero() true-arm: the function recomputes now internally. With a
|
||||
// zero-value Monitor both managers are nil, so no counts accrue.
|
||||
counts := sentryCounts()
|
||||
assert.NotPanics(t, func() {
|
||||
accumulateInstallOutcomeCounts(&counts, &Monitor{}, time.Time{})
|
||||
})
|
||||
assert.Equal(t, sentryCounts(), counts)
|
||||
})
|
||||
|
||||
t.Run("zero_monitor_real_now_skips_nil_managers", func(t *testing.T) {
|
||||
// now.IsZero() false-arm plus the nil alertManager and nil
|
||||
// notificationManager branches: nothing is accumulated.
|
||||
counts := sentryCounts()
|
||||
mon := &Monitor{}
|
||||
require.Nil(t, mon.GetAlertManager())
|
||||
require.Nil(t, mon.GetNotificationManager())
|
||||
accumulateInstallOutcomeCounts(&counts, mon, now)
|
||||
assert.Equal(t, sentryCounts(), counts)
|
||||
})
|
||||
|
||||
t.Run("alert_manager_with_empty_history_increments_nothing", func(t *testing.T) {
|
||||
// Covers the alertManager != nil true-arm. A freshly-constructed alert
|
||||
// manager has no history, so GetAlertHistorySince returns nothing and
|
||||
// no alert-outcome counts accrue. (Deeper history-driven counting is
|
||||
// exercised directly on accumulateAlertOutcomeCounts above; history
|
||||
// cannot be injected into an alerts.Manager from outside the alerts
|
||||
// package without driving the full evaluation pipeline.)
|
||||
alertMgr := alerts.NewManagerWithDataDir(t.TempDir())
|
||||
defer alertMgr.Stop()
|
||||
|
||||
mon := &Monitor{alertManager: alertMgr}
|
||||
require.NotNil(t, mon.GetAlertManager())
|
||||
|
||||
counts := sentryCounts()
|
||||
accumulateInstallOutcomeCounts(&counts, mon, now)
|
||||
assert.Equal(t, sentryCounts(), counts, "empty history must not change any count")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_AggregateInstallSnapshotCounts covers the uncovered arms
|
||||
// of AggregateInstallSnapshotCounts that the existing
|
||||
// TestReloadableMonitorAggregateInstallSnapshotCountsIncludesProvisionedTenants
|
||||
// does not reach: nil mtMonitor, nil persistence (default-only path), the
|
||||
// ListOrganizations error fallback, and the GetMonitor-skip arm.
|
||||
func TestBranchcov0723Am_AggregateInstallSnapshotCounts(t *testing.T) {
|
||||
t.Run("nil_mtmonitor_returns_empty", func(t *testing.T) {
|
||||
cfg := &config.Config{DataPath: t.TempDir()}
|
||||
rm, err := NewReloadableMonitor(cfg, config.NewMultiTenantPersistence(cfg.DataPath), nil)
|
||||
require.NoError(t, err)
|
||||
rm.mtMonitor = nil
|
||||
|
||||
assert.Equal(t, InstallSnapshotCounts{}, rm.AggregateInstallSnapshotCounts())
|
||||
})
|
||||
|
||||
t.Run("nil_persistence_aggregates_default_only", func(t *testing.T) {
|
||||
// persistence == nil keeps orgIDs as ["default"]. Pre-seed the default
|
||||
// monitor so GetMonitor returns it without touching persistence.
|
||||
cfg := &config.Config{DataPath: t.TempDir()}
|
||||
rm, err := NewReloadableMonitor(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
mtm := rm.GetMultiTenantMonitor()
|
||||
require.NotNil(t, mtm)
|
||||
mtm.monitors["default"] = testTelemetryMonitor(
|
||||
[]models.Node{{ID: "n1", Name: "n1", Instance: "pve-1"}},
|
||||
[]models.VM{{ID: "vm1", VMID: 1, Name: "vm1", Instance: "pve-1"}},
|
||||
nil, nil, nil, nil, nil, 2,
|
||||
)
|
||||
|
||||
counts := rm.AggregateInstallSnapshotCounts()
|
||||
assert.Equal(t, 1, counts.PVENodes, "default org aggregated on nil-persistence path")
|
||||
assert.Equal(t, 1, counts.VMs)
|
||||
assert.Equal(t, 2, counts.ActiveAlerts)
|
||||
})
|
||||
|
||||
t.Run("list_organizations_error_falls_back_to_default", func(t *testing.T) {
|
||||
// Make the orgs entry a regular file so ReadDir fails with a
|
||||
// non-IsNotExist error, triggering the ListOrganizations error branch
|
||||
// and the fallback to orgIDs=["default"].
|
||||
baseDir := t.TempDir()
|
||||
cfg := &config.Config{DataPath: baseDir}
|
||||
persistence := config.NewMultiTenantPersistence(baseDir)
|
||||
|
||||
rm, err := NewReloadableMonitor(cfg, persistence, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
orgsPath := filepath.Join(persistence.BaseDataDir(), "orgs")
|
||||
// The orgs entry may not exist yet at construction time; only remove if
|
||||
// present, then replace it with a regular file so ReadDir fails.
|
||||
if err := os.Remove(orgsPath); err != nil && !os.IsNotExist(err) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, os.WriteFile(orgsPath, []byte("not a directory"), 0o644))
|
||||
|
||||
mtm := rm.GetMultiTenantMonitor()
|
||||
require.NotNil(t, mtm)
|
||||
mtm.monitors["default"] = testTelemetryMonitor(
|
||||
[]models.Node{{ID: "n1", Name: "n1", Instance: "pve-1"}},
|
||||
nil, nil, nil, nil, nil, nil, 0,
|
||||
)
|
||||
|
||||
counts := rm.AggregateInstallSnapshotCounts()
|
||||
assert.Equal(t, 1, counts.PVENodes, "default aggregated despite tenant listing failure")
|
||||
})
|
||||
|
||||
t.Run("deleting_org_is_skipped", func(t *testing.T) {
|
||||
// Provision a second org so persistence lists it, then mark it as
|
||||
// being-deleted so GetMonitor errors and it is skipped while default is
|
||||
// still aggregated.
|
||||
baseDir := t.TempDir()
|
||||
cfg := &config.Config{DataPath: baseDir}
|
||||
persistence := config.NewMultiTenantPersistence(baseDir)
|
||||
_, err := persistence.GetPersistence("ghost")
|
||||
require.NoError(t, err)
|
||||
|
||||
rm, err := NewReloadableMonitor(cfg, persistence, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
mtm := rm.GetMultiTenantMonitor()
|
||||
require.NotNil(t, mtm)
|
||||
mtm.monitors["default"] = testTelemetryMonitor(
|
||||
[]models.Node{{ID: "n1", Name: "n1", Instance: "pve-1"}},
|
||||
nil, nil, nil, nil, nil, nil, 0,
|
||||
)
|
||||
mtm.tenantDeleting["ghost"] = struct{}{}
|
||||
|
||||
counts := rm.AggregateInstallSnapshotCounts()
|
||||
assert.Equal(t, 1, counts.PVENodes, "default aggregated, deleting org contributed nothing")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
|
||||
)
|
||||
|
||||
// TestBranchcov0723Am_DeploymentMethod covers every branch of the deployment
|
||||
// method precedence chain, including the cfg-takes-precedence-over-env rule,
|
||||
// whitespace/case normalization, the env-var fallback, and both IsDocker
|
||||
// fallbacks when nothing matches.
|
||||
func TestBranchcov0723Am_DeploymentMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
envMethod string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "cfg closed value docker_compose returned verbatim",
|
||||
cfg: Config{DeploymentMethod: "docker_compose"},
|
||||
envMethod: "",
|
||||
want: "docker_compose",
|
||||
},
|
||||
{
|
||||
name: "cfg closed value docker_run returned verbatim",
|
||||
cfg: Config{DeploymentMethod: "docker_run"},
|
||||
envMethod: "",
|
||||
want: "docker_run",
|
||||
},
|
||||
{
|
||||
name: "cfg closed value container_other returned verbatim",
|
||||
cfg: Config{DeploymentMethod: "container_other"},
|
||||
envMethod: "",
|
||||
want: "container_other",
|
||||
},
|
||||
{
|
||||
name: "cfg closed value systemd returned verbatim",
|
||||
cfg: Config{DeploymentMethod: "systemd"},
|
||||
envMethod: "",
|
||||
want: "systemd",
|
||||
},
|
||||
{
|
||||
name: "cfg closed value binary_other returned verbatim",
|
||||
cfg: Config{DeploymentMethod: "binary_other"},
|
||||
envMethod: "",
|
||||
want: "binary_other",
|
||||
},
|
||||
{
|
||||
name: "cfg closed value other returned verbatim",
|
||||
cfg: Config{DeploymentMethod: "other"},
|
||||
envMethod: "",
|
||||
want: "other",
|
||||
},
|
||||
{
|
||||
name: "cfg value trimmed and lowercased before matching",
|
||||
cfg: Config{DeploymentMethod: " Docker_Compose "},
|
||||
envMethod: "",
|
||||
want: "docker_compose",
|
||||
},
|
||||
{
|
||||
name: "cfg empty falls back to env closed value",
|
||||
cfg: Config{},
|
||||
envMethod: "docker_run",
|
||||
want: "docker_run",
|
||||
},
|
||||
{
|
||||
name: "cfg whitespace-only falls back to env closed value",
|
||||
cfg: Config{DeploymentMethod: " "},
|
||||
envMethod: "systemd",
|
||||
want: "systemd",
|
||||
},
|
||||
{
|
||||
name: "cfg value wins over conflicting env value",
|
||||
cfg: Config{DeploymentMethod: "systemd"},
|
||||
envMethod: "docker_run",
|
||||
want: "systemd",
|
||||
},
|
||||
{
|
||||
name: "cfg invalid value ignores env and falls back to docker branch",
|
||||
cfg: Config{DeploymentMethod: "tarball", IsDocker: true},
|
||||
envMethod: "systemd",
|
||||
want: "container_other",
|
||||
},
|
||||
{
|
||||
name: "cfg invalid value ignores env and falls back to binary branch",
|
||||
cfg: Config{DeploymentMethod: "tarball", IsDocker: false},
|
||||
envMethod: "systemd",
|
||||
want: "binary_other",
|
||||
},
|
||||
{
|
||||
name: "nothing set and docker falls back to container_other",
|
||||
cfg: Config{IsDocker: true},
|
||||
envMethod: "",
|
||||
want: "container_other",
|
||||
},
|
||||
{
|
||||
name: "nothing set and binary falls back to binary_other",
|
||||
cfg: Config{IsDocker: false},
|
||||
envMethod: "",
|
||||
want: "binary_other",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("PULSE_DEPLOYMENT_METHOD", tt.envMethod)
|
||||
if got := deploymentMethod(tt.cfg); got != tt.want {
|
||||
t.Fatalf("deploymentMethod(%+v) = %q, want %q", tt.cfg, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_DurationBucket covers the empty-boundaries case, values
|
||||
// below/between/past boundaries, the exact-at-boundary semantics (the source
|
||||
// uses a strict `<` comparison so a value equal to an upper bound falls through
|
||||
// to the next bucket), one-unit-either-side of every boundary, and a negative
|
||||
// duration.
|
||||
func TestBranchcov0723Am_DurationBucket(t *testing.T) {
|
||||
boundaries := []durationBoundary{
|
||||
{upper: 10 * time.Second, label: "a"},
|
||||
{upper: 1 * time.Minute, label: "b"},
|
||||
{upper: 1 * time.Hour, label: "c"},
|
||||
}
|
||||
const overflow = "over"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value time.Duration
|
||||
want string
|
||||
}{
|
||||
{"empty boundaries slice returns overflow regardless of value", 5 * time.Second, overflow}, // boundaries == nil below
|
||||
{"value well below first boundary", 5 * time.Second, "a"},
|
||||
{"value one nanosecond below first boundary", 10*time.Second - time.Nanosecond, "a"},
|
||||
{"value exactly on first boundary falls through to next bucket", 10 * time.Second, "b"},
|
||||
{"value one nanosecond above first boundary", 10*time.Second + time.Nanosecond, "b"},
|
||||
{"value strictly between first and second boundary", 30 * time.Second, "b"},
|
||||
{"value one nanosecond below second boundary", 1*time.Minute - time.Nanosecond, "b"},
|
||||
{"value exactly on second boundary falls through to next bucket", 1 * time.Minute, "c"},
|
||||
{"value one nanosecond above second boundary", 1*time.Minute + time.Nanosecond, "c"},
|
||||
{"value one nanosecond below last boundary", 1*time.Hour - time.Nanosecond, "c"},
|
||||
{"value exactly on last boundary returns overflow", 1 * time.Hour, overflow},
|
||||
{"value one nanosecond above last boundary returns overflow", 1*time.Hour + time.Nanosecond, overflow},
|
||||
{"value well past last boundary returns overflow", 2 * time.Hour, overflow},
|
||||
{"negative duration lands in the first bucket", -1 * time.Second, "a"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
if tt.name == "empty boundaries slice returns overflow regardless of value" {
|
||||
got = durationBucket(tt.value, nil, overflow)
|
||||
} else {
|
||||
got = durationBucket(tt.value, boundaries, overflow)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("durationBucket(%v) = %q, want %q", tt.value, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Anchor the strict-`<` boundary semantics against the real
|
||||
// knownInstallAge boundaries used in production so the at-boundary
|
||||
// behaviour is asserted on actual production values.
|
||||
t.Run("real knownInstallAge 24h boundary lands one bucket up", func(t *testing.T) {
|
||||
realBoundaries := []durationBoundary{
|
||||
{24 * time.Hour, "under_1d"},
|
||||
{7 * 24 * time.Hour, "1_7d"},
|
||||
{30 * 24 * time.Hour, "8_30d"},
|
||||
{90 * 24 * time.Hour, "31_90d"},
|
||||
{365 * 24 * time.Hour, "91_365d"},
|
||||
}
|
||||
if got := durationBucket(24*time.Hour-time.Nanosecond, realBoundaries, "over_365d"); got != "under_1d" {
|
||||
t.Fatalf("just below 24h = %q, want under_1d", got)
|
||||
}
|
||||
if got := durationBucket(24*time.Hour, realBoundaries, "over_365d"); got != "1_7d" {
|
||||
t.Fatalf("exactly 24h = %q, want 1_7d (strict < falls through)", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ActivationStage covers every stage the function can
|
||||
// return in the precedence order the source uses: each outcome trigger
|
||||
// individually, then monitoring, connected, secured, the default started stage
|
||||
// for a zero-value Ping, and a precedence check that the highest stage wins.
|
||||
func TestBranchcov0723Am_ActivationStage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ping Ping
|
||||
want string
|
||||
}{
|
||||
{"zero ping defaults to started", Ping{}, "started"},
|
||||
{"auth configured reaches secured", Ping{AuthConfigured: true}, "secured"},
|
||||
{"configured connections reach connected", Ping{ConfiguredConnections: 1}, "connected"},
|
||||
{"a single monitored resource reaches monitoring", Ping{PVENodes: 1}, "monitoring"},
|
||||
{"active alerts trigger outcome_observed", Ping{ActiveAlerts: 1}, "outcome_observed"},
|
||||
{"fired alerts trigger outcome_observed", Ping{AlertsFired30d: 1}, "outcome_observed"},
|
||||
{"resolved alerts trigger outcome_observed", Ping{AlertsResolved30d: 1}, "outcome_observed"},
|
||||
{"notification deliveries trigger outcome_observed", Ping{NotificationDeliveries7d: 1}, "outcome_observed"},
|
||||
{"monitoring outranks connected", Ping{PVENodes: 1, ConfiguredConnections: 9}, "monitoring"},
|
||||
{
|
||||
"outcome outranks every lower stage",
|
||||
Ping{ActiveAlerts: 1, AlertsFired30d: 1, AlertsResolved30d: 1, NotificationDeliveries7d: 1, PVENodes: 5, ConfiguredConnections: 9, AuthConfigured: true},
|
||||
"outcome_observed",
|
||||
},
|
||||
// AlertsAcknowledged30d is intentionally NOT a trigger in the source;
|
||||
// with only that field set the stage must not jump to outcome_observed.
|
||||
{"alerts acknowledged alone is not an outcome trigger", Ping{AlertsAcknowledged30d: 1}, "started"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := activationStage(tt.ping); got != tt.want {
|
||||
t.Fatalf("activationStage(%+v) = %q, want %q", tt.ping, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ActivationStageRank covers every known stage plus an
|
||||
// unknown and empty string, asserts the exact sentinel ranks, and proves the
|
||||
// ranks are strictly ordered (and that unknown collapses to the started rank).
|
||||
func TestBranchcov0723Am_ActivationStageRank(t *testing.T) {
|
||||
tests := []struct {
|
||||
stage string
|
||||
want int
|
||||
}{
|
||||
{"started", 1},
|
||||
{"secured", 2},
|
||||
{"connected", 3},
|
||||
{"monitoring", 4},
|
||||
{"outcome_observed", 5},
|
||||
{"", 1},
|
||||
{"bogus_stage", 1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.stage, func(t *testing.T) {
|
||||
if got := activationStageRank(tt.stage); got != tt.want {
|
||||
t.Fatalf("activationStageRank(%q) = %d, want %d", tt.stage, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("ranks strictly ordered by precedence", func(t *testing.T) {
|
||||
stages := []string{"started", "secured", "connected", "monitoring", "outcome_observed"}
|
||||
prev := -1
|
||||
for _, s := range stages {
|
||||
r := activationStageRank(s)
|
||||
if r <= prev {
|
||||
t.Fatalf("rank(%q) = %d not strictly greater than previous %d", s, r, prev)
|
||||
}
|
||||
prev = r
|
||||
}
|
||||
if rUnknown, rStarted := activationStageRank("nope"), activationStageRank("started"); rUnknown != rStarted {
|
||||
t.Fatalf("unknown rank %d should equal started rank %d", rUnknown, rStarted)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ValidActivationStage returns true for each known stage
|
||||
// and false for unknown, empty, whitespace, and wrong-case inputs.
|
||||
func TestBranchcov0723Am_ValidActivationStage(t *testing.T) {
|
||||
tests := []struct {
|
||||
stage string
|
||||
want bool
|
||||
}{
|
||||
{"started", true},
|
||||
{"secured", true},
|
||||
{"connected", true},
|
||||
{"monitoring", true},
|
||||
{"outcome_observed", true},
|
||||
{"", false},
|
||||
{" ", false},
|
||||
{"Started", false},
|
||||
{"outcome_observed ", false},
|
||||
{"bogus", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.stage, func(t *testing.T) {
|
||||
if got := validActivationStage(tt.stage); got != tt.want {
|
||||
t.Fatalf("validActivationStage(%q) = %v, want %v", tt.stage, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_MonitoredResourceCount covers a zero Ping, a Ping with
|
||||
// each counted field populated individually (proving each field contributes
|
||||
// exactly one), and a Ping with all counted fields populated.
|
||||
func TestBranchcov0723Am_MonitoredResourceCount(t *testing.T) {
|
||||
resourceFields := []struct {
|
||||
name string
|
||||
set func(p *Ping)
|
||||
}{
|
||||
{"PVENodes", func(p *Ping) { p.PVENodes = 1 }},
|
||||
{"PBSInstances", func(p *Ping) { p.PBSInstances = 1 }},
|
||||
{"PMGInstances", func(p *Ping) { p.PMGInstances = 1 }},
|
||||
{"VMs", func(p *Ping) { p.VMs = 1 }},
|
||||
{"Containers", func(p *Ping) { p.Containers = 1 }},
|
||||
{"AgentHosts", func(p *Ping) { p.AgentHosts = 1 }},
|
||||
{"DockerHosts", func(p *Ping) { p.DockerHosts = 1 }},
|
||||
{"DockerContainers", func(p *Ping) { p.DockerContainers = 1 }},
|
||||
{"KubernetesClusters", func(p *Ping) { p.KubernetesClusters = 1 }},
|
||||
{"KubernetesNodes", func(p *Ping) { p.KubernetesNodes = 1 }},
|
||||
{"KubernetesPods", func(p *Ping) { p.KubernetesPods = 1 }},
|
||||
{"KubernetesDeployments", func(p *Ping) { p.KubernetesDeployments = 1 }},
|
||||
{"StoragePools", func(p *Ping) { p.StoragePools = 1 }},
|
||||
{"PhysicalDisks", func(p *Ping) { p.PhysicalDisks = 1 }},
|
||||
{"CephClusters", func(p *Ping) { p.CephClusters = 1 }},
|
||||
{"NetworkShares", func(p *Ping) { p.NetworkShares = 1 }},
|
||||
{"TrueNASSystems", func(p *Ping) { p.TrueNASSystems = 1 }},
|
||||
{"TrueNASVMs", func(p *Ping) { p.TrueNASVMs = 1 }},
|
||||
{"TrueNASApps", func(p *Ping) { p.TrueNASApps = 1 }},
|
||||
{"VMwareHosts", func(p *Ping) { p.VMwareHosts = 1 }},
|
||||
{"VMwareVMs", func(p *Ping) { p.VMwareVMs = 1 }},
|
||||
{"VMwareDatastores", func(p *Ping) { p.VMwareDatastores = 1 }},
|
||||
{"AvailabilityTargets", func(p *Ping) { p.AvailabilityTargets = 1 }},
|
||||
}
|
||||
|
||||
t.Run("zero ping counts nothing", func(t *testing.T) {
|
||||
if got := monitoredResourceCount(Ping{}); got != 0 {
|
||||
t.Fatalf("monitoredResourceCount(zero Ping) = %d, want 0", got)
|
||||
}
|
||||
})
|
||||
|
||||
for _, f := range resourceFields {
|
||||
t.Run(f.name+"/contributes_one", func(t *testing.T) {
|
||||
var p Ping
|
||||
f.set(&p)
|
||||
if got := monitoredResourceCount(p); got != 1 {
|
||||
t.Fatalf("monitoredResourceCount with only %s set = %d, want 1", f.name, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("all counted fields sum to total", func(t *testing.T) {
|
||||
var p Ping
|
||||
for _, f := range resourceFields {
|
||||
f.set(&p)
|
||||
}
|
||||
if got, want := monitoredResourceCount(p), len(resourceFields); got != want {
|
||||
t.Fatalf("monitoredResourceCount(all fields) = %d, want %d", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_EstateSizeBucket covers 0, a negative value, each bucket
|
||||
// interior, every boundary value, and a very large count.
|
||||
func TestBranchcov0723Am_EstateSizeBucket(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
resources int
|
||||
want string
|
||||
}{
|
||||
{"zero is empty", 0, "empty"},
|
||||
{"negative is empty", -5, "empty"},
|
||||
{"one is in 1_10", 1, "1_10"},
|
||||
{"upper boundary of 1_10 inclusive", 10, "1_10"},
|
||||
{"lower boundary of 11_50 inclusive", 11, "11_50"},
|
||||
{"upper boundary of 11_50 inclusive", 50, "11_50"},
|
||||
{"lower boundary of 51_200 inclusive", 51, "51_200"},
|
||||
{"upper boundary of 51_200 inclusive", 200, "51_200"},
|
||||
{"lower boundary of 201_1000 inclusive", 201, "201_1000"},
|
||||
{"upper boundary of 201_1000 inclusive", 1000, "201_1000"},
|
||||
{"one above top boundary is over_1000", 1001, "over_1000"},
|
||||
{"very large count is over_1000", 1_000_000, "over_1000"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := estateSizeBucket(tt.resources); got != tt.want {
|
||||
t.Fatalf("estateSizeBucket(%d) = %q, want %q", tt.resources, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ClassifyUpdateFailureCategory covers every category arm
|
||||
// the source can return, including the status-precedence over error text, both
|
||||
// OR operands of the disk_space and extract arms, the lowercasing that makes
|
||||
// matching case-insensitive, first-match precedence between categories, and the
|
||||
// unknown/default arm via nil error, unmatched text, and a non-failure status.
|
||||
func TestBranchcov0723Am_ClassifyUpdateFailureCategory(t *testing.T) {
|
||||
errOf := func(code, message, details string) *updates.UpdateError {
|
||||
return &updates.UpdateError{Code: code, Message: message, Details: details}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entry updates.UpdateHistoryEntry
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "rolled back status short circuits before error text",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusRolledBack, Error: errOf("", "download timed out", "")},
|
||||
want: "rolled_back",
|
||||
},
|
||||
{
|
||||
name: "cancelled status short circuits before error text",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusCancelled},
|
||||
want: "cancelled",
|
||||
},
|
||||
{
|
||||
name: "failed with nil error is unknown",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed},
|
||||
want: "unknown",
|
||||
},
|
||||
{
|
||||
name: "signature substring in code",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("SIGNATURE_INVALID", "", "")},
|
||||
want: "signature",
|
||||
},
|
||||
{
|
||||
name: "checksum substring in message",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "checksum verify failed", "")},
|
||||
want: "checksum",
|
||||
},
|
||||
{
|
||||
name: "download substring in details",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "", "download stream reset")},
|
||||
want: "download",
|
||||
},
|
||||
{
|
||||
name: "disk space substring in message",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "not enough disk space to extract", "")},
|
||||
want: "disk_space",
|
||||
},
|
||||
{
|
||||
name: "insufficient disk substring without disk space",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "insufficient disk quota remaining", "")},
|
||||
want: "disk_space",
|
||||
},
|
||||
{
|
||||
name: "extract substring in message",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "could not extract layer", "")},
|
||||
want: "extract",
|
||||
},
|
||||
{
|
||||
name: "archive substring in code",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("ARCHIVE_CORRUPT", "", "")},
|
||||
want: "extract",
|
||||
},
|
||||
{
|
||||
name: "backup substring in details",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "", "backup snapshot failed")},
|
||||
want: "backup",
|
||||
},
|
||||
{
|
||||
name: "apply substring in message",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "apply step rolled back", "")},
|
||||
want: "apply",
|
||||
},
|
||||
{
|
||||
name: "restart substring in message",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "service restart timed out", "")},
|
||||
want: "restart",
|
||||
},
|
||||
{
|
||||
name: "unmatched failure text falls through to unknown",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("HTTP_503", "upstream refused", "raw log stays local")},
|
||||
want: "unknown",
|
||||
},
|
||||
{
|
||||
name: "uppercase error text is lowercased before matching",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "DOWNLOAD FAILED", "")},
|
||||
want: "download",
|
||||
},
|
||||
{
|
||||
name: "first matching category wins between signature and checksum",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusFailed, Error: errOf("", "signature then checksum", "")},
|
||||
want: "signature",
|
||||
},
|
||||
{
|
||||
name: "success status with nil error is unknown",
|
||||
entry: updates.UpdateHistoryEntry{Status: updates.StatusSuccess},
|
||||
want: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyUpdateFailureCategory(tt.entry); got != tt.want {
|
||||
t.Fatalf("classifyUpdateFailureCategory(status=%q, err=%+v) = %q, want %q",
|
||||
tt.entry.Status, tt.entry.Error, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// decodeLifecycleRaw reads and JSON-decodes the lifecycle state file WITHOUT
|
||||
// applying the validActivationStage normalization that the production
|
||||
// readLifecycleRecord performs. It is used to assert the exact on-disk state
|
||||
// independently of the function under test.
|
||||
func decodeLifecycleRaw(t *testing.T, dir string) lifecycleRecord {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(filepath.Join(dir, lifecycleStateFile))
|
||||
if err != nil {
|
||||
t.Fatalf("read lifecycle state: %v", err)
|
||||
}
|
||||
var rec lifecycleRecord
|
||||
if err := json.Unmarshal(data, &rec); err != nil {
|
||||
t.Fatalf("unmarshal lifecycle state: %v", err)
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
// lifecycleIsZero reports whether r is the zero lifecycleRecord value.
|
||||
func lifecycleIsZero(r lifecycleRecord) bool {
|
||||
return r.FirstObservedAt.IsZero() &&
|
||||
r.FirstMonitoredResourceAt == nil &&
|
||||
r.HighestObservedActivation == ""
|
||||
}
|
||||
|
||||
// unwritableDataDir returns a dataDir whose parent component is a regular
|
||||
// file, so that any os.MkdirAll / write attempt on it fails deterministically.
|
||||
// The blocking file lives under t.TempDir() and is cleaned up automatically.
|
||||
func unwritableDataDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
blocker := filepath.Join(t.TempDir(), "blocker")
|
||||
if err := os.WriteFile(blocker, []byte("x"), 0600); err != nil {
|
||||
t.Fatalf("create blocker file: %v", err)
|
||||
}
|
||||
return filepath.Join(blocker, "child")
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ReadLifecycleRecord covers every return path of
|
||||
// readLifecycleRecord: a missing directory, an existing directory with no
|
||||
// file, a present-but-malformed file, a present file whose activation stage is
|
||||
// invalid (it must be blanked while the other fields are kept), and a fully
|
||||
// valid record whose every field round-trips.
|
||||
func TestBranchcov0723Am_ReadLifecycleRecord(t *testing.T) {
|
||||
t.Run("missing directory returns zero record", func(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "does-not-exist")
|
||||
if got := readLifecycleRecord(dir); !lifecycleIsZero(got) {
|
||||
t.Fatalf("readLifecycleRecord(missing dir) = %+v, want zero value", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("existing directory with file absent returns zero record", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if got := readLifecycleRecord(dir); !lifecycleIsZero(got) {
|
||||
t.Fatalf("readLifecycleRecord(no file) = %+v, want zero value", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed JSON returns zero record", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, lifecycleStateFile), []byte("{not json"), 0600); err != nil {
|
||||
t.Fatalf("write file: %v", err)
|
||||
}
|
||||
if got := readLifecycleRecord(dir); !lifecycleIsZero(got) {
|
||||
t.Fatalf("readLifecycleRecord(malformed) = %+v, want zero value", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid activation stage is blanked but other fields kept", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
firstObs := time.Date(2026, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
raw := fmt.Sprintf(`{"first_observed_at":%q,"highest_observed_activation":"totally_bogus"}`, firstObs.Format(time.RFC3339Nano))
|
||||
if err := os.WriteFile(filepath.Join(dir, lifecycleStateFile), []byte(raw), 0600); err != nil {
|
||||
t.Fatalf("write file: %v", err)
|
||||
}
|
||||
got := readLifecycleRecord(dir)
|
||||
if !got.FirstObservedAt.Equal(firstObs) {
|
||||
t.Fatalf("FirstObservedAt = %v, want %v (must be preserved)", got.FirstObservedAt, firstObs)
|
||||
}
|
||||
if got.HighestObservedActivation != "" {
|
||||
t.Fatalf("HighestObservedActivation = %q, want empty after blanking", got.HighestObservedActivation)
|
||||
}
|
||||
if got.FirstMonitoredResourceAt != nil {
|
||||
t.Fatalf("FirstMonitoredResourceAt = %v, want nil", got.FirstMonitoredResourceAt)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid record round-trips every field", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
firstObs := time.Date(2026, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
firstMon := time.Date(2026, 1, 16, 8, 0, 0, 0, time.UTC)
|
||||
seed := lifecycleRecord{
|
||||
FirstObservedAt: firstObs,
|
||||
FirstMonitoredResourceAt: &firstMon,
|
||||
HighestObservedActivation: "monitoring",
|
||||
}
|
||||
encoded, err := json.Marshal(seed)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal seed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, lifecycleStateFile), encoded, 0600); err != nil {
|
||||
t.Fatalf("write file: %v", err)
|
||||
}
|
||||
|
||||
got := readLifecycleRecord(dir)
|
||||
if !got.FirstObservedAt.Equal(firstObs) {
|
||||
t.Fatalf("FirstObservedAt = %v, want %v", got.FirstObservedAt, firstObs)
|
||||
}
|
||||
if got.HighestObservedActivation != "monitoring" {
|
||||
t.Fatalf("HighestObservedActivation = %q, want monitoring", got.HighestObservedActivation)
|
||||
}
|
||||
if got.FirstMonitoredResourceAt == nil {
|
||||
t.Fatal("FirstMonitoredResourceAt = nil, want non-nil pointer")
|
||||
}
|
||||
if !got.FirstMonitoredResourceAt.Equal(firstMon) {
|
||||
t.Fatalf("FirstMonitoredResourceAt = %v, want %v", *got.FirstMonitoredResourceAt, firstMon)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_WriteLifecycleRecord covers the happy path (writing into
|
||||
// a fresh directory, which must be created) with a full round-trip read-back,
|
||||
// and the os.MkdirAll failure path when the dataDir's parent is a regular file.
|
||||
func TestBranchcov0723Am_WriteLifecycleRecord(t *testing.T) {
|
||||
t.Run("happy path round-trips every field and creates the directory", func(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "nested")
|
||||
firstObs := time.Date(2026, 2, 3, 4, 5, 6, 0, time.UTC)
|
||||
firstMon := time.Date(2026, 2, 4, 5, 6, 7, 0, time.UTC)
|
||||
seed := lifecycleRecord{
|
||||
FirstObservedAt: firstObs,
|
||||
FirstMonitoredResourceAt: &firstMon,
|
||||
HighestObservedActivation: "outcome_observed",
|
||||
}
|
||||
if err := writeLifecycleRecord(dir, seed); err != nil {
|
||||
t.Fatalf("writeLifecycleRecord: %v", err)
|
||||
}
|
||||
|
||||
got := decodeLifecycleRaw(t, dir)
|
||||
if !got.FirstObservedAt.Equal(firstObs) {
|
||||
t.Fatalf("FirstObservedAt = %v, want %v", got.FirstObservedAt, firstObs)
|
||||
}
|
||||
if got.HighestObservedActivation != "outcome_observed" {
|
||||
t.Fatalf("HighestObservedActivation = %q, want outcome_observed", got.HighestObservedActivation)
|
||||
}
|
||||
if got.FirstMonitoredResourceAt == nil || !got.FirstMonitoredResourceAt.Equal(firstMon) {
|
||||
t.Fatalf("FirstMonitoredResourceAt = %v, want %v", got.FirstMonitoredResourceAt, firstMon)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, lifecycleStateFile)); err != nil {
|
||||
t.Fatalf("lifecycle file not created under fresh nested dir: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dataDir whose parent is a regular file returns non-nil error", func(t *testing.T) {
|
||||
dir := unwritableDataDir(t)
|
||||
if err := writeLifecycleRecord(dir, lifecycleRecord{}); err == nil {
|
||||
t.Fatal("writeLifecycleRecord with unwritable dataDir = nil error, want non-nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("read only data dir fails at temp file creation", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
sub := filepath.Join(dir, "read-only")
|
||||
if err := os.MkdirAll(sub, 0700); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
if err := os.Chmod(sub, 0500); err != nil {
|
||||
t.Fatalf("chmod read-only: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Chmod(sub, 0700); err != nil {
|
||||
t.Errorf("restore dir mode: %v", err)
|
||||
}
|
||||
})
|
||||
// MkdirAll on the existing directory is a no-op even when read-only,
|
||||
// so the failure surfaces at os.CreateTemp inside writeLifecycleRecord.
|
||||
err := writeLifecycleRecord(sub, lifecycleRecord{HighestObservedActivation: "started"})
|
||||
if err == nil {
|
||||
t.Skip("temp file creation succeeded despite read-only dir (e.g. running as root); CreateTemp error arm not exercisable here")
|
||||
}
|
||||
// No final lifecycle file should exist.
|
||||
if _, err := os.Stat(filepath.Join(sub, lifecycleStateFile)); err == nil {
|
||||
t.Fatal("lifecycle file must not exist after a failed write")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ApplyLifecycle covers the nil-ping no-op, the first-ever
|
||||
// ping with and without monitored resources (which selects the
|
||||
// present-at-first-observation vs not_observed time-to-monitoring arms), a
|
||||
// subsequent ping that preserves FirstObservedAt while recording the first
|
||||
// monitored resource later, a subsequent lower-stage ping that preserves the
|
||||
// highest observed activation, and the outcome-observed signal path.
|
||||
func TestBranchcov0723Am_ApplyLifecycle(t *testing.T) {
|
||||
t.Run("nil ping is a no-op and writes nothing", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
applyLifecycle(nil, dir, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
if _, err := os.Stat(filepath.Join(dir, lifecycleStateFile)); !os.IsNotExist(err) {
|
||||
t.Fatalf("nil ping must not create a lifecycle file; stat err = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("first ever ping without monitoring records first observed as not observed", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
|
||||
ping := &Ping{AuthConfigured: true}
|
||||
applyLifecycle(ping, dir, now)
|
||||
|
||||
if ping.ActivationStage != "secured" {
|
||||
t.Fatalf("ActivationStage = %q, want secured", ping.ActivationStage)
|
||||
}
|
||||
if ping.MonitoringActive {
|
||||
t.Fatal("MonitoringActive = true, want false (no monitored resources)")
|
||||
}
|
||||
if ping.OutcomeObserved30d {
|
||||
t.Fatal("OutcomeObserved30d = true, want false")
|
||||
}
|
||||
if ping.KnownInstallAgeBucket != "under_1d" {
|
||||
t.Fatalf("KnownInstallAgeBucket = %q, want under_1d", ping.KnownInstallAgeBucket)
|
||||
}
|
||||
if ping.EstateSizeBucket != "empty" {
|
||||
t.Fatalf("EstateSizeBucket = %q, want empty", ping.EstateSizeBucket)
|
||||
}
|
||||
if ping.TimeToFirstMonitoredResourceBucket != "not_observed" {
|
||||
t.Fatalf("TimeToFirstMonitoredResourceBucket = %q, want not_observed", ping.TimeToFirstMonitoredResourceBucket)
|
||||
}
|
||||
|
||||
got := decodeLifecycleRaw(t, dir)
|
||||
if !got.FirstObservedAt.Equal(now) {
|
||||
t.Fatalf("persisted FirstObservedAt = %v, want %v", got.FirstObservedAt, now)
|
||||
}
|
||||
if got.HighestObservedActivation != "secured" {
|
||||
t.Fatalf("persisted HighestObservedActivation = %q, want secured", got.HighestObservedActivation)
|
||||
}
|
||||
if got.FirstMonitoredResourceAt != nil {
|
||||
t.Fatalf("persisted FirstMonitoredResourceAt = %v, want nil", got.FirstMonitoredResourceAt)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("first ever ping with monitoring marks present at first observation", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
|
||||
ping := &Ping{PVENodes: 3}
|
||||
applyLifecycle(ping, dir, now)
|
||||
|
||||
if ping.ActivationStage != "monitoring" {
|
||||
t.Fatalf("ActivationStage = %q, want monitoring", ping.ActivationStage)
|
||||
}
|
||||
if !ping.MonitoringActive {
|
||||
t.Fatal("MonitoringActive = false, want true")
|
||||
}
|
||||
if ping.EstateSizeBucket != "1_10" {
|
||||
t.Fatalf("EstateSizeBucket = %q, want 1_10", ping.EstateSizeBucket)
|
||||
}
|
||||
if ping.TimeToFirstMonitoredResourceBucket != "present_at_first_observation" {
|
||||
t.Fatalf("TimeToFirstMonitoredResourceBucket = %q, want present_at_first_observation", ping.TimeToFirstMonitoredResourceBucket)
|
||||
}
|
||||
|
||||
got := decodeLifecycleRaw(t, dir)
|
||||
if !got.FirstObservedAt.Equal(now) {
|
||||
t.Fatalf("persisted FirstObservedAt = %v, want %v", got.FirstObservedAt, now)
|
||||
}
|
||||
if got.FirstMonitoredResourceAt == nil || !got.FirstMonitoredResourceAt.Equal(now) {
|
||||
t.Fatalf("persisted FirstMonitoredResourceAt = %v, want %v", got.FirstMonitoredResourceAt, now)
|
||||
}
|
||||
if got.HighestObservedActivation != "monitoring" {
|
||||
t.Fatalf("persisted HighestObservedActivation = %q, want monitoring", got.HighestObservedActivation)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("subsequent ping preserves first observed and records first monitored later", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
start := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
|
||||
seed := lifecycleRecord{
|
||||
FirstObservedAt: start,
|
||||
HighestObservedActivation: "secured",
|
||||
}
|
||||
encoded, err := json.Marshal(seed)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal seed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, lifecycleStateFile), encoded, 0600); err != nil {
|
||||
t.Fatalf("write seed: %v", err)
|
||||
}
|
||||
|
||||
later := start.Add(2 * time.Hour)
|
||||
ping := &Ping{AuthConfigured: true, PVENodes: 1}
|
||||
applyLifecycle(ping, dir, later)
|
||||
|
||||
if ping.ActivationStage != "monitoring" {
|
||||
t.Fatalf("ActivationStage = %q, want monitoring (upgraded)", ping.ActivationStage)
|
||||
}
|
||||
if !ping.MonitoringActive {
|
||||
t.Fatal("MonitoringActive = false, want true")
|
||||
}
|
||||
if ping.KnownInstallAgeBucket != "under_1d" {
|
||||
t.Fatalf("KnownInstallAgeBucket = %q, want under_1d", ping.KnownInstallAgeBucket)
|
||||
}
|
||||
if ping.TimeToFirstMonitoredResourceBucket != "1_6h" {
|
||||
t.Fatalf("TimeToFirstMonitoredResourceBucket = %q, want 1_6h", ping.TimeToFirstMonitoredResourceBucket)
|
||||
}
|
||||
|
||||
got := decodeLifecycleRaw(t, dir)
|
||||
if !got.FirstObservedAt.Equal(start) {
|
||||
t.Fatalf("persisted FirstObservedAt = %v, want preserved %v", got.FirstObservedAt, start)
|
||||
}
|
||||
if got.HighestObservedActivation != "monitoring" {
|
||||
t.Fatalf("persisted HighestObservedActivation = %q, want monitoring", got.HighestObservedActivation)
|
||||
}
|
||||
if got.FirstMonitoredResourceAt == nil || !got.FirstMonitoredResourceAt.Equal(later) {
|
||||
t.Fatalf("persisted FirstMonitoredResourceAt = %v, want %v", got.FirstMonitoredResourceAt, later)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("subsequent lower stage ping preserves highest observed activation and first monitored", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
start := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
|
||||
firstMon := start.Add(30 * time.Minute)
|
||||
seed := lifecycleRecord{
|
||||
FirstObservedAt: start,
|
||||
FirstMonitoredResourceAt: &firstMon,
|
||||
HighestObservedActivation: "monitoring",
|
||||
}
|
||||
encoded, err := json.Marshal(seed)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal seed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, lifecycleStateFile), encoded, 0600); err != nil {
|
||||
t.Fatalf("write seed: %v", err)
|
||||
}
|
||||
|
||||
later := start.Add(48 * time.Hour)
|
||||
ping := &Ping{AuthConfigured: true}
|
||||
applyLifecycle(ping, dir, later)
|
||||
|
||||
if ping.ActivationStage != "monitoring" {
|
||||
t.Fatalf("ActivationStage = %q, want preserved monitoring", ping.ActivationStage)
|
||||
}
|
||||
if ping.MonitoringActive {
|
||||
t.Fatal("MonitoringActive = true, want false")
|
||||
}
|
||||
if ping.KnownInstallAgeBucket != "1_7d" {
|
||||
t.Fatalf("KnownInstallAgeBucket = %q, want 1_7d", ping.KnownInstallAgeBucket)
|
||||
}
|
||||
if ping.TimeToFirstMonitoredResourceBucket != "15m_1h" {
|
||||
t.Fatalf("TimeToFirstMonitoredResourceBucket = %q, want 15m_1h", ping.TimeToFirstMonitoredResourceBucket)
|
||||
}
|
||||
|
||||
got := decodeLifecycleRaw(t, dir)
|
||||
if !got.FirstObservedAt.Equal(start) {
|
||||
t.Fatalf("persisted FirstObservedAt = %v, want %v", got.FirstObservedAt, start)
|
||||
}
|
||||
if got.HighestObservedActivation != "monitoring" {
|
||||
t.Fatalf("persisted HighestObservedActivation = %q, want monitoring", got.HighestObservedActivation)
|
||||
}
|
||||
if got.FirstMonitoredResourceAt == nil || !got.FirstMonitoredResourceAt.Equal(firstMon) {
|
||||
t.Fatalf("persisted FirstMonitoredResourceAt = %v, want preserved %v", got.FirstMonitoredResourceAt, firstMon)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("outcome signals set OutcomeObserved30d and upgrade stage to outcome observed", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
|
||||
ping := &Ping{PVENodes: 1, AlertsFired30d: 2}
|
||||
applyLifecycle(ping, dir, now)
|
||||
|
||||
if !ping.OutcomeObserved30d {
|
||||
t.Fatal("OutcomeObserved30d = false, want true")
|
||||
}
|
||||
if !ping.MonitoringActive {
|
||||
t.Fatal("MonitoringActive = false, want true")
|
||||
}
|
||||
if ping.ActivationStage != "outcome_observed" {
|
||||
t.Fatalf("ActivationStage = %q, want outcome_observed", ping.ActivationStage)
|
||||
}
|
||||
|
||||
got := decodeLifecycleRaw(t, dir)
|
||||
if got.HighestObservedActivation != "outcome_observed" {
|
||||
t.Fatalf("persisted HighestObservedActivation = %q, want outcome_observed", got.HighestObservedActivation)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("corrupted record with first monitored before first observed clamps elapsed to zero", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
firstObs := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
|
||||
beforeFirstObs := firstObs.Add(-30 * time.Minute)
|
||||
seed := lifecycleRecord{
|
||||
FirstObservedAt: firstObs,
|
||||
FirstMonitoredResourceAt: &beforeFirstObs,
|
||||
HighestObservedActivation: "monitoring",
|
||||
}
|
||||
encoded, err := json.Marshal(seed)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal seed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, lifecycleStateFile), encoded, 0600); err != nil {
|
||||
t.Fatalf("write seed: %v", err)
|
||||
}
|
||||
|
||||
// No monitored resources -> MonitoringActive stays false and the stored
|
||||
// (corrupted) FirstMonitoredResourceAt is preserved, exercising the
|
||||
// negative-elapsed clamp.
|
||||
ping := &Ping{AuthConfigured: true}
|
||||
applyLifecycle(ping, dir, firstObs.Add(time.Hour))
|
||||
|
||||
if ping.TimeToFirstMonitoredResourceBucket != "under_15m" {
|
||||
t.Fatalf("TimeToFirstMonitoredResourceBucket = %q, want under_15m (negative elapsed clamped to 0)", ping.TimeToFirstMonitoredResourceBucket)
|
||||
}
|
||||
if ping.ActivationStage != "monitoring" {
|
||||
t.Fatalf("ActivationStage = %q, want preserved monitoring", ping.ActivationStage)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unwritable data dir still populates ping and tolerates write failure", func(t *testing.T) {
|
||||
dir := unwritableDataDir(t)
|
||||
now := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
|
||||
ping := &Ping{AuthConfigured: true}
|
||||
// Must not panic; the in-memory record is still applied to the ping even
|
||||
// though persistence fails.
|
||||
applyLifecycle(ping, dir, now)
|
||||
|
||||
if ping.ActivationStage != "secured" {
|
||||
t.Fatalf("ActivationStage = %q, want secured", ping.ActivationStage)
|
||||
}
|
||||
if ping.KnownInstallAgeBucket != "under_1d" {
|
||||
t.Fatalf("KnownInstallAgeBucket = %q, want under_1d", ping.KnownInstallAgeBucket)
|
||||
}
|
||||
if ping.TimeToFirstMonitoredResourceBucket != "not_observed" {
|
||||
t.Fatalf("TimeToFirstMonitoredResourceBucket = %q, want not_observed", ping.TimeToFirstMonitoredResourceBucket)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ParseInstallIDRecord covers empty/whitespace input,
|
||||
// garbage, a legacy plaintext UUID, valid JSON with an invalid UUID, valid JSON
|
||||
// with a valid UUID but a zero IssuedAt, malformed JSON, a fully valid record,
|
||||
// and the whitespace-trimming of the install_id field.
|
||||
func TestBranchcov0723Am_ParseInstallIDRecord(t *testing.T) {
|
||||
id := uuid.New().String()
|
||||
issuedAt := time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
wantOK bool
|
||||
}{
|
||||
{"empty input returns false", []byte(""), false},
|
||||
{"whitespace only input returns false", []byte(" \n\t "), false},
|
||||
{"garbage non json returns false", []byte("not-a-uuid"), false},
|
||||
{"legacy plaintext uuid returns false", []byte(id + "\n"), false},
|
||||
{"valid json but invalid uuid returns false", []byte(`{"install_id":"not-a-uuid","issued_at":"2026-03-28T12:00:00Z"}`), false},
|
||||
{"valid json valid uuid but zero issued at returns false", []byte(`{"install_id":"` + id + `"}`), false},
|
||||
{"malformed json returns false", []byte(`{"install_id":}`), false},
|
||||
{"valid record with valid uuid and issued at returns true", []byte(fmt.Sprintf(`{"install_id":%q,"issued_at":%q}`, id, issuedAt.Format(time.RFC3339Nano))), true},
|
||||
{"whitespace around install id is trimmed and record still valid", []byte(fmt.Sprintf(`{"install_id":%q,"issued_at":%q}`, " "+id+" ", issuedAt.Format(time.RFC3339Nano))), true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
record, ok := parseInstallIDRecord(tt.input)
|
||||
if ok != tt.wantOK {
|
||||
t.Fatalf("parseInstallIDRecord(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
|
||||
}
|
||||
if tt.wantOK {
|
||||
if record.InstallID != id {
|
||||
t.Fatalf("record.InstallID = %q, want %q", record.InstallID, id)
|
||||
}
|
||||
if !record.IssuedAt.Equal(issuedAt) {
|
||||
t.Fatalf("record.IssuedAt = %v, want %v", record.IssuedAt, issuedAt)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ShouldKeepInstallIDRecord covers each keep/discard arm:
|
||||
// an invalid UUID, a zero IssuedAt, a future IssuedAt, an expired record at the
|
||||
// exact rotation-window boundary (strict <), and a recent valid record.
|
||||
func TestBranchcov0723Am_ShouldKeepInstallIDRecord(t *testing.T) {
|
||||
id := uuid.New().String()
|
||||
now := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
record installIDRecord
|
||||
now time.Time
|
||||
want bool
|
||||
}{
|
||||
{"invalid uuid discarded", installIDRecord{InstallID: "nope", IssuedAt: now.Add(-time.Hour)}, now, false},
|
||||
{"zero issued at discarded", installIDRecord{InstallID: id}, now, false},
|
||||
{"future issued at discarded", installIDRecord{InstallID: id, IssuedAt: now.Add(time.Hour)}, now, false},
|
||||
{"expired at exact rotation window boundary discarded", installIDRecord{InstallID: id, IssuedAt: now.Add(-installIDRotationWindow)}, now, false},
|
||||
{"recent valid record kept", installIDRecord{InstallID: id, IssuedAt: now.Add(-time.Hour)}, now, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := shouldKeepInstallIDRecord(tt.record, tt.now); got != tt.want {
|
||||
t.Fatalf("shouldKeepInstallIDRecord(%+v, now) = %v, want %v", tt.record, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_WriteInstallIDRecordAt covers the happy path (persisting
|
||||
// a record that round-trips exactly) and the os.MkdirAll failure path when the
|
||||
// dataDir's parent is a regular file.
|
||||
func TestBranchcov0723Am_WriteInstallIDRecordAt(t *testing.T) {
|
||||
t.Run("happy path persists record exactly", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 4, 1, 9, 0, 0, 0, time.UTC)
|
||||
record := installIDRecord{InstallID: uuid.New().String(), IssuedAt: now}
|
||||
if err := writeInstallIDRecordAt(dir, record); err != nil {
|
||||
t.Fatalf("writeInstallIDRecordAt: %v", err)
|
||||
}
|
||||
got := decodeInstallIDRecordFile(t, filepath.Join(dir, installIDFile))
|
||||
if got.InstallID != record.InstallID {
|
||||
t.Fatalf("persisted InstallID = %q, want %q", got.InstallID, record.InstallID)
|
||||
}
|
||||
if !got.IssuedAt.Equal(now) {
|
||||
t.Fatalf("persisted IssuedAt = %v, want %v", got.IssuedAt, now)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dataDir whose parent is a regular file returns non-nil error", func(t *testing.T) {
|
||||
dir := unwritableDataDir(t)
|
||||
err := writeInstallIDRecordAt(dir, installIDRecord{InstallID: uuid.New().String(), IssuedAt: time.Now()})
|
||||
if err == nil {
|
||||
t.Fatal("writeInstallIDRecordAt with unwritable dataDir = nil error, want non-nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_ResetInstallIDAt covers the happy path (a new non-empty
|
||||
// ID persisted with the given IssuedAt) and the write-failure path (empty ID
|
||||
// and non-nil error).
|
||||
func TestBranchcov0723Am_ResetInstallIDAt(t *testing.T) {
|
||||
t.Run("happy path writes new id with given issued at", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 5, 1, 8, 0, 0, 0, time.UTC)
|
||||
id, err := resetInstallIDAt(dir, now)
|
||||
if err != nil {
|
||||
t.Fatalf("resetInstallIDAt: %v", err)
|
||||
}
|
||||
if id == "" {
|
||||
t.Fatal("expected non-empty install id")
|
||||
}
|
||||
got := decodeInstallIDRecordFile(t, filepath.Join(dir, installIDFile))
|
||||
if got.InstallID != id {
|
||||
t.Fatalf("persisted InstallID = %q, want %q", got.InstallID, id)
|
||||
}
|
||||
if !got.IssuedAt.Equal(now) {
|
||||
t.Fatalf("persisted IssuedAt = %v, want %v", got.IssuedAt, now)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write error returns empty id and non-nil error", func(t *testing.T) {
|
||||
dir := unwritableDataDir(t)
|
||||
id, err := resetInstallIDAt(dir, time.Date(2026, 5, 1, 8, 0, 0, 0, time.UTC))
|
||||
if err == nil {
|
||||
t.Fatal("resetInstallIDAt with unwritable dataDir = nil error, want non-nil")
|
||||
}
|
||||
if id != "" {
|
||||
t.Fatalf("resetInstallIDAt returned id %q, want empty on write failure", id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_GetOrCreateInstallIDAt covers the previously-uncovered
|
||||
// write-failure arm: when persistence fails the caller still receives a fresh,
|
||||
// valid, non-empty ID for the session, even though nothing lands on disk.
|
||||
// (The create-when-absent, reuse-when-present, and rotation arms are already
|
||||
// exercised by sibling tests in telemetry_test.go.)
|
||||
func TestBranchcov0723Am_GetOrCreateInstallIDAt(t *testing.T) {
|
||||
t.Run("write failure still returns a generated id for the session", func(t *testing.T) {
|
||||
dir := unwritableDataDir(t)
|
||||
now := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC)
|
||||
id := getOrCreateInstallIDAt(dir, now)
|
||||
if id == "" {
|
||||
t.Fatal("getOrCreateInstallIDAt returned empty id; a fresh id should be generated even when persistence fails")
|
||||
}
|
||||
if _, err := uuid.Parse(id); err != nil {
|
||||
t.Fatalf("generated id %q is not a valid uuid: %v", id, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, installIDFile)); err == nil {
|
||||
t.Fatal("install id file should not be accessible after a write failure")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBranchcov0723Am_InstallIDWrappers covers the thin non-"At" wrappers
|
||||
// ResetInstallID and getOrCreateInstallID. Both take an explicit dataDir, so
|
||||
// they are pointed at t.TempDir() and never touch a real config path. They use
|
||||
// time.Now() internally, so the assertions are on non-emptiness, persistence,
|
||||
// reuse, and rotation (ID change) rather than exact timestamps.
|
||||
func TestBranchcov0723Am_InstallIDWrappers(t *testing.T) {
|
||||
t.Run("getOrCreateInstallID creates then reuses across calls", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
first := getOrCreateInstallID(dir)
|
||||
if first == "" {
|
||||
t.Fatal("getOrCreateInstallID returned empty id on first call")
|
||||
}
|
||||
if _, err := uuid.Parse(first); err != nil {
|
||||
t.Fatalf("first id %q is not a valid uuid: %v", first, err)
|
||||
}
|
||||
got := decodeInstallIDRecordFile(t, filepath.Join(dir, installIDFile))
|
||||
if got.InstallID != first {
|
||||
t.Fatalf("persisted id = %q, want %q", got.InstallID, first)
|
||||
}
|
||||
if second := getOrCreateInstallID(dir); second != first {
|
||||
t.Fatalf("second getOrCreateInstallID = %q, want same %q", second, first)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ResetInstallID rotates and the new id persists and is reused", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
original := getOrCreateInstallID(dir)
|
||||
if original == "" {
|
||||
t.Fatal("getOrCreateInstallID returned empty id")
|
||||
}
|
||||
rotated, err := ResetInstallID(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("ResetInstallID: %v", err)
|
||||
}
|
||||
if rotated == "" {
|
||||
t.Fatal("ResetInstallID returned empty id")
|
||||
}
|
||||
if rotated == original {
|
||||
t.Fatalf("ResetInstallID returned the same id %q, want a new rotated one", rotated)
|
||||
}
|
||||
if next := getOrCreateInstallID(dir); next != rotated {
|
||||
t.Fatalf("getOrCreateInstallID after reset = %q, want persisted %q", next, rotated)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user