108 lines
4.3 KiB
Go
108 lines
4.3 KiB
Go
package agentapi
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// R-921: the pre-check reads the agent's real wire shapes — GET /backup/tiers, then GET /backup/status per
|
|
// target — and reports exactly the tiers whose job is running or snapshotted (the agent's in-flight set).
|
|
func TestBackupJobsInFlight_ReportsRunningAndSnapshottedTiers(t *testing.T) {
|
|
phases := map[string]string{"local": "done", "felhom-pbs": "snapshotted", "extra": "running"}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /backup/tiers", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"tiers":[` +
|
|
`{"target":"local","cadence_seconds":86400,"primary":true,"storage":"present"},` +
|
|
`{"target":"felhom-pbs","cadence_seconds":604800,"primary":false,"storage":"present"},` +
|
|
`{"target":"extra","cadence_seconds":604800,"primary":false,"storage":"present"}]}}`))
|
|
})
|
|
mux.HandleFunc("GET /backup/status", func(w http.ResponseWriter, r *http.Request) {
|
|
tg := r.URL.Query().Get("target")
|
|
ph, ok := phases[tg]
|
|
if !ok {
|
|
http.Error(w, "unexpected target "+tg, http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"phase":"` + ph + `","target":"` + tg + `"}}`))
|
|
})
|
|
s := httptest.NewTLSServer(mux)
|
|
defer s.Close()
|
|
c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://"))
|
|
|
|
got, err := c.BackupJobsInFlight(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("BackupJobsInFlight: %v", err)
|
|
}
|
|
if want := []string{"felhom-pbs", "extra"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("in-flight tiers = %v, want %v (done must not count; running and snapshotted must)", got, want)
|
|
}
|
|
}
|
|
|
|
// A pre-R-82 agent (no /backup/tiers → 404) has no per-tier jobs: nil, nil — never an error that would be
|
|
// read as "cannot tell", and never a busy verdict.
|
|
func TestBackupJobsInFlight_PreR82AgentIsNotBusy(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
s := httptest.NewTLSServer(mux) // every route 404s
|
|
defer s.Close()
|
|
c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://"))
|
|
|
|
got, err := c.BackupJobsInFlight(context.Background())
|
|
if err != nil || got != nil {
|
|
t.Fatalf("pre-R-82 agent: got %v, %v — want nil, nil", got, err)
|
|
}
|
|
}
|
|
|
|
// A status read that fails is returned, so the loop can fail toward backing up and say so.
|
|
func TestBackupJobsInFlight_StatusErrorIsReturned(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /backup/tiers", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"tiers":[{"target":"local","primary":true}]}}`))
|
|
})
|
|
mux.HandleFunc("GET /backup/status", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "boom", http.StatusInternalServerError)
|
|
})
|
|
s := httptest.NewTLSServer(mux)
|
|
defer s.Close()
|
|
c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://"))
|
|
|
|
if _, err := c.BackupJobsInFlight(context.Background()); err == nil {
|
|
t.Fatal("a failed status read was swallowed — the caller could not tell it from 'nothing in flight'")
|
|
}
|
|
}
|
|
|
|
// R-921 part 2: HostBusy reads the agent's real wire shape (untargeted GET /backup/status, `host_busy`); an agent
|
|
// without the field is „not known" — never „free".
|
|
func TestHostBusy_ReadsTheAgentsGate(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, body string
|
|
busy, known bool
|
|
what string
|
|
}{
|
|
{"held", `{"ok":true,"data":{"vmid":9201,"phase":"idle","host_busy":{"busy":true,"what":"os-pve-step"}}}`, true, true, "os-pve-step"},
|
|
{"free", `{"ok":true,"data":{"vmid":9201,"phase":"idle","host_busy":{"busy":false}}}`, false, true, ""},
|
|
{"older agent", `{"ok":true,"data":{"vmid":9201,"phase":"idle"}}`, false, false, ""},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /backup/status", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.RawQuery != "" {
|
|
http.Error(w, "want the untargeted status", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(tc.body))
|
|
})
|
|
s := httptest.NewTLSServer(mux)
|
|
defer s.Close()
|
|
c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://"))
|
|
busy, what, known, err := c.HostBusy(context.Background())
|
|
if err != nil || busy != tc.busy || known != tc.known || what != tc.what {
|
|
t.Fatalf("HostBusy = %v %q known=%v err=%v, want %v %q known=%v", busy, what, known, err, tc.busy, tc.what, tc.known)
|
|
}
|
|
})
|
|
}
|
|
}
|