package alertapi import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/itworx/pulse/internal/alert" "github.com/itworx/pulse/internal/audit" "github.com/itworx/pulse/internal/auth" "github.com/itworx/pulse/internal/metriccatalog" ) type fakeStore struct { rule alert.Rule getCalls, createCalls, updateCalls, toggleCalls int } func (f *fakeStore) Create(_ context.Context, _ string, document alert.Document, _ string) (alert.Rule, alert.Version, error) { f.createCalls++ f.rule = alert.Rule{Document: document, Revision: 1, CurrentVersion: 1} return f.rule, alert.Version{RuleID: document.ID, VersionNumber: 1, Document: document}, nil } func (f *fakeStore) Get(_ context.Context, _ string) (alert.Rule, error) { f.getCalls++ return f.rule, nil } func (f *fakeStore) List(_ context.Context, _ int) ([]alert.Rule, error) { return []alert.Rule{f.rule}, nil } func (f *fakeStore) Update(_ context.Context, id, _ string, _ int64, document alert.Document, _ string) (alert.Rule, error) { f.updateCalls++ document.ID = id f.rule.Document = document f.rule.Revision++ return f.rule, nil } func (f *fakeStore) Versions(_ context.Context, _ string, _ int) ([]alert.Version, error) { return []alert.Version{{Document: f.rule.Document, VersionNumber: 1}}, nil } func (f *fakeStore) SetEnabled(_ context.Context, _ string, _ int64, enabled bool) (alert.Rule, error) { f.toggleCalls++ f.rule.Enabled = enabled f.rule.Document.Enabled = enabled f.rule.Revision++ return f.rule, nil } func requestWithPrincipal(method, path string, body any, role auth.Role) *http.Request { data, _ := json.Marshal(body) request := httptest.NewRequest(method, path, strings.NewReader(string(data))) request.Header.Set("Content-Type", "application/json") return request.WithContext(auth.WithPrincipal(request.Context(), auth.Principal{Subject: "editor-1", Role: role})) } func TestViewerCannotCreateAlertRule(t *testing.T) { document, registry := validDocumentForHandler(t) store := &fakeStore{} handler := Handler{Repository: store, Registry: registry} response := httptest.NewRecorder() handler.ServeHTTP(response, requestWithPrincipal(http.MethodPost, "/api/v1/alert-rules", document, auth.RoleViewer)) if response.Code != http.StatusForbidden || store.createCalls != 0 { t.Fatalf("status=%d creates=%d", response.Code, store.createCalls) } } func TestPreviewDoesNotWriteOrAudit(t *testing.T) { document, registry := validDocumentForHandler(t) store := &fakeStore{} auditStore := &audit.MemoryStore{} handler := Handler{Repository: store, Registry: registry, Audit: auditStore} response := httptest.NewRecorder() handler.ServeHTTP(response, requestWithPrincipal(http.MethodPost, "/api/v1/alert-rules/"+document.ID+"/test", map[string]any{"rule": document, "value": 90}, auth.RoleEditor)) if response.Code != http.StatusOK { t.Fatalf("preview status=%d body=%s", response.Code, response.Body.String()) } if store.createCalls != 0 || store.updateCalls != 0 || store.toggleCalls != 0 || len(auditStore.Events) != 0 { t.Fatalf("preview had side effects: store=%#v audit=%d", store, len(auditStore.Events)) } var body map[string]any if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["preview"] == nil { t.Fatal("preview result missing") } } func TestEnableIsAudited(t *testing.T) { document, registry := validDocumentForHandler(t) store := &fakeStore{rule: alert.Rule{Document: document, Revision: 1}} auditStore := &audit.MemoryStore{} handler := Handler{Repository: store, Registry: registry, Audit: auditStore} request := requestWithPrincipal(http.MethodPost, "/api/v1/alert-rules/"+document.ID+"/enable?revision=1", map[string]any{}, auth.RoleEditor) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || store.toggleCalls != 1 { t.Fatalf("status=%d toggles=%d", response.Code, store.toggleCalls) } if len(auditStore.Events) != 1 || auditStore.Events[0].Action != "alert_rule.enable" { t.Fatalf("audit=%#v", auditStore.Events) } } func validDocumentForHandler(t *testing.T) (alert.Document, metriccatalog.Registry) { t.Helper() registry, err := metriccatalog.DefaultRegistry() if err != nil { t.Fatal(err) } return alert.Document{ SchemaVersion: 1, ID: alert.NewID(), Name: "CPU aandacht", Severity: alert.SeverityAttention, Scope: map[string]any{"entityType": "host"}, Condition: alert.Condition{InputType: "metric", Metric: registry.Metrics()[0].SemanticName, Operator: ">", Threshold: float64(80)}, EvaluationIntervalSeconds: 30, UnknownBehavior: alert.UnknownRetain, Message: alert.Message{TitleKey: "alerts.cpu.title", BodyKey: "alerts.cpu.body"}, }, registry }