package http import ( "bytes" "context" "encoding/json" stdhttp "net/http" "testing" "time" "github.com/oklog/ulid/v2" "gitea.dcglab.co.uk/steve/restic-manager/internal/api" "gitea.dcglab.co.uk/steve/restic-manager/internal/store" ) func TestRunNowBackupRejectsEmptyPaths(t *testing.T) { t.Parallel() _, ts, st := rawTestServer(t) cookie := loginAsAdmin(t, st) hostID := makeHost(t, st, "empty-backup-host") req, _ := stdhttp.NewRequest(stdhttp.MethodPost, ts.URL+"/api/hosts/"+hostID+"/jobs", bytes.NewReader([]byte(`{"kind":"backup"}`))) req.Header.Set("Content-Type", "application/json") req.AddCookie(cookie) res, err := stdhttp.DefaultClient.Do(req) if err != nil { t.Fatalf("post run-now: %v", err) } defer res.Body.Close() if res.StatusCode != stdhttp.StatusUnprocessableEntity { t.Fatalf("status: got %d, want 422", res.StatusCode) } body := readJSONError(t, res.Body) if body.Code != "backup_paths_required" { t.Fatalf("code: got %q", body.Code) } var jobs int _ = st.DB().QueryRow(`SELECT COUNT(*) FROM jobs WHERE host_id = ?`, hostID).Scan(&jobs) if jobs != 0 { t.Fatalf("invalid backup created %d jobs", jobs) } } func TestRunNowForgetShipsRetentionGroupsAndDryRun(t *testing.T) { t.Parallel() srv, ts, st := rawTestServer(t) hostID, token := enrolHostForWS(t, srv, st, "manual-forget-host") seedInitJob(t, st, hostID) keepDaily := 7 if err := st.CreateSourceGroup(context.Background(), &store.SourceGroup{ ID: ulid.Make().String(), HostID: hostID, Name: "documents", Includes: []string{"/home/documents"}, RetentionPolicy: store.RetentionPolicy{KeepDaily: &keepDaily}, }); err != nil { t.Fatalf("create source group: %v", err) } c := agentDial(t, srv, ts, hostID, token) sendHello(t, c, "manual-forget-host") _ = drainUntil(t, c, api.MsgScheduleSet) body, err := json.Marshal(runNowRequest{Kind: api.JobForget, Args: []string{"--dry-run"}}) if err != nil { t.Fatalf("marshal request: %v", err) } req, err := stdhttp.NewRequest(stdhttp.MethodPost, ts.URL+"/api/hosts/"+hostID+"/jobs", bytes.NewReader(body)) if err != nil { t.Fatalf("new request: %v", err) } req.Header.Set("Content-Type", "application/json") req.AddCookie(loginAsAdmin(t, st)) res, err := stdhttp.DefaultClient.Do(req) if err != nil { t.Fatalf("post run-now: %v", err) } defer res.Body.Close() if res.StatusCode != stdhttp.StatusAccepted { t.Fatalf("status: got %d, want %d", res.StatusCode, stdhttp.StatusAccepted) } got := readNextCommandRun(t, c, time.Now().Add(2*time.Second)) if got == nil { t.Fatal("no command.run received") } if len(got.Args) != 1 || got.Args[0] != "--dry-run" { t.Fatalf("Args: got %q, want [--dry-run]", got.Args) } if len(got.ForgetGroups) != 1 { t.Fatalf("ForgetGroups: got %d, want 1", len(got.ForgetGroups)) } group := got.ForgetGroups[0] if group.Tag != "documents" || group.Policy.KeepDaily == nil || *group.Policy.KeepDaily != 7 { t.Fatalf("ForgetGroups[0]: got %+v", group) } } func TestRunNowForgetRejectsHostWithoutRetention(t *testing.T) { t.Parallel() srv, ts, st := rawTestServer(t) hostID, token := enrolHostForWS(t, srv, st, "no-manual-retention-host") seedInitJob(t, st, hostID) c := agentDial(t, srv, ts, hostID, token) sendHello(t, c, "no-manual-retention-host") _ = drainUntil(t, c, api.MsgScheduleSet) body := []byte(`{"kind":"forget","args":["--dry-run"]}`) req, err := stdhttp.NewRequest(stdhttp.MethodPost, ts.URL+"/api/hosts/"+hostID+"/jobs", bytes.NewReader(body)) if err != nil { t.Fatalf("new request: %v", err) } req.Header.Set("Content-Type", "application/json") req.AddCookie(loginAsAdmin(t, st)) res, err := stdhttp.DefaultClient.Do(req) if err != nil { t.Fatalf("post run-now: %v", err) } defer res.Body.Close() if res.StatusCode != stdhttp.StatusUnprocessableEntity { t.Fatalf("status: got %d, want %d", res.StatusCode, stdhttp.StatusUnprocessableEntity) } var jobs int if err := st.DB().QueryRow(`SELECT COUNT(*) FROM jobs WHERE host_id = ? AND kind = 'forget'`, hostID).Scan(&jobs); err != nil { t.Fatalf("count forget jobs: %v", err) } if jobs != 0 { t.Fatalf("forget jobs: got %d, want 0", jobs) } }