node_client_expiry_sync_test.go 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. package service
  2. import (
  3. "fmt"
  4. "testing"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  9. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  10. )
  11. // TestMergeActivationExpiry covers the pure reconciliation rule in isolation.
  12. func TestMergeActivationExpiry(t *testing.T) {
  13. const (
  14. dur = int64(-2592000000) // 30 days as a "start after first connect" duration
  15. early = int64(1000) // earliest absolute deadline (first connection)
  16. late = int64(2000) // a later absolute deadline
  17. )
  18. cases := []struct {
  19. name string
  20. existing, node int64
  21. want int64
  22. }{
  23. {"master unset takes node duration", 0, dur, dur},
  24. {"master unset takes node activation", 0, early, early},
  25. {"activation adopted over stored duration", dur, early, early},
  26. {"node still un-activated does not reset deadline", early, dur, early},
  27. {"node un-activated zero does not reset deadline", early, 0, early},
  28. {"master absolute ignores later node absolute", early, late, early},
  29. {"node value equal to master is a no-op", early, early, early},
  30. {"stale earlier absolute does not clobber later", late, early, late},
  31. {"both un-activated keep node value", dur, dur, dur},
  32. }
  33. for _, c := range cases {
  34. t.Run(c.name, func(t *testing.T) {
  35. if got := mergeActivationExpiry(c.existing, c.node); got != c.want {
  36. t.Fatalf("mergeActivationExpiry(%d,%d) = %d, want %d", c.existing, c.node, got, c.want)
  37. }
  38. })
  39. }
  40. }
  41. func TestNodeDisableIsStale(t *testing.T) {
  42. now := time.Now().UnixMilli()
  43. const quota = int64(100)
  44. cases := []struct {
  45. name string
  46. master *xray.ClientTraffic
  47. node xray.ClientTraffic
  48. deltaUp int64
  49. deltaDown int64
  50. wantStale bool
  51. }{
  52. {name: "nil master", node: xray.ClientTraffic{ExpiryTime: earlyAbs}},
  53. {
  54. name: "matching limits are a genuine verdict",
  55. master: &xray.ClientTraffic{ExpiryTime: lateAbs, Total: quota},
  56. node: xray.ClientTraffic{ExpiryTime: lateAbs, Total: quota},
  57. wantStale: false,
  58. },
  59. {
  60. name: "node still holds the pre-extension deadline",
  61. master: &xray.ClientTraffic{ExpiryTime: lateAbs},
  62. node: xray.ClientTraffic{ExpiryTime: earlyAbs},
  63. wantStale: true,
  64. },
  65. {
  66. name: "node still holds the pre-top-up quota",
  67. master: &xray.ClientTraffic{ExpiryTime: lateAbs, Total: 2 * quota, Up: 60, Down: 50},
  68. node: xray.ClientTraffic{ExpiryTime: lateAbs, Total: quota},
  69. wantStale: true,
  70. },
  71. {
  72. name: "master itself expired",
  73. master: &xray.ClientTraffic{ExpiryTime: earlyAbs},
  74. node: xray.ClientTraffic{ExpiryTime: earlyAbs - 1000},
  75. },
  76. {
  77. name: "master itself over quota",
  78. master: &xray.ClientTraffic{ExpiryTime: lateAbs, Total: quota, Up: 60, Down: 50},
  79. node: xray.ClientTraffic{ExpiryTime: earlyAbs, Total: quota},
  80. },
  81. {
  82. name: "this tick's deltas cross the master quota",
  83. master: &xray.ClientTraffic{ExpiryTime: lateAbs, Total: quota, Up: 40, Down: 50},
  84. node: xray.ClientTraffic{ExpiryTime: earlyAbs, Total: quota},
  85. deltaUp: 10,
  86. deltaDown: 10,
  87. },
  88. {
  89. // Same row without the deltas: the crossing above is the deltas' doing.
  90. name: "under the master quota before this tick's deltas",
  91. master: &xray.ClientTraffic{ExpiryTime: lateAbs, Total: quota, Up: 40, Down: 50},
  92. node: xray.ClientTraffic{ExpiryTime: earlyAbs, Total: quota},
  93. wantStale: true,
  94. },
  95. {
  96. name: "un-activated node duration against an absolute master",
  97. master: &xray.ClientTraffic{ExpiryTime: lateAbs},
  98. node: xray.ClientTraffic{ExpiryTime: -2592000000},
  99. wantStale: true,
  100. },
  101. }
  102. for _, c := range cases {
  103. t.Run(c.name, func(t *testing.T) {
  104. if got := nodeDisableIsStale(c.master, c.node, now, c.deltaUp, c.deltaDown); got != c.wantStale {
  105. t.Fatalf("nodeDisableIsStale(...) = %v, want %v", got, c.wantStale)
  106. }
  107. })
  108. }
  109. }
  110. // TestNodeFirstConnectExpiry_NotClobbered reproduces the multi-node bug: a
  111. // client is attached to inbounds on two nodes with a "start after first connect"
  112. // expiry. The client connects only on node 1, which activates an absolute
  113. // deadline; node 2 never sees a connection and keeps reporting the negative
  114. // duration. The shared per-email client_traffics row must hold the activated
  115. // deadline — a later node-2 sync must not reset it back to "not started".
  116. func TestNodeFirstConnectExpiry_NotClobbered(t *testing.T) {
  117. db := initTrafficTestDB(t)
  118. createNodeInbound(t, db, 1, "n1-in", 41001)
  119. createNodeInbound(t, db, 2, "n2-in", 41002)
  120. svc := &InboundService{}
  121. const email = "delayed"
  122. const duration = int64(-2592000000) // 30 days, not yet started
  123. // Both nodes start out reporting the un-activated negative duration.
  124. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 0, Down: 0, ExpiryTime: duration, Enable: true})
  125. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 0, Down: 0, ExpiryTime: duration, Enable: true})
  126. if got := readTraffic(t, db, email).ExpiryTime; got != duration {
  127. t.Fatalf("before any connection: expiry = %d, want %d", got, duration)
  128. }
  129. // Client connects on node 1: it activates an absolute deadline.
  130. const activated = int64(1893456000000) // some absolute ms timestamp
  131. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 100, Down: 100, ExpiryTime: activated, Enable: true})
  132. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  133. t.Fatalf("after node 1 activation: expiry = %d, want %d", got, activated)
  134. }
  135. // Node 2 (no connection there) keeps reporting the negative duration. This
  136. // must NOT reset the activated deadline.
  137. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 0, Down: 0, ExpiryTime: duration, Enable: true})
  138. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  139. t.Fatalf("node 2 clobbered the activated deadline: expiry = %d, want %d", got, activated)
  140. }
  141. // Subsequent node 1 syncs keep the same absolute deadline.
  142. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 200, Down: 200, ExpiryTime: activated, Enable: true})
  143. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  144. t.Fatalf("after further node 1 sync: expiry = %d, want %d", got, activated)
  145. }
  146. }
  147. // TestNodeFirstConnectExpiry_NotClobbered_WithSettings exercises the full
  148. // production sync path — snapshots carrying real settings JSON, which drives the
  149. // GetClients/SyncInbound branch inside setRemoteTrafficLocked — to prove that
  150. // branch does not re-derive the per-email client_traffics.expiry_time from the
  151. // node's (still negative) settings and undo the merge guard.
  152. func TestNodeFirstConnectExpiry_NotClobbered_WithSettings(t *testing.T) {
  153. db := initTrafficTestDB(t)
  154. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "delayed")
  155. createNodeInboundWithClient(t, db, 2, "n2-in", 41002, "delayed")
  156. svc := &InboundService{}
  157. const email = "delayed"
  158. const duration = int64(-2592000000)
  159. const activated = int64(1893456000000)
  160. negSettings := `{"clients":[{"email":"delayed","enable":true,"expiryTime":-2592000000}]}`
  161. actSettings := `{"clients":[{"email":"delayed","enable":true,"expiryTime":1893456000000}]}`
  162. // Both nodes start un-activated.
  163. syncNodeWithSettings(t, svc, 1, "n1-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
  164. syncNodeWithSettings(t, svc, 2, "n2-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
  165. // Node 1 activates (both its ClientStats and its settings now carry the
  166. // absolute deadline, like a real node after adjustTraffics).
  167. syncNodeWithSettings(t, svc, 1, "n1-in", actSettings, xray.ClientTraffic{Email: email, Up: 100, Down: 100, ExpiryTime: activated, Enable: true})
  168. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  169. t.Fatalf("after node 1 activation: expiry = %d, want %d", got, activated)
  170. }
  171. // Node 2 still reports the negative duration in BOTH ClientStats and
  172. // settings. Neither the merge nor SyncInbound may reset the deadline.
  173. syncNodeWithSettings(t, svc, 2, "n2-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
  174. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  175. t.Fatalf("node 2 settings-sync clobbered the deadline: expiry = %d, want %d", got, activated)
  176. }
  177. }
  178. // TestNodeRenewExtendsExpiry: node auto-renew (reset + later expiry + counter
  179. // drop) must still move master expiry forward via nodeClientRenewed.
  180. func TestNodeRenewExtendsExpiry(t *testing.T) {
  181. db := initTrafficTestDB(t)
  182. createNodeInbound(t, db, 1, "n1-in", 41001)
  183. svc := &InboundService{}
  184. const email = "renewing"
  185. const first = int64(1893456000000)
  186. const renewed = first + int64(2592000000) // +30 days after auto-renew
  187. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  188. Email: email, Up: 0, Down: 0, ExpiryTime: first, Reset: 30, Enable: true,
  189. })
  190. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  191. Email: email, Up: 100, Down: 100, ExpiryTime: first, Reset: 30, Enable: true,
  192. })
  193. if got := readTraffic(t, db, email).ExpiryTime; got != first {
  194. t.Fatalf("after activation: expiry = %d, want %d", got, first)
  195. }
  196. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  197. Email: email, Up: 5, Down: 5, ExpiryTime: renewed, Reset: 30, Enable: true,
  198. })
  199. if got := readTraffic(t, db, email).ExpiryTime; got != renewed {
  200. t.Fatalf("node renewal did not propagate: expiry = %d, want %d", got, renewed)
  201. }
  202. }
  203. // TestNodeRenew_WithMatchingSettings: renew still applies when settings JSON
  204. // also carries the later absolute (guard must not block real renewals).
  205. func TestNodeRenew_WithMatchingSettings(t *testing.T) {
  206. db := initTrafficTestDB(t)
  207. createNodeInbound(t, db, 1, "n1-in", 41001)
  208. svc := &InboundService{}
  209. const email = "renew-settings"
  210. firstSettings := fmt.Sprintf(
  211. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, renewFirstExpiry)
  212. renewSettings := fmt.Sprintf(
  213. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, renewSecondExpiry)
  214. syncNodeWithSettings(t, svc, 1, "n1-in", firstSettings, xray.ClientTraffic{
  215. Email: email, Up: 0, Down: 0, ExpiryTime: renewFirstExpiry, Reset: renewPeriodDays, Enable: true,
  216. })
  217. syncNodeWithSettings(t, svc, 1, "n1-in", firstSettings, xray.ClientTraffic{
  218. Email: email, Up: 100, Down: 100, ExpiryTime: renewFirstExpiry, Reset: renewPeriodDays, Enable: true,
  219. })
  220. syncNodeWithSettings(t, svc, 1, "n1-in", renewSettings, xray.ClientTraffic{
  221. Email: email, Up: 5, Down: 5, ExpiryTime: renewSecondExpiry, Reset: renewPeriodDays, Enable: true,
  222. })
  223. if got := readTraffic(t, db, email).ExpiryTime; got != renewSecondExpiry {
  224. t.Fatalf("renewal with matching settings: got %d want %d", got, renewSecondExpiry)
  225. }
  226. }
  227. // A node still holding the pre-extension deadline must not undo the extension on
  228. // traffics, client records or the adopted settings JSON (#6228).
  229. func TestNodeStaleExpiryAfterExtend_NotClobbered(t *testing.T) {
  230. db := initTrafficTestDB(t)
  231. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "extended")
  232. svc := &InboundService{}
  233. const email = "extended"
  234. expired, extended := earlyAbs, lateAbs
  235. staleSettings := fmt.Sprintf(
  236. `{"clients":[{"email":%q,"enable":false,"expiryTime":%d}]}`, email, expired)
  237. syncNodeWithSettings(t, svc, 1, "n1-in", staleSettings,
  238. xray.ClientTraffic{Email: email, ExpiryTime: expired, Enable: false})
  239. if got := readTraffic(t, db, email); got.ExpiryTime != expired || got.Enable {
  240. t.Fatalf("after expiry: expiry=%d enable=%v, want expiry=%d enable=false",
  241. got.ExpiryTime, got.Enable, expired)
  242. }
  243. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  244. Updates(map[string]any{"expiry_time": extended, "enable": true}).Error; err != nil {
  245. t.Fatalf("master extend traffic: %v", err)
  246. }
  247. if err := db.Model(&model.ClientRecord{}).Where("email = ?", email).
  248. Updates(map[string]any{"expiry_time": extended, "enable": true}).Error; err != nil {
  249. t.Fatalf("master extend record: %v", err)
  250. }
  251. syncNodeWithSettings(t, svc, 1, "n1-in", staleSettings,
  252. xray.ClientTraffic{Email: email, ExpiryTime: expired, Enable: false})
  253. got := readTraffic(t, db, email)
  254. if got.ExpiryTime != extended {
  255. t.Fatalf("stale node expiry clobbered traffics: expiry=%d, want %d", got.ExpiryTime, extended)
  256. }
  257. if !got.Enable {
  258. t.Fatal("stale node disable latched traffics.enable off after extension")
  259. }
  260. var rec model.ClientRecord
  261. if err := db.Where("email = ?", email).First(&rec).Error; err != nil {
  262. t.Fatalf("read client record: %v", err)
  263. }
  264. if rec.ExpiryTime != extended {
  265. t.Fatalf("stale SyncInbound clobbered record expiry: %d, want %d", rec.ExpiryTime, extended)
  266. }
  267. if !rec.Enable {
  268. t.Fatal("stale SyncInbound latched clients.enable off after extension")
  269. }
  270. var ib model.Inbound
  271. if err := db.Where("tag = ?", "n1-in").First(&ib).Error; err != nil {
  272. t.Fatalf("read inbound: %v", err)
  273. }
  274. clients, err := svc.GetClients(&ib)
  275. if err != nil {
  276. t.Fatalf("GetClients: %v", err)
  277. }
  278. var found bool
  279. for _, c := range clients {
  280. if c.Email != email {
  281. continue
  282. }
  283. found = true
  284. if c.ExpiryTime != extended {
  285. t.Fatalf("adopted settings kept stale expiry: %d, want %d", c.ExpiryTime, extended)
  286. }
  287. if !c.Enable {
  288. t.Fatal("adopted settings kept enable=false after extension")
  289. }
  290. }
  291. if !found {
  292. t.Fatal("client missing from adopted inbound settings after stale sync")
  293. }
  294. }
  295. // TestNodeStaleLift_MarksNodeDirty: a lifecycle lift must mark the node dirty
  296. // and store lifted settings centrally so reconcile can re-push (#6228).
  297. func TestNodeStaleLift_MarksNodeDirty(t *testing.T) {
  298. db := initTrafficTestDB(t)
  299. node := &model.Node{Name: "lift-n", Address: "127.0.0.1", Port: 2097, ApiToken: "tok", Enable: true, Status: "online"}
  300. if err := db.Create(node).Error; err != nil {
  301. t.Fatalf("create node: %v", err)
  302. }
  303. createNodeInboundWithClient(t, db, node.Id, "n1-in", 41001, "extended")
  304. svc := &InboundService{}
  305. const email = "extended"
  306. expired, extended := earlyAbs, lateAbs
  307. staleSettings := fmt.Sprintf(
  308. `{"clients":[{"email":%q,"enable":false,"expiryTime":%d}]}`, email, expired)
  309. syncNodeWithSettings(t, svc, node.Id, "n1-in", staleSettings,
  310. xray.ClientTraffic{Email: email, ExpiryTime: expired, Enable: false})
  311. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  312. Updates(map[string]any{"expiry_time": extended, "enable": true}).Error; err != nil {
  313. t.Fatalf("master extend: %v", err)
  314. }
  315. if err := db.Model(model.Node{}).Where("id = ?", node.Id).
  316. Updates(map[string]any{"config_dirty": false, "config_dirty_at": int64(0)}).Error; err != nil {
  317. t.Fatalf("clear dirty: %v", err)
  318. }
  319. syncNodeWithSettings(t, svc, node.Id, "n1-in", staleSettings,
  320. xray.ClientTraffic{Email: email, ExpiryTime: expired, Enable: false})
  321. var n model.Node
  322. if err := db.Select("config_dirty").Where("id = ?", node.Id).First(&n).Error; err != nil {
  323. t.Fatalf("read node: %v", err)
  324. }
  325. if !n.ConfigDirty {
  326. t.Fatal("lifecycle lift must mark the node dirty so reconcile re-pushes")
  327. }
  328. var ib model.Inbound
  329. if err := db.Where("tag = ?", "n1-in").First(&ib).Error; err != nil {
  330. t.Fatalf("read inbound: %v", err)
  331. }
  332. clients, err := svc.GetClients(&ib)
  333. if err != nil {
  334. t.Fatalf("GetClients: %v", err)
  335. }
  336. var found bool
  337. for _, c := range clients {
  338. if c.Email != email {
  339. continue
  340. }
  341. found = true
  342. if c.ExpiryTime != extended || !c.Enable {
  343. t.Fatalf("lifted settings not stored: expiry=%d enable=%v", c.ExpiryTime, c.Enable)
  344. }
  345. }
  346. if !found {
  347. t.Fatal("client missing from lifted inbound settings")
  348. }
  349. if ib.Settings == staleSettings {
  350. t.Fatal("central settings must differ from pre-lift wire blob (FP basis)")
  351. }
  352. }
  353. // A node disable decided on the master's own limits is genuine and must still
  354. // one-way-merge enable=false onto the master (#4917).
  355. func TestNodeQuotaDisable_SameExpiryStillLatches(t *testing.T) {
  356. db := initTrafficTestDB(t)
  357. createNodeInbound(t, db, 1, "n1-in", 41001)
  358. svc := &InboundService{}
  359. const email = "quota"
  360. const expiry = int64(1893456000000)
  361. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  362. Email: email, Up: 10, Down: 10, Total: 100, ExpiryTime: expiry, Enable: true,
  363. })
  364. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  365. Email: email, Up: 60, Down: 50, Total: 100, ExpiryTime: expiry, Enable: false,
  366. })
  367. if got := readTraffic(t, db, email); got.Enable {
  368. t.Fatal("same-expiry node disable must still latch master enable off (#4917)")
  369. }
  370. }
  371. // Once the master row is itself depleted, a node disable latches even though the
  372. // node's limits lag — otherwise genuine quota cuts would be skipped.
  373. func TestNodeQuotaDisable_OlderExpiryStillLatchesWhenOverQuota(t *testing.T) {
  374. db := initTrafficTestDB(t)
  375. createNodeInbound(t, db, 1, "n1-in", 41001)
  376. svc := &InboundService{}
  377. const email = "quota-lag"
  378. early, late := earlyAbs, lateAbs
  379. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  380. Email: email, Up: 10, Down: 10, Total: 100, ExpiryTime: late, Enable: true,
  381. })
  382. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  383. Updates(map[string]any{
  384. "expiry_time": late, "enable": true, "up": int64(60), "down": int64(50), "total": int64(100),
  385. }).Error; err != nil {
  386. t.Fatalf("seed over-quota master: %v", err)
  387. }
  388. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  389. Email: email, Up: 60, Down: 50, Total: 100, ExpiryTime: early, Enable: false,
  390. })
  391. if got := readTraffic(t, db, email); got.Enable {
  392. t.Fatal("over-quota master must still adopt node disable despite older node expiry")
  393. }
  394. if got := readTraffic(t, db, email); got.ExpiryTime != late {
  395. t.Fatalf("expiry should stay at master extension: got %d want %d", got.ExpiryTime, late)
  396. }
  397. }
  398. // TestNodeMasterShorten_NotClobberedWhileDirty: master shortened expiry while
  399. // config_dirty; a lagging longer node snapshot must not raise client_traffics.
  400. func TestNodeMasterShorten_NotClobberedWhileDirty(t *testing.T) {
  401. db := initTrafficTestDB(t)
  402. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "shortened")
  403. svc := &InboundService{}
  404. const email = "shortened"
  405. longExp, shortExp := lateAbs, earlyAbs
  406. longSettings := fmt.Sprintf(
  407. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, longExp)
  408. syncNodeWithSettings(t, svc, 1, "n1-in", longSettings,
  409. xray.ClientTraffic{Email: email, ExpiryTime: longExp, Enable: true})
  410. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  411. Updates(map[string]any{"expiry_time": shortExp, "enable": true}).Error; err != nil {
  412. t.Fatalf("master shorten: %v", err)
  413. }
  414. if err := db.Model(model.Node{}).Where("id = ?", 1).
  415. Updates(map[string]any{"config_dirty": true, "config_dirty_at": int64(1)}).Error; err != nil {
  416. t.Fatalf("mark dirty: %v", err)
  417. }
  418. snap := &runtime.TrafficSnapshot{
  419. Inbounds: []*model.Inbound{{
  420. Tag: "n1-in", Settings: longSettings,
  421. ClientStats: []xray.ClientTraffic{{Email: email, ExpiryTime: longExp, Enable: true, Up: 5, Down: 5}},
  422. }},
  423. }
  424. before := readTraffic(t, db, email)
  425. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  426. Update("total", int64(999)).Error; err != nil {
  427. t.Fatalf("master total: %v", err)
  428. }
  429. if _, err := svc.setRemoteTrafficLocked(1, snap, true, false); err != nil {
  430. t.Fatalf("dirty sync: %v", err)
  431. }
  432. got := readTraffic(t, db, email)
  433. if got.ExpiryTime != shortExp {
  434. t.Fatalf("dirty sync raised expiry: got %d want %d", got.ExpiryTime, shortExp)
  435. }
  436. if got.Total != 999 {
  437. t.Fatalf("dirty sync adopted node total: got %d want 999", got.Total)
  438. }
  439. if got.Up < before.Up+5 || got.Down < before.Down+5 {
  440. t.Fatalf("dirty sync must still accumulate traffic: before=(%d,%d) after=(%d,%d)",
  441. before.Up, before.Down, got.Up, got.Down)
  442. }
  443. }
  444. // The master's shortened absolute survives a snapshot whose ClientStats still
  445. // report the longer deadline (#6228).
  446. func TestNodeMasterShorten_LaggingClientStatsIgnored(t *testing.T) {
  447. db := initTrafficTestDB(t)
  448. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "short-settings")
  449. svc := &InboundService{}
  450. const email = "short-settings"
  451. longExp, shortExp := lateAbs, earlyAbs
  452. longSettings := fmt.Sprintf(
  453. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, longExp)
  454. shortSettings := fmt.Sprintf(
  455. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, shortExp)
  456. syncNodeWithSettings(t, svc, 1, "n1-in", longSettings,
  457. xray.ClientTraffic{Email: email, ExpiryTime: longExp, Enable: true})
  458. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  459. Updates(map[string]any{"expiry_time": shortExp, "enable": true}).Error; err != nil {
  460. t.Fatalf("master shorten: %v", err)
  461. }
  462. snap := &runtime.TrafficSnapshot{
  463. Inbounds: []*model.Inbound{{
  464. Tag: "n1-in", Settings: shortSettings,
  465. ClientStats: []xray.ClientTraffic{{Email: email, ExpiryTime: longExp, Enable: true}},
  466. }},
  467. }
  468. if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
  469. t.Fatalf("clean sync: %v", err)
  470. }
  471. if got := readTraffic(t, db, email); got.ExpiryTime != shortExp {
  472. t.Fatalf("lagging ClientStats raised expiry: got %d want %d", got.ExpiryTime, shortExp)
  473. }
  474. }
  475. // TestNodeMasterShorten_CleanSiblingCannotRaise: a clean sibling still holding
  476. // the longer deadline in settings+ClientStats must not undo a master shorten.
  477. func TestNodeMasterShorten_CleanSiblingCannotRaise(t *testing.T) {
  478. db := initTrafficTestDB(t)
  479. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "sib-short")
  480. createNodeInboundWithClient(t, db, 2, "n2-in", 41002, "sib-short")
  481. svc := &InboundService{}
  482. const email = "sib-short"
  483. longExp, shortExp := lateAbs, earlyAbs
  484. longSettings := fmt.Sprintf(
  485. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, longExp)
  486. syncNodeWithSettings(t, svc, 1, "n1-in", longSettings,
  487. xray.ClientTraffic{Email: email, ExpiryTime: longExp, Enable: true})
  488. syncNodeWithSettings(t, svc, 2, "n2-in", longSettings,
  489. xray.ClientTraffic{Email: email, ExpiryTime: longExp, Enable: true})
  490. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  491. Updates(map[string]any{"expiry_time": shortExp, "enable": true}).Error; err != nil {
  492. t.Fatalf("master shorten: %v", err)
  493. }
  494. // Sibling 2 is clean and still reports the old longer deadline.
  495. syncNodeWithSettings(t, svc, 2, "n2-in", longSettings,
  496. xray.ClientTraffic{Email: email, ExpiryTime: longExp, Enable: true})
  497. if got := readTraffic(t, db, email); got.ExpiryTime != shortExp {
  498. t.Fatalf("clean sibling raised shortened expiry: got %d want %d", got.ExpiryTime, shortExp)
  499. }
  500. }
  501. // TestNodeMasterShorten_LaggingStatsNotTreatedAsRenew: after shorten, ClientStats
  502. // may still show the longer deadline with Reset+dip — must not call renewal.
  503. func TestNodeMasterShorten_LaggingStatsNotTreatedAsRenew(t *testing.T) {
  504. db := initTrafficTestDB(t)
  505. createNodeInbound(t, db, 1, "n1-in", 41001)
  506. svc := &InboundService{}
  507. const email = "short-renew-trap"
  508. longExp, shortExp := lateAbs, earlyAbs
  509. shortSettings := fmt.Sprintf(
  510. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, shortExp)
  511. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  512. Email: email, Up: 0, Down: 0, ExpiryTime: longExp, Reset: 30, Enable: true,
  513. })
  514. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  515. Email: email, Up: 100, Down: 100, ExpiryTime: longExp, Reset: 30, Enable: true,
  516. })
  517. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  518. Updates(map[string]any{"expiry_time": shortExp, "enable": true}).Error; err != nil {
  519. t.Fatalf("master shorten: %v", err)
  520. }
  521. syncNodeWithSettings(t, svc, 1, "n1-in", shortSettings,
  522. xray.ClientTraffic{Email: email, Up: 5, Down: 5, ExpiryTime: longExp, Reset: 30, Enable: true})
  523. if got := readTraffic(t, db, email); got.ExpiryTime != shortExp {
  524. t.Fatalf("lagging stats treated as renew: got %d want %d", got.ExpiryTime, shortExp)
  525. }
  526. }
  527. // TestNodeStaleExpiryAfterExtend_WhileDirty keeps master extend while dirty.
  528. func TestNodeStaleExpiryAfterExtend_WhileDirty(t *testing.T) {
  529. db := initTrafficTestDB(t)
  530. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "extended-dirty")
  531. svc := &InboundService{}
  532. const email = "extended-dirty"
  533. expired, extended := earlyAbs, lateAbs
  534. staleSettings := fmt.Sprintf(
  535. `{"clients":[{"email":%q,"enable":false,"expiryTime":%d}]}`, email, expired)
  536. syncNodeWithSettings(t, svc, 1, "n1-in", staleSettings,
  537. xray.ClientTraffic{Email: email, ExpiryTime: expired, Enable: false})
  538. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  539. Updates(map[string]any{"expiry_time": extended, "enable": true}).Error; err != nil {
  540. t.Fatalf("master extend: %v", err)
  541. }
  542. if err := db.Model(model.Node{}).Where("id = ?", 1).
  543. Updates(map[string]any{"config_dirty": true, "config_dirty_at": int64(1)}).Error; err != nil {
  544. t.Fatalf("mark dirty: %v", err)
  545. }
  546. snap := &runtime.TrafficSnapshot{
  547. Inbounds: []*model.Inbound{{
  548. Tag: "n1-in", Settings: staleSettings,
  549. ClientStats: []xray.ClientTraffic{{Email: email, ExpiryTime: expired, Enable: false}},
  550. }},
  551. }
  552. if _, err := svc.setRemoteTrafficLocked(1, snap, true, false); err != nil {
  553. t.Fatalf("dirty sync: %v", err)
  554. }
  555. got := readTraffic(t, db, email)
  556. if got.ExpiryTime != extended || !got.Enable {
  557. t.Fatalf("dirty sync clobbered extend: expiry=%d enable=%v", got.ExpiryTime, got.Enable)
  558. }
  559. }
  560. // TestNodeRenewal_SkippedWhileDirty: renewal-shaped stats must not adopt
  561. // expiry/enable over a pending master push.
  562. func TestNodeRenewal_SkippedWhileDirty(t *testing.T) {
  563. db := initTrafficTestDB(t)
  564. createNodeInbound(t, db, 1, "n1-in", 41001)
  565. svc := &InboundService{}
  566. const email = "renew-dirty"
  567. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  568. Email: email, Up: 100, Down: 100, ExpiryTime: earlyAbs, Enable: true, Reset: 30,
  569. })
  570. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  571. Updates(map[string]any{"expiry_time": earlyAbs, "enable": true, "up": int64(100), "down": int64(100)}).Error; err != nil {
  572. t.Fatalf("seed master: %v", err)
  573. }
  574. if err := db.Model(model.Node{}).Where("id = ?", 1).
  575. Updates(map[string]any{"config_dirty": true, "config_dirty_at": int64(1)}).Error; err != nil {
  576. t.Fatalf("mark dirty: %v", err)
  577. }
  578. // Renewal shape: later expiry + counters below baseline.
  579. snap := &runtime.TrafficSnapshot{
  580. Inbounds: []*model.Inbound{{
  581. Tag: "n1-in",
  582. ClientStats: []xray.ClientTraffic{{
  583. Email: email, Up: 10, Down: 10, ExpiryTime: lateAbs, Enable: true, Reset: 30,
  584. }},
  585. }},
  586. }
  587. if _, err := svc.setRemoteTrafficLocked(1, snap, true, false); err != nil {
  588. t.Fatalf("dirty sync: %v", err)
  589. }
  590. got := readTraffic(t, db, email)
  591. if got.ExpiryTime != earlyAbs {
  592. t.Fatalf("renewal while dirty raised expiry: got %d want %d", got.ExpiryTime, earlyAbs)
  593. }
  594. // Same renewal-shaped stats on a clean tick must still advance expiry —
  595. // dirty must not have burned the dipped baseline.
  596. if err := db.Model(model.Node{}).Where("id = ?", 1).
  597. Updates(map[string]any{"config_dirty": false, "config_dirty_at": int64(0)}).Error; err != nil {
  598. t.Fatalf("clear dirty: %v", err)
  599. }
  600. if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
  601. t.Fatalf("clean sync: %v", err)
  602. }
  603. if got := readTraffic(t, db, email); got.ExpiryTime != lateAbs {
  604. t.Fatalf("renewal after dirty clear did not apply: got %d want %d", got.ExpiryTime, lateAbs)
  605. }
  606. }
  607. // TestNodeExtend_FreshSettingsLaggingDisable: after extend, settings may already
  608. // show the new absolute while ClientStats still report enable=false + old expiry.
  609. func TestNodeExtend_FreshSettingsLaggingDisable(t *testing.T) {
  610. db := initTrafficTestDB(t)
  611. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "ext-lag")
  612. svc := &InboundService{}
  613. const email = "ext-lag"
  614. expired, extended := earlyAbs, lateAbs
  615. staleSettings := fmt.Sprintf(
  616. `{"clients":[{"email":%q,"enable":false,"expiryTime":%d}]}`, email, expired)
  617. freshSettings := fmt.Sprintf(
  618. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, extended)
  619. syncNodeWithSettings(t, svc, 1, "n1-in", staleSettings,
  620. xray.ClientTraffic{Email: email, ExpiryTime: expired, Enable: false})
  621. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  622. Updates(map[string]any{"expiry_time": extended, "enable": true}).Error; err != nil {
  623. t.Fatalf("master extend: %v", err)
  624. }
  625. snap := &runtime.TrafficSnapshot{
  626. Inbounds: []*model.Inbound{{
  627. Tag: "n1-in", Settings: freshSettings,
  628. ClientStats: []xray.ClientTraffic{{Email: email, ExpiryTime: expired, Enable: false}},
  629. }},
  630. }
  631. if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
  632. t.Fatalf("clean sync: %v", err)
  633. }
  634. got := readTraffic(t, db, email)
  635. if got.ExpiryTime != extended {
  636. t.Fatalf("expiry clobbered: got %d want %d", got.ExpiryTime, extended)
  637. }
  638. if !got.Enable {
  639. t.Fatal("lagging ClientStats enable=false latched master off after extend")
  640. }
  641. }
  642. // A client that used no traffic never dips below its baseline, so the renewal
  643. // counter is the only evidence the node auto-renewed (#6228).
  644. func TestNodeRenew_ZeroTrafficUsesResetCount(t *testing.T) {
  645. db := initTrafficTestDB(t)
  646. createNodeInbound(t, db, 1, "n1-in", 41001)
  647. svc := &InboundService{}
  648. const email = "idle-renew"
  649. firstSettings := fmt.Sprintf(
  650. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, earlyAbs)
  651. renewSettings := fmt.Sprintf(
  652. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, lateAbs)
  653. syncNodeWithSettings(t, svc, 1, "n1-in", firstSettings, xray.ClientTraffic{
  654. Email: email, ExpiryTime: earlyAbs, Reset: 30, Enable: true,
  655. })
  656. syncNodeWithSettings(t, svc, 1, "n1-in", renewSettings, xray.ClientTraffic{
  657. Email: email, ExpiryTime: lateAbs, Reset: 30, ResetCount: 1, Enable: true,
  658. })
  659. got := readTraffic(t, db, email)
  660. if got.ExpiryTime != lateAbs {
  661. t.Fatalf("zero-traffic renewal dropped: expiry=%d want %d", got.ExpiryTime, lateAbs)
  662. }
  663. if got.ResetCount != 1 {
  664. t.Fatalf("renewal count not persisted, so the next renewal cannot be seen: got %d want 1", got.ResetCount)
  665. }
  666. }
  667. // A quota top-up leaves the expiry alone, so the node's lagging disable must be
  668. // recognised by the stale quota it was decided against (#6228).
  669. func TestNodeQuotaTopUp_LaggingDisableIgnored(t *testing.T) {
  670. db := initTrafficTestDB(t)
  671. createNodeInbound(t, db, 1, "n1-in", 41001)
  672. svc := &InboundService{}
  673. const email = "topped-up"
  674. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  675. Email: email, Up: 10, Down: 10, Total: 100, ExpiryTime: lateAbs, Enable: true,
  676. })
  677. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  678. Updates(map[string]any{
  679. "total": int64(500), "enable": true, "up": int64(60), "down": int64(50),
  680. }).Error; err != nil {
  681. t.Fatalf("master top-up: %v", err)
  682. }
  683. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  684. Email: email, Up: 60, Down: 50, Total: 100, ExpiryTime: lateAbs, Enable: false,
  685. })
  686. if got := readTraffic(t, db, email); !got.Enable {
  687. t.Fatal("node disable decided on the pre-top-up quota latched over the raised one")
  688. }
  689. }
  690. // The settings lift is authoritative in both directions: a blob predating a
  691. // master disable must not carry enable=true back into central settings (#4917).
  692. func TestNodeStaleEnable_LiftedOffInSettings(t *testing.T) {
  693. db := initTrafficTestDB(t)
  694. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "cut-off")
  695. svc := &InboundService{}
  696. const email = "cut-off"
  697. liveSettings := fmt.Sprintf(
  698. `{"clients":[{"email":%q,"enable":true,"expiryTime":%d}]}`, email, lateAbs)
  699. syncNodeWithSettings(t, svc, 1, "n1-in", liveSettings,
  700. xray.ClientTraffic{Email: email, ExpiryTime: lateAbs, Enable: true})
  701. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  702. Update("enable", false).Error; err != nil {
  703. t.Fatalf("master disable: %v", err)
  704. }
  705. syncNodeWithSettings(t, svc, 1, "n1-in", liveSettings,
  706. xray.ClientTraffic{Email: email, ExpiryTime: lateAbs, Enable: true})
  707. var ib model.Inbound
  708. if err := db.Where("tag = ?", "n1-in").First(&ib).Error; err != nil {
  709. t.Fatalf("read inbound: %v", err)
  710. }
  711. clients, err := svc.GetClients(&ib)
  712. if err != nil {
  713. t.Fatalf("GetClients: %v", err)
  714. }
  715. var found bool
  716. for _, c := range clients {
  717. if c.Email != email {
  718. continue
  719. }
  720. found = true
  721. if c.Enable {
  722. t.Fatal("adopted settings re-enabled a client the master disabled")
  723. }
  724. }
  725. if !found {
  726. t.Fatal("client missing from adopted inbound settings")
  727. }
  728. }
  729. // The tick whose push just landed freezes only the lifecycle merge: adoption,
  730. // new client rows and traffic accumulation must keep working (#6228).
  731. func TestNodeJustPushed_FreezesLifecycleOnly(t *testing.T) {
  732. db := initTrafficTestDB(t)
  733. createNodeInbound(t, db, 1, "n1-in", 41001)
  734. svc := &InboundService{}
  735. const kept = "kept"
  736. const fresh = "fresh"
  737. extended := lateAbs + 86400000
  738. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{
  739. Email: kept, Up: 10, Down: 10, Total: 100, ExpiryTime: lateAbs, Enable: true,
  740. })
  741. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", kept).
  742. Updates(map[string]any{"expiry_time": extended, "total": int64(500)}).Error; err != nil {
  743. t.Fatalf("master edit: %v", err)
  744. }
  745. snap := &runtime.TrafficSnapshot{
  746. Inbounds: []*model.Inbound{{
  747. Tag: "n1-in",
  748. ClientStats: []xray.ClientTraffic{
  749. {Email: kept, Up: 20, Down: 20, Total: 100, ExpiryTime: lateAbs, Enable: false},
  750. {Email: fresh, Up: 5, Down: 5, ExpiryTime: lateAbs, Enable: true},
  751. },
  752. }},
  753. }
  754. if _, err := svc.setRemoteTrafficLocked(1, snap, false, true); err != nil {
  755. t.Fatalf("just-pushed sync: %v", err)
  756. }
  757. got := readTraffic(t, db, kept)
  758. if got.ExpiryTime != extended || got.Total != 500 || !got.Enable {
  759. t.Fatalf("just-pushed tick adopted lagging lifecycle: expiry=%d total=%d enable=%v",
  760. got.ExpiryTime, got.Total, got.Enable)
  761. }
  762. if got.Up != 10 || got.Down != 10 {
  763. t.Fatalf("just-pushed tick dropped this tick's traffic: up=%d down=%d, want 10/10", got.Up, got.Down)
  764. }
  765. if row := readTraffic(t, db, fresh); row.Email != fresh {
  766. t.Fatalf("just-pushed tick skipped adoption of a new client: %+v", row)
  767. }
  768. }
  769. // TestClientTrafficMergeSQLMatchesHelpers pins the dialect SQL expressions
  770. // against the Go helpers so the in-memory replay after UPDATE cannot drift.
  771. func TestClientTrafficMergeSQLMatchesHelpers(t *testing.T) {
  772. db := initTrafficTestDB(t)
  773. const email = "sql-merge"
  774. now := time.Now().UnixMilli()
  775. cases := []struct {
  776. name string
  777. masterExpiry int64
  778. masterEnable bool
  779. masterUp, masterDown, tot int64
  780. deltaUp, deltaDown int64
  781. nodeExpiry, nodeTotal int64
  782. nodeEnable bool
  783. wantExpiry int64
  784. wantEnable bool
  785. }{
  786. {
  787. name: "stale expiry+disable after extend",
  788. masterExpiry: lateAbs, masterEnable: true,
  789. nodeExpiry: earlyAbs, nodeEnable: false,
  790. wantExpiry: lateAbs, wantEnable: true,
  791. },
  792. {
  793. name: "same expiry quota disable",
  794. masterExpiry: lateAbs, masterEnable: true,
  795. masterUp: 60, masterDown: 50, tot: 100,
  796. nodeExpiry: lateAbs, nodeTotal: 100, nodeEnable: false,
  797. wantExpiry: lateAbs, wantEnable: false,
  798. },
  799. {
  800. name: "older expiry but master over quota",
  801. masterExpiry: lateAbs, masterEnable: true,
  802. masterUp: 60, masterDown: 50, tot: 100,
  803. nodeExpiry: earlyAbs, nodeTotal: 100, nodeEnable: false,
  804. wantExpiry: lateAbs, wantEnable: false,
  805. },
  806. {
  807. name: "master absolute ignores later node",
  808. masterExpiry: earlyAbs, masterEnable: true,
  809. nodeExpiry: lateAbs, nodeEnable: true,
  810. wantExpiry: earlyAbs, wantEnable: true,
  811. },
  812. {
  813. name: "negative node keeps absolute",
  814. masterExpiry: lateAbs, masterEnable: true,
  815. nodeExpiry: -2592000000, nodeEnable: true,
  816. wantExpiry: lateAbs, wantEnable: true,
  817. },
  818. {
  819. name: "expired master latches node disable",
  820. masterExpiry: earlyAbs, masterEnable: true,
  821. nodeExpiry: earlyAbs - 1000, nodeEnable: false,
  822. wantExpiry: earlyAbs, wantEnable: false,
  823. },
  824. {
  825. name: "older expiry crosses quota via deltas",
  826. masterExpiry: lateAbs, masterEnable: true,
  827. masterUp: 40, masterDown: 50, tot: 100,
  828. deltaUp: 10, deltaDown: 10,
  829. nodeExpiry: earlyAbs, nodeTotal: 100, nodeEnable: false,
  830. wantExpiry: lateAbs, wantEnable: false,
  831. },
  832. }
  833. enableExpr := database.ClientTrafficEnableMergeExpr()
  834. expiryExpr := database.ClientTrafficExpiryMergeExpr()
  835. for i, c := range cases {
  836. t.Run(c.name, func(t *testing.T) {
  837. rowEmail := fmt.Sprintf("%s-%d", email, i)
  838. if err := db.Create(&xray.ClientTraffic{
  839. InboundId: 1, Email: rowEmail, Enable: c.masterEnable,
  840. ExpiryTime: c.masterExpiry, Up: c.masterUp, Down: c.masterDown, Total: c.tot,
  841. }).Error; err != nil {
  842. t.Fatalf("seed: %v", err)
  843. }
  844. master := &xray.ClientTraffic{
  845. ExpiryTime: c.masterExpiry, Enable: c.masterEnable,
  846. Up: c.masterUp, Down: c.masterDown, Total: c.tot,
  847. }
  848. wantExpiry := mergeActivationExpiry(c.masterExpiry, c.nodeExpiry)
  849. wantEnable := c.masterEnable
  850. node := xray.ClientTraffic{ExpiryTime: c.nodeExpiry, Total: c.nodeTotal}
  851. if !c.nodeEnable && !nodeDisableIsStale(master, node, now, c.deltaUp, c.deltaDown) {
  852. wantEnable = false
  853. }
  854. if wantExpiry != c.wantExpiry || wantEnable != c.wantEnable {
  855. t.Fatalf("helper expectation drift: helpers=(%d,%v) fixture=(%d,%v)",
  856. wantExpiry, wantEnable, c.wantExpiry, c.wantEnable)
  857. }
  858. if err := db.Exec(
  859. fmt.Sprintf(
  860. `UPDATE client_traffics SET enable = %s, expiry_time = %s WHERE email = ?`,
  861. enableExpr, expiryExpr,
  862. ),
  863. c.nodeEnable, c.nodeExpiry, c.nodeTotal, now, c.deltaUp, c.deltaDown,
  864. c.nodeExpiry,
  865. rowEmail,
  866. ).Error; err != nil {
  867. t.Fatalf("SQL merge: %v", err)
  868. }
  869. got := readTraffic(t, db, rowEmail)
  870. if got.ExpiryTime != c.wantExpiry || got.Enable != c.wantEnable {
  871. t.Fatalf("SQL merge got expiry=%d enable=%v, want expiry=%d enable=%v",
  872. got.ExpiryTime, got.Enable, c.wantExpiry, c.wantEnable)
  873. }
  874. })
  875. }
  876. }
  877. // Relative to the run: the merge rules now compare the master deadline against
  878. // wall-clock now, so fixed timestamps would rot into the wrong side of it.
  879. var (
  880. earlyAbs = time.Now().UnixMilli() - 30*86400000
  881. lateAbs = time.Now().UnixMilli() + 30*86400000
  882. )
  883. // TestNodeActivationLiftsClientRecordExpiry reproduces #5714: the node activates
  884. // the deadline (positive ClientStats) while its settings JSON still carries the
  885. // negative duration, so SyncInbound keeps writing the stale value into the
  886. // client record and the Clients page shows "not started" forever.
  887. func TestNodeActivationLiftsClientRecordExpiry(t *testing.T) {
  888. db := initTrafficTestDB(t)
  889. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "delayed")
  890. svc := &InboundService{}
  891. const email = "delayed"
  892. const duration = int64(-2592000000)
  893. const activated = int64(1798448344010)
  894. negSettings := `{"clients":[{"email":"delayed","enable":true,"expiryTime":-2592000000}]}`
  895. if err := db.Create(&model.ClientRecord{Email: email, Enable: true, ExpiryTime: duration}).Error; err != nil {
  896. t.Fatalf("seed client record: %v", err)
  897. }
  898. readRecordExpiry := func() int64 {
  899. t.Helper()
  900. var rec model.ClientRecord
  901. if err := db.Where("email = ?", email).First(&rec).Error; err != nil {
  902. t.Fatalf("read client record: %v", err)
  903. }
  904. return rec.ExpiryTime
  905. }
  906. syncNodeWithSettings(t, svc, 1, "n1-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
  907. if got := readRecordExpiry(); got != duration {
  908. t.Fatalf("before activation: record expiry = %d, want %d", got, duration)
  909. }
  910. syncNodeWithSettings(t, svc, 1, "n1-in", negSettings, xray.ClientTraffic{Email: email, Up: 100, Down: 100, ExpiryTime: activated, Enable: true})
  911. if got := readTraffic(t, db, email).ExpiryTime; got != activated {
  912. t.Fatalf("client_traffics not activated: expiry = %d, want %d", got, activated)
  913. }
  914. if got := readRecordExpiry(); got != activated {
  915. t.Fatalf("client record kept stale duration (#5714): expiry = %d, want %d", got, activated)
  916. }
  917. }