global_traffic_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/internal/database"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  6. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  7. )
  8. func seedClientRow(t *testing.T, email string, inboundId int, up, down, total int64) {
  9. t.Helper()
  10. db := database.GetDB()
  11. if err := db.Create(&xray.ClientTraffic{InboundId: inboundId, Email: email, Enable: true, Up: up, Down: down, Total: total}).Error; err != nil {
  12. t.Fatalf("seed client_traffics %q: %v", email, err)
  13. }
  14. }
  15. func TestAcceptGlobalTraffic_SideTableOnly(t *testing.T) {
  16. db := initTrafficTestDB(t)
  17. svc := &InboundService{}
  18. seedClientRow(t, "alice", 1, 100, 100, 0)
  19. err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{
  20. {Email: "alice", Up: 900, Down: 800},
  21. {Email: "ghost", Up: 5, Down: 5}, // not hosted here — must be dropped
  22. })
  23. if err != nil {
  24. t.Fatalf("AcceptGlobalTraffic: %v", err)
  25. }
  26. local := readTraffic(t, db, "alice")
  27. if local.Up != 100 || local.Down != 100 {
  28. t.Errorf("local counters must stay pure, got up=%d down=%d", local.Up, local.Down)
  29. }
  30. var globals []model.ClientGlobalTraffic
  31. if err := db.Find(&globals).Error; err != nil {
  32. t.Fatalf("read globals: %v", err)
  33. }
  34. if len(globals) != 1 || globals[0].Email != "alice" || globals[0].Up != 900 || globals[0].Down != 800 {
  35. t.Errorf("unexpected globals: %+v", globals)
  36. }
  37. }
  38. func TestAcceptGlobalTraffic_OverwriteAndMultiMaster(t *testing.T) {
  39. db := initTrafficTestDB(t)
  40. svc := &InboundService{}
  41. seedClientRow(t, "alice", 1, 0, 0, 0)
  42. must := func(guid string, up, down int64) {
  43. t.Helper()
  44. if err := svc.AcceptGlobalTraffic(guid, []*xray.ClientTraffic{{Email: "alice", Up: up, Down: down}}); err != nil {
  45. t.Fatalf("AcceptGlobalTraffic(%s): %v", guid, err)
  46. }
  47. }
  48. must("master-a", 900, 900)
  49. must("master-a", 50, 50) // a master-side reset propagates by overwrite
  50. must("master-b", 500, 400)
  51. rows := []*xray.ClientTraffic{{Email: "alice", Up: 10, Down: 10}}
  52. overlayGlobalTraffic(db, rows)
  53. if rows[0].Up != 500 || rows[0].Down != 400 {
  54. t.Errorf("overlay should fold per-master max, got up=%d down=%d", rows[0].Up, rows[0].Down)
  55. }
  56. }
  57. func TestGlobalUsage_DisablesClient(t *testing.T) {
  58. db := initTrafficTestDB(t)
  59. svc := &InboundService{}
  60. // 200 of 1000 used locally — local check alone would never trip.
  61. seedClientRow(t, "cap", 1, 100, 100, 1000)
  62. if err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: "cap", Up: 800, Down: 700}}); err != nil {
  63. t.Fatalf("AcceptGlobalTraffic: %v", err)
  64. }
  65. if _, count, _, err := svc.disableInvalidClients(db); err != nil {
  66. t.Fatalf("disableInvalidClients: %v", err)
  67. } else if count != 1 {
  68. t.Fatalf("expected 1 client disabled, got %d", count)
  69. }
  70. if got := readTraffic(t, db, "cap"); got.Enable {
  71. t.Error("client should be disabled by global usage exceeding its quota")
  72. }
  73. }
  74. func TestGlobalRows_ClearedOnReset(t *testing.T) {
  75. db := initTrafficTestDB(t)
  76. svc := &InboundService{}
  77. seedClientRow(t, "alice", 1, 50, 50, 1000)
  78. if err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: "alice", Up: 999, Down: 999}}); err != nil {
  79. t.Fatalf("AcceptGlobalTraffic: %v", err)
  80. }
  81. if err := svc.ResetClientTrafficByEmail("alice"); err != nil {
  82. t.Fatalf("ResetClientTrafficByEmail: %v", err)
  83. }
  84. var cnt int64
  85. db.Model(&model.ClientGlobalTraffic{}).Count(&cnt)
  86. if cnt != 0 {
  87. t.Errorf("global rows should be cleared on reset, found %d", cnt)
  88. }
  89. }
  90. // The full inbound list doubles as the traffic snapshot masters poll, so it
  91. // must report pure local counters; the slim list only feeds this panel's UI,
  92. // so it carries the cross-panel overlay.
  93. func TestSnapshotListNotOverlaid_SlimOverlaid(t *testing.T) {
  94. db := initTrafficTestDB(t)
  95. svc := &InboundService{}
  96. settings := `{"clients": [{"email": "alice", "enable": true}]}`
  97. ib := &model.Inbound{UserId: 1, Tag: "in-a", Enable: true, Port: 42001, Protocol: model.VLESS, Settings: settings}
  98. if err := db.Create(ib).Error; err != nil {
  99. t.Fatalf("create inbound: %v", err)
  100. }
  101. seedClientRow(t, "alice", ib.Id, 100, 100, 0)
  102. if err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: "alice", Up: 900, Down: 900}}); err != nil {
  103. t.Fatalf("AcceptGlobalTraffic: %v", err)
  104. }
  105. full, err := svc.GetInbounds(1)
  106. if err != nil {
  107. t.Fatalf("GetInbounds: %v", err)
  108. }
  109. if len(full) != 1 || len(full[0].ClientStats) != 1 {
  110. t.Fatalf("unexpected full list shape: %d inbounds", len(full))
  111. }
  112. if full[0].ClientStats[0].Up != 100 {
  113. t.Errorf("full list (master snapshot) must stay un-overlaid, got up=%d", full[0].ClientStats[0].Up)
  114. }
  115. slim, err := svc.GetInboundsSlim(1)
  116. if err != nil {
  117. t.Fatalf("GetInboundsSlim: %v", err)
  118. }
  119. if len(slim) != 1 || len(slim[0].ClientStats) != 1 {
  120. t.Fatalf("unexpected slim list shape: %d inbounds", len(slim))
  121. }
  122. if slim[0].ClientStats[0].Up != 900 {
  123. t.Errorf("slim list should carry the global overlay, got up=%d", slim[0].ClientStats[0].Up)
  124. }
  125. }