package api import ( "encoding/json" "fmt" "net/http" "strings" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // `09` §3 decision 185 (D1), the design's red test (4), wire half: the report reply lists a pending // operator action until a result arrives, then not; a result naming another customer's id is ignored; // a closed result becomes a hub-minted event. func ackActions(t *testing.T, h *Handler, customerID, extra string) []store.OperatorActionDirective { t.Helper() body := `{"customer_id":"` + customerID + `"` + extra + `}` rr := do(h, http.MethodPost, "/report", globalKey, body) if rr.Code != http.StatusOK { t.Fatalf("report: %d %s", rr.Code, rr.Body.String()) } var ack struct { OperatorActions []store.OperatorActionDirective `json:"operator_actions"` } if err := json.Unmarshal(rr.Body.Bytes(), &ack); err != nil { t.Fatal(err) } return ack.OperatorActions } func TestReportACK_OperatorActionsUntilResult(t *testing.T) { h, st, _ := newTestHandler(t) id, err := st.CreateOperatorAction("c1", "offsite_backup_now", "", "operator browser session from 10.0.0.1") if err != nil { t.Fatal(err) } // Listed to c1, not to c2. if got := ackActions(t, h, "c1", ""); len(got) != 1 || got[0].ID != id || got[0].Action != "offsite_backup_now" { t.Fatalf("c1 ACK = %+v", got) } if got := ackActions(t, h, "c2", ""); len(got) != 0 { t.Fatalf("c2 sees c1's action: %+v", got) } // c2 reports a result for c1's id: ignored — c1's ACK still lists it, and no event is saved. res := fmt.Sprintf(`,"operator_action_results":[{"id":%d,"outcome":"done","message":"forged"}]`, id) ackActions(t, h, "c2", res) if got := ackActions(t, h, "c1", ""); len(got) != 1 { t.Fatalf("a cross-customer result closed c1's action: %+v", got) } if evs, _ := st.GetRecentEvents("c2", 10); len(evs) != 0 { t.Fatalf("a cross-customer result minted an event: %+v", evs) } // c1's own result: closed, and THE SAME reply no longer lists it. res = fmt.Sprintf(`,"operator_action_results":[{"id":%d,"outcome":"done","message":"the off-site backup finished"}]`, id) if got := ackActions(t, h, "c1", res); len(got) != 0 { t.Fatalf("still listed in the reply to its own result: %+v", got) } evs, _ := st.GetRecentEvents("c1", 10) if len(evs) != 1 || evs[0].EventType != "operator_action" || evs[0].Source != "hub" || !strings.Contains(evs[0].Message, "offsite_backup_now") || !strings.Contains(evs[0].Message, "10.0.0.1") { t.Fatalf("events = %+v", evs) } // The box re-sends until it sees the id gone: a no-op, no second event. ackActions(t, h, "c1", res) if evs, _ := st.GetRecentEvents("c1", 10); len(evs) != 1 { t.Fatalf("a re-sent result minted %d events", len(evs)) } }