a73x

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)
}