1
0

client_paging_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. package service
  2. import (
  3. "slices"
  4. "strconv"
  5. "testing"
  6. "time"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  10. )
  11. const (
  12. pagingDay = int64(86400000)
  13. pagingGB = int64(1) << 30
  14. )
  15. type pagingSeed struct {
  16. email string
  17. enable bool
  18. totalGB int64
  19. expiryTime int64
  20. used int64
  21. subID string
  22. uuid string
  23. password string
  24. auth string
  25. comment string
  26. group string
  27. tgID int64
  28. reset int
  29. lastOnline int64
  30. inbounds []int
  31. }
  32. // seedPagingClients writes one vless and one trojan inbound plus a fixed client
  33. // set covering every bucket, sort key and search field ListPaged supports.
  34. // Returns "now" so the expectations can be phrased relative to it.
  35. func seedPagingClients(t *testing.T) (int64, []pagingSeed) {
  36. t.Helper()
  37. db := database.GetDB()
  38. now := time.Now().UnixMilli()
  39. vless := &model.Inbound{UserId: 1, Tag: "in-vless", Enable: true, Port: 40001, Protocol: model.VLESS, Settings: `{"clients":[]}`}
  40. trojan := &model.Inbound{UserId: 1, Tag: "in-trojan", Enable: true, Port: 40002, Protocol: model.Trojan, Settings: `{"clients":[]}`}
  41. for _, ib := range []*model.Inbound{vless, trojan} {
  42. if err := db.Create(ib).Error; err != nil {
  43. t.Fatalf("create inbound %s: %v", ib.Tag, err)
  44. }
  45. }
  46. seeds := []pagingSeed{
  47. {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},
  48. {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},
  49. {email: "charlie@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now + 30*pagingDay, used: 10 * pagingGB, auth: "auth-charlie", inbounds: []int{trojan.Id}},
  50. {email: "delta@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now - pagingDay, used: pagingGB, inbounds: []int{vless.Id}},
  51. {email: "echo@x", enable: false, totalGB: 10 * pagingGB, expiryTime: now + 30*pagingDay, inbounds: []int{vless.Id}},
  52. {email: "foxtrot@x", enable: false, totalGB: 10 * pagingGB, expiryTime: now - pagingDay, inbounds: []int{trojan.Id}},
  53. {email: "golf@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now + 2*pagingDay, inbounds: []int{vless.Id}},
  54. {email: "hotel@x", enable: true, totalGB: 10 * pagingGB, expiryTime: now + 30*pagingDay, used: 10*pagingGB - pagingGB/2, inbounds: []int{vless.Id}},
  55. {email: "india@x", enable: true, totalGB: 0, expiryTime: -5 * pagingDay, inbounds: []int{vless.Id}},
  56. {email: "juliet@x", enable: true, comment: " vip customer ", group: "VIP", tgID: 555, reset: 7, inbounds: []int{vless.Id}},
  57. {email: "kilo_1@x", enable: true, group: "vip", inbounds: nil},
  58. {email: "kilo1@x", enable: true, inbounds: []int{trojan.Id}},
  59. }
  60. for i, s := range seeds {
  61. rec := model.ClientRecord{
  62. Email: s.email,
  63. SubID: s.subID,
  64. UUID: s.uuid,
  65. Password: s.password,
  66. Auth: s.auth,
  67. Comment: s.comment,
  68. Group: s.group,
  69. TgID: s.tgID,
  70. Reset: s.reset,
  71. Enable: s.enable,
  72. TotalGB: s.totalGB,
  73. ExpiryTime: s.expiryTime,
  74. CreatedAt: now - int64(len(seeds)-i)*pagingDay,
  75. UpdatedAt: now - int64(i)*pagingDay,
  76. }
  77. if err := db.Create(&rec).Error; err != nil {
  78. t.Fatalf("create client %s: %v", s.email, err)
  79. }
  80. if !s.enable {
  81. // clients.enable carries a `default:true` tag, so GORM leaves the
  82. // zero value out of the INSERT and the column comes back true.
  83. // Restate updated_at so the autoUpdateTime hook cannot reshuffle
  84. // the sort fixtures.
  85. if err := db.Model(&model.ClientRecord{}).Where("id = ?", rec.Id).
  86. Updates(map[string]any{"enable": false, "updated_at": rec.UpdatedAt}).Error; err != nil {
  87. t.Fatalf("disable %s: %v", s.email, err)
  88. }
  89. }
  90. traffic := xray.ClientTraffic{
  91. Email: s.email,
  92. Enable: s.enable,
  93. Up: s.used / 2,
  94. Down: s.used - s.used/2,
  95. Total: s.totalGB,
  96. ExpiryTime: s.expiryTime,
  97. LastOnline: s.lastOnline,
  98. }
  99. if err := db.Create(&traffic).Error; err != nil {
  100. t.Fatalf("create traffic %s: %v", s.email, err)
  101. }
  102. for _, id := range s.inbounds {
  103. if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: id}).Error; err != nil {
  104. t.Fatalf("attach %s to %d: %v", s.email, id, err)
  105. }
  106. }
  107. }
  108. return now, seeds
  109. }
  110. func pagedEmails(items []ClientSlim) []string {
  111. out := make([]string, 0, len(items))
  112. for _, it := range items {
  113. out = append(out, it.Email)
  114. }
  115. return out
  116. }
  117. func setupPagingServices(t *testing.T) (*ClientService, *InboundService, *SettingService) {
  118. t.Helper()
  119. setupBulkDB(t)
  120. settingSvc := &SettingService{}
  121. if err := settingSvc.setInt("expireDiff", 3); err != nil {
  122. t.Fatalf("set expireDiff: %v", err)
  123. }
  124. if err := settingSvc.setInt("trafficDiff", 1); err != nil {
  125. t.Fatalf("set trafficDiff: %v", err)
  126. }
  127. return &ClientService{}, &InboundService{}, settingSvc
  128. }
  129. func TestListPagedFilters(t *testing.T) {
  130. svc, inboundSvc, settingSvc := setupPagingServices(t)
  131. now, _ := seedPagingClients(t)
  132. tests := []struct {
  133. name string
  134. params ClientPageParams
  135. want []string
  136. }{
  137. {
  138. name: "no filter returns every client in id order",
  139. params: ClientPageParams{PageSize: 50},
  140. 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"},
  141. },
  142. {
  143. name: "depleted bucket covers quota and expiry",
  144. params: ClientPageParams{PageSize: 50, Filter: "depleted"},
  145. want: []string{"charlie@x", "delta@x", "foxtrot@x"},
  146. },
  147. {
  148. name: "deactive bucket is every disabled client",
  149. params: ClientPageParams{PageSize: 50, Filter: "deactive"},
  150. want: []string{"echo@x", "foxtrot@x"},
  151. },
  152. {
  153. name: "expiring bucket covers near expiry and near quota",
  154. params: ClientPageParams{PageSize: 50, Filter: "expiring"},
  155. want: []string{"golf@x", "hotel@x"},
  156. },
  157. {
  158. name: "active bucket keeps enabled clients that still have room",
  159. params: ClientPageParams{PageSize: 50, Filter: "active"},
  160. want: []string{"alpha@x", "bravo@x", "golf@x", "hotel@x", "india@x", "juliet@x", "kilo_1@x", "kilo1@x"},
  161. },
  162. {
  163. name: "buckets are ORed",
  164. params: ClientPageParams{PageSize: 50, Filter: "depleted,expiring"},
  165. want: []string{"charlie@x", "delta@x", "foxtrot@x", "golf@x", "hotel@x"},
  166. },
  167. {
  168. name: "unknown bucket keeps matching everything",
  169. params: ClientPageParams{PageSize: 50, Filter: "nonsense"},
  170. 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"},
  171. },
  172. {
  173. name: "protocol filter follows the attachments",
  174. params: ClientPageParams{PageSize: 50, Protocol: "trojan"},
  175. want: []string{"bravo@x", "charlie@x", "foxtrot@x", "kilo1@x"},
  176. },
  177. {
  178. name: "inbound filter follows the attachments",
  179. params: ClientPageParams{PageSize: 50, Inbound: "2"},
  180. want: []string{"bravo@x", "charlie@x", "foxtrot@x", "kilo1@x"},
  181. },
  182. {
  183. name: "search matches the email",
  184. params: ClientPageParams{PageSize: 50, Search: "KILO"},
  185. want: []string{"kilo_1@x", "kilo1@x"},
  186. },
  187. {
  188. name: "search treats LIKE wildcards literally",
  189. params: ClientPageParams{PageSize: 50, Search: "kilo_1"},
  190. want: []string{"kilo_1@x"},
  191. },
  192. {
  193. name: "search matches the subId",
  194. params: ClientPageParams{PageSize: 50, Search: "sub-alpha"},
  195. want: []string{"alpha@x"},
  196. },
  197. {
  198. name: "search matches the uuid",
  199. params: ClientPageParams{PageSize: 50, Search: "uuid-alpha"},
  200. want: []string{"alpha@x"},
  201. },
  202. {
  203. name: "search matches the password",
  204. params: ClientPageParams{PageSize: 50, Search: "pw-bravo"},
  205. want: []string{"bravo@x"},
  206. },
  207. {
  208. name: "search matches the auth",
  209. params: ClientPageParams{PageSize: 50, Search: "auth-charlie"},
  210. want: []string{"charlie@x"},
  211. },
  212. {
  213. name: "search matches the comment",
  214. params: ClientPageParams{PageSize: 50, Search: "vip customer"},
  215. want: []string{"juliet@x"},
  216. },
  217. {
  218. name: "search matches the telegram id",
  219. params: ClientPageParams{PageSize: 50, Search: "555"},
  220. want: []string{"juliet@x"},
  221. },
  222. {
  223. name: "group filter is case insensitive",
  224. params: ClientPageParams{PageSize: 50, Group: "vip"},
  225. want: []string{"juliet@x", "kilo_1@x"},
  226. },
  227. {
  228. name: "hasComment yes",
  229. params: ClientPageParams{PageSize: 50, HasComment: "yes"},
  230. want: []string{"juliet@x"},
  231. },
  232. {
  233. name: "hasTgId yes",
  234. params: ClientPageParams{PageSize: 50, HasTgID: "yes"},
  235. want: []string{"juliet@x"},
  236. },
  237. {
  238. name: "autoRenew on",
  239. params: ClientPageParams{PageSize: 50, AutoRenew: "on"},
  240. want: []string{"juliet@x"},
  241. },
  242. {
  243. name: "usage range is inclusive on both bounds",
  244. params: ClientPageParams{PageSize: 50, UsageFrom: pagingGB, UsageTo: 5 * pagingGB},
  245. want: []string{"alpha@x", "bravo@x", "delta@x"},
  246. },
  247. {
  248. name: "expiry range excludes never and delayed start",
  249. params: ClientPageParams{PageSize: 50, ExpiryFrom: now, ExpiryTo: now + 10*pagingDay},
  250. want: []string{"golf@x"},
  251. },
  252. {
  253. name: "filters combine with AND",
  254. params: ClientPageParams{PageSize: 50, Filter: "depleted", Protocol: "trojan"},
  255. want: []string{"charlie@x", "foxtrot@x"},
  256. },
  257. }
  258. for _, tc := range tests {
  259. t.Run(tc.name, func(t *testing.T) {
  260. resp, err := svc.ListPaged(inboundSvc, settingSvc, tc.params)
  261. if err != nil {
  262. t.Fatalf("ListPaged: %v", err)
  263. }
  264. got := pagedEmails(resp.Items)
  265. if !slices.Equal(got, tc.want) {
  266. t.Fatalf("emails = %v, want %v", got, tc.want)
  267. }
  268. if resp.Filtered != len(tc.want) {
  269. t.Fatalf("filtered = %d, want %d", resp.Filtered, len(tc.want))
  270. }
  271. if resp.Total != 12 {
  272. t.Fatalf("total = %d, want 12", resp.Total)
  273. }
  274. })
  275. }
  276. }
  277. func TestListPagedSorting(t *testing.T) {
  278. svc, inboundSvc, settingSvc := setupPagingServices(t)
  279. seedPagingClients(t)
  280. tests := []struct {
  281. name string
  282. sort string
  283. order string
  284. want []string
  285. }{
  286. {
  287. name: "no sort key keeps insertion order",
  288. want: []string{"alpha@x", "bravo@x", "charlie@x"},
  289. },
  290. {
  291. name: "email ascending", sort: "email", order: "ascend",
  292. want: []string{"alpha@x", "bravo@x", "charlie@x"},
  293. },
  294. {
  295. name: "email descending", sort: "email", order: "descend",
  296. want: []string{"kilo_1@x", "kilo1@x", "juliet@x"},
  297. },
  298. {
  299. name: "traffic descending", sort: "traffic", order: "descend",
  300. want: []string{"charlie@x", "hotel@x", "alpha@x"},
  301. },
  302. {
  303. name: "remaining descending puts unlimited quotas first", sort: "remaining", order: "descend",
  304. want: []string{"alpha@x", "india@x", "juliet@x"},
  305. },
  306. {
  307. name: "expiry ascending starts with the expired", sort: "expiryTime", order: "ascend",
  308. want: []string{"delta@x", "foxtrot@x", "golf@x"},
  309. },
  310. {
  311. name: "createdAt ascending follows insertion", sort: "createdAt", order: "ascend",
  312. want: []string{"alpha@x", "bravo@x", "charlie@x"},
  313. },
  314. {
  315. name: "updatedAt descending starts with the newest", sort: "updatedAt", order: "descend",
  316. want: []string{"alpha@x", "bravo@x", "charlie@x"},
  317. },
  318. {
  319. name: "lastOnline descending breaks ties on the id, reversed too", sort: "lastOnline", order: "descend",
  320. want: []string{"bravo@x", "alpha@x", "kilo1@x"},
  321. },
  322. {
  323. name: "enable ascending puts disabled first", sort: "enable", order: "ascend",
  324. want: []string{"echo@x", "foxtrot@x", "alpha@x"},
  325. },
  326. {
  327. name: "inboundIds descending puts the widest attachment first", sort: "inboundIds", order: "descend",
  328. want: []string{"bravo@x", "alpha@x", "charlie@x"},
  329. },
  330. }
  331. for _, tc := range tests {
  332. t.Run(tc.name, func(t *testing.T) {
  333. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 3, Sort: tc.sort, Order: tc.order})
  334. if err != nil {
  335. t.Fatalf("ListPaged: %v", err)
  336. }
  337. got := pagedEmails(resp.Items)
  338. if !slices.Equal(got, tc.want) {
  339. t.Fatalf("emails = %v, want %v", got, tc.want)
  340. }
  341. })
  342. }
  343. }
  344. func TestListPagedPagination(t *testing.T) {
  345. svc, inboundSvc, settingSvc := setupPagingServices(t)
  346. seedPagingClients(t)
  347. t.Run("second page continues where the first stopped", func(t *testing.T) {
  348. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 2, PageSize: 5, Sort: "email", Order: "ascend"})
  349. if err != nil {
  350. t.Fatalf("ListPaged: %v", err)
  351. }
  352. want := []string{"foxtrot@x", "golf@x", "hotel@x", "india@x", "juliet@x"}
  353. if got := pagedEmails(resp.Items); !slices.Equal(got, want) {
  354. t.Fatalf("emails = %v, want %v", got, want)
  355. }
  356. if resp.Page != 2 || resp.PageSize != 5 {
  357. t.Fatalf("page/pageSize = %d/%d, want 2/5", resp.Page, resp.PageSize)
  358. }
  359. })
  360. t.Run("page past the end is empty but keeps the counts", func(t *testing.T) {
  361. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 9, PageSize: 5})
  362. if err != nil {
  363. t.Fatalf("ListPaged: %v", err)
  364. }
  365. if len(resp.Items) != 0 {
  366. t.Fatalf("items = %v, want none", pagedEmails(resp.Items))
  367. }
  368. if resp.Filtered != 12 || resp.Total != 12 {
  369. t.Fatalf("filtered/total = %d/%d, want 12/12", resp.Filtered, resp.Total)
  370. }
  371. })
  372. t.Run("page size is clamped to the maximum", func(t *testing.T) {
  373. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 1, PageSize: 5000})
  374. if err != nil {
  375. t.Fatalf("ListPaged: %v", err)
  376. }
  377. if resp.PageSize != clientPageMaxSize {
  378. t.Fatalf("pageSize = %d, want %d", resp.PageSize, clientPageMaxSize)
  379. }
  380. })
  381. }
  382. func TestListPagedRowContents(t *testing.T) {
  383. svc, inboundSvc, settingSvc := setupPagingServices(t)
  384. seedPagingClients(t)
  385. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 50})
  386. if err != nil {
  387. t.Fatalf("ListPaged: %v", err)
  388. }
  389. byEmail := make(map[string]ClientSlim, len(resp.Items))
  390. for _, it := range resp.Items {
  391. byEmail[it.Email] = it
  392. }
  393. t.Run("attachments are reported in inbound order", func(t *testing.T) {
  394. got := byEmail["bravo@x"].InboundIds
  395. if !slices.Equal(got, []int{1, 2}) {
  396. t.Fatalf("inboundIds = %v, want [1 2]", got)
  397. }
  398. })
  399. t.Run("an unattached client reports no inbounds", func(t *testing.T) {
  400. if got := byEmail["kilo_1@x"].InboundIds; len(got) != 0 {
  401. t.Fatalf("inboundIds = %v, want none", got)
  402. }
  403. })
  404. t.Run("traffic counters ride along with the row", func(t *testing.T) {
  405. got := byEmail["hotel@x"].Traffic
  406. if got == nil {
  407. t.Fatal("traffic = nil, want the seeded counters")
  408. }
  409. if want := 10*pagingGB - pagingGB/2; got.Up+got.Down != want {
  410. t.Fatalf("used = %d, want %d", got.Up+got.Down, want)
  411. }
  412. })
  413. t.Run("groups list every name in use", func(t *testing.T) {
  414. if !slices.Equal(resp.Groups, []string{"vip", "VIP"}) && !slices.Equal(resp.Groups, []string{"VIP", "vip"}) {
  415. t.Fatalf("groups = %v, want VIP and vip", resp.Groups)
  416. }
  417. })
  418. }
  419. func TestListPagedSummary(t *testing.T) {
  420. svc, inboundSvc, settingSvc := setupPagingServices(t)
  421. seedPagingClients(t)
  422. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 5, Filter: "depleted"})
  423. if err != nil {
  424. t.Fatalf("ListPaged: %v", err)
  425. }
  426. s := resp.Summary
  427. t.Run("counts stay whole-panel while the page is filtered", func(t *testing.T) {
  428. if s.Total != 12 {
  429. t.Fatalf("total = %d, want 12", s.Total)
  430. }
  431. if s.DepletedCount != 3 {
  432. t.Fatalf("depletedCount = %d, want 3", s.DepletedCount)
  433. }
  434. if s.ExpiringCount != 2 {
  435. t.Fatalf("expiringCount = %d, want 2", s.ExpiringCount)
  436. }
  437. if s.DeactiveCount != 1 {
  438. t.Fatalf("deactiveCount = %d, want 1", s.DeactiveCount)
  439. }
  440. if s.Active != 6 {
  441. t.Fatalf("active = %d, want 6", s.Active)
  442. }
  443. })
  444. t.Run("every client lands in exactly one counter", func(t *testing.T) {
  445. if sum := s.Active + s.DepletedCount + s.ExpiringCount + s.DeactiveCount; sum != s.Total {
  446. t.Fatalf("buckets sum to %d, want %d", sum, s.Total)
  447. }
  448. })
  449. t.Run("bucket lists carry the matching emails", func(t *testing.T) {
  450. if want := []string{"charlie@x", "delta@x", "foxtrot@x"}; !slices.Equal(s.Depleted, want) {
  451. t.Fatalf("depleted = %v, want %v", s.Depleted, want)
  452. }
  453. if want := []string{"golf@x", "hotel@x"}; !slices.Equal(s.Expiring, want) {
  454. t.Fatalf("expiring = %v, want %v", s.Expiring, want)
  455. }
  456. if want := []string{"echo@x"}; !slices.Equal(s.Deactive, want) {
  457. t.Fatalf("deactive = %v, want %v", s.Deactive, want)
  458. }
  459. })
  460. }
  461. func TestListPagedSummaryEmailListsAreCapped(t *testing.T) {
  462. svc, inboundSvc, settingSvc := setupPagingServices(t)
  463. db := database.GetDB()
  464. const n = clientSummaryEmailCap + 25
  465. past := time.Now().UnixMilli() - pagingDay
  466. for i := range n {
  467. rec := model.ClientRecord{Email: "bulk-" + strconv.Itoa(i) + "@x", Enable: true, TotalGB: pagingGB, ExpiryTime: past}
  468. if err := db.Create(&rec).Error; err != nil {
  469. t.Fatalf("create client %d: %v", i, err)
  470. }
  471. }
  472. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 25})
  473. if err != nil {
  474. t.Fatalf("ListPaged: %v", err)
  475. }
  476. if resp.Summary.DepletedCount != n {
  477. t.Fatalf("depletedCount = %d, want %d", resp.Summary.DepletedCount, n)
  478. }
  479. if len(resp.Summary.Depleted) != clientSummaryEmailCap {
  480. t.Fatalf("depleted list = %d entries, want %d", len(resp.Summary.Depleted), clientSummaryEmailCap)
  481. }
  482. }
  483. func TestListPagedGlobalTrafficOverlay(t *testing.T) {
  484. svc, inboundSvc, settingSvc := setupPagingServices(t)
  485. seedPagingClients(t)
  486. // bravo has used 1GB of its 10GB locally; a master reporting 10GB of
  487. // cross-panel usage has to move it into the depleted bucket.
  488. if err := inboundSvc.AcceptGlobalTraffic("master-guid", []*xray.ClientTraffic{
  489. {Email: "bravo@x", Up: 4 * pagingGB, Down: 6 * pagingGB},
  490. }); err != nil {
  491. t.Fatalf("AcceptGlobalTraffic: %v", err)
  492. }
  493. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{PageSize: 50, Filter: "depleted"})
  494. if err != nil {
  495. t.Fatalf("ListPaged: %v", err)
  496. }
  497. want := []string{"bravo@x", "charlie@x", "delta@x", "foxtrot@x"}
  498. if got := pagedEmails(resp.Items); !slices.Equal(got, want) {
  499. t.Fatalf("depleted = %v, want %v", got, want)
  500. }
  501. if resp.Summary.DepletedCount != 4 {
  502. t.Fatalf("depletedCount = %d, want 4", resp.Summary.DepletedCount)
  503. }
  504. for _, it := range resp.Items {
  505. if it.Email != "bravo@x" {
  506. continue
  507. }
  508. if it.Traffic == nil || it.Traffic.Up+it.Traffic.Down != 10*pagingGB {
  509. t.Fatalf("bravo traffic = %+v, want the overlaid 10GB", it.Traffic)
  510. }
  511. }
  512. }
  513. func TestClientQueryOnlineEmails(t *testing.T) {
  514. _, _, _ = setupPagingServices(t)
  515. seedPagingClients(t)
  516. q := newClientQuery(database.GetDB(), time.Now().UnixMilli(), 0, 0)
  517. emails, count, err := q.onlineEmails([]string{"alpha@x", "echo@x", "ghost@x", "kilo1@x"})
  518. if err != nil {
  519. t.Fatalf("onlineEmails: %v", err)
  520. }
  521. if want := []string{"alpha@x", "kilo1@x"}; !slices.Equal(emails, want) {
  522. t.Fatalf("online = %v, want %v (disabled and unknown emails drop out)", emails, want)
  523. }
  524. if count != 2 {
  525. t.Fatalf("count = %d, want 2", count)
  526. }
  527. }
  528. func TestEmailInCondChunksLargeSets(t *testing.T) {
  529. emails := make([]string, sqlInChunk+1)
  530. for i := range emails {
  531. emails[i] = "e" + strconv.Itoa(i)
  532. }
  533. cond, args := emailInCond("c.email", emails)
  534. if want := "(c.email IN ? OR c.email IN ?)"; cond != want {
  535. t.Fatalf("cond = %q, want %q", cond, want)
  536. }
  537. if len(args) != 2 {
  538. t.Fatalf("args = %d chunks, want 2", len(args))
  539. }
  540. if first, ok := args[0].([]string); !ok || len(first) != sqlInChunk {
  541. t.Fatalf("first chunk = %v, want %d entries", args[0], sqlInChunk)
  542. }
  543. emptyCond, emptyArgs := emailInCond("c.email", nil)
  544. if emptyCond != "1 = 0" || emptyArgs != nil {
  545. t.Fatalf("empty set = %q/%v, want an always-false predicate", emptyCond, emptyArgs)
  546. }
  547. }
  548. func TestEscapeLikeLiteral(t *testing.T) {
  549. tests := []struct {
  550. in string
  551. want string
  552. }{
  553. {"plain", "plain"},
  554. {"a_b", `a\_b`},
  555. {"50%", `50\%`},
  556. {`back\slash`, `back\\slash`},
  557. }
  558. for _, tc := range tests {
  559. if got := escapeLikeLiteral(tc.in); got != tc.want {
  560. t.Fatalf("escapeLikeLiteral(%q) = %q, want %q", tc.in, got, tc.want)
  561. }
  562. }
  563. }
  564. func TestListPagedEmptyPanel(t *testing.T) {
  565. svc, inboundSvc, settingSvc := setupPagingServices(t)
  566. resp, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{})
  567. if err != nil {
  568. t.Fatalf("ListPaged on a panel with no clients: %v", err)
  569. }
  570. if len(resp.Items) != 0 || resp.Total != 0 || resp.Filtered != 0 {
  571. t.Fatalf("items/total/filtered = %d/%d/%d, want 0/0/0", len(resp.Items), resp.Total, resp.Filtered)
  572. }
  573. if resp.Summary.Active != 0 || resp.Summary.DepletedCount != 0 {
  574. t.Fatalf("summary = %+v, want zeroed counters", resp.Summary)
  575. }
  576. if resp.Groups == nil {
  577. t.Fatal("groups = nil, want an empty list so the filter drawer renders")
  578. }
  579. }