Files
Pulse/internal/api/router_wrappers_additional_test.go

315 lines
10 KiB
Go

package api
import (
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/baseline"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/patterns"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/proxmox"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/metrics"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
)
func TestForecastStateProviderWrapper_GetState(t *testing.T) {
state := models.NewState()
state.VMs = []models.VM{{ID: "vm-1", Name: "vm-one"}}
state.Containers = []models.Container{{ID: "ct-1", Name: "ct-one"}}
state.Nodes = []models.Node{{ID: "node-1", Name: "node-one"}}
state.Storage = []models.Storage{{ID: "store-1", Name: "store-one"}}
monitor := &monitoring.Monitor{}
setUnexportedField(t, monitor, "state", state)
wrapper := &forecastStateProviderWrapper{monitor: monitor}
snapshot := wrapper.GetState()
if len(snapshot.VMs) != 1 || snapshot.VMs[0].ID != "vm-1" || snapshot.VMs[0].Name != "vm-one" {
t.Fatalf("unexpected VM snapshot: %#v", snapshot.VMs)
}
if len(snapshot.Containers) != 1 || snapshot.Containers[0].ID != "ct-1" || snapshot.Containers[0].Name != "ct-one" {
t.Fatalf("unexpected container snapshot: %#v", snapshot.Containers)
}
if len(snapshot.Nodes) != 1 || snapshot.Nodes[0].ID != "node-1" || snapshot.Nodes[0].Name != "node-one" {
t.Fatalf("unexpected node snapshot: %#v", snapshot.Nodes)
}
if len(snapshot.Storage) != 1 || snapshot.Storage[0].ID != "store-1" || snapshot.Storage[0].Name != "store-one" {
t.Fatalf("unexpected storage snapshot: %#v", snapshot.Storage)
}
}
func TestForecastStateProviderWrapper_NilMonitor(t *testing.T) {
wrapper := &forecastStateProviderWrapper{}
snapshot := wrapper.GetState()
if len(snapshot.VMs) != 0 || len(snapshot.Containers) != 0 || len(snapshot.Nodes) != 0 || len(snapshot.Storage) != 0 {
t.Fatalf("expected empty snapshot, got %#v", snapshot)
}
}
func TestIncidentRecorderProviderWrapper(t *testing.T) {
now := time.Now().UTC()
end := now.Add(5 * time.Minute)
activeWindow := &metrics.IncidentWindow{
ID: "win-active",
ResourceID: "res-1",
ResourceName: "Resource",
ResourceType: "vm",
TriggerType: "alert",
TriggerID: "alert-1",
StartTime: now,
EndTime: &end,
Status: metrics.IncidentWindowStatusRecording,
DataPoints: []metrics.IncidentDataPoint{
{Timestamp: now, Metrics: map[string]float64{"cpu": 10}},
},
Summary: &metrics.IncidentSummary{
Duration: 5 * time.Minute,
DataPoints: 1,
Peaks: map[string]float64{"cpu": 10},
Lows: map[string]float64{"cpu": 5},
Averages: map[string]float64{"cpu": 7},
Changes: map[string]float64{"cpu": 2},
},
}
completedWindow := &metrics.IncidentWindow{
ID: "win-complete",
ResourceID: "res-1",
ResourceName: "Resource",
ResourceType: "vm",
TriggerType: "alert",
TriggerID: "alert-2",
StartTime: now.Add(-time.Hour),
Status: metrics.IncidentWindowStatusComplete,
DataPoints: []metrics.IncidentDataPoint{
{Timestamp: now.Add(-time.Hour), Metrics: map[string]float64{"cpu": 20}},
},
}
recorder := metrics.NewIncidentRecorder(metrics.DefaultIncidentRecorderConfig())
setUnexportedField(t, recorder, "activeWindows", map[string]*metrics.IncidentWindow{"win-active": activeWindow})
setUnexportedField(t, recorder, "completedWindows", []*metrics.IncidentWindow{completedWindow})
wrapper := &incidentRecorderProviderWrapper{recorder: recorder}
windows := wrapper.GetWindowsForResource("res-1", 10)
if len(windows) != 2 {
t.Fatalf("expected 2 windows, got %d", len(windows))
}
ids := []string{windows[0].ID, windows[1].ID}
if !containsStringSlice(ids, "win-active") || !containsStringSlice(ids, "win-complete") {
t.Fatalf("unexpected window ids %v", ids)
}
window := wrapper.GetWindow("win-active")
if window == nil || window.ResourceID != "res-1" || window.Status == "" {
t.Fatalf("unexpected window: %#v", window)
}
}
func TestIncidentRecorderProviderWrapper_NilRecorder(t *testing.T) {
wrapper := &incidentRecorderProviderWrapper{}
if got := wrapper.GetWindowsForResource("res-1", 5); got != nil {
t.Fatalf("expected nil windows, got %#v", got)
}
if got := wrapper.GetWindow("win-1"); got != nil {
t.Fatalf("expected nil window, got %#v", got)
}
}
func TestConvertIncidentWindowNil(t *testing.T) {
if got := convertIncidentWindow(nil); got != nil {
t.Fatalf("expected nil window, got %#v", got)
}
}
func TestEventCorrelatorProviderWrapper(t *testing.T) {
now := time.Now().UTC()
corr := proxmox.EventCorrelation{
ID: "corr-1",
Event: proxmox.ProxmoxEvent{
ID: "evt-1",
Type: proxmox.EventBackupStart,
Timestamp: now,
ResourceID: "res-1",
ResourceName: "Resource",
},
Anomalies: []proxmox.MetricAnomaly{{ResourceID: "res-1", Metric: "cpu", Value: 10}},
Explanation: "Backup caused spike",
Confidence: 0.75,
ImpactedResources: []string{
"res-1",
},
}
correlator := proxmox.NewEventCorrelator(proxmox.EventCorrelatorConfig{})
setUnexportedField(t, correlator, "correlations", []proxmox.EventCorrelation{corr})
wrapper := &eventCorrelatorProviderWrapper{correlator: correlator}
results := wrapper.GetCorrelationsForResource("res-1", 30*time.Minute)
if len(results) != 1 {
t.Fatalf("expected 1 correlation, got %d", len(results))
}
result := results[0]
if result.EventType != string(corr.Event.Type) || result.ResourceID != "res-1" || result.Description == "" {
t.Fatalf("unexpected correlation: %#v", result)
}
if result.Metadata["event_id"] != "evt-1" {
t.Fatalf("expected metadata event_id, got %#v", result.Metadata)
}
}
func TestEventCorrelatorProviderWrapper_NilCorrelator(t *testing.T) {
wrapper := &eventCorrelatorProviderWrapper{}
if got := wrapper.GetCorrelationsForResource("res-1", time.Minute); got != nil {
t.Fatalf("expected nil correlations, got %#v", got)
}
}
func TestMetricsSourceWrapper(t *testing.T) {
history := monitoring.NewMetricsHistory(10, time.Hour)
now := time.Now()
history.AddGuestMetric("guest-1", "cpu", 0.5, now)
history.AddGuestMetric("guest-1", "memory", 0.7, now)
history.AddNodeMetric("node-1", "cpu", 0.2, now)
wrapper := &metricsSourceWrapper{history: history}
guestCPU := wrapper.GetGuestMetrics("guest-1", "cpu", time.Hour)
if len(guestCPU) != 1 || guestCPU[0].Value != 0.5 {
t.Fatalf("unexpected guest metrics: %#v", guestCPU)
}
nodeCPU := wrapper.GetNodeMetrics("node-1", "cpu", time.Hour)
if len(nodeCPU) != 1 || nodeCPU[0].Value != 0.2 {
t.Fatalf("unexpected node metrics: %#v", nodeCPU)
}
allGuest := wrapper.GetAllGuestMetrics("guest-1", time.Hour)
if len(allGuest) == 0 || len(allGuest["cpu"]) != 1 {
t.Fatalf("unexpected all guest metrics: %#v", allGuest)
}
}
func TestConvertMetricPoints(t *testing.T) {
now := time.Now()
points := []monitoring.MetricPoint{{Value: 1.23, Timestamp: now}}
got := convertMetricPoints(points)
if len(got) != 1 || got[0].Value != 1.23 || !got[0].Timestamp.Equal(now) {
t.Fatalf("unexpected metric points: %#v", got)
}
}
func TestBaselineSourceWrapper(t *testing.T) {
store := baseline.NewStore(baseline.StoreConfig{MinSamples: 1})
now := time.Now()
points := []baseline.MetricPoint{{Value: 1.0, Timestamp: now}}
if err := store.Learn("res-1", "vm", "cpu", points); err != nil {
t.Fatalf("learn baseline: %v", err)
}
wrapper := &baselineSourceWrapper{store: store}
mean, stddev, samples, ok := wrapper.GetBaseline("res-1", "cpu")
if !ok || samples != 1 || mean != 1.0 || stddev != 0 {
t.Fatalf("unexpected baseline: mean=%v stddev=%v samples=%d ok=%v", mean, stddev, samples, ok)
}
all := wrapper.GetAllBaselines()
if all == nil {
t.Fatalf("expected baselines map, got nil")
}
resourceBaselines, ok := all["res-1"]
if !ok {
t.Fatalf("expected baselines for res-1, got %#v", all)
}
cpuBaseline, ok := resourceBaselines["cpu"]
if !ok || cpuBaseline.SampleCount != 1 {
t.Fatalf("unexpected cpu baseline: %#v", cpuBaseline)
}
}
func TestBaselineSourceWrapper_NilStore(t *testing.T) {
wrapper := &baselineSourceWrapper{}
if _, _, _, ok := wrapper.GetBaseline("res-1", "cpu"); ok {
t.Fatalf("expected baseline lookup to fail")
}
if got := wrapper.GetAllBaselines(); got != nil {
t.Fatalf("expected nil baselines, got %#v", got)
}
}
func TestPatternSourceWrapper(t *testing.T) {
detector := patterns.NewDetector(patterns.DetectorConfig{MinOccurrences: 1})
now := time.Now()
pattern := &patterns.Pattern{
ResourceID: "res-1",
EventType: patterns.EventType("reboot"),
Occurrences: 2,
Confidence: 0.9,
LastOccurrence: now.Add(-time.Hour),
NextPredicted: now.Add(24 * time.Hour),
}
patternsMap := map[string]*patterns.Pattern{
"res-1:reboot": pattern,
"nil-pattern": nil,
}
setUnexportedField(t, detector, "patterns", patternsMap)
wrapper := &patternSourceWrapper{detector: detector}
gotPatterns := wrapper.GetPatterns()
if len(gotPatterns) != 1 {
t.Fatalf("expected 1 pattern, got %d", len(gotPatterns))
}
if gotPatterns[0].ResourceID != "res-1" || gotPatterns[0].PatternType != "reboot" {
t.Fatalf("unexpected pattern: %#v", gotPatterns[0])
}
predictions := wrapper.GetPredictions()
if len(predictions) != 1 || predictions[0].ResourceID != "res-1" {
t.Fatalf("unexpected predictions: %#v", predictions)
}
}
func TestPatternSourceWrapper_NilDetector(t *testing.T) {
wrapper := &patternSourceWrapper{}
if got := wrapper.GetPatterns(); got != nil {
t.Fatalf("expected nil patterns, got %#v", got)
}
if got := wrapper.GetPredictions(); got != nil {
t.Fatalf("expected nil predictions, got %#v", got)
}
}
func TestUpdatesConfigWrapper(t *testing.T) {
wrapper := &updatesConfigWrapper{}
if !wrapper.IsDockerUpdateActionsEnabled() {
t.Fatalf("expected docker update actions enabled by default")
}
wrapper = &updatesConfigWrapper{cfg: &config.Config{DisableDockerUpdateActions: true}}
if wrapper.IsDockerUpdateActionsEnabled() {
t.Fatalf("expected docker update actions disabled")
}
}
func containsStringSlice(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func TestConvertMetricPoints_Empty(t *testing.T) {
if got := convertMetricPoints(nil); len(got) != 0 {
t.Fatalf("expected empty slice, got %#v", got)
}
}