package agentapi import ( "context" "net/http" "net/http/httptest" "strings" "testing" ) // backupStub serves the agent's backup endpoints with the documented payload shapes so the client's // parse + the trigger POST are asserted against real JSON (non-hollow). func backupStub(t *testing.T, started chan<- struct{}) (*httptest.Server, string) { mux := http.NewServeMux() mux.HandleFunc("GET /backup/status", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"phase":"done","job_id":"backup-9201-1",` + `"backup":{"target_id":"local","vmid":9201,"archive":"local:backup/vzdump-lxc-9201-x.tar.zst",` + `"mode":"snapshot","crash_consistent":true,"size_bytes":1399160221,"success":true,` + `"started_at":"2026-06-12T07:37:41Z","duration_seconds":31.14}}}`)) }) mux.HandleFunc("GET /backup/due", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"due":false,"reason":"within cadence window","age_seconds":5234}}`)) }) mux.HandleFunc("GET /restore-test/status", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"ok":true,"data":{"restore_test":{"source_archive":"local:backup/x","source_tier":"local",` + `"pass":true,"verified":"boot+running","tested_at":"2026-06-11T03:00:00Z","duration_seconds":42.0}}}`)) }) mux.HandleFunc("POST /backup", func(w http.ResponseWriter, r *http.Request) { if started != nil { started <- struct{}{} } w.WriteHeader(http.StatusAccepted) _, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"job_id":"backup-9201-2","phase":"running"}}`)) }) s := httptest.NewTLSServer(mux) return s, strings.TrimPrefix(s.URL, "https://") } func TestBackupStatus_ParsesRecord(t *testing.T) { s, ep := backupStub(t, nil) defer s.Close() c := clientFor(t, s, ep) st, err := c.BackupStatus(context.Background()) if err != nil { t.Fatalf("BackupStatus: %v", err) } if st.Phase != "done" || st.JobID != "backup-9201-1" { t.Fatalf("status fields: %+v", st) } if st.Backup == nil { t.Fatal("expected a backup record") } if st.Backup.TargetID != "local" || st.Backup.Mode != "snapshot" || !st.Backup.Success { t.Fatalf("backup record: %+v", st.Backup) } if st.Backup.SizeBytes != 1399160221 || st.Backup.Archive == "" || st.Backup.StartedAt == "" { t.Fatalf("backup record details: %+v", st.Backup) } } func TestBackupDue_Parses(t *testing.T) { s, ep := backupStub(t, nil) defer s.Close() c := clientFor(t, s, ep) due, err := c.BackupDue(context.Background()) if err != nil { t.Fatalf("BackupDue: %v", err) } if due.Due || due.Reason != "within cadence window" { t.Fatalf("due: %+v", due) } } func TestRestoreTestStatus_Parses(t *testing.T) { s, ep := backupStub(t, nil) defer s.Close() c := clientFor(t, s, ep) rt, err := c.RestoreTestStatus(context.Background()) if err != nil { t.Fatalf("RestoreTestStatus: %v", err) } if rt == nil { t.Fatal("expected a restore-test record") } if !rt.Pass || rt.Verified != "boot+running" || rt.SourceTier != "local" { t.Fatalf("restore-test: %+v", rt) } } func TestRestoreTestStatus_Null(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("GET /restore-test/status", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"ok":true,"data":{"restore_test":null}}`)) }) s := httptest.NewTLSServer(mux) defer s.Close() c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://")) rt, err := c.RestoreTestStatus(context.Background()) if err != nil { t.Fatalf("RestoreTestStatus(null): %v", err) } if rt != nil { t.Fatalf("expected nil restore-test when none has run, got %+v", rt) } } // StartBackup must POST to /backup (the manual-trigger contract goes through this). func TestStartBackup_PostsToBackup(t *testing.T) { started := make(chan struct{}, 1) s, ep := backupStub(t, started) defer s.Close() c := clientFor(t, s, ep) resp, err := c.StartBackup(context.Background()) if err != nil { t.Fatalf("StartBackup: %v", err) } select { case <-started: default: t.Fatal("StartBackup did not POST to /backup") } if resp.JobID != "backup-9201-2" || resp.Phase != "running" { t.Fatalf("StartBackup response: %+v", resp) } }