Files
felhom.eu/hub/internal/api/deliberate_off_test.go
T
admin a6acb0f762
gates / gates (push) Successful in 6m27s
Hub v0.149.0: clamp a future deliberate-off time; hold a customer only when all its hosts are off (R-998)
Security review of 50e47e29: requested_at is the box's clock — a future value is cut to the hub's
now; CustomerDeliberatelyOff now needs every host of the customer held (one machine of two no
longer silences the other), and fails closed on a read error or no host. Both red-proofed.
2026-10-13 19:52:12 +02:00

61 lines
2.6 KiB
Go

package api
import (
"net/http"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// R-998: the agent's deliberate_off field sets the host row's hold; the next report without it (a new boot — the box
// was switched back on) clears it. Each change leaves one info event on the customer's list; a repeat leaves none.
func TestHostReport_DeliberateOffSetThenCleared(t *testing.T) {
h, st, _ := newTestHandler(t)
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "p"})
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})
when := time.Now().UTC().Add(-time.Hour).Truncate(time.Second)
withOff := strings.TrimSuffix(validReportBody("h1"), "}") + `,"deliberate_off":{"requested_at":"` + when.Format(time.RFC3339) + `"}}`
for i := 0; i < 2; i++ { // the field rides every report until the box is down; the second must not re-record
if rr := do(h, http.MethodPost, "/host-report", "HKEY", withOff); rr.Code != 200 {
t.Fatalf("report %d: %d %s", i, rr.Code, rr.Body.String())
}
}
at := st.HostDeliberateOffAt("h1")
if at == nil || !at.Equal(when) {
t.Fatalf("hold = %v, want %v", at, when)
}
evs, _ := st.GetEventsByType("c1", eventHostDeliberateOff, time.Now().Add(-time.Hour))
if len(evs) != 1 || evs[0].Severity != "info" {
t.Fatalf("set events = %+v, want exactly one info event", evs)
}
if rr := do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1")); rr.Code != 200 {
t.Fatalf("plain report: %d", rr.Code)
}
if st.HostDeliberateOffAt("h1") != nil {
t.Fatal("a report without the field must clear the hold")
}
ended, _ := st.GetEventsByType("c1", eventHostDeliberateOffEnded, time.Now().Add(-time.Hour))
if len(ended) != 1 {
t.Fatalf("ended events = %d, want 1", len(ended))
}
}
// A future requested_at (the box's clock) must not stretch the hold: it is cut to the hub's now.
func TestHostReport_DeliberateOffFutureTimeClamped(t *testing.T) {
h, st, _ := newTestHandler(t)
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "p"})
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})
body := strings.TrimSuffix(validReportBody("h1"), "}") + `,"deliberate_off":{"requested_at":"2099-01-01T00:00:00Z"}}`
if rr := do(h, http.MethodPost, "/host-report", "HKEY", body); rr.Code != 200 {
t.Fatalf("report: %d", rr.Code)
}
at := st.HostDeliberateOffAt("h1")
if at == nil || at.After(time.Now().Add(time.Minute)) {
t.Fatalf("hold anchored at %v — a future box time must be cut to the hub's now", at)
}
}