Files
NexusGate/panel-go/summary_test.go
T
aaaamoooo 6e82d0d06a feat(panel): customers stats strip with 3x-ui depletion semantics (round-2 #1)
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).
2026-10-20 20:56:28 +00:00

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)
}
}