Files
uptop/internal/monitor/sla_test.go
T
lerko 60592ef810
CI / test (pull_request) Successful in 2m35s
CI / lint (pull_request) Successful in 56s
CI / vulncheck (pull_request) Successful in 41s
feat(tui): add SLA reporting view
Full-screen SLA report accessible via [s] from detail panel.
Computes uptime%, downtime, outage count, longest outage, MTTR,
and MTBF from state_changes table. Includes daily breakdown with
bar chart, switchable time periods (24h/7d/30d/90d), and
scrollable viewport. LATE/STALE treated as UP for SLA purposes.
2026-06-04 14:24:39 -04:00

166 lines
4.2 KiB
Go

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
}