1
0

inbound_client_traffic_test.go 8.9 KB

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