Files
ez-api/internal/api/dashboard_handler_test.go
zenfun 08a8a1e42f feat(api): add trend analysis and extended periods to dashboard summary
- Add `include_trends` query parameter to enable trend calculation
- Implement trend comparison logic (delta % and direction) against previous periods
- Add support for `last7d`, `last30d`, and `all` time period options
- Update `DashboardSummaryResponse` to include optional `trends` field
- Add helper functions for custom time window aggregation
- Add unit tests for trend calculation and period window logic
- Update feature documentation with new parameters and response schemas
2026-01-02 22:30:38 +08:00

238 lines
5.6 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 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
}