package monitor import ( "math" "testing" "time" "gitea.lerkolabs.com/lerkolabs/uptop/internal/models" ) func TestComputeSLA_NoChanges_CurrentlyUp(t *testing.T) { r := ComputeSLA(nil, "UP", 24*time.Hour) if r.UptimePct != 100 { t.Errorf("expected 100%% uptime, got %.2f%%", r.UptimePct) } if r.Downtime != 0 { t.Errorf("expected 0 downtime, got %v", r.Downtime) } } func TestComputeSLA_NoChanges_CurrentlyDown(t *testing.T) { r := ComputeSLA(nil, "DOWN", 24*time.Hour) if r.UptimePct != 0 { t.Errorf("expected 0%% uptime, got %.2f%%", r.UptimePct) } } func TestComputeSLA_SingleOutage(t *testing.T) { now := time.Now() // DOWN 2 hours ago, recovered 1 hour ago → 1 hour downtime in 24h window changes := []models.StateChange{ {ToStatus: "UP", ChangedAt: now.Add(-1 * time.Hour)}, {ToStatus: "DOWN", FromStatus: "UP", ChangedAt: now.Add(-2 * time.Hour)}, } r := ComputeSLA(changes, "UP", 24*time.Hour) if r.OutageCount != 1 { t.Errorf("expected 1 outage, got %d", r.OutageCount) } expectedDowntime := 1 * time.Hour if absDuration(r.Downtime-expectedDowntime) > time.Minute { t.Errorf("expected ~1h downtime, got %v", r.Downtime) } expectedPct := float64(23) / float64(24) * 100 if math.Abs(r.UptimePct-expectedPct) > 0.5 { t.Errorf("expected ~%.1f%% uptime, got %.2f%%", expectedPct, r.UptimePct) } if r.LongestOut < 55*time.Minute || r.LongestOut > 65*time.Minute { t.Errorf("expected longest outage ~1h, got %v", r.LongestOut) } } func TestComputeSLA_CurrentlyDown(t *testing.T) { now := time.Now() // Went down 3 hours ago, still down changes := []models.StateChange{ {ToStatus: "DOWN", FromStatus: "UP", ChangedAt: now.Add(-3 * time.Hour)}, } r := ComputeSLA(changes, "DOWN", 24*time.Hour) if r.OutageCount != 1 { t.Errorf("expected 1 outage, got %d", r.OutageCount) } expectedDowntime := 3 * time.Hour if absDuration(r.Downtime-expectedDowntime) > time.Minute { t.Errorf("expected ~3h downtime, got %v", r.Downtime) } } func TestComputeSLA_MultipleOutages(t *testing.T) { now := time.Now() // Two outages: 6h-5h ago and 2h-1h ago changes := []models.StateChange{ {ToStatus: "UP", ChangedAt: now.Add(-1 * time.Hour)}, {ToStatus: "DOWN", FromStatus: "UP", ChangedAt: now.Add(-2 * time.Hour)}, {ToStatus: "UP", ChangedAt: now.Add(-5 * time.Hour)}, {ToStatus: "DOWN", FromStatus: "UP", ChangedAt: now.Add(-6 * time.Hour)}, } r := ComputeSLA(changes, "UP", 24*time.Hour) if r.OutageCount != 2 { t.Errorf("expected 2 outages, got %d", r.OutageCount) } expectedDowntime := 2 * time.Hour if absDuration(r.Downtime-expectedDowntime) > time.Minute { t.Errorf("expected ~2h downtime, got %v", r.Downtime) } if r.MTTR < 55*time.Minute || r.MTTR > 65*time.Minute { t.Errorf("expected MTTR ~1h, got %v", r.MTTR) } } func TestComputeSLA_LateNotDown(t *testing.T) { now := time.Now() // LATE for 2 hours is not downtime changes := []models.StateChange{ {ToStatus: "UP", ChangedAt: now.Add(-1 * time.Hour)}, {ToStatus: "LATE", FromStatus: "UP", ChangedAt: now.Add(-3 * time.Hour)}, } r := ComputeSLA(changes, "UP", 24*time.Hour) if r.OutageCount != 0 { t.Errorf("expected 0 outages for LATE, got %d", r.OutageCount) } if r.UptimePct != 100 { t.Errorf("expected 100%% uptime (LATE is not down), got %.2f%%", r.UptimePct) } } func TestComputeDailyBreakdown(t *testing.T) { now := time.Now() changes := []models.StateChange{ {ToStatus: "UP", ChangedAt: now.Add(-1 * time.Hour)}, {ToStatus: "DOWN", FromStatus: "UP", ChangedAt: now.Add(-2 * time.Hour)}, } days := ComputeDailyBreakdown(changes, "UP", 7) if len(days) != 7 { t.Fatalf("expected 7 days, got %d", len(days)) } // Today should have less than 100% uptime if days[0].UptimePct >= 100 { t.Errorf("expected today < 100%%, got %.2f%%", days[0].UptimePct) } } func TestIsDown(t *testing.T) { if !isDown("DOWN") { t.Error("DOWN should be down") } if !isDown("SSL EXP") { t.Error("SSL EXP should be down") } if isDown("UP") { t.Error("UP should not be down") } if isDown("LATE") { t.Error("LATE should not be down") } if isDown("STALE") { t.Error("STALE should not be down") } if isDown("PENDING") { t.Error("PENDING should not be down") } } func absDuration(d time.Duration) time.Duration { if d < 0 { return -d } return d }