node_client_expiry_sync_test.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  5. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  6. )
  7. // TestMergeActivationExpiry covers the pure reconciliation rule in isolation.
  8. func TestMergeActivationExpiry(t *testing.T) {
  9. const (
  10. dur = int64(-2592000000) // 30 days as a "start after first connect" duration
  11. early = int64(1000) // earliest absolute deadline (first connection)
  12. late = int64(2000) // a later absolute deadline
  13. )
  14. cases := []struct {
  15. name string
  16. existing, node int64
  17. want int64
  18. }{
  19. {"master unset takes node duration", 0, dur, dur},
  20. {"master unset takes node activation", 0, early, early},
  21. {"activation adopted over stored duration", dur, early, early},
  22. {"node still un-activated does not reset deadline", early, dur, early},
  23. {"node un-activated zero does not reset deadline", early, 0, early},
  24. {"node renewal extends the deadline forward", early, late, late},
  25. {"node positive adopted even if earlier", late, early, early},
  26. {"both un-activated keep node value", dur, dur, dur},
  27. }
  28. for _, c := range cases {
  29. t.Run(c.name, func(t *testing.T) {
  30. if got := mergeActivationExpiry(c.existing, c.node); got != c.want {
  31. t.Fatalf("mergeActivationExpiry(%d,%d) = %d, want %d", c.existing, c.node, got, c.want)
  32. }
  33. })
  34. }
  35. }
  36. // TestNodeFirstConnectExpiry_NotClobbered reproduces the multi-node bug: a
  37. // client is attached to inbounds on two nodes with a "start after first connect"
  38. // expiry. The client connects only on node 1, which activates an absolute
  39. // deadline; node 2 never sees a connection and keeps reporting the negative
  40. // duration. The shared per-email client_traffics row must hold the activated
  41. // deadline — a later node-2 sync must not reset it back to "not started".
  42. func TestNodeFirstConnectExpiry_NotClobbered(t *testing.T) {
  43. db := initTrafficTestDB(t)
  44. createNodeInbound(t, db, 1, "n1-in", 41001)
  45. createNodeInbound(t, db, 2, "n2-in", 41002)
  46. svc := &InboundService{}
  47. const email = "delayed"
  48. const duration = int64(-2592000000) // 30 days, not yet started
  49. // Both nodes start out reporting the un-activated negative duration.
  50. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 0, Down: 0, ExpiryTime: duration, Enable: true})
  51. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 0, Down: 0, ExpiryTime: duration, Enable: true})
  52. if got := readTraffic(t, db, email).ExpiryTime; got != duration {
  53. t.Fatalf("before any connection: expiry = %d, want %d", got, duration)
  54. }
  55. // Client connects on node 1: it activates an absolute deadline.
  56. const activated = int64(1893456000000) // some absolute ms timestamp
  57. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 100, Down: 100, ExpiryTime: activated, Enable: true})
  58. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  59. t.Fatalf("after node 1 activation: expiry = %d, want %d", got, activated)
  60. }
  61. // Node 2 (no connection there) keeps reporting the negative duration. This
  62. // must NOT reset the activated deadline.
  63. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 0, Down: 0, ExpiryTime: duration, Enable: true})
  64. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  65. t.Fatalf("node 2 clobbered the activated deadline: expiry = %d, want %d", got, activated)
  66. }
  67. // Subsequent node 1 syncs keep the same absolute deadline.
  68. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 200, Down: 200, ExpiryTime: activated, Enable: true})
  69. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  70. t.Fatalf("after further node 1 sync: expiry = %d, want %d", got, activated)
  71. }
  72. }
  73. // TestNodeFirstConnectExpiry_NotClobbered_WithSettings exercises the full
  74. // production sync path — snapshots carrying real settings JSON, which drives the
  75. // GetClients/SyncInbound branch inside setRemoteTrafficLocked — to prove that
  76. // branch does not re-derive the per-email client_traffics.expiry_time from the
  77. // node's (still negative) settings and undo the merge guard.
  78. func TestNodeFirstConnectExpiry_NotClobbered_WithSettings(t *testing.T) {
  79. db := initTrafficTestDB(t)
  80. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "delayed")
  81. createNodeInboundWithClient(t, db, 2, "n2-in", 41002, "delayed")
  82. svc := &InboundService{}
  83. const email = "delayed"
  84. const duration = int64(-2592000000)
  85. const activated = int64(1893456000000)
  86. negSettings := `{"clients":[{"email":"delayed","enable":true,"expiryTime":-2592000000}]}`
  87. actSettings := `{"clients":[{"email":"delayed","enable":true,"expiryTime":1893456000000}]}`
  88. // Both nodes start un-activated.
  89. syncNodeWithSettings(t, svc, 1, "n1-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
  90. syncNodeWithSettings(t, svc, 2, "n2-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
  91. // Node 1 activates (both its ClientStats and its settings now carry the
  92. // absolute deadline, like a real node after adjustTraffics).
  93. syncNodeWithSettings(t, svc, 1, "n1-in", actSettings, xray.ClientTraffic{Email: email, Up: 100, Down: 100, ExpiryTime: activated, Enable: true})
  94. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  95. t.Fatalf("after node 1 activation: expiry = %d, want %d", got, activated)
  96. }
  97. // Node 2 still reports the negative duration in BOTH ClientStats and
  98. // settings. Neither the merge nor SyncInbound may reset the deadline.
  99. syncNodeWithSettings(t, svc, 2, "n2-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
  100. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  101. t.Fatalf("node 2 settings-sync clobbered the deadline: expiry = %d, want %d", got, activated)
  102. }
  103. }
  104. // TestNodeRenewExtendsExpiry guards against over-correcting: a node that renews
  105. // a client (traffic reset / auto-renew) legitimately moves the deadline FORWARD
  106. // to a later absolute timestamp, and that must still propagate to the master.
  107. // The guard only rejects un-activated (<= 0) values, never a positive one.
  108. func TestNodeRenewExtendsExpiry(t *testing.T) {
  109. db := initTrafficTestDB(t)
  110. createNodeInbound(t, db, 1, "n1-in", 41001)
  111. svc := &InboundService{}
  112. const email = "renewing"
  113. const first = int64(1893456000000)
  114. const renewed = first + int64(2592000000) // +30 days after auto-renew
  115. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 10, Down: 10, ExpiryTime: first, Enable: true})
  116. if got := readTraffic(t, db, email).ExpiryTime; got != first {
  117. t.Fatalf("after activation: expiry = %d, want %d", got, first)
  118. }
  119. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 20, Down: 20, ExpiryTime: renewed, Enable: true})
  120. if got := readTraffic(t, db, email).ExpiryTime; got != renewed {
  121. t.Fatalf("node renewal did not propagate: expiry = %d, want %d", got, renewed)
  122. }
  123. }
  124. // TestNodeActivationLiftsClientRecordExpiry reproduces #5714: the node activates
  125. // the deadline (positive ClientStats) while its settings JSON still carries the
  126. // negative duration, so SyncInbound keeps writing the stale value into the
  127. // client record and the Clients page shows "not started" forever.
  128. func TestNodeActivationLiftsClientRecordExpiry(t *testing.T) {
  129. db := initTrafficTestDB(t)
  130. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "delayed")
  131. svc := &InboundService{}
  132. const email = "delayed"
  133. const duration = int64(-2592000000)
  134. const activated = int64(1798448344010)
  135. negSettings := `{"clients":[{"email":"delayed","enable":true,"expiryTime":-2592000000}]}`
  136. if err := db.Create(&model.ClientRecord{Email: email, Enable: true, ExpiryTime: duration}).Error; err != nil {
  137. t.Fatalf("seed client record: %v", err)
  138. }
  139. readRecordExpiry := func() int64 {
  140. t.Helper()
  141. var rec model.ClientRecord
  142. if err := db.Where("email = ?", email).First(&rec).Error; err != nil {
  143. t.Fatalf("read client record: %v", err)
  144. }
  145. return rec.ExpiryTime
  146. }
  147. syncNodeWithSettings(t, svc, 1, "n1-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
  148. if got := readRecordExpiry(); got != duration {
  149. t.Fatalf("before activation: record expiry = %d, want %d", got, duration)
  150. }
  151. syncNodeWithSettings(t, svc, 1, "n1-in", negSettings, xray.ClientTraffic{Email: email, Up: 100, Down: 100, ExpiryTime: activated, Enable: true})
  152. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  153. t.Fatalf("client_traffics not activated: expiry = %d, want %d", got, activated)
  154. }
  155. if got := readRecordExpiry(); got != activated {
  156. t.Fatalf("client record kept stale duration (#5714): expiry = %d, want %d", got, activated)
  157. }
  158. }