internal/server/registry/registry_test.go
Ref: Size: 5.6 KiB History
package registry
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetReturnsDefensiveCopy(t *testing.T) {
r := New(time.Now)
r.UpdateReport("h1", Report{
VMs: []VMStatus{{VMID: "vm1", Phase: "ready"}},
Quarantined: []QuarantinedVM{{VMID: "q1", VMSpecJSON: []byte("{}")}},
})
st, _ := r.Get("h1")
st.VMs[0].Phase = "corrupted"
st.Quarantined[0].VMSpecJSON[0] = 'X'
st2, _ := r.Get("h1")
assert.Equal(t, "ready", st2.VMs[0].Phase)
assert.Equal(t, byte('{'), st2.Quarantined[0].VMSpecJSON[0])
}
func TestReportRoundTripsAndOnlineWindow(t *testing.T) {
now := time.Now()
r := New(func() time.Time { return now })
r.UpdateReport("h1", Report{
VMs: []VMStatus{{VMID: "vm1", PowerState: "running", Phase: "ready", IP: "10.77.1.2"}},
Capacity: Capacity{VCPUs: 8, MemMB: 16384, DiskGB: 200},
LastSeenEpoch: 4,
})
st, ok := r.Get("h1")
assert.True(t, ok)
assert.Equal(t, "ready", st.VMs[0].Phase)
assert.True(t, st.Online)
now = now.Add(2 * OnlineWindow)
st, _ = r.Get("h1")
assert.False(t, st.Online, "stale heartbeat means offline")
}
func TestUnknownHostNotFound(t *testing.T) {
r := New(time.Now)
_, ok := r.Get("nope")
assert.False(t, ok)
}
// TestStalenessDerivation pins the online/stale/age signals derived from the age
// of the last report: fresh is online and not stale; past half the window it is
// still online but stale (the early "degrading" warning); past the full window
// it is offline (and remains stale).
func TestStalenessDerivation(t *testing.T) {
cases := []struct {
name string
age time.Duration
wantOnline bool
wantStale bool
}{
{"fresh", 0, true, false},
{"just under stale", StaleWindow - time.Second, true, false},
{"at stale window", StaleWindow, true, true},
{"online but stale", OnlineWindow - time.Second, true, true},
{"just offline", OnlineWindow, false, true},
{"long offline", 5 * OnlineWindow, false, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
now := time.Now()
r := New(func() time.Time { return now })
r.UpdateReport("h1", Report{})
now = now.Add(tc.age)
st, ok := r.Get("h1")
assert.True(t, ok)
assert.Equal(t, tc.wantOnline, st.Online, "online")
assert.Equal(t, tc.wantStale, st.Stale, "stale")
assert.Equal(t, tc.age, st.SinceLastSeen, "age since last seen")
})
}
}
func TestReportCarriesMetrics(t *testing.T) {
r := New(time.Now)
r.UpdateReport("h1", Report{
Metrics: Metrics{UptimeS: 3600, MemUsedMB: 2048, Load1: 1.5, DiskFreeGB: 80},
})
st, ok := r.Get("h1")
require.True(t, ok)
assert.Equal(t, int64(3600), st.Metrics.UptimeS)
assert.Equal(t, int64(2048), st.Metrics.MemUsedMB)
assert.InDelta(t, 1.5, st.Metrics.Load1, 0.001)
assert.Equal(t, int64(80), st.Metrics.DiskFreeGB)
}
// TestSessionsCountAndReportPreservation pins that RecordConnect counts agent
// (re)connects, that a report preserves the counter (flapping stays visible),
// and that a connect before any report leaves the host offline with no age.
func TestSessionsCountAndReportPreservation(t *testing.T) {
now := time.Now()
r := New(func() time.Time { return now })
// Connect before any report: counted, but offline with no last-seen age.
r.RecordConnect("h1")
st, ok := r.Get("h1")
assert.True(t, ok)
assert.Equal(t, 1, st.Sessions)
assert.False(t, st.Online)
assert.True(t, st.LastSeen.IsZero())
assert.Zero(t, st.SinceLastSeen)
// A report does not reset the session counter.
r.UpdateReport("h1", Report{Capacity: Capacity{VCPUs: 4}})
st, _ = r.Get("h1")
assert.Equal(t, 1, st.Sessions, "report must preserve session count")
assert.True(t, st.Online)
// A reconnect increments and keeps the live report.
r.RecordConnect("h1")
st, _ = r.Get("h1")
assert.Equal(t, 2, st.Sessions)
assert.Equal(t, int64(4), st.Capacity.VCPUs, "reconnect must not blank live state")
}
func TestAgentVersionSurvivesReports(t *testing.T) {
now := time.Now()
r := New(func() time.Time { return now })
r.SetAgentVersion("h1", "v0.0.2")
r.UpdateReport("h1", Report{})
st, ok := r.Get("h1")
if !ok || st.AgentVersion != "v0.0.2" {
t.Fatalf("AgentVersion = %q ok=%v, want v0.0.2 true", st.AgentVersion, ok)
}
}
// TestHostNetworksSurviveReports: the advertised network names arrive in the
// Hello and never again, so the report path — which replaces the whole state —
// must carry them forward. Create-time admission reads them on every create; a
// report that blanked them would refuse a bridged VM on the host that serves it.
func TestHostNetworksSurviveReports(t *testing.T) {
now := time.Now()
r := New(func() time.Time { return now })
r.SetHostNetworks("h1", []string{"lan"})
r.UpdateReport("h1", Report{})
st, ok := r.Get("h1")
require.True(t, ok)
assert.Equal(t, []string{"lan"}, st.HostNetworks, "a report must preserve what the Hello advertised")
// A reconnect re-advertises: an agent restarted without its --host-network
// flags advertises none, and the registry must say so rather than hold the
// networks it used to serve.
r.SetHostNetworks("h1", nil)
st, _ = r.Get("h1")
assert.Empty(t, st.HostNetworks, "a Hello with no networks retracts the old set")
}
// TestGetClonesExposureStatuses pins that a caller cannot corrupt registry
// state through the slice it is handed — the same rule the VM rows follow.
func TestGetClonesExposureStatuses(t *testing.T) {
r := New(time.Now)
r.UpdateReport("h1", Report{Exposures: []ExposureStatus{{ID: "e1", State: "active"}}})
got, ok := r.Get("h1")
require.True(t, ok)
got.Report.Exposures[0].State = "clobbered"
again, _ := r.Get("h1")
assert.Equal(t, "active", again.Report.Exposures[0].State)
}