hub v0.31.0: accept 'critical' severity at event ingest + UI badges/CSS; event_test.go (red-proofed); REUSE.md §1/§3 updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-03 11:08:44 +02:00
parent d331eb26d1
commit b5f00509ee
9 changed files with 141 additions and 12 deletions
+92
View File
@@ -0,0 +1,92 @@
package api
import (
"net/http"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
func eventBody(severity, eventType string) string {
return `{"customer_id":"c1","event_type":"` + eventType + `","severity":"` + severity + `","message":"probe"}`
}
func newEventTestHandler(t *testing.T) (*Handler, *store.Store) {
t.Helper()
h, st, _ := newTestHandler(t)
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "p"}); err != nil {
t.Fatalf("SaveCustomerConfig: %v", err)
}
return h, st
}
// Scenario A: a controller-POSTed "critical" event must be stored as "critical",
// not coerced to "info" (pre-v0.31.0 bug: the severity switch omitted "critical",
// so critical events silently became info and never notified).
func TestHandleEvent_CriticalPreserved(t *testing.T) {
h, st := newEventTestHandler(t)
rr := do(h, http.MethodPost, "/event", "ckey", eventBody("critical", "test"))
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rr.Code, rr.Body.String())
}
evs, err := st.GetRecentEvents("c1", 10)
if err != nil {
t.Fatalf("GetRecentEvents: %v", err)
}
if len(evs) != 1 {
t.Fatalf("stored events = %d, want 1", len(evs))
}
if evs[0].Severity != "critical" {
t.Errorf("stored severity = %q, want %q (critical must survive ingest)", evs[0].Severity, "critical")
}
}
// Scenario B: unknown severities still coerce to "info" — the coercion contract is
// exact-match lowercase; only "critical" joined the known set.
func TestHandleEvent_UnknownSeverityCoercesToInfo(t *testing.T) {
h, st := newEventTestHandler(t)
rr := do(h, http.MethodPost, "/event", "ckey", eventBody("banana", "test"))
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rr.Code, rr.Body.String())
}
evs, err := st.GetRecentEvents("c1", 10)
if err != nil {
t.Fatalf("GetRecentEvents: %v", err)
}
if len(evs) != 1 || evs[0].Severity != "info" {
t.Errorf("stored = %+v, want exactly one event with severity info", evs)
}
}
// Scenario C: an event_type outside allowedEventTypes is rejected with 400 and
// nothing is stored (locks the allowlist behavior).
func TestHandleEvent_UnknownEventTypeRejected(t *testing.T) {
h, st := newEventTestHandler(t)
rr := do(h, http.MethodPost, "/event", "ckey", eventBody("error", "not-a-real-type"))
if rr.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400; body=%s", rr.Code, rr.Body.String())
}
evs, err := st.GetRecentEvents("c1", 10)
if err != nil {
t.Fatalf("GetRecentEvents: %v", err)
}
if len(evs) != 0 {
t.Errorf("stored events = %d, want 0 after a rejected event_type", len(evs))
}
}
// Auth: no bearer at all is a 401 and stores nothing.
func TestHandleEvent_Unauthorized(t *testing.T) {
h, st := newEventTestHandler(t)
rr := do(h, http.MethodPost, "/event", "", eventBody("critical", "test"))
if rr.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401", rr.Code)
}
if evs, _ := st.GetRecentEvents("c1", 10); len(evs) != 0 {
t.Errorf("stored events = %d, want 0 without auth", len(evs))
}
}
+2 -2
View File
@@ -1153,9 +1153,9 @@ func (h *Handler) handleEvent(w http.ResponseWriter, r *http.Request) {
return
}
// Validate/default severity
// Validate/default severity (exact-match lowercase; unknown values coerce to info)
switch payload.Severity {
case "info", "warning", "error":
case "info", "warning", "error", "critical":
default:
payload.Severity = "info"
}