| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- 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 is every disabled client",
- params: ClientPageParams{PageSize: 50, Filter: "deactive"},
- want: []string{"echo@x", "foxtrot@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 keeps enabled clients that still have room",
- params: ClientPageParams{PageSize: 50, Filter: "active"},
- want: []string{"alpha@x", "bravo@x", "golf@x", "hotel@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("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")
- }
- }
|