Files
felhom-controller/controller/internal/agentapi/backup_test.go
T
admin bbed5af662 controller v0.47.0: backups page — whole-guest backup visibility + manual trigger
Part 2 of the USB/backup spec. agentapi: StatusResponse.Backup record, DueResponse
age_seconds, RestoreTestStatus(). New "Rendszermentés (teljes mentés)" section
(read-only: last backup/target PBS-vs-local/next-due/restore-test) + "Mentés most"
manual trigger that goes through the quiesce loop (controller owns quiescing):
quiesce.Loop gains mutex + TriggerNow() (single-flight, async). New
/api/guest-backup/{trigger,status} (distinct from apiRouter's /api/backup/*).
App-data rows relabeled under an "Alkalmazás-mentések" divider. Config → slice 10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:15:25 +02:00

131 lines
4.1 KiB
Go

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