inbound_client_traffic_test.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package service
  2. import (
  3. "path/filepath"
  4. "testing"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/database"
  7. "github.com/mhsanaei/3x-ui/v3/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/xray"
  9. )
  10. // TestAddClientTraffic_MatchesByEmail covers two scenarios that share one fix:
  11. // client_traffics is keyed by email (one shared row per email no matter how many
  12. // inbounds the client is attached to), so local traffic must be applied by email
  13. // regardless of which inbound_id the row happens to carry.
  14. //
  15. // - staleEmail: the row points at an inbound id that no longer exists (a deleted
  16. // earlier incarnation, AddClientStat's OnConflict-DoNothing never refreshes it).
  17. // - dualEmail: the client is attached to both a node inbound and the mother inbound,
  18. // but the node inbound was attached first, so the shared row carries the node
  19. // inbound's id (issue #4921). The old `inbound_id NOT IN (node inbounds)` filter
  20. // dropped this client's local traffic, leaving it stuck at zero and offline.
  21. //
  22. // Both must have their local traffic counted.
  23. func TestAddClientTraffic_MatchesByEmail(t *testing.T) {
  24. dbDir := t.TempDir()
  25. t.Setenv("XUI_DB_FOLDER", dbDir)
  26. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  27. t.Fatalf("InitDB: %v", err)
  28. }
  29. t.Cleanup(func() { _ = database.CloseDB() })
  30. db := database.GetDB()
  31. const staleEmail = "stale-user"
  32. const dualEmail = "dual-user"
  33. localInbound := &model.Inbound{UserId: 1, Tag: "local-in", Enable: true, Port: 40001, Protocol: model.VLESS}
  34. if err := db.Create(localInbound).Error; err != nil {
  35. t.Fatalf("create local inbound: %v", err)
  36. }
  37. nodeID := 1
  38. nodeInbound := &model.Inbound{UserId: 1, Tag: "node-in", Enable: true, Port: 40002, Protocol: model.VLESS, NodeID: &nodeID}
  39. if err := db.Create(nodeInbound).Error; err != nil {
  40. t.Fatalf("create node inbound: %v", err)
  41. }
  42. if err := db.Create(&xray.ClientTraffic{InboundId: 9999, Email: staleEmail, Enable: true}).Error; err != nil {
  43. t.Fatalf("create stale client_traffics: %v", err)
  44. }
  45. // Attached to both inbounds, but the node inbound won the OnConflict so the
  46. // shared row is owned by the node inbound id.
  47. if err := db.Create(&xray.ClientTraffic{InboundId: nodeInbound.Id, Email: dualEmail, Enable: true}).Error; err != nil {
  48. t.Fatalf("create dual client_traffics: %v", err)
  49. }
  50. svc := InboundService{}
  51. err := svc.addClientTraffic(db, []*xray.ClientTraffic{
  52. {Email: staleEmail, Up: 10, Down: 20},
  53. {Email: dualEmail, Up: 30, Down: 40},
  54. })
  55. if err != nil {
  56. t.Fatalf("addClientTraffic: %v", err)
  57. }
  58. var stale xray.ClientTraffic
  59. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", staleEmail).First(&stale).Error; err != nil {
  60. t.Fatalf("reload stale row: %v", err)
  61. }
  62. if stale.Up != 10 || stale.Down != 20 {
  63. t.Errorf("stale-pointer row not updated: up=%d down=%d, want 10/20", stale.Up, stale.Down)
  64. }
  65. if stale.LastOnline == 0 {
  66. t.Errorf("stale-pointer row LastOnline not set")
  67. }
  68. var dual xray.ClientTraffic
  69. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", dualEmail).First(&dual).Error; err != nil {
  70. t.Fatalf("reload dual row: %v", err)
  71. }
  72. if dual.Up != 30 || dual.Down != 40 {
  73. t.Errorf("node-owned row not updated by local traffic (issue #4921): up=%d down=%d, want 30/40", dual.Up, dual.Down)
  74. }
  75. if dual.LastOnline == 0 {
  76. t.Errorf("node-owned row LastOnline not set (client stayed offline)")
  77. }
  78. }
  79. // TestAdjustTraffics_DelayedStartConvertsDespiteStaleInboundId covers "Start After
  80. // First Use": a delayed-start client carries a negative expiry (the duration) that
  81. // must convert to an absolute deadline on its first traffic tick. When the client's
  82. // email-keyed client_traffics row still points at a deleted inbound (stale inbound_id
  83. // after an inbound delete+recreate), the conversion used to resolve no inbound and
  84. // silently skip, leaving the client perpetually "not started". The fix resolves the
  85. // owning inbound via the client_inbounds link instead.
  86. func TestAdjustTraffics_DelayedStartConvertsDespiteStaleInboundId(t *testing.T) {
  87. dbDir := t.TempDir()
  88. t.Setenv("XUI_DB_FOLDER", dbDir)
  89. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  90. t.Fatalf("InitDB: %v", err)
  91. }
  92. t.Cleanup(func() { _ = database.CloseDB() })
  93. db := database.GetDB()
  94. const email = "delayed-user"
  95. const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0d001"
  96. const sevenDays = int64(7 * 86400000)
  97. client := model.Client{Email: email, ID: uid, Auth: uid, Enable: true, ExpiryTime: -sevenDays}
  98. inbound := &model.Inbound{
  99. Tag: "vless-delayed", Enable: true, Port: 45001, Protocol: model.VLESS,
  100. StreamSettings: `{"network":"tcp","security":"reality"}`,
  101. Settings: clientsSettings(t, []model.Client{client}),
  102. }
  103. if err := db.Create(inbound).Error; err != nil {
  104. t.Fatalf("create inbound: %v", err)
  105. }
  106. svc := InboundService{}
  107. if err := svc.clientService.SyncInbound(db, inbound.Id, []model.Client{client}); err != nil {
  108. t.Fatalf("SyncInbound: %v", err)
  109. }
  110. // The email-keyed traffic row survives an inbound delete+recreate pointing at a
  111. // dead inbound id; client_inbounds still links the client to the live inbound.
  112. if err := db.Create(&xray.ClientTraffic{InboundId: 9999, Email: email, Enable: true, ExpiryTime: -sevenDays}).Error; err != nil {
  113. t.Fatalf("create stale traffic row: %v", err)
  114. }
  115. before := time.Now().UnixMilli()
  116. if err := svc.addClientTraffic(db, []*xray.ClientTraffic{{Email: email, Up: 100, Down: 200}}); err != nil {
  117. t.Fatalf("addClientTraffic: %v", err)
  118. }
  119. var row xray.ClientTraffic
  120. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).First(&row).Error; err != nil {
  121. t.Fatalf("reload traffic row: %v", err)
  122. }
  123. if row.ExpiryTime <= 0 {
  124. t.Fatalf("delayed-start expiry not converted: still %d (stale inbound_id skipped the conversion)", row.ExpiryTime)
  125. }
  126. if row.ExpiryTime < before+sevenDays-5000 || row.ExpiryTime > before+sevenDays+5000 {
  127. t.Errorf("converted expiry = %d, want ~now+7d (%d)", row.ExpiryTime, before+sevenDays)
  128. }
  129. reloaded, err := svc.GetInbound(inbound.Id)
  130. if err != nil {
  131. t.Fatalf("GetInbound: %v", err)
  132. }
  133. cs, err := svc.GetClients(reloaded)
  134. if err != nil {
  135. t.Fatalf("GetClients: %v", err)
  136. }
  137. if len(cs) != 1 || cs[0].ExpiryTime <= 0 {
  138. t.Errorf("inbound settings expiry not converted: %#v", cs)
  139. }
  140. }