package agentapi import ( "context" "crypto/sha256" "encoding/hex" "errors" "net/http" "net/http/httptest" "strings" "testing" ) // memProber satisfies the SupportProber contract (NetVerifyStatus) PLUS the memory probe's // type-asserted GuestMemory + the AgentVersion fast-path — the shape the web memAgent seam has. type memProber struct { memErr error ver string } func (p *memProber) NetVerifyStatus(context.Context) (NetVerifyStatus, error) { return NetVerifyStatus{Phase: "none"}, nil } func (p *memProber) GuestMemory(context.Context) (GuestMemoryInfo, error) { return GuestMemoryInfo{AllocatedMB: 8192}, p.memErr } func (p *memProber) AgentVersion() string { return p.ver } // The capability table: v0.90.0 → Yes, v0.89.0 → No via the version fast-path; the probe (no // version) classifies 404 → No, nil → Yes. func TestGuestMemory_Capability(t *testing.T) { t.Run("version >= 0.90 → Yes", func(t *testing.T) { var sc SupportCache if got := sc.Supports(context.Background(), &memProber{ver: "0.90.0"}, FeatureGuestMemoryResize); got != SupportYes { t.Errorf("v0.90.0 = %v, want SupportYes", got) } }) t.Run("version < 0.90 → No", func(t *testing.T) { var sc SupportCache if got := sc.Supports(context.Background(), &memProber{ver: "0.89.0"}, FeatureGuestMemoryResize); got != SupportNo { t.Errorf("v0.89.0 = %v, want SupportNo", got) } }) t.Run("no version, probe 404 → No", func(t *testing.T) { var sc SupportCache p := &memProber{memErr: &StatusError{Path: "/guest/memory", Code: http.StatusNotFound}} if got := sc.Supports(context.Background(), p, FeatureGuestMemoryResize); got != SupportNo { t.Errorf("probe 404 = %v, want SupportNo", got) } }) t.Run("no version, probe ok → Yes", func(t *testing.T) { var sc SupportCache if got := sc.Supports(context.Background(), &memProber{}, FeatureGuestMemoryResize); got != SupportYes { t.Errorf("probe ok = %v, want SupportYes", got) } }) } // GuestMemory decodes the agent's payload; a 404 (pre-0.90) is the typed *StatusError. func TestClient_GuestMemory(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(405) return } _, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"allocated_mb":8192,"usage_mb":3000,"host_total_mb":16384,"min_mb":2048,"max_mb":14336,"floor_mb":3512,"running":true}}`)) }) c := newMemTestClient(t, mux) info, err := c.GuestMemory(context.Background()) if err != nil { t.Fatalf("GuestMemory: %v", err) } if info.AllocatedMB != 8192 || info.UsageMB != 3000 || info.MaxMB != 14336 || info.FloorMB != 3512 || !info.Running { t.Errorf("decoded wrong: %+v", info) } } func TestClient_GuestMemory_404(t *testing.T) { c := newMemTestClient(t, http.NewServeMux()) // no route → 404 _, err := c.GuestMemory(context.Background()) var se *StatusError if !errors.As(err, &se) || se.Code != http.StatusNotFound { t.Fatalf("pre-0.90 agent must 404 as *StatusError, got %T: %v", err, err) } } // ResizeMemory: success returns old→new; a 412 refusal surfaces *MemoryRefusedError with the code. func TestClient_ResizeMemory(t *testing.T) { t.Run("success", func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"old_mb":8192,"new_mb":12288,"unchanged":false}}`)) }) c := newMemTestClient(t, mux) res, err := c.ResizeMemory(context.Background(), 12288) if err != nil { t.Fatalf("ResizeMemory: %v", err) } if res.OldMB != 8192 || res.NewMB != 12288 { t.Errorf("result = %+v", res) } }) t.Run("refusal carries the code", func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusPreconditionFailed) _, _ = w.Write([]byte(`{"ok":false,"error":"requested 3300 MB is too close to current usage 3000 MB (floor 3512 MB)","data":{"code":"below_usage_floor","usage_mb":3000,"floor_mb":3512,"min_mb":2048,"max_mb":14336}}`)) }) c := newMemTestClient(t, mux) _, err := c.ResizeMemory(context.Background(), 3300) var refused *MemoryRefusedError if !errors.As(err, &refused) { t.Fatalf("want *MemoryRefusedError, got %T: %v", err, err) } if refused.Code != "below_usage_floor" || refused.Bounds.UsageMB != 3000 || refused.Bounds.FloorMB != 3512 { t.Errorf("refusal = %+v", refused) } }) } func newMemTestClient(t *testing.T, mux *http.ServeMux) *Client { t.Helper() srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) fp := sha256.Sum256(srv.Certificate().Raw) c, err := New(strings.TrimPrefix(srv.URL, "https://"), "test-token", hex.EncodeToString(fp[:])) if err != nil { t.Fatalf("New: %v", err) } return c }