Files
ez-api/internal/api/dashboard_handler_test.go
zenfun 5c01497ce0 fix(api): handle zero-baseline edge cases in trend calculation
Introduce `CalculateTrendFloatWithBaseline` to correctly handle scenarios where previous period metrics (Error Rate, Latency) are zero or missing. This prevents arithmetic errors and distinguishes between "new" data and actual increases ("up") when starting from zero.

Also updates the admin panel dashboard documentation to reflect current project status.
2026-01-02 23:17:55 +08:00

320 lines
7.5 KiB
Go

package api
import (
"testing"
"time"
)
func TestCalculateTrend(t *testing.T) {
tests := []struct {
name string
current int64
previous int64
wantDelta *float64
wantDirection string
}{
{
name: "zero previous, zero current - stable",
current: 0,
previous: 0,
wantDelta: nil,
wantDirection: "stable",
},
{
name: "zero previous, positive current - new",
current: 100,
previous: 0,
wantDelta: nil,
wantDirection: "new",
},
{
name: "100% increase - up",
current: 200,
previous: 100,
wantDelta: ptr(100.0),
wantDirection: "up",
},
{
name: "50% decrease - down",
current: 50,
previous: 100,
wantDelta: ptr(-50.0),
wantDirection: "down",
},
{
name: "0.3% increase - stable",
current: 1003,
previous: 1000,
wantDelta: ptr(0.3),
wantDirection: "stable",
},
{
name: "0.5% increase - stable (boundary)",
current: 1005,
previous: 1000,
wantDelta: ptr(0.5),
wantDirection: "stable",
},
{
name: "0.6% increase - up",
current: 1006,
previous: 1000,
wantDelta: ptr(0.6),
wantDirection: "up",
},
{
name: "0.6% decrease - down",
current: 994,
previous: 1000,
wantDelta: ptr(-0.6),
wantDirection: "down",
},
{
name: "no change - stable",
current: 100,
previous: 100,
wantDelta: ptr(0.0),
wantDirection: "stable",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CalculateTrend(tt.current, tt.previous)
if tt.wantDelta == nil {
if got.Delta != nil {
t.Errorf("expected nil delta, got %v", *got.Delta)
}
} else {
if got.Delta == nil {
t.Errorf("expected delta %v, got nil", *tt.wantDelta)
} else if *got.Delta != *tt.wantDelta {
t.Errorf("expected delta %v, got %v", *tt.wantDelta, *got.Delta)
}
}
if got.Direction != tt.wantDirection {
t.Errorf("expected direction %q, got %q", tt.wantDirection, got.Direction)
}
})
}
}
func TestCalculateTrendFloat(t *testing.T) {
tests := []struct {
name string
current float64
previous float64
wantDelta *float64
wantDirection string
}{
{
name: "zero previous, zero current - stable",
current: 0,
previous: 0,
wantDelta: nil,
wantDirection: "stable",
},
{
name: "zero previous, positive current - new",
current: 0.5,
previous: 0,
wantDelta: nil,
wantDirection: "new",
},
{
name: "50% increase - up",
current: 0.15,
previous: 0.10,
wantDelta: ptr(50.0),
wantDirection: "up",
},
{
name: "33.3% decrease - down",
current: 0.10,
previous: 0.15,
wantDelta: ptr(-33.3),
wantDirection: "down",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CalculateTrendFloat(tt.current, tt.previous)
if tt.wantDelta == nil {
if got.Delta != nil {
t.Errorf("expected nil delta, got %v", *got.Delta)
}
} else {
if got.Delta == nil {
t.Errorf("expected delta %v, got nil", *tt.wantDelta)
} else if *got.Delta != *tt.wantDelta {
t.Errorf("expected delta %v, got %v", *tt.wantDelta, *got.Delta)
}
}
if got.Direction != tt.wantDirection {
t.Errorf("expected direction %q, got %q", tt.wantDirection, got.Direction)
}
})
}
}
func TestCalculateTrendFloatWithBaseline(t *testing.T) {
tests := []struct {
name string
current float64
previous float64
hasBaseline bool
wantDelta *float64
wantDirection string
}{
{
name: "no baseline, zero current",
current: 0,
previous: 0,
hasBaseline: false,
wantDelta: nil,
wantDirection: "stable",
},
{
name: "no baseline, positive current",
current: 0.2,
previous: 0,
hasBaseline: false,
wantDelta: nil,
wantDirection: "new",
},
{
name: "baseline present, zero previous and current",
current: 0,
previous: 0,
hasBaseline: true,
wantDelta: nil,
wantDirection: "stable",
},
{
name: "baseline present, zero previous to positive",
current: 0.2,
previous: 0,
hasBaseline: true,
wantDelta: nil,
wantDirection: "up",
},
{
name: "baseline present, increase",
current: 0.15,
previous: 0.10,
hasBaseline: true,
wantDelta: ptr(50.0),
wantDirection: "up",
},
{
name: "baseline present, no change",
current: 0.10,
previous: 0.10,
hasBaseline: true,
wantDelta: ptr(0.0),
wantDirection: "stable",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CalculateTrendFloatWithBaseline(tt.current, tt.previous, tt.hasBaseline)
if tt.wantDelta == nil {
if got.Delta != nil {
t.Errorf("expected nil delta, got %v", *got.Delta)
}
} else {
if got.Delta == nil {
t.Errorf("expected delta %v, got nil", *tt.wantDelta)
} else if *got.Delta != *tt.wantDelta {
t.Errorf("expected delta %v, got %v", *tt.wantDelta, *got.Delta)
}
}
if got.Direction != tt.wantDirection {
t.Errorf("expected direction %q, got %q", tt.wantDirection, got.Direction)
}
})
}
}
func TestPreviousPeriodWindow(t *testing.T) {
// These tests verify the structure of the period calculation
// Not testing exact timestamps since they depend on current time
tests := []struct {
period string
expectZero bool
expectWindow bool
}{
{"today", false, true},
{"last7d", false, true},
{"last30d", false, true},
{"week", false, true},
{"month", false, true},
{"all", true, false},
{"invalid", true, false},
}
for _, tt := range tests {
t.Run(tt.period, func(t *testing.T) {
start, end := previousPeriodWindow(tt.period)
if tt.expectZero {
if !start.IsZero() || !end.IsZero() {
t.Errorf("expected zero times for period %q", tt.period)
}
} else {
if start.IsZero() || end.IsZero() {
t.Errorf("expected non-zero times for period %q", tt.period)
}
if !start.Before(end) {
t.Errorf("expected start < end for period %q", tt.period)
}
}
})
}
}
func TestPreviousPeriodWindowToday(t *testing.T) {
start, end := previousPeriodWindow("today")
// Previous period for "today" should be "yesterday"
now := time.Now().UTC()
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
yesterday := startOfToday.AddDate(0, 0, -1)
if !start.Equal(yesterday) {
t.Errorf("expected start to be yesterday (%v), got %v", yesterday, start)
}
if !end.Equal(startOfToday) {
t.Errorf("expected end to be start of today (%v), got %v", startOfToday, end)
}
}
func TestPreviousPeriodWindowLast7d(t *testing.T) {
start, end := previousPeriodWindow("last7d")
now := time.Now().UTC()
expected14DaysAgo := now.AddDate(0, 0, -14)
expected7DaysAgo := now.AddDate(0, 0, -7)
// Allow 1 second tolerance for test execution time
if start.Sub(expected14DaysAgo).Abs() > time.Second {
t.Errorf("expected start ~14 days ago, got %v (expected %v)", start, expected14DaysAgo)
}
if end.Sub(expected7DaysAgo).Abs() > time.Second {
t.Errorf("expected end ~7 days ago, got %v (expected %v)", end, expected7DaysAgo)
}
}
func ptr(v float64) *float64 {
return &v
}