87 lines
3.4 KiB
Go
87 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
|
)
|
|
|
|
// Operator brief 2026-10-29, Part 1: the AGENT's operator actions ride the host-report reply as `operator_actions`
|
|
// (the controller's shape) and its answer rides the next host report as `operator_action_results`; the row closes and
|
|
// one `operator_action` event is stored. A controller row never reaches the agent. RED-PROOF: seen failing before the
|
|
// reply listed the agent rows (no `operator_actions` key).
|
|
func TestHostReport_AgentOperatorActionRoundTrip(t *testing.T) {
|
|
h, st, _ := newTestHandler(t)
|
|
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})
|
|
if _, err := st.CreateOperatorAction("c1", "clear_hold", "radicale", "operator"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, err := st.CreateHostOperatorAction("c1", "h1", "restart_controller", "9201", "operator browser session from 192.168.0.5")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rr := do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1"))
|
|
var env struct {
|
|
OperatorActions []store.OperatorActionDirective `json:"operator_actions"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &env); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(env.OperatorActions) != 1 || env.OperatorActions[0].ID != id || env.OperatorActions[0].Action != "restart_controller" ||
|
|
env.OperatorActions[0].Arg != "9201" {
|
|
t.Fatalf("the agent's reply lists %+v, want only the restart #%d (never the controller's clear_hold)", env.OperatorActions, id)
|
|
}
|
|
body := strings.Replace(validReportBody("h1"), `"audit_tail":[]`,
|
|
`"audit_tail":[],"operator_action_results":[{"id":`+i64s(id)+`,"outcome":"done","message":"the controller answered after 14 s"}]`, 1)
|
|
if rr := do(h, http.MethodPost, "/host-report", "HKEY", body); rr.Code != http.StatusOK {
|
|
t.Fatalf("result heartbeat = %d %s", rr.Code, rr.Body.String())
|
|
}
|
|
if left, _ := st.PendingHostOperatorActions("h1"); len(left) != 0 {
|
|
t.Fatalf("the row is still pending: %+v", left)
|
|
}
|
|
rows, _ := st.ListOperatorActions("c1", 10)
|
|
var got *store.OperatorActionRow
|
|
for i := range rows {
|
|
if rows[i].ID == id {
|
|
got = &rows[i]
|
|
}
|
|
}
|
|
if got == nil || got.Outcome != "done" || got.Message != "the controller answered after 14 s" {
|
|
t.Fatalf("row = %+v", got)
|
|
}
|
|
evs, _ := st.GetEventsByType("c1", "operator_action", time.Time{})
|
|
n := 0
|
|
for _, e := range evs {
|
|
if e.EventType == "operator_action" && strings.Contains(e.Message, "restart_controller 9201") {
|
|
n++
|
|
}
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("operator_action events for the restart = %d, want 1: %+v", n, evs)
|
|
}
|
|
// once answered, the reply stops listing it
|
|
rr = do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1"))
|
|
env.OperatorActions = nil
|
|
_ = json.Unmarshal(rr.Body.Bytes(), &env)
|
|
if len(env.OperatorActions) != 0 {
|
|
t.Fatalf("still listed after its result: %+v", env.OperatorActions)
|
|
}
|
|
}
|
|
|
|
func i64s(n int64) string { b, _ := json.Marshal(n); return string(b) }
|
|
|
|
// R-1344: the hub-generated controller_unresponsive is listed (the crash-loop pair's rule) and operator-only.
|
|
func TestControllerUnresponsive_RegisteredAndOperatorOnly(t *testing.T) {
|
|
if !allowedEventTypes["controller_unresponsive"] {
|
|
t.Error("controller_unresponsive is not in allowedEventTypes")
|
|
}
|
|
if !notify.IsOperatorOnly("controller_unresponsive") {
|
|
t.Error("controller_unresponsive is not operator-only")
|
|
}
|
|
}
|