package store import ( "reflect" "testing" "time" ) // `09` ยง3 decision 185 (D1). See opactions.go. // The list is CLOSED, on the hub side too โ€” this fails when an action or a job is added, by design. The // controller pins the same four (internal/report TestOpActions_ClosedList). func TestOperatorActions_ClosedList(t *testing.T) { if got, want := OperatorActionNames(), []string{"abandon_extend", "abandon_stop", "offsite_backup_now", "run_job"}; !reflect.DeepEqual(got, want) { t.Fatalf("operator actions = %v, want exactly %v โ€” a new action needs the operator's word (decision 185)", got, want) } if got, want := OperatorJobNames(), []string{"disk-health-check", "fill-watch", "offsite-integrity", "offsite-proof"}; !reflect.DeepEqual(got, want) { t.Fatalf("run_job names = %v, want exactly %v", got, want) } } func TestOperatorActions_RefusedBeforeStoring(t *testing.T) { s := newTestStore(t) bad := []struct{ action, arg string }{ {"delete_everything", ""}, {"run_job", "offsite-abandon-sweep"}, {"run_job", ""}, {"abandon_extend", "0"}, {"abandon_extend", "31"}, {"abandon_extend", "x"}, {"abandon_stop", "1"}, {"offsite_backup_now", "now"}, } for _, b := range bad { if _, err := s.CreateOperatorAction("c1", b.action, b.arg, "operator"); err == nil { t.Errorf("%s(%q) was accepted", b.action, b.arg) } } if rows, _ := s.ListOperatorActions("c1", 50); len(rows) != 0 { t.Fatalf("a refused action stored %d row(s)", len(rows)) } for _, ok := range []struct{ action, arg string }{{"offsite_backup_now", ""}, {"abandon_stop", ""}, {"abandon_extend", "1"}, {"abandon_extend", "30"}, {"run_job", "fill-watch"}} { if _, err := s.CreateOperatorAction("c1", ok.action, ok.arg, "operator"); err != nil { t.Errorf("%s(%q) refused: %v", ok.action, ok.arg, err) } } } // Red test (4), store half: listed until a result arrives, then not; a result naming another // customer's id changes nothing. func TestOperatorActions_ListedUntilResult_OtherCustomerIgnored(t *testing.T) { s := newTestStore(t) id, err := s.CreateOperatorAction("c1", "run_job", "fill-watch", "operator session from 10.0.0.1") if err != nil { t.Fatal(err) } p, _ := s.PendingOperatorActions("c1") if len(p) != 1 || p[0] != (OperatorActionDirective{ID: id, Action: "run_job", Arg: "fill-watch"}) { t.Fatalf("pending = %+v", p) } if p2, _ := s.PendingOperatorActions("c2"); len(p2) != 0 { t.Fatalf("another customer sees c1's action: %+v", p2) } // c2 reports a result for c1's id โ†’ ignored. if row, err := s.RecordOperatorActionResult("c2", id, "done", "x"); err != nil || row != nil { t.Fatalf("cross-customer result recorded: row=%+v err=%v", row, err) } if p, _ := s.PendingOperatorActions("c1"); len(p) != 1 { t.Fatal("a cross-customer result closed the row") } if _, err := s.RecordOperatorActionResult("c1", id, "exploded", "x"); err == nil { t.Fatal("an unknown outcome was accepted") } row, err := s.RecordOperatorActionResult("c1", id, "done", "the job fill-watch ran") if err != nil || row == nil || row.Outcome != "done" || row.DoneAt == nil || row.RequestedBy != "operator session from 10.0.0.1" { t.Fatalf("record: row=%+v err=%v", row, err) } if p, _ := s.PendingOperatorActions("c1"); len(p) != 0 { t.Fatalf("still listed after its result: %+v", p) } // A repeated result (the box re-sends until it sees the id gone) is a no-op. if row, _ := s.RecordOperatorActionResult("c1", id, "failed", "late"); row != nil { t.Fatal("a second result overwrote the first") } } func TestOperatorActions_ExpireAndResetCancel(t *testing.T) { s := newTestStore(t) old, _ := s.CreateOperatorAction("c1", "offsite_backup_now", "", "operator") if _, err := s.db.Exec(`UPDATE operator_actions SET requested_at = ? WHERE id = ?`, time.Now().UTC().Add(-25*time.Hour), old); err != nil { t.Fatal(err) } fresh, _ := s.CreateOperatorAction("c1", "abandon_stop", "", "operator") p, _ := s.PendingOperatorActions("c1") if len(p) != 1 || p[0].ID != fresh { t.Fatalf("a day-old action is still listed: %+v", p) } if r, _ := s.getOperatorAction(old); r == nil || r.Outcome != OperatorActionExpired { t.Fatalf("old row = %+v, want expired", r) } if err := s.PurgeCustomerResetDBState("c1", false); err != nil { t.Fatal(err) } if p, _ := s.PendingOperatorActions("c1"); len(p) != 0 { t.Fatalf("a RESET left a pending action for the next box: %+v", p) } if r, _ := s.getOperatorAction(fresh); r == nil || r.Outcome != OperatorActionCancelled { t.Fatalf("fresh row after reset = %+v, want cancelled (kept as a record)", r) } }