GET /api/customers gains a summary object (total/online/depleted/ depletingSoon/disabled/enabled) computed server-side; the customers page renders it as a six-cell stats strip above the toolbar. Predicates aligned verbatim with 3x-ui client_paging.go: - depleted = (limit>0 && used>=limit) || (expiry set && expiry<=now), counted regardless of status; - depletingSoon = active && !depleted && (expiry within expireDiff days || remaining traffic < trafficDiff GB), thresholds from new settings expireDiff/trafficDiff with 3x-ui's factory default 0 (nobody counts until configured); - enabled = status active; disabled = manual suspensions only. Online reuses the existing 3-minute presence projection. Golden diff confined to the additive summary keys; summary_test.go covers every bucket with thresholds off (soon=0) and on (soon=2).
66 lines
2.6 KiB
Go
66 lines
2.6 KiB
Go
package main
|
|
|
|
// Tests for CustomerSummary (round-2 item 1): the six counts on the
|
|
// customers page, with depleted/depletingSoon predicates aligned to
|
|
// 3x-ui's depletedExpr/expiringExpr (client_paging.go) — including its
|
|
// panel-configurable expireDiff (days) / trafficDiff (GB) thresholds,
|
|
// which default to 0 and then count nobody as depleting soon.
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func summaryFixture(now time.Time) *Data {
|
|
iso := func(t time.Time) *string { s := t.UTC().Format(time.RFC3339); return &s }
|
|
manual := "manual"
|
|
expiredReason := "expired"
|
|
gb := float64(1073741824)
|
|
return &Data{Customers: []*Customer{
|
|
// active + online + plain
|
|
{ID: "cus_a", Name: "A", Status: "active", LastOnlineAt: iso(now)},
|
|
// active, traffic exhausted
|
|
{ID: "cus_b", Name: "B", Status: "active", TrafficLimitBytes: 1000, UsedBytes: 1000},
|
|
// active, 0.5GB remaining of 2GB
|
|
{ID: "cus_c", Name: "C", Status: "active", TrafficLimitBytes: 2 * gb, UsedBytes: int64(1.5 * gb)},
|
|
// active, expires in 2 days
|
|
{ID: "cus_d", Name: "D", Status: "active", ExpiresAt: iso(now.Add(48 * time.Hour))},
|
|
// active, expires in 10 days
|
|
{ID: "cus_e", Name: "E", Status: "active", ExpiresAt: iso(now.Add(240 * time.Hour))},
|
|
// manually disabled
|
|
{ID: "cus_f", Name: "F", Status: "suspended", SuspendReason: &manual, SuspendReasonPresent: true},
|
|
// expired (auto-suspended): depleted, not "disabled"
|
|
{ID: "cus_g", Name: "G", Status: "suspended", SuspendReason: &expiredReason, SuspendReasonPresent: true, ExpiresAt: iso(now.Add(-time.Hour))},
|
|
// active, unlimited with heavy usage: never depleted
|
|
{ID: "cus_h", Name: "H", Status: "active", UsedBytes: int64(9 * gb)},
|
|
}}
|
|
}
|
|
|
|
func TestCustomerSummaryDefaultThresholds(t *testing.T) {
|
|
now := time.Now()
|
|
d := summaryFixture(now)
|
|
s := CustomerSummary(d, now.UnixMilli())
|
|
want := map[string]int{"total": 8, "online": 1, "depleted": 2, "depletingSoon": 0, "disabled": 1, "enabled": 6}
|
|
for k, v := range want {
|
|
if s[k] != v {
|
|
t.Fatalf("summary[%s] = %v, want %d (full: %v)", k, s[k], v, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCustomerSummaryWithThresholds(t *testing.T) {
|
|
now := time.Now()
|
|
d := summaryFixture(now)
|
|
d.Settings.ExpireDiff = 3 // days
|
|
d.Settings.TrafficDiff = 1 // GB
|
|
s := CustomerSummary(d, now.UnixMilli())
|
|
// C (0.5GB remaining < 1GB) and D (expiry in 2d < 3d) become depleting soon;
|
|
// E (10d) does not. Nothing else moves.
|
|
if s["depletingSoon"] != 2 {
|
|
t.Fatalf("depletingSoon = %v, want 2 (full: %v)", s["depletingSoon"], s)
|
|
}
|
|
if s["depleted"] != 2 || s["enabled"] != 6 || s["disabled"] != 1 {
|
|
t.Fatalf("other counts moved: %v", s)
|
|
}
|
|
}
|