mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
760 lines
26 KiB
Go
760 lines
26 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/forecast"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/learning"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/proxmox"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/remediation"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/unified"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
type stubForecastProvider struct {
|
|
points []forecast.MetricDataPoint
|
|
err error
|
|
}
|
|
|
|
func (s stubForecastProvider) GetMetricHistory(_ string, _ string, _, _ time.Time) ([]forecast.MetricDataPoint, error) {
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
return s.points, nil
|
|
}
|
|
|
|
type stubForecastStateProvider struct {
|
|
state forecast.StateSnapshot
|
|
}
|
|
|
|
func (s stubForecastStateProvider) GetState() forecast.StateSnapshot {
|
|
return s.state
|
|
}
|
|
|
|
func makeForecastPoints(count int, start time.Time, startValue, step float64) []forecast.MetricDataPoint {
|
|
points := make([]forecast.MetricDataPoint, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
points = append(points, forecast.MetricDataPoint{
|
|
Timestamp: start.Add(time.Duration(i) * time.Hour),
|
|
Value: startValue + float64(i)*step,
|
|
})
|
|
}
|
|
return points
|
|
}
|
|
|
|
func addBaseline(t *testing.T, store *ai.BaselineStore, resourceID, resourceType, metric string, value float64) {
|
|
t.Helper()
|
|
points := []ai.BaselineMetricPoint{{Value: value, Timestamp: time.Now()}}
|
|
if err := store.Learn(resourceID, resourceType, metric, points); err != nil {
|
|
t.Fatalf("baseline Learn error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetAnomalies_NoStateProvider(t *testing.T) {
|
|
svc := newEnabledAIService(t)
|
|
store := ai.NewBaselineStore(ai.BaselineConfig{MinSamples: 1})
|
|
svc.SetBaselineStore(store)
|
|
|
|
handler := &AISettingsHandler{legacyAIService: svc}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/anomalies", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetAnomalies(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["message"] != "State provider not available" {
|
|
t.Fatalf("unexpected message: %#v", payload["message"])
|
|
}
|
|
}
|
|
|
|
func TestHandleGetAnomalies_MixedResources(t *testing.T) {
|
|
svc := newEnabledAIService(t)
|
|
store := ai.NewBaselineStore(ai.BaselineConfig{MinSamples: 1})
|
|
addBaseline(t, store, "vm-1", "vm", "cpu", 10)
|
|
addBaseline(t, store, "vm-1", "vm", "memory", 10)
|
|
addBaseline(t, store, "vm-1", "vm", "disk", 10)
|
|
addBaseline(t, store, "ct-1", "container", "cpu", 10)
|
|
addBaseline(t, store, "ct-1", "container", "memory", 10)
|
|
addBaseline(t, store, "ct-1", "container", "disk", 10)
|
|
addBaseline(t, store, "node-1", "node", "cpu", 10)
|
|
addBaseline(t, store, "node-1", "node", "memory", 10)
|
|
svc.SetBaselineStore(store)
|
|
|
|
state := models.StateSnapshot{
|
|
VMs: []models.VM{
|
|
{ID: "vm-1", Name: "vm-one", Status: "running", CPU: 0.9, Memory: models.Memory{Usage: 85}, Disk: models.Disk{Usage: 90}},
|
|
{ID: "vm-template", Template: true, Status: "running", CPU: 0.9, Memory: models.Memory{Usage: 90}},
|
|
{ID: "vm-stopped", Status: "stopped", CPU: 0.9, Memory: models.Memory{Usage: 90}},
|
|
},
|
|
Containers: []models.Container{
|
|
{ID: "ct-1", Name: "ct-one", Status: "running", CPU: 0.9, Memory: models.Memory{Usage: 85}, Disk: models.Disk{Usage: 90}},
|
|
{ID: "ct-template", Template: true, Status: "running", CPU: 0.9, Memory: models.Memory{Usage: 90}},
|
|
{ID: "ct-stopped", Status: "stopped", CPU: 0.9, Memory: models.Memory{Usage: 90}},
|
|
},
|
|
Nodes: []models.Node{
|
|
{ID: "node-1", Name: "node-one", CPU: 0.9, Memory: models.Memory{Usage: 85}},
|
|
},
|
|
}
|
|
svc.SetStateProvider(snapshotStateProvider{state: state})
|
|
|
|
handler := &AISettingsHandler{legacyAIService: svc}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/anomalies", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetAnomalies(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
|
|
anomalies, ok := payload["anomalies"].([]interface{})
|
|
if !ok || len(anomalies) == 0 {
|
|
t.Fatalf("expected anomalies, got %#v", payload["anomalies"])
|
|
}
|
|
|
|
types := map[string]bool{}
|
|
for _, item := range anomalies {
|
|
row, _ := item.(map[string]interface{})
|
|
if rtype, ok := row["resource_type"].(string); ok {
|
|
types[rtype] = true
|
|
}
|
|
}
|
|
if !types["vm"] || !types["container"] || !types["node"] {
|
|
t.Fatalf("expected vm, container, node anomalies, got %#v", types)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLearningStatus_WaitingAndActive(t *testing.T) {
|
|
t.Run("waiting", func(t *testing.T) {
|
|
svc := newEnabledAIService(t)
|
|
store := ai.NewBaselineStore(ai.BaselineConfig{MinSamples: 1})
|
|
svc.SetBaselineStore(store)
|
|
|
|
handler := &AISettingsHandler{legacyAIService: svc}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/learning", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetLearningStatus(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["status"] != "waiting" {
|
|
t.Fatalf("expected status waiting, got %#v", payload["status"])
|
|
}
|
|
})
|
|
|
|
t.Run("active", func(t *testing.T) {
|
|
svc := newEnabledAIService(t)
|
|
store := ai.NewBaselineStore(ai.BaselineConfig{MinSamples: 1})
|
|
for i := 0; i < 5; i++ {
|
|
id := fmt.Sprintf("res-%d", i)
|
|
addBaseline(t, store, id, "vm", "cpu", 10)
|
|
}
|
|
svc.SetBaselineStore(store)
|
|
|
|
handler := &AISettingsHandler{legacyAIService: svc}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/learning", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetLearningStatus(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["status"] != "active" {
|
|
t.Fatalf("expected status active, got %#v", payload["status"])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleGetLearningPreferences_Stats(t *testing.T) {
|
|
store := learning.NewLearningStore(learning.LearningStoreConfig{})
|
|
store.RecordFeedback(learning.FeedbackRecord{
|
|
FindingID: "finding-1",
|
|
ResourceID: "res-1",
|
|
Category: "performance",
|
|
Severity: "warning",
|
|
Action: learning.ActionAcknowledge,
|
|
})
|
|
|
|
handler := &AISettingsHandler{}
|
|
handler.SetLearningStore(store)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/learning/preferences", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetLearningPreferences(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if _, ok := payload["statistics"]; !ok {
|
|
t.Fatalf("expected statistics in response")
|
|
}
|
|
}
|
|
|
|
func TestHandleGetUnifiedFindings_Statuses(t *testing.T) {
|
|
store := unified.NewUnifiedStore(unified.DefaultAlertToFindingConfig())
|
|
now := time.Now()
|
|
resolvedAt := now.Add(-2 * time.Hour)
|
|
snoozedUntil := now.Add(2 * time.Hour)
|
|
|
|
store.AddFromAI(&unified.UnifiedFinding{
|
|
ID: "finding-active",
|
|
Source: unified.SourceAIPatrol,
|
|
Severity: unified.SeverityWarning,
|
|
Category: unified.CategoryPerformance,
|
|
ResourceID: "res-1",
|
|
ResourceName: "res-1",
|
|
ResourceType: "vm",
|
|
Title: "Active",
|
|
Description: "active",
|
|
DetectedAt: now,
|
|
LastSeenAt: now,
|
|
})
|
|
store.AddFromAI(&unified.UnifiedFinding{
|
|
ID: "finding-resolved",
|
|
Source: unified.SourceThreshold,
|
|
Severity: unified.SeverityCritical,
|
|
Category: unified.CategoryCapacity,
|
|
ResourceID: "res-1",
|
|
ResourceName: "res-1",
|
|
ResourceType: "vm",
|
|
Title: "Resolved",
|
|
Description: "resolved",
|
|
ResolvedAt: &resolvedAt,
|
|
DetectedAt: now,
|
|
LastSeenAt: now,
|
|
})
|
|
store.AddFromAI(&unified.UnifiedFinding{
|
|
ID: "finding-snoozed",
|
|
Source: unified.SourceAIPatrol,
|
|
Severity: unified.SeverityWarning,
|
|
Category: unified.CategoryPerformance,
|
|
ResourceID: "res-1",
|
|
ResourceName: "res-1",
|
|
ResourceType: "vm",
|
|
Title: "Snoozed",
|
|
Description: "snoozed",
|
|
SnoozedUntil: &snoozedUntil,
|
|
DetectedAt: now,
|
|
LastSeenAt: now,
|
|
})
|
|
store.AddFromAI(&unified.UnifiedFinding{
|
|
ID: "finding-dismissed",
|
|
Source: unified.SourceAIPatrol,
|
|
Severity: unified.SeverityInfo,
|
|
Category: unified.CategoryGeneral,
|
|
ResourceID: "res-1",
|
|
ResourceName: "res-1",
|
|
ResourceType: "vm",
|
|
Title: "Dismissed",
|
|
Description: "dismissed",
|
|
DismissedReason: "noise",
|
|
Suppressed: true,
|
|
DetectedAt: now,
|
|
LastSeenAt: now,
|
|
})
|
|
|
|
handler := &AISettingsHandler{}
|
|
handler.SetUnifiedStore(store)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/unified/findings?resource_id=res-1&include_resolved=true", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetUnifiedFindings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["count"] != float64(4) {
|
|
t.Fatalf("expected count 4, got %#v", payload["count"])
|
|
}
|
|
if payload["active_count"] != float64(1) {
|
|
t.Fatalf("expected active_count 1, got %#v", payload["active_count"])
|
|
}
|
|
}
|
|
|
|
func TestHandleGetForecast_MissingParams(t *testing.T) {
|
|
handler := &AISettingsHandler{forecastService: forecast.NewService(forecast.DefaultForecastConfig())}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/forecast?metric=cpu", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetForecast(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetForecast_Success(t *testing.T) {
|
|
points := makeForecastPoints(60, time.Now().Add(-60*time.Hour), 50, 0.1)
|
|
svc := forecast.NewService(forecast.DefaultForecastConfig())
|
|
svc.SetDataProvider(stubForecastProvider{points: points})
|
|
|
|
handler := &AISettingsHandler{forecastService: svc}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/forecast?resource_id=vm-1&resource_name=vm-one&metric=cpu&horizon_hours=2&threshold=60", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetForecast(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
forecastVal, ok := payload["forecast"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected forecast object")
|
|
}
|
|
if forecastVal["resource_id"] != "vm-1" {
|
|
t.Fatalf("unexpected resource_id: %#v", forecastVal["resource_id"])
|
|
}
|
|
if forecastVal["metric"] != "cpu" {
|
|
t.Fatalf("unexpected metric: %#v", forecastVal["metric"])
|
|
}
|
|
}
|
|
|
|
func TestHandleGetForecastOverview_Success(t *testing.T) {
|
|
points := makeForecastPoints(60, time.Now().Add(-60*time.Hour), 50, 0.1)
|
|
svc := forecast.NewService(forecast.DefaultForecastConfig())
|
|
svc.SetDataProvider(stubForecastProvider{points: points})
|
|
svc.SetStateProvider(stubForecastStateProvider{state: forecast.StateSnapshot{
|
|
VMs: []forecast.VMInfo{{ID: "vm-1", Name: "vm-one"}},
|
|
Containers: []forecast.ContainerInfo{{ID: "ct-1", Name: "ct-one"}},
|
|
Nodes: []forecast.NodeInfo{{ID: "node-1", Name: "node-one"}},
|
|
}})
|
|
|
|
handler := &AISettingsHandler{forecastService: svc}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/forecasts/overview?metric=cpu&horizon_hours=24&threshold=60", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetForecastOverview(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["metric"] != "cpu" {
|
|
t.Fatalf("unexpected metric: %#v", payload["metric"])
|
|
}
|
|
if payload["threshold"] != float64(60) {
|
|
t.Fatalf("unexpected threshold: %#v", payload["threshold"])
|
|
}
|
|
forecasts, ok := payload["forecasts"].([]interface{})
|
|
if !ok || len(forecasts) == 0 {
|
|
t.Fatalf("expected forecasts, got %#v", payload["forecasts"])
|
|
}
|
|
}
|
|
|
|
func TestHandleGetProxmoxEvents_ResourceFilter(t *testing.T) {
|
|
correlator := proxmox.NewEventCorrelator(proxmox.EventCorrelatorConfig{})
|
|
correlator.RecordEvent(proxmox.ProxmoxEvent{
|
|
ID: "evt-1",
|
|
Type: proxmox.EventVMStart,
|
|
ResourceID: "vm-1",
|
|
})
|
|
|
|
handler := &AISettingsHandler{proxmoxCorrelator: correlator}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/proxmox/events?resource_id=vm-1&limit=1", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetProxmoxEvents(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
events, ok := payload["events"].([]interface{})
|
|
if !ok || len(events) != 1 {
|
|
t.Fatalf("expected 1 event, got %#v", payload["events"])
|
|
}
|
|
}
|
|
|
|
func TestHandleGetProxmoxCorrelations_ResourceFilter(t *testing.T) {
|
|
correlator := proxmox.NewEventCorrelator(proxmox.EventCorrelatorConfig{})
|
|
corr := proxmox.EventCorrelation{
|
|
ID: "corr-1",
|
|
Event: proxmox.ProxmoxEvent{
|
|
ID: "evt-1",
|
|
Type: proxmox.EventVMStart,
|
|
ResourceID: "vm-1",
|
|
},
|
|
ImpactedResources: []string{"vm-1"},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
setUnexportedField(t, correlator, "correlations", []proxmox.EventCorrelation{corr})
|
|
|
|
handler := &AISettingsHandler{proxmoxCorrelator: correlator}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/proxmox/correlations?resource_id=vm-1", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetProxmoxCorrelations(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
correlations, ok := payload["correlations"].([]interface{})
|
|
if !ok || len(correlations) != 1 {
|
|
t.Fatalf("expected 1 correlation, got %#v", payload["correlations"])
|
|
}
|
|
}
|
|
|
|
func TestHandleExecuteRemediationPlan_Errors(t *testing.T) {
|
|
t.Run("no_engine", func(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/remediation/plans/plan-1/execute", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleExecuteRemediationPlan(rec, req)
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid_body", func(t *testing.T) {
|
|
handler := &AISettingsHandler{remediationEngine: remediation.NewEngine(remediation.EngineConfig{})}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/remediation/plans/plan-1/execute", bytes.NewBufferString("bad-json"))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleExecuteRemediationPlan(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("missing_ids", func(t *testing.T) {
|
|
handler := &AISettingsHandler{remediationEngine: remediation.NewEngine(remediation.EngineConfig{})}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/remediation/plans/plan-1/execute", bytes.NewBufferString("{}"))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleExecuteRemediationPlan(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("execution_error", func(t *testing.T) {
|
|
engine := remediation.NewEngine(remediation.EngineConfig{})
|
|
plan := &remediation.RemediationPlan{
|
|
ID: "plan-no-exec",
|
|
FindingID: "finding-1",
|
|
ResourceID: "res-1",
|
|
Title: "Restart",
|
|
Description: "Restart service",
|
|
Steps: []remediation.RemediationStep{
|
|
{
|
|
Order: 0,
|
|
Description: "Restart",
|
|
Command: "echo ok",
|
|
Target: "host-1",
|
|
},
|
|
},
|
|
}
|
|
if err := engine.CreatePlan(plan); err != nil {
|
|
t.Fatalf("CreatePlan: %v", err)
|
|
}
|
|
handler := &AISettingsHandler{remediationEngine: engine}
|
|
body := bytes.NewBufferString(`{"plan_id":"plan-no-exec"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/remediation/plans/plan-no-exec/execute", body)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleExecuteRemediationPlan(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleRollbackRemediationPlan_Errors(t *testing.T) {
|
|
t.Run("no_engine", func(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/remediation/plans/plan-1/rollback", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleRollbackRemediationPlan(rec, req)
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid_body", func(t *testing.T) {
|
|
handler := &AISettingsHandler{remediationEngine: remediation.NewEngine(remediation.EngineConfig{})}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/remediation/plans/plan-1/rollback", bytes.NewBufferString("bad-json"))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleRollbackRemediationPlan(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("missing_execution_id", func(t *testing.T) {
|
|
handler := &AISettingsHandler{remediationEngine: remediation.NewEngine(remediation.EngineConfig{})}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/remediation/plans/plan-1/rollback", bytes.NewBufferString("{}"))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleRollbackRemediationPlan(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleGetIncidentData_Errors(t *testing.T) {
|
|
t.Run("invalid_path", func(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/incident/abc", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetIncidentData(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("missing_resource", func(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/incidents/", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetIncidentData(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("no_service", func(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/incidents/res-1", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleGetIncidentData(rec, req)
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleGetRecentIncidents_NoService(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/incidents", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetRecentIncidents(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["message"] != "Pulse Patrol service not available" {
|
|
t.Fatalf("unexpected message: %#v", payload["message"])
|
|
}
|
|
}
|
|
|
|
func TestHandleGetCircuitBreakerStatus_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/circuit/status", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetCircuitBreakerStatus(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetForecastOverview_NoStateProvider(t *testing.T) {
|
|
points := makeForecastPoints(40, time.Now().Add(-40*time.Hour), 20, 1)
|
|
svc := forecast.NewService(forecast.DefaultForecastConfig())
|
|
svc.SetDataProvider(stubForecastProvider{points: points})
|
|
|
|
handler := &AISettingsHandler{forecastService: svc}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/forecasts/overview?metric=cpu", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetForecastOverview(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["error"] == nil {
|
|
t.Fatalf("expected error in response")
|
|
}
|
|
}
|
|
|
|
func TestHandleGetForecast_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{forecastService: forecast.NewService(forecast.DefaultForecastConfig())}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/forecast", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetForecast(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetIncidentData_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/incidents/res-1", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetIncidentData(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetRecentIncidents_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/incidents", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetRecentIncidents(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetAnomalies_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/intelligence/anomalies", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetAnomalies(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetForecastOverview_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/forecasts/overview", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetForecastOverview(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLearningPreferences_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/learning/preferences", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetLearningPreferences(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetUnifiedFindings_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/unified/findings", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetUnifiedFindings(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetProxmoxEvents_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/proxmox/events", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetProxmoxEvents(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetProxmoxCorrelations_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/proxmox/correlations", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetProxmoxCorrelations(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleExecuteRemediationPlan_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/remediation/plans/plan-1/execute", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleExecuteRemediationPlan(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleRollbackRemediationPlan_MethodNotAllowed(t *testing.T) {
|
|
handler := &AISettingsHandler{}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/remediation/plans/plan-1/rollback", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleRollbackRemediationPlan(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
|
|
}
|
|
}
|