Files
uptop/internal/monitor/sla_test.go
T
lerko f00acbc280 refactor(models): typed Status constants with IsBroken() predicate
Replace ~150 bare status string comparisons with typed models.Status
constants (StatusUp, StatusDown, StatusPending, StatusLate, StatusStale,
StatusSSLExp). Single IsBroken() method replaces the duplicated
isBroken lambda in monitor.go and isDown function in sla.go.

Adding a new status value (e.g. DEGRADED) now requires one constant
definition instead of grep-and-pray across 16 files.

CheckResult.Status stays string — the checker is the boundary between
raw protocol results and typed status. Cast happens at the edge in
handleStatusChange.
2026-06-11 15:56:51 -04:00

167 lines
4.5 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) {
// Use a fixed time well past midnight so the outage always falls within today's window.
now := time.Date(2026, 6, 4, 15, 0, 0, 0, time.UTC)
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, now)
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 TestIsBroken(t *testing.T) {
if !models.StatusDown.IsBroken() {
t.Error("DOWN should be broken")
}
if !models.StatusSSLExp.IsBroken() {
t.Error("SSL EXP should be broken")
}
if models.StatusUp.IsBroken() {
t.Error("UP should not be broken")
}
if models.StatusLate.IsBroken() {
t.Error("LATE should not be broken")
}
if models.StatusStale.IsBroken() {
t.Error("STALE should not be broken")
}
if models.StatusPending.IsBroken() {
t.Error("PENDING should not be broken")
}
}
func absDuration(d time.Duration) time.Duration {
if d < 0 {
return -d
}
return d
}