mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
5008906c4c
Each card on the Clients page now toggles its status bucket as the sole filter, and the Clients card clears it. The bucket filters used to be wider than the card counts: "active" still included clients near depletion and "deactive" included disabled clients that had run out, so a filtered list could disagree with the number on the card. Both filters now reuse the summary expressions, and a test pins each card's count to the size of its filtered list.
638 lines
21 KiB
Go
638 lines
21 KiB
Go
package service
|
|
|
|
import (
|
|
"slices"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
)
|
|
|
|
const (
|
|
pagingDay = int64(86400000)
|
|
pagingGB = int64(1) << 30
|
|
)
|
|
|
|
type pagingSeed struct {
|
|
email string
|
|
enable bool
|
|
totalGB int64
|
|
expiryTime int64
|
|
used int64
|
|
subID string
|
|
uuid string
|
|
password string
|
|
auth string
|
|
comment string
|
|
group string
|
|
tgID int64
|
|
reset int
|
|
lastOnline int64
|
|
inbounds []int
|
|
}
|
|
|
|
// seedPagingClients writes one vless and one trojan inbound plus a fixed client
|
|
// set covering every bucket, sort key and search field ListPaged supports.
|
|
// Returns "now" so the expectations can be phrased relative to it.
|
|
func seedPagingClients(t *testing.T) (int64, []pagingSeed) {
|
|
t.Helper()
|
|
db := database.GetDB()
|
|
now := time.Now().UnixMilli()
|
|
|
|
vless := &model.Inbound{UserId: 1, Tag: "in-vless", Enable: true, Port: 40001, Protocol: model.VLESS, Settings: `{"clients":[]}`}
|
|
trojan := &model.Inbound{UserId: 1, Tag: "in-trojan", Enable: true, Port: 40002, Protocol: model.Trojan, Settings: `{"clients":[]}`}
|
|
for _, ib := range []*model.Inbound{vless, trojan} {
|
|
if err := db.Create(ib).Error; err != nil {
|
|
t.Fatalf("create inbound %s: %v", ib.Tag, err)
|
|
}
|
|
}
|
|
|
|
seeds := []pagingSeed{
|
|
{email: "alpha@x", enable: true, totalGB: 0, expiryTime: 0, used: 5 * pagingGB, subID: "sub-alpha", uuid: "uuid-alpha", inbounds: []int{vless.Id}, lastOnline: now - 10*pagingDay},
|
|
{email: "bravo@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now + 30*pagingDay, used: pagingGB, password: "pw-bravo", inbounds: []int{vless.Id, trojan.Id}, lastOnline: now - pagingDay},
|
|
{email: "charlie@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now + 30*pagingDay, used: 10 * pagingGB, auth: "auth-charlie", inbounds: []int{trojan.Id}},
|
|
{email: "delta@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now - pagingDay, used: pagingGB, inbounds: []int{vless.Id}},
|
|
{email: "echo@x", enable: false, totalGB: 10 * pagingGB, expiryTime: now + 30*pagingDay, inbounds: []int{vless.Id}},
|
|
{email: "foxtrot@x", enable: false, totalGB: 10 * pagingGB, expiryTime: now - pagingDay, inbounds: []int{trojan.Id}},
|
|
{email: "golf@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now + 2*pagingDay, inbounds: []int{vless.Id}},
|
|
{email: "hotel@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now + 30*pagingDay, used: 10*pagingGB - pagingGB/2, inbounds: []int{vless.Id}},
|
|
{email: "india@x", enable: true, totalGB: 0, expiryTime: -5 * pagingDay, inbounds: []int{vless.Id}},
|
|
{email: "juliet@x", enable: true, comment: " vip customer ", group: "VIP", tgID: 555, reset: 7, inbounds: []int{vless.Id}},
|
|
{email: "kilo_1@x", enable: true, group: "vip", inbounds: nil},
|
|
{email: "kilo1@x", enable: true, inbounds: []int{trojan.Id}},
|
|
}
|
|
|
|
for i, s := range seeds {
|
|
rec := model.ClientRecord{
|
|
Email: s.email,
|
|
SubID: s.subID,
|
|
UUID: s.uuid,
|
|
Password: s.password,
|
|
Auth: s.auth,
|
|
Comment: s.comment,
|
|
Group: s.group,
|
|
TgID: s.tgID,
|
|
Reset: s.reset,
|
|
Enable: s.enable,
|
|
TotalGB: s.totalGB,
|
|
ExpiryTime: s.expiryTime,
|
|
CreatedAt: now - int64(len(seeds)-i)*pagingDay,
|
|
UpdatedAt: now - int64(i)*pagingDay,
|
|
}
|
|
if err := db.Create(&rec).Error; err != nil {
|
|
t.Fatalf("create client %s: %v", s.email, err)
|
|
}
|
|
if !s.enable {
|
|
// clients.enable carries a `default:true` tag, so GORM leaves the
|
|
// zero value out of the INSERT and the column comes back true.
|
|
// Restate updated_at so the autoUpdateTime hook cannot reshuffle
|
|
// the sort fixtures.
|
|
if err := db.Model(&model.ClientRecord{}).Where("id = ?", rec.Id).
|
|
Updates(map[string]any{"enable": false, "updated_at": rec.UpdatedAt}).Error; err != nil {
|
|
t.Fatalf("disable %s: %v", s.email, err)
|
|
}
|
|
}
|
|
traffic := xray.ClientTraffic{
|
|
Email: s.email,
|
|
Enable: s.enable,
|
|
Up: s.used / 2,
|
|
Down: s.used - s.used/2,
|
|
Total: s.totalGB,
|
|
ExpiryTime: s.expiryTime,
|
|
LastOnline: s.lastOnline,
|
|
}
|
|
if err := db.Create(&traffic).Error; err != nil {
|
|
t.Fatalf("create traffic %s: %v", s.email, err)
|
|
}
|
|
for _, id := range s.inbounds {
|
|
if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: id}).Error; err != nil {
|
|
t.Fatalf("attach %s to %d: %v", s.email, id, err)
|
|
}
|
|
}
|
|
}
|
|
return now, seeds
|
|
}
|
|
|
|
func pagedEmails(items []ClientSlim) []string {
|
|
out := make([]string, 0, len(items))
|
|
for _, it := range items {
|
|
out = append(out, it.Email)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func setupPagingServices(t *testing.T) (*ClientService, *InboundService, *SettingService) {
|
|
t.Helper()
|
|
setupBulkDB(t)
|
|
settingSvc := &SettingService{}
|
|
if err := settingSvc.setInt("expireDiff", 3); err != nil {
|
|
t.Fatalf("set expireDiff: %v", err)
|
|
}
|
|
if err := settingSvc.setInt("trafficDiff", 1); err != nil {
|
|
t.Fatalf("set trafficDiff: %v", err)
|
|
}
|
|
return &ClientService{}, &InboundService{}, settingSvc
|
|
}
|
|
|
|
func TestListPagedFilters(t *testing.T) {
|
|
svc, inboundSvc, settingSvc := setupPagingServices(t)
|
|
now, _ := seedPagingClients(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
params ClientPageParams
|
|
want []string
|
|
}{
|
|
{
|
|
name: "no filter returns every client in id order",
|
|
params: ClientPageParams{PageSize: 50},
|
|
want: []string{"alpha@x", "bravo@x", "charlie@x", "delta@x", "echo@x", "foxtrot@x", "golf@x", "hotel@x", "india@x", "juliet@x", "kilo_1@x", "kilo1@x"},
|
|
},
|
|
{
|
|
name: "depleted bucket covers quota and expiry",
|
|
params: ClientPageParams{PageSize: 50, Filter: "depleted"},
|
|
want: []string{"charlie@x", "delta@x", "foxtrot@x"},
|
|
},
|
|
{
|
|
name: "deactive bucket leaves a disabled client that ran out to depleted",
|
|
params: ClientPageParams{PageSize: 50, Filter: "deactive"},
|
|
want: []string{"echo@x"},
|
|
},
|
|
{
|
|
name: "expiring bucket covers near expiry and near quota",
|
|
params: ClientPageParams{PageSize: 50, Filter: "expiring"},
|
|
want: []string{"golf@x", "hotel@x"},
|
|
},
|
|
{
|
|
name: "active bucket leaves clients near depletion to expiring",
|
|
params: ClientPageParams{PageSize: 50, Filter: "active"},
|
|
want: []string{"alpha@x", "bravo@x", "india@x", "juliet@x", "kilo_1@x", "kilo1@x"},
|
|
},
|
|
{
|
|
name: "buckets are ORed",
|
|
params: ClientPageParams{PageSize: 50, Filter: "depleted,expiring"},
|
|
want: []string{"charlie@x", "delta@x", "foxtrot@x", "golf@x", "hotel@x"},
|
|
},
|
|
{
|
|
name: "unknown bucket keeps matching everything",
|
|
params: ClientPageParams{PageSize: 50, Filter: "nonsense"},
|
|
want: []string{"alpha@x", "bravo@x", "charlie@x", "delta@x", "echo@x", "foxtrot@x", "golf@x", "hotel@x", "india@x", "juliet@x", "kilo_1@x", "kilo1@x"},
|
|
},
|
|
{
|
|
name: "protocol filter follows the attachments",
|
|
params: ClientPageParams{PageSize: 50, Protocol: "trojan"},
|
|
want: []string{"bravo@x", "charlie@x", "foxtrot@x", "kilo1@x"},
|
|
},
|
|
{
|
|
name: "inbound filter follows the attachments",
|
|
params: ClientPageParams{PageSize: 50, Inbound: "2"},
|
|
want: []string{"bravo@x", "charlie@x", "foxtrot@x", "kilo1@x"},
|
|
},
|
|
{
|
|
name: "search matches the email",
|
|
params: ClientPageParams{PageSize: 50, Search: "KILO"},
|
|
want: []string{"kilo_1@x", "kilo1@x"},
|
|
},
|
|
{
|
|
name: "search treats LIKE wildcards literally",
|
|
params: ClientPageParams{PageSize: 50, Search: "kilo_1"},
|
|
want: []string{"kilo_1@x"},
|
|
},
|
|
{
|
|
name: "search matches the subId",
|
|
params: ClientPageParams{PageSize: 50, Search: "sub-alpha"},
|
|
want: []string{"alpha@x"},
|
|
},
|
|
{
|
|
name: "search matches the uuid",
|
|
params: ClientPageParams{PageSize: 50, Search: "uuid-alpha"},
|
|
want: []string{"alpha@x"},
|
|
},
|
|
{
|
|
name: "search matches the password",
|
|
params: ClientPageParams{PageSize: 50, Search: "pw-bravo"},
|
|
want: []string{"bravo@x"},
|
|
},
|
|
{
|
|
name: "search matches the auth",
|
|
params: ClientPageParams{PageSize: 50, Search: "auth-charlie"},
|
|
want: []string{"charlie@x"},
|
|
},
|
|
{
|
|
name: "search matches the comment",
|
|
params: ClientPageParams{PageSize: 50, Search: "vip customer"},
|
|
want: []string{"juliet@x"},
|
|
},
|
|
{
|
|
name: "search matches the telegram id",
|
|
params: ClientPageParams{PageSize: 50, Search: "555"},
|
|
want: []string{"juliet@x"},
|
|
},
|
|
{
|
|
name: "group filter is case insensitive",
|
|
params: ClientPageParams{PageSize: 50, Group: "vip"},
|
|
want: []string{"juliet@x", "kilo_1@x"},
|
|
},
|
|
{
|
|
name: "hasComment yes",
|
|
params: ClientPageParams{PageSize: 50, HasComment: "yes"},
|
|
want: []string{"juliet@x"},
|
|
},
|
|
{
|
|
name: "hasTgId yes",
|
|
params: ClientPageParams{PageSize: 50, HasTgID: "yes"},
|
|
want: []string{"juliet@x"},
|
|
},
|
|
{
|
|
name: "autoRenew on",
|
|
params: ClientPageParams{PageSize: 50, AutoRenew: "on"},
|
|
want: []string{"juliet@x"},
|
|
},
|
|
{
|
|
name: "usage range is inclusive on both bounds",
|
|
params: ClientPageParams{PageSize: 50, UsageFrom: pagingGB, UsageTo: 5 * pagingGB},
|
|
want: []string{"alpha@x", "bravo@x", "delta@x"},
|
|
},
|
|
{
|
|
name: "expiry range excludes never and delayed start",
|
|
params: ClientPageParams{PageSize: 50, ExpiryFrom: now, ExpiryTo: now + 10*pagingDay},
|
|
want: []string{"golf@x"},
|
|
},
|
|
{
|
|
name: "filters combine with AND",
|
|
params: ClientPageParams{PageSize: 50, Filter: "depleted", Protocol: "trojan"},
|
|
want: []string{"charlie@x", "foxtrot@x"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, tc.params)
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
got := pagedEmails(resp.Items)
|
|
if !slices.Equal(got, tc.want) {
|
|
t.Fatalf("emails = %v, want %v", got, tc.want)
|
|
}
|
|
if resp.Filtered != len(tc.want) {
|
|
t.Fatalf("filtered = %d, want %d", resp.Filtered, len(tc.want))
|
|
}
|
|
if resp.Total != 12 {
|
|
t.Fatalf("total = %d, want 12", resp.Total)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListPagedSorting(t *testing.T) {
|
|
svc, inboundSvc, settingSvc := setupPagingServices(t)
|
|
seedPagingClients(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
sort string
|
|
order string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "no sort key keeps insertion order",
|
|
want: []string{"alpha@x", "bravo@x", "charlie@x"},
|
|
},
|
|
{
|
|
name: "email ascending", sort: "email", order: "ascend",
|
|
want: []string{"alpha@x", "bravo@x", "charlie@x"},
|
|
},
|
|
{
|
|
name: "email descending", sort: "email", order: "descend",
|
|
want: []string{"kilo_1@x", "kilo1@x", "juliet@x"},
|
|
},
|
|
{
|
|
name: "traffic descending", sort: "traffic", order: "descend",
|
|
want: []string{"charlie@x", "hotel@x", "alpha@x"},
|
|
},
|
|
{
|
|
name: "remaining descending puts unlimited quotas first", sort: "remaining", order: "descend",
|
|
want: []string{"alpha@x", "india@x", "juliet@x"},
|
|
},
|
|
{
|
|
name: "expiry ascending starts with the expired", sort: "expiryTime", order: "ascend",
|
|
want: []string{"delta@x", "foxtrot@x", "golf@x"},
|
|
},
|
|
{
|
|
name: "createdAt ascending follows insertion", sort: "createdAt", order: "ascend",
|
|
want: []string{"alpha@x", "bravo@x", "charlie@x"},
|
|
},
|
|
{
|
|
name: "updatedAt descending starts with the newest", sort: "updatedAt", order: "descend",
|
|
want: []string{"alpha@x", "bravo@x", "charlie@x"},
|
|
},
|
|
{
|
|
name: "lastOnline descending breaks ties on the id, reversed too", sort: "lastOnline", order: "descend",
|
|
want: []string{"bravo@x", "alpha@x", "kilo1@x"},
|
|
},
|
|
{
|
|
name: "enable ascending puts disabled first", sort: "enable", order: "ascend",
|
|
want: []string{"echo@x", "foxtrot@x", "alpha@x"},
|
|
},
|
|
{
|
|
name: "inboundIds descending puts the widest attachment first", sort: "inboundIds", order: "descend",
|
|
want: []string{"bravo@x", "alpha@x", "charlie@x"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 3, Sort: tc.sort, Order: tc.order})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
got := pagedEmails(resp.Items)
|
|
if !slices.Equal(got, tc.want) {
|
|
t.Fatalf("emails = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListPagedPagination(t *testing.T) {
|
|
svc, inboundSvc, settingSvc := setupPagingServices(t)
|
|
seedPagingClients(t)
|
|
|
|
t.Run("second page continues where the first stopped", func(t *testing.T) {
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 2, PageSize: 5, Sort: "email", Order: "ascend"})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
want := []string{"foxtrot@x", "golf@x", "hotel@x", "india@x", "juliet@x"}
|
|
if got := pagedEmails(resp.Items); !slices.Equal(got, want) {
|
|
t.Fatalf("emails = %v, want %v", got, want)
|
|
}
|
|
if resp.Page != 2 || resp.PageSize != 5 {
|
|
t.Fatalf("page/pageSize = %d/%d, want 2/5", resp.Page, resp.PageSize)
|
|
}
|
|
})
|
|
|
|
t.Run("page past the end is empty but keeps the counts", func(t *testing.T) {
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 9, PageSize: 5})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
if len(resp.Items) != 0 {
|
|
t.Fatalf("items = %v, want none", pagedEmails(resp.Items))
|
|
}
|
|
if resp.Filtered != 12 || resp.Total != 12 {
|
|
t.Fatalf("filtered/total = %d/%d, want 12/12", resp.Filtered, resp.Total)
|
|
}
|
|
})
|
|
|
|
t.Run("page size is clamped to the maximum", func(t *testing.T) {
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 1, PageSize: 5000})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
if resp.PageSize != clientPageMaxSize {
|
|
t.Fatalf("pageSize = %d, want %d", resp.PageSize, clientPageMaxSize)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListPagedRowContents(t *testing.T) {
|
|
svc, inboundSvc, settingSvc := setupPagingServices(t)
|
|
seedPagingClients(t)
|
|
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 50})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
byEmail := make(map[string]ClientSlim, len(resp.Items))
|
|
for _, it := range resp.Items {
|
|
byEmail[it.Email] = it
|
|
}
|
|
|
|
t.Run("attachments are reported in inbound order", func(t *testing.T) {
|
|
got := byEmail["bravo@x"].InboundIds
|
|
if !slices.Equal(got, []int{1, 2}) {
|
|
t.Fatalf("inboundIds = %v, want [1 2]", got)
|
|
}
|
|
})
|
|
|
|
t.Run("an unattached client reports no inbounds", func(t *testing.T) {
|
|
if got := byEmail["kilo_1@x"].InboundIds; len(got) != 0 {
|
|
t.Fatalf("inboundIds = %v, want none", got)
|
|
}
|
|
})
|
|
|
|
t.Run("traffic counters ride along with the row", func(t *testing.T) {
|
|
got := byEmail["hotel@x"].Traffic
|
|
if got == nil {
|
|
t.Fatal("traffic = nil, want the seeded counters")
|
|
}
|
|
if want := 10*pagingGB - pagingGB/2; got.Up+got.Down != want {
|
|
t.Fatalf("used = %d, want %d", got.Up+got.Down, want)
|
|
}
|
|
})
|
|
|
|
t.Run("groups list every name in use", func(t *testing.T) {
|
|
if !slices.Equal(resp.Groups, []string{"vip", "VIP"}) && !slices.Equal(resp.Groups, []string{"VIP", "vip"}) {
|
|
t.Fatalf("groups = %v, want VIP and vip", resp.Groups)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListPagedSummary(t *testing.T) {
|
|
svc, inboundSvc, settingSvc := setupPagingServices(t)
|
|
seedPagingClients(t)
|
|
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 5, Filter: "depleted"})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
s := resp.Summary
|
|
|
|
t.Run("counts stay whole-panel while the page is filtered", func(t *testing.T) {
|
|
if s.Total != 12 {
|
|
t.Fatalf("total = %d, want 12", s.Total)
|
|
}
|
|
if s.DepletedCount != 3 {
|
|
t.Fatalf("depletedCount = %d, want 3", s.DepletedCount)
|
|
}
|
|
if s.ExpiringCount != 2 {
|
|
t.Fatalf("expiringCount = %d, want 2", s.ExpiringCount)
|
|
}
|
|
if s.DeactiveCount != 1 {
|
|
t.Fatalf("deactiveCount = %d, want 1", s.DeactiveCount)
|
|
}
|
|
if s.Active != 6 {
|
|
t.Fatalf("active = %d, want 6", s.Active)
|
|
}
|
|
})
|
|
|
|
t.Run("every client lands in exactly one counter", func(t *testing.T) {
|
|
if sum := s.Active + s.DepletedCount + s.ExpiringCount + s.DeactiveCount; sum != s.Total {
|
|
t.Fatalf("buckets sum to %d, want %d", sum, s.Total)
|
|
}
|
|
})
|
|
|
|
t.Run("clicking a stat card filters to exactly the clients it counts", func(t *testing.T) {
|
|
cards := map[string]int{"active": s.Active, "depleted": s.DepletedCount, "expiring": s.ExpiringCount, "deactive": s.DeactiveCount}
|
|
for bucket, count := range cards {
|
|
page, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 50, Filter: bucket})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged(%s): %v", bucket, err)
|
|
}
|
|
if page.Filtered != count {
|
|
t.Fatalf("filter %q matched %d clients, card counts %d", bucket, page.Filtered, count)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("bucket lists carry the matching emails", func(t *testing.T) {
|
|
if want := []string{"charlie@x", "delta@x", "foxtrot@x"}; !slices.Equal(s.Depleted, want) {
|
|
t.Fatalf("depleted = %v, want %v", s.Depleted, want)
|
|
}
|
|
if want := []string{"golf@x", "hotel@x"}; !slices.Equal(s.Expiring, want) {
|
|
t.Fatalf("expiring = %v, want %v", s.Expiring, want)
|
|
}
|
|
if want := []string{"echo@x"}; !slices.Equal(s.Deactive, want) {
|
|
t.Fatalf("deactive = %v, want %v", s.Deactive, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListPagedSummaryEmailListsAreCapped(t *testing.T) {
|
|
svc, inboundSvc, settingSvc := setupPagingServices(t)
|
|
db := database.GetDB()
|
|
|
|
const n = clientSummaryEmailCap + 25
|
|
past := time.Now().UnixMilli() - pagingDay
|
|
for i := range n {
|
|
rec := model.ClientRecord{Email: "bulk-" + strconv.Itoa(i) + "@x", Enable: true, TotalGB: pagingGB, ExpiryTime: past}
|
|
if err := db.Create(&rec).Error; err != nil {
|
|
t.Fatalf("create client %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 25})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
if resp.Summary.DepletedCount != n {
|
|
t.Fatalf("depletedCount = %d, want %d", resp.Summary.DepletedCount, n)
|
|
}
|
|
if len(resp.Summary.Depleted) != clientSummaryEmailCap {
|
|
t.Fatalf("depleted list = %d entries, want %d", len(resp.Summary.Depleted), clientSummaryEmailCap)
|
|
}
|
|
}
|
|
|
|
func TestListPagedGlobalTrafficOverlay(t *testing.T) {
|
|
svc, inboundSvc, settingSvc := setupPagingServices(t)
|
|
seedPagingClients(t)
|
|
|
|
// bravo has used 1GB of its 10GB locally; a master reporting 10GB of
|
|
// cross-panel usage has to move it into the depleted bucket.
|
|
if err := inboundSvc.AcceptGlobalTraffic("master-guid", []*xray.ClientTraffic{
|
|
{Email: "bravo@x", Up: 4 * pagingGB, Down: 6 * pagingGB},
|
|
}); err != nil {
|
|
t.Fatalf("AcceptGlobalTraffic: %v", err)
|
|
}
|
|
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 50, Filter: "depleted"})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged: %v", err)
|
|
}
|
|
want := []string{"bravo@x", "charlie@x", "delta@x", "foxtrot@x"}
|
|
if got := pagedEmails(resp.Items); !slices.Equal(got, want) {
|
|
t.Fatalf("depleted = %v, want %v", got, want)
|
|
}
|
|
if resp.Summary.DepletedCount != 4 {
|
|
t.Fatalf("depletedCount = %d, want 4", resp.Summary.DepletedCount)
|
|
}
|
|
for _, it := range resp.Items {
|
|
if it.Email != "bravo@x" {
|
|
continue
|
|
}
|
|
if it.Traffic == nil || it.Traffic.Up+it.Traffic.Down != 10*pagingGB {
|
|
t.Fatalf("bravo traffic = %+v, want the overlaid 10GB", it.Traffic)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClientQueryOnlineEmails(t *testing.T) {
|
|
_, _, _ = setupPagingServices(t)
|
|
seedPagingClients(t)
|
|
|
|
q := newClientQuery(database.GetDB(), time.Now().UnixMilli(), 0, 0)
|
|
emails, count, err := q.onlineEmails([]string{"alpha@x", "echo@x", "ghost@x", "kilo1@x"})
|
|
if err != nil {
|
|
t.Fatalf("onlineEmails: %v", err)
|
|
}
|
|
if want := []string{"alpha@x", "kilo1@x"}; !slices.Equal(emails, want) {
|
|
t.Fatalf("online = %v, want %v (disabled and unknown emails drop out)", emails, want)
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("count = %d, want 2", count)
|
|
}
|
|
}
|
|
|
|
func TestEmailInCondChunksLargeSets(t *testing.T) {
|
|
emails := make([]string, sqlInChunk+1)
|
|
for i := range emails {
|
|
emails[i] = "e" + strconv.Itoa(i)
|
|
}
|
|
|
|
cond, args := emailInCond("c.email", emails)
|
|
if want := "(c.email IN ? OR c.email IN ?)"; cond != want {
|
|
t.Fatalf("cond = %q, want %q", cond, want)
|
|
}
|
|
if len(args) != 2 {
|
|
t.Fatalf("args = %d chunks, want 2", len(args))
|
|
}
|
|
if first, ok := args[0].([]string); !ok || len(first) != sqlInChunk {
|
|
t.Fatalf("first chunk = %v, want %d entries", args[0], sqlInChunk)
|
|
}
|
|
|
|
emptyCond, emptyArgs := emailInCond("c.email", nil)
|
|
if emptyCond != "1 = 0" || emptyArgs != nil {
|
|
t.Fatalf("empty set = %q/%v, want an always-false predicate", emptyCond, emptyArgs)
|
|
}
|
|
}
|
|
|
|
func TestEscapeLikeLiteral(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"plain", "plain"},
|
|
{"a_b", `a\_b`},
|
|
{"50%", `50\%`},
|
|
{`back\slash`, `back\\slash`},
|
|
}
|
|
for _, tc := range tests {
|
|
if got := escapeLikeLiteral(tc.in); got != tc.want {
|
|
t.Fatalf("escapeLikeLiteral(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListPagedEmptyPanel(t *testing.T) {
|
|
svc, inboundSvc, settingSvc := setupPagingServices(t)
|
|
|
|
resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{})
|
|
if err != nil {
|
|
t.Fatalf("ListPaged on a panel with no clients: %v", err)
|
|
}
|
|
if len(resp.Items) != 0 || resp.Total != 0 || resp.Filtered != 0 {
|
|
t.Fatalf("items/total/filtered = %d/%d/%d, want 0/0/0", len(resp.Items), resp.Total, resp.Filtered)
|
|
}
|
|
if resp.Summary.Active != 0 || resp.Summary.DepletedCount != 0 {
|
|
t.Fatalf("summary = %+v, want zeroed counters", resp.Summary)
|
|
}
|
|
if resp.Groups == nil {
|
|
t.Fatal("groups = nil, want an empty list so the filter drawer renders")
|
|
}
|
|
}
|