mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
966 lines
33 KiB
Go
966 lines
33 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/memory"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
testifymock "github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
// Mock implementations for testing
|
|
type MockAlertManager struct {
|
|
testifymock.Mock
|
|
}
|
|
|
|
func (m *MockAlertManager) GetConfig() alerts.AlertConfig {
|
|
args := m.Called()
|
|
return args.Get(0).(alerts.AlertConfig)
|
|
}
|
|
|
|
func TestExportIncident_UsesCanonicalEmptyCollections(t *testing.T) {
|
|
openedAt := time.Unix(1700000000, 0).UTC()
|
|
view := exportIncident(&memory.Incident{
|
|
ID: "inc-1",
|
|
AlertIdentifier: "canonical:inc-1",
|
|
AlertType: "cpu",
|
|
Level: "warning",
|
|
ResourceID: "node/pve-1",
|
|
ResourceName: "pve-1",
|
|
Status: memory.IncidentStatusOpen,
|
|
OpenedAt: openedAt,
|
|
Events: []memory.IncidentEvent{
|
|
{
|
|
ID: "evt-1",
|
|
Type: memory.IncidentEventAlertFired,
|
|
Timestamp: openedAt,
|
|
Summary: "Alert fired",
|
|
},
|
|
},
|
|
})
|
|
|
|
payload, err := json.Marshal(view)
|
|
if err != nil {
|
|
t.Fatalf("marshal exported incident: %v", err)
|
|
}
|
|
if !strings.Contains(string(payload), `"events":[`) {
|
|
t.Fatalf("expected exported incident to retain events array, got %s", payload)
|
|
}
|
|
if !strings.Contains(string(payload), `"details":{}`) {
|
|
t.Fatalf("expected exported incident event to retain empty details map, got %s", payload)
|
|
}
|
|
}
|
|
|
|
func (m *MockAlertManager) UpdateConfig(cfg alerts.AlertConfig) {
|
|
m.Called(cfg)
|
|
}
|
|
|
|
func (m *MockAlertManager) GetActiveAlerts() []alerts.Alert {
|
|
args := m.Called()
|
|
return args.Get(0).([]alerts.Alert)
|
|
}
|
|
|
|
func (m *MockAlertManager) NotifyExistingAlert(id string) {
|
|
m.Called(id)
|
|
}
|
|
|
|
func (m *MockAlertManager) ClearAlertHistory() error {
|
|
args := m.Called()
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockAlertManager) UnacknowledgeAlert(id string) error {
|
|
args := m.Called(id)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockAlertManager) AcknowledgeAlert(id, user string) error {
|
|
args := m.Called(id, user)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockAlertManager) ClearAlert(id string) bool {
|
|
args := m.Called(id)
|
|
return args.Bool(0)
|
|
}
|
|
|
|
func (m *MockAlertManager) GetAlertHistory(limit int) []alerts.Alert {
|
|
args := m.Called(limit)
|
|
return args.Get(0).([]alerts.Alert)
|
|
}
|
|
|
|
func (m *MockAlertManager) GetAlertHistorySince(since time.Time, limit int) []alerts.Alert {
|
|
args := m.Called(since, limit)
|
|
return args.Get(0).([]alerts.Alert)
|
|
}
|
|
|
|
type MockAlertMonitor struct {
|
|
testifymock.Mock
|
|
}
|
|
|
|
func (m *MockAlertMonitor) GetAlertManager() AlertManager {
|
|
args := m.Called()
|
|
return args.Get(0).(AlertManager)
|
|
}
|
|
|
|
func (m *MockAlertMonitor) GetConfigPersistence() ConfigPersistence {
|
|
args := m.Called()
|
|
return args.Get(0).(ConfigPersistence)
|
|
}
|
|
|
|
func (m *MockAlertMonitor) GetIncidentStore() *memory.IncidentStore {
|
|
args := m.Called()
|
|
if store := args.Get(0); store != nil {
|
|
return store.(*memory.IncidentStore)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockAlertMonitor) GetNotificationManager() *notifications.NotificationManager {
|
|
args := m.Called()
|
|
return args.Get(0).(*notifications.NotificationManager)
|
|
}
|
|
|
|
func (m *MockAlertMonitor) SyncAlertState() {
|
|
m.Called()
|
|
}
|
|
|
|
func (m *MockAlertMonitor) BuildFrontendState() models.StateFrontend {
|
|
args := m.Called()
|
|
return args.Get(0).(models.StateFrontend)
|
|
}
|
|
|
|
type MockConfigPersistence struct {
|
|
testifymock.Mock
|
|
}
|
|
|
|
func (m *MockConfigPersistence) SaveAlertConfig(cfg alerts.AlertConfig) error {
|
|
args := m.Called(cfg)
|
|
return args.Error(0)
|
|
}
|
|
|
|
// Tests
|
|
func TestGetAlertConfig(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
cfg := alerts.AlertConfig{Enabled: true}
|
|
mockManager.On("GetConfig").Return(cfg)
|
|
|
|
req := httptest.NewRequest("GET", "/api/alerts/config", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.GetAlertConfig(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
var resp alerts.AlertConfig
|
|
_ = json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.True(t, resp.Enabled)
|
|
}
|
|
|
|
func TestUpdateAlertConfig(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockPersist := new(MockConfigPersistence)
|
|
notificationMgr := notifications.NewNotificationManagerWithDataDir("", t.TempDir())
|
|
defer notificationMgr.Stop()
|
|
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("GetConfigPersistence").Return(mockPersist)
|
|
mockMonitor.On("GetNotificationManager").Return(notificationMgr)
|
|
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
cfg := alerts.AlertConfig{Enabled: true, ActivationState: alerts.ActivationPending}
|
|
mockManager.On("UpdateConfig", testifymock.Anything).Return()
|
|
mockManager.On("GetConfig").Return(cfg)
|
|
mockPersist.On("SaveAlertConfig", testifymock.Anything).Return(nil)
|
|
|
|
body, _ := json.Marshal(cfg)
|
|
req := httptest.NewRequest("POST", "/api/alerts/config", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.UpdateAlertConfig(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
assert.False(t, notificationMgr.IsEnabled())
|
|
}
|
|
|
|
func TestActivateAlerts_EnablesNotificationManager(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockPersist := new(MockConfigPersistence)
|
|
notificationMgr := notifications.NewNotificationManagerWithDataDir("", t.TempDir())
|
|
defer notificationMgr.Stop()
|
|
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("GetConfigPersistence").Return(mockPersist)
|
|
mockMonitor.On("GetNotificationManager").Return(notificationMgr)
|
|
|
|
initialConfig := alerts.AlertConfig{Enabled: true, ActivationState: alerts.ActivationPending}
|
|
updatedConfig := alerts.AlertConfig{Enabled: true, ActivationState: alerts.ActivationActive}
|
|
mockManager.On("GetConfig").Return(initialConfig).Once()
|
|
mockManager.On("UpdateConfig", testifymock.AnythingOfType("alerts.AlertConfig")).Run(func(args testifymock.Arguments) {
|
|
cfg := args.Get(0).(alerts.AlertConfig)
|
|
if cfg.ActivationState != alerts.ActivationActive {
|
|
t.Fatalf("expected activation state to be active, got %q", cfg.ActivationState)
|
|
}
|
|
}).Return()
|
|
mockPersist.On("SaveAlertConfig", testifymock.Anything).Return(nil)
|
|
mockManager.On("GetActiveAlerts").Return([]alerts.Alert{}).Maybe()
|
|
mockManager.On("GetConfig").Return(updatedConfig)
|
|
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
req := httptest.NewRequest("POST", "/api/alerts/activate", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.ActivateAlerts(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
assert.True(t, notificationMgr.IsEnabled())
|
|
}
|
|
|
|
func TestGetActiveAlerts(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("GetActiveAlerts").Return([]alerts.Alert{{ID: "a1"}})
|
|
|
|
req := httptest.NewRequest("GET", "/api/alerts/active", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.GetActiveAlerts(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
var resp []alerts.Alert
|
|
_ = json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.Len(t, resp, 1)
|
|
assert.Equal(t, "a1", resp[0].ID)
|
|
}
|
|
|
|
func TestValidateAlertIdentifier(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
id string
|
|
valid bool
|
|
}{
|
|
{name: "basic", id: "guest-powered-off-pve-101", valid: true},
|
|
{name: "with spaces", id: "cluster one-node-101-cpu", valid: true},
|
|
{name: "with slash and colon", id: "pve1:qemu/101-cpu", valid: true},
|
|
{name: "empty", id: "", valid: false},
|
|
{name: "too long", id: string(make([]byte, 501)), valid: false},
|
|
{name: "control char", id: "bad\nvalue", valid: false},
|
|
{name: "path traversal", id: "../etc/passwd", valid: false},
|
|
{name: "path traversal middle", id: "pve/../secret", valid: false},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
if got := validateAlertIdentifier(tc.id); got != tc.valid {
|
|
t.Errorf("validateAlertIdentifier(%s) = %v, want %v", tc.name, got, tc.valid)
|
|
}
|
|
}
|
|
}
|
|
func TestAlertHandlers_SetMonitor(t *testing.T) {
|
|
mockMonitor1 := new(MockAlertMonitor)
|
|
mockMonitor2 := new(MockAlertMonitor)
|
|
h := NewAlertHandlers(nil, mockMonitor1, nil)
|
|
assert.Equal(t, mockMonitor1, h.defaultMonitor)
|
|
h.SetMonitor(mockMonitor2)
|
|
assert.Equal(t, mockMonitor2, h.defaultMonitor)
|
|
}
|
|
|
|
func TestAlertHandlers_SetMonitorConcurrentAccess(t *testing.T) {
|
|
mockMonitor1 := new(MockAlertMonitor)
|
|
mockMonitor2 := new(MockAlertMonitor)
|
|
h := NewAlertHandlers(nil, mockMonitor1, nil)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 8; i++ {
|
|
wg.Add(1)
|
|
go func(worker int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 500; j++ {
|
|
if (worker+j)%2 == 0 {
|
|
h.SetMonitor(mockMonitor1)
|
|
} else {
|
|
h.SetMonitor(mockMonitor2)
|
|
}
|
|
_ = h.getMonitor(context.Background())
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
assert.NotNil(t, h.getMonitor(context.Background()))
|
|
}
|
|
|
|
func TestGetAlertHistory(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("GetAlertHistory", testifymock.Anything).Return([]alerts.Alert{{ID: "h1"}})
|
|
|
|
req := httptest.NewRequest("GET", "/api/alerts/history?limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
h.GetAlertHistory(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
var resp []alerts.Alert
|
|
_ = json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.Len(t, resp, 1)
|
|
}
|
|
|
|
func TestClearAlertHistory(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("ClearAlertHistory").Return(nil).Once()
|
|
|
|
req := httptest.NewRequest("POST", "/api/alerts/history/clear", nil)
|
|
w := httptest.NewRecorder()
|
|
h.ClearAlertHistory(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestSaveAlertIncidentNote(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
// Create an incident first so RecordNote has something to attach to
|
|
alert := &alerts.Alert{ID: "a1", Type: "test"}
|
|
mockStore.RecordAlertFired(alert)
|
|
|
|
body := `{"alertIdentifier": "a1", "note": "test note", "user": "admin"}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/note", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.SaveAlertIncidentNote(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestGetAlertIncidentTimeline_ExportsCanonicalAlertIdentifier(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
alert := &alerts.Alert{
|
|
ID: "canonical:a1",
|
|
Type: "cpu",
|
|
Level: alerts.AlertLevelWarning,
|
|
ResourceID: "resource-1",
|
|
ResourceName: "resource-1",
|
|
Message: "test",
|
|
StartTime: time.Now(),
|
|
}
|
|
mockStore.RecordAlertFired(alert)
|
|
|
|
req := httptest.NewRequest("GET", "/api/alerts/incidents?alertIdentifier=canonical:a1", nil)
|
|
w := httptest.NewRecorder()
|
|
h.GetAlertIncidentTimeline(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
var incident map[string]interface{}
|
|
_ = json.NewDecoder(w.Body).Decode(&incident)
|
|
assert.Equal(t, "canonical:a1", incident["alertIdentifier"])
|
|
}
|
|
|
|
func TestGetAlertIncidentTimeline_AcceptsCamelCaseAlertIdentifierQuery(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
alert := &alerts.Alert{
|
|
ID: "canonical:a1",
|
|
Type: "cpu",
|
|
Level: alerts.AlertLevelWarning,
|
|
ResourceID: "resource-1",
|
|
ResourceName: "resource-1",
|
|
Message: "test",
|
|
StartTime: time.Now(),
|
|
}
|
|
mockStore.RecordAlertFired(alert)
|
|
|
|
req := httptest.NewRequest("GET", "/api/alerts/incidents?alertIdentifier=canonical:a1", nil)
|
|
w := httptest.NewRecorder()
|
|
h.GetAlertIncidentTimeline(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
var incident map[string]interface{}
|
|
_ = json.NewDecoder(w.Body).Decode(&incident)
|
|
assert.Equal(t, "canonical:a1", incident["alertIdentifier"])
|
|
}
|
|
|
|
func TestGetAlertIncidentTimeline_ListExportsCanonicalAlertIdentifier(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
alert := &alerts.Alert{
|
|
ID: "canonical:a1",
|
|
Type: "cpu",
|
|
Level: alerts.AlertLevelWarning,
|
|
ResourceID: "resource-1",
|
|
ResourceName: "resource-1",
|
|
Message: "test",
|
|
StartTime: time.Now(),
|
|
}
|
|
mockStore.RecordAlertFired(alert)
|
|
|
|
req := httptest.NewRequest("GET", "/api/alerts/incidents?resource_id=resource-1", nil)
|
|
w := httptest.NewRecorder()
|
|
h.GetAlertIncidentTimeline(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
var incidents []map[string]interface{}
|
|
_ = json.NewDecoder(w.Body).Decode(&incidents)
|
|
assert.Len(t, incidents, 1)
|
|
assert.Equal(t, "canonical:a1", incidents[0]["alertIdentifier"])
|
|
}
|
|
|
|
func TestGetAlertIncidentTimeline_ProjectsCanonicalLifecycleAndRemediation(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
canonicalStore := unifiedresources.NewMemoryStore()
|
|
incidentStore.SetResourceTimelineStore(canonicalStore)
|
|
mockMonitor.On("GetIncidentStore").Return(incidentStore)
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
alertStartedAt := time.Now().UTC().Add(-20 * time.Minute).Truncate(time.Second)
|
|
alert := &alerts.Alert{
|
|
ID: "canonical:projected-a1",
|
|
Type: "cpu",
|
|
Level: alerts.AlertLevelCritical,
|
|
ResourceID: "resource-1",
|
|
ResourceName: "resource-1",
|
|
Message: "CPU high",
|
|
StartTime: alertStartedAt,
|
|
Value: 95,
|
|
Threshold: 80,
|
|
}
|
|
|
|
incidentStore.RecordAlertFired(alert)
|
|
incidentStore.RecordAnalysis(alert.ID, "Pulse Patrol analysis completed", map[string]interface{}{"source": "test"})
|
|
|
|
changes := []*unifiedresources.ResourceChange{
|
|
unifiedresources.BuildAlertTimelineChange(alert.ResourceID, unifiedresources.ChangeAlertFired, alert.StartTime, "", unifiedresources.AlertTimelineChange{
|
|
AlertIdentifier: alert.ID,
|
|
AlertType: alert.Type,
|
|
AlertLevel: string(alert.Level),
|
|
AlertMessage: alert.Message,
|
|
AlertValue: alert.Value,
|
|
AlertThreshold: alert.Threshold,
|
|
}),
|
|
unifiedresources.BuildAlertTimelineChange(alert.ResourceID, unifiedresources.ChangeAlertAcknowledged, alert.StartTime.Add(2*time.Minute), "operator", unifiedresources.AlertTimelineChange{
|
|
AlertIdentifier: alert.ID,
|
|
AlertType: alert.Type,
|
|
AlertLevel: string(alert.Level),
|
|
AlertMessage: alert.Message,
|
|
}),
|
|
unifiedresources.BuildRunbookExecutionChange(alert.ResourceID, alert.ID, "agent:pulse-patrol", "rb-1", "Restart service", "resolved", true, "Recovered", nil),
|
|
unifiedresources.BuildAlertTimelineChange(alert.ResourceID, unifiedresources.ChangeAlertResolved, alert.StartTime.Add(5*time.Minute), "", unifiedresources.AlertTimelineChange{
|
|
AlertIdentifier: alert.ID,
|
|
AlertType: alert.Type,
|
|
AlertLevel: string(alert.Level),
|
|
AlertMessage: "CPU normalized",
|
|
}),
|
|
}
|
|
for _, change := range changes {
|
|
if change == nil {
|
|
t.Fatal("expected canonical change to be built")
|
|
}
|
|
if err := canonicalStore.RecordChange(*change); err != nil {
|
|
t.Fatalf("RecordChange(%s): %v", change.ID, err)
|
|
}
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/alerts/incidents?alertIdentifier=canonical:projected-a1", nil)
|
|
w := httptest.NewRecorder()
|
|
h.GetAlertIncidentTimeline(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
var incident map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&incident); err != nil {
|
|
t.Fatalf("decode incident response: %v", err)
|
|
}
|
|
|
|
assert.Equal(t, "canonical:projected-a1", incident["alertIdentifier"])
|
|
assert.Equal(t, string(memory.IncidentStatusResolved), incident["status"])
|
|
assert.Equal(t, true, incident["acknowledged"])
|
|
assert.Equal(t, "operator", incident["ackUser"])
|
|
|
|
events, ok := incident["events"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected events array, got %#v", incident["events"])
|
|
}
|
|
|
|
foundAnalysis := false
|
|
foundAck := false
|
|
foundRunbook := false
|
|
foundResolved := false
|
|
for _, rawEvent := range events {
|
|
event, ok := rawEvent.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected event object, got %#v", rawEvent)
|
|
}
|
|
switch event["type"] {
|
|
case string(memory.IncidentEventAnalysis):
|
|
foundAnalysis = true
|
|
case string(memory.IncidentEventAlertAcknowledged):
|
|
foundAck = true
|
|
case string(memory.IncidentEventRunbook):
|
|
foundRunbook = true
|
|
case string(memory.IncidentEventAlertResolved):
|
|
foundResolved = true
|
|
}
|
|
}
|
|
|
|
assert.True(t, foundAnalysis, "expected analysis annotation in exported timeline")
|
|
assert.True(t, foundAck, "expected canonical acknowledgement in exported timeline")
|
|
assert.True(t, foundRunbook, "expected canonical runbook in exported timeline")
|
|
assert.True(t, foundResolved, "expected canonical resolution in exported timeline")
|
|
}
|
|
|
|
func TestBulkAcknowledgeAlerts(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("AcknowledgeAlert", "a1", testifymock.Anything).Return(nil)
|
|
mockManager.On("AcknowledgeAlert", "a2", testifymock.Anything).Return(fmt.Errorf("error"))
|
|
|
|
body := `{"alertIdentifiers": ["a1", "a2"], "user": "admin"}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/bulk/acknowledge", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.BulkAcknowledgeAlerts(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
var resp struct {
|
|
Results []map[string]interface{} `json:"results"`
|
|
}
|
|
_ = json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.Len(t, resp.Results, 2)
|
|
}
|
|
|
|
func TestHandleAlerts(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("GetConfigPersistence").Return(new(MockConfigPersistence))
|
|
mockMonitor.On("GetNotificationManager").Return(¬ifications.NotificationManager{})
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
type route struct {
|
|
method string
|
|
path string
|
|
setup func()
|
|
}
|
|
|
|
routes := []route{
|
|
{"GET", "/api/alerts/active", func() { mockManager.On("GetActiveAlerts").Return([]alerts.Alert{}).Once() }},
|
|
{"GET", "/api/alerts/history", func() {
|
|
mockManager.On("GetAlertHistory", mock.MatchedBy(func(int) bool { return true })).Return([]alerts.Alert{}).Once()
|
|
}},
|
|
{"GET", "/api/alerts/incidents?alertIdentifier=a1", func() {
|
|
mockMonitor.On("GetIncidentStore").Return(memory.NewIncidentStore(memory.IncidentStoreConfig{})).Once()
|
|
}},
|
|
{"POST", "/api/alerts/incidents/note", func() {
|
|
store := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
store.RecordAlertFired(&alerts.Alert{ID: "a1", Type: "test"})
|
|
mockMonitor.On("GetIncidentStore").Return(store).Once()
|
|
}},
|
|
{"DELETE", "/api/alerts/history", func() { mockManager.On("ClearAlertHistory").Return(nil).Once() }},
|
|
{"POST", "/api/alerts/bulk/acknowledge", func() {
|
|
mockManager.On("AcknowledgeAlert", mock.Anything, mock.Anything).Return(nil)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
}},
|
|
{"POST", "/api/alerts/bulk/clear", func() {
|
|
mockManager.On("ClearAlert", mock.Anything).Return(true)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
}},
|
|
{"POST", "/api/alerts/acknowledge", func() {
|
|
mockManager.On("AcknowledgeAlert", "a1", testifymock.Anything).Return(nil).Once()
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
}},
|
|
{"POST", "/api/alerts/unacknowledge", func() {
|
|
mockManager.On("UnacknowledgeAlert", "a1").Return(nil).Once()
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
}},
|
|
{"POST", "/api/alerts/clear", func() {
|
|
mockManager.On("ClearAlert", "a1").Return(true).Once()
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
}},
|
|
}
|
|
|
|
for _, rt := range routes {
|
|
t.Run(rt.method+"_"+rt.path, func(t *testing.T) {
|
|
rt.setup()
|
|
var body []byte
|
|
if rt.method == "POST" || rt.method == "PUT" || rt.method == "DELETE" {
|
|
if strings.Contains(rt.path, "bulk") {
|
|
body = []byte(`{"alertIdentifiers": ["a1"]}`)
|
|
} else if strings.Contains(rt.path, "note") {
|
|
body = []byte(`{"alertIdentifier": "a1", "note": "test"}`)
|
|
} else {
|
|
body = []byte(`{"alertIdentifier": "a1", "user": "admin"}`)
|
|
}
|
|
}
|
|
req := httptest.NewRequest(rt.method, rt.path, bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.HandleAlerts(w, req)
|
|
assert.Equal(t, 200, w.Code)
|
|
})
|
|
}
|
|
|
|
// Test NotFound
|
|
req := httptest.NewRequest("GET", "/api/alerts/unknown", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleAlerts(w, req)
|
|
assert.Equal(t, 404, w.Code)
|
|
}
|
|
|
|
func TestBulkClearAlerts(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("ClearAlert", "a1").Return(true)
|
|
mockManager.On("ClearAlert", "a2").Return(false)
|
|
|
|
body := `{"alertIdentifiers": ["a1", "a2"]}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/bulk/clear", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.BulkClearAlerts(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestAcknowledgeAlertByBody_Success(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("AcknowledgeAlert", "a1", testifymock.Anything).Return(nil)
|
|
|
|
body := `{"alertIdentifier": "a1", "user": "admin"}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/acknowledge", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.AcknowledgeAlertByBody(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestAcknowledgeAlertByBody_CanonicalIdentifierSuccess(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("AcknowledgeAlert", "canonical:a1", testifymock.Anything).Return(nil)
|
|
|
|
body := `{"alertIdentifier": "canonical:a1"}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/acknowledge", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.AcknowledgeAlertByBody(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestUnacknowledgeAlertByBody_Success(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("UnacknowledgeAlert", "a1").Return(nil)
|
|
|
|
body := `{"alertIdentifier": "a1"}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/unacknowledge", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.UnacknowledgeAlertByBody(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestUnacknowledgeAlertByBody_CanonicalIdentifierSuccess(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("UnacknowledgeAlert", "canonical:a1").Return(nil)
|
|
|
|
body := `{"alertIdentifier": "canonical:a1"}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/unacknowledge", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.UnacknowledgeAlertByBody(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestClearAlertByBody_Success(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("ClearAlert", "a1").Return(true)
|
|
|
|
body := `{"alertIdentifier": "a1"}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/clear", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.ClearAlertByBody(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestClearAlertByBody_CanonicalIdentifierSuccess(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
mockMonitor.On("SyncAlertState").Return()
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
mockManager.On("ClearAlert", "canonical:a1").Return(true)
|
|
|
|
body := `{"alertIdentifier": "canonical:a1"}`
|
|
req := httptest.NewRequest("POST", "/api/alerts/clear", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.ClearAlertByBody(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestAlertHandlers_ErrorCases(t *testing.T) {
|
|
mockMonitor := new(MockAlertMonitor)
|
|
mockManager := new(MockAlertManager)
|
|
mockMonitor.On("GetAlertManager").Return(mockManager)
|
|
h := NewAlertHandlers(nil, mockMonitor, nil)
|
|
|
|
t.Run("AcknowledgeAlertByBody_InvalidJSON", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/acknowledge", strings.NewReader(`{invalid`))
|
|
w := httptest.NewRecorder()
|
|
h.AcknowledgeAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("AcknowledgeAlertByBody_MissingID", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/acknowledge", strings.NewReader(`{"alertIdentifier": ""}`))
|
|
w := httptest.NewRecorder()
|
|
h.AcknowledgeAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("AcknowledgeAlertByBody_InvalidID", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/acknowledge", strings.NewReader(`{"alertIdentifier": "bad\x01"}`))
|
|
w := httptest.NewRecorder()
|
|
h.AcknowledgeAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("AcknowledgeAlertByBody_ManagerError", func(t *testing.T) {
|
|
mockManager.On("AcknowledgeAlert", "a1", testifymock.Anything).Return(fmt.Errorf("error")).Once()
|
|
req := httptest.NewRequest("POST", "/api/alerts/acknowledge", strings.NewReader(`{"alertIdentifier": "a1", "user": "admin"}`))
|
|
w := httptest.NewRecorder()
|
|
h.AcknowledgeAlertByBody(w, req)
|
|
assert.Equal(t, 404, w.Code)
|
|
})
|
|
|
|
t.Run("UnacknowledgeAlertByBody_MissingID", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/unacknowledge", strings.NewReader(`{"alertIdentifier": ""}`))
|
|
w := httptest.NewRecorder()
|
|
h.UnacknowledgeAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("UnacknowledgeAlertByBody_InvalidID", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/unacknowledge", strings.NewReader(`{"alertIdentifier": "bad\x01"}`))
|
|
w := httptest.NewRecorder()
|
|
h.UnacknowledgeAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("ClearAlertByBody_MissingID", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/clear", strings.NewReader(`{"alertIdentifier": ""}`))
|
|
w := httptest.NewRecorder()
|
|
h.ClearAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("ClearAlertByBody_InvalidID", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/clear", strings.NewReader(`{"alertIdentifier": "bad\x01"}`))
|
|
w := httptest.NewRecorder()
|
|
h.ClearAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("ClearAlertByBody_NotFound", func(t *testing.T) {
|
|
mockManager.On("ClearAlert", "unknown").Return(false).Once()
|
|
req := httptest.NewRequest("POST", "/api/alerts/clear", strings.NewReader(`{"alertIdentifier": "unknown"}`))
|
|
w := httptest.NewRecorder()
|
|
h.ClearAlertByBody(w, req)
|
|
assert.Equal(t, 404, w.Code)
|
|
})
|
|
|
|
t.Run("BulkAcknowledgeAlerts_InvalidJSON", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/bulk/acknowledge", strings.NewReader(`{invalid`))
|
|
w := httptest.NewRecorder()
|
|
h.BulkAcknowledgeAlerts(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("BulkAcknowledgeAlerts_NoIDs", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/bulk/acknowledge", strings.NewReader(`{"alertIdentifiers": []}`))
|
|
w := httptest.NewRecorder()
|
|
h.BulkAcknowledgeAlerts(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("BulkAcknowledgeAlerts_CanonicalIdentifiers", func(t *testing.T) {
|
|
mockManager.On("AcknowledgeAlert", "canonical:a1", testifymock.Anything).Return(nil).Once()
|
|
mockMonitor.On("SyncAlertState").Return().Once()
|
|
req := httptest.NewRequest("POST", "/api/alerts/bulk/acknowledge", strings.NewReader(`{"alertIdentifiers": ["canonical:a1"]}`))
|
|
w := httptest.NewRecorder()
|
|
h.BulkAcknowledgeAlerts(w, req)
|
|
assert.Equal(t, 200, w.Code)
|
|
})
|
|
|
|
t.Run("UnacknowledgeAlertByBody_InvalidJSON", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/unacknowledge", strings.NewReader(`{invalid`))
|
|
w := httptest.NewRecorder()
|
|
h.UnacknowledgeAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("ClearAlertByBody_InvalidJSON", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/alerts/clear", strings.NewReader(`{invalid`))
|
|
w := httptest.NewRecorder()
|
|
h.ClearAlertByBody(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("SaveAlertIncidentNote_NoStore", func(t *testing.T) {
|
|
mockMonitor2 := new(MockAlertMonitor)
|
|
mockMonitor2.On("GetIncidentStore").Return(nil)
|
|
h2 := NewAlertHandlers(nil, mockMonitor2, nil)
|
|
req := httptest.NewRequest("POST", "/api/alerts/note", strings.NewReader(`{}`))
|
|
w := httptest.NewRecorder()
|
|
h2.SaveAlertIncidentNote(w, req)
|
|
assert.Equal(t, 503, w.Code)
|
|
})
|
|
|
|
t.Run("SaveAlertIncidentNote_InvalidBody", func(t *testing.T) {
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
req := httptest.NewRequest("POST", "/api/alerts/note", strings.NewReader(`{invalid`))
|
|
w := httptest.NewRecorder()
|
|
h.SaveAlertIncidentNote(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("SaveAlertIncidentNote_MissingIDs", func(t *testing.T) {
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
req := httptest.NewRequest("POST", "/api/alerts/note", strings.NewReader(`{"note": "test"}`))
|
|
w := httptest.NewRecorder()
|
|
h.SaveAlertIncidentNote(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("SaveAlertIncidentNote_CanonicalIdentifier", func(t *testing.T) {
|
|
mockMonitor3 := new(MockAlertMonitor)
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor3.On("GetIncidentStore").Return(mockStore)
|
|
h3 := NewAlertHandlers(nil, mockMonitor3, nil)
|
|
incidentAlert := &alerts.Alert{
|
|
ID: "canonical:a1",
|
|
Type: "cpu",
|
|
Level: alerts.AlertLevelWarning,
|
|
ResourceID: "resource-1",
|
|
ResourceName: "resource-1",
|
|
Message: "test",
|
|
StartTime: time.Now(),
|
|
}
|
|
mockStore.RecordAlertFired(incidentAlert)
|
|
req := httptest.NewRequest("POST", "/api/alerts/note", strings.NewReader(`{"alertIdentifier": "canonical:a1", "note": "test"}`))
|
|
w := httptest.NewRecorder()
|
|
h3.SaveAlertIncidentNote(w, req)
|
|
assert.Equal(t, 200, w.Code)
|
|
})
|
|
|
|
t.Run("SaveAlertIncidentNote_InvalidAlertIdentifier", func(t *testing.T) {
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
req := httptest.NewRequest("POST", "/api/alerts/note", strings.NewReader(`{"alertIdentifier": "bad\x01", "note": "test"}`))
|
|
w := httptest.NewRecorder()
|
|
h.SaveAlertIncidentNote(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("SaveAlertIncidentNote_MissingNote", func(t *testing.T) {
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
req := httptest.NewRequest("POST", "/api/alerts/note", strings.NewReader(`{"alertIdentifier": "a1", "note": ""}`))
|
|
w := httptest.NewRecorder()
|
|
h.SaveAlertIncidentNote(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("SaveAlertIncidentNote_NotFound", func(t *testing.T) {
|
|
mockStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
|
mockMonitor.On("GetIncidentStore").Return(mockStore)
|
|
req := httptest.NewRequest("POST", "/api/alerts/note", strings.NewReader(`{"alertIdentifier": "none", "note": "test"}`))
|
|
w := httptest.NewRecorder()
|
|
h.SaveAlertIncidentNote(w, req)
|
|
assert.Equal(t, 400, w.Code)
|
|
})
|
|
|
|
t.Run("ClearAlertHistory_Error", func(t *testing.T) {
|
|
mockManager.On("ClearAlertHistory").Return(errors.New("failed")).Once()
|
|
req := httptest.NewRequest("POST", "/api/alerts/history/clear", nil)
|
|
w := httptest.NewRecorder()
|
|
h.ClearAlertHistory(w, req)
|
|
assert.Equal(t, 500, w.Code)
|
|
})
|
|
}
|