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())
}
}