package incidentapi import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/itworx/pulse/internal/audit" "github.com/itworx/pulse/internal/auth" "github.com/itworx/pulse/internal/incident" ) type memoryStore struct { item incident.Incident association incident.Association associations int } func (s *memoryStore) List(context.Context, int, incident.Status) ([]incident.Incident, error) { return []incident.Incident{s.item}, nil } func (s *memoryStore) Get(context.Context, string) (incident.Incident, error) { return s.item, nil } func (s *memoryStore) UpsertCandidate(context.Context, incident.Candidate) (incident.Incident, bool, error) { return s.item, false, nil } func (s *memoryStore) AssociateAlert(_ context.Context, _, id, rationale string, confidence float64, actor string) (incident.Association, bool, error) { s.associations++ s.association = incident.Association{AlertID: id, Rationale: rationale, Confidence: confidence, CorrelationMethod: "manual", Manual: true, AddedBy: actor} return s.association, s.associations > 1, nil } func (s *memoryStore) DisassociateAlert(context.Context, string, string) error { return nil } func (s *memoryStore) UpdateStatus(context.Context, string, incident.Status, int64, time.Time) (incident.Incident, error) { return s.item, nil } func requestWithPrincipal(method, path, body string, role auth.Role) *http.Request { return httptest.NewRequest(method, path, strings.NewReader(body)).WithContext(auth.WithPrincipal(context.Background(), auth.Principal{Subject: "operator-1", Role: role})) } func TestIncidentHandlerRBACAndManualAssociationAudit(t *testing.T) { store := &memoryStore{item: incident.Incident{ID: "incident-1", Title: "Host down", Status: incident.StatusOpen, Severity: incident.SeverityCritical, Revision: 1}} auditStore := &audit.MemoryStore{} handler := Handler{Store: store, Audit: auditStore} request := requestWithPrincipal(http.MethodPost, "/api/v1/incidents/incident-1/alerts/alert-1", `{"rationale":"confirmed dependency","confidence":1}`, auth.RoleViewer) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusForbidden { t.Fatalf("viewer status=%d", response.Code) } request = requestWithPrincipal(http.MethodPost, "/api/v1/incidents/incident-1/alerts/alert-1", `{"rationale":"confirmed dependency","confidence":1}`, auth.RoleOperator) response = httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"manual":true`) { t.Fatalf("association status=%d body=%s", response.Code, response.Body.String()) } if len(auditStore.Events) != 1 || auditStore.Events[0].Action != "incident.alert.associate" { t.Fatalf("audit=%+v", auditStore.Events) } request = requestWithPrincipal(http.MethodGet, "/api/v1/incidents?status=open", ``, auth.RoleViewer) response = httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("list status=%d", response.Code) } } func (s *memoryStore) UpdateOwner(_ context.Context, _ string, owner string, _ int64) (incident.Incident, error) { s.item.OwnerUserID = owner s.item.Revision++ return s.item, nil } func (s *memoryStore) AddNote(_ context.Context, id, author, body string) (incident.Note, error) { sanitized, err := incident.SanitizeNote(body) if err != nil { return incident.Note{}, err } return incident.Note{ID: "note-1", IncidentID: id, Author: author, Body: sanitized, CreatedAt: time.Now().UTC()}, nil } func (s *memoryStore) ListNotes(context.Context, string, int) ([]incident.Note, error) { return nil, nil } func TestIncidentHandlerNotesAndOwner(t *testing.T) { store := &memoryStore{item: incident.Incident{ID: "incident-1", Title: "Host down", Status: incident.StatusOpen, Severity: incident.SeverityCritical, Revision: 1}} handler := Handler{Store: store, Audit: &audit.MemoryStore{}} request := requestWithPrincipal(http.MethodPost, "/api/v1/incidents/incident-1/notes", `{"body":"confirmed"}`, auth.RoleViewer) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusForbidden { t.Fatalf("viewer note status=%d", response.Code) } request = requestWithPrincipal(http.MethodPost, "/api/v1/incidents/incident-1/notes", `{"body":"confirmed"}`, auth.RoleOperator) response = httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusCreated || strings.Contains(response.Body.String(), "") || !strings.Contains(response.Body.String(), "confirmed") { t.Fatalf("note status=%d body=%s", response.Code, response.Body.String()) } request = requestWithPrincipal(http.MethodPatch, "/api/v1/incidents/incident-1", `{"ownerUserId":"owner-1","revision":1}`, auth.RoleOperator) response = httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "owner-1") { t.Fatalf("owner status=%d body=%s", response.Code, response.Body.String()) } }