1
0

node_dirty_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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/web/runtime"
  7. )
  8. // While a node is config-dirty (a local edit committed before it could be
  9. // mirrored to the node), the traffic pull must not overwrite the central
  10. // inbound's config columns from the node's stale snapshot — only traffic
  11. // counters may advance. Otherwise a reconnecting node reverts the edit.
  12. func TestSetRemoteTraffic_DirtyPreservesConfig(t *testing.T) {
  13. setupConflictDB(t)
  14. db := database.GetDB()
  15. node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
  16. if err := db.Create(node).Error; err != nil {
  17. t.Fatalf("create node: %v", err)
  18. }
  19. id := node.Id
  20. const desiredSettings = `{"clients":[{"email":"a@x"}]}`
  21. central := &model.Inbound{
  22. UserId: 1,
  23. NodeID: &id,
  24. Tag: "in-443-tcp",
  25. Enable: true,
  26. Port: 443,
  27. Protocol: model.VLESS,
  28. Settings: desiredSettings,
  29. }
  30. if err := db.Create(central).Error; err != nil {
  31. t.Fatalf("create inbound: %v", err)
  32. }
  33. snap := &runtime.TrafficSnapshot{
  34. Inbounds: []*model.Inbound{{
  35. Tag: "in-443-tcp",
  36. Enable: true,
  37. Port: 443,
  38. Protocol: model.VLESS,
  39. Settings: `{"clients":[{"email":"b@x"}]}`,
  40. Up: 500,
  41. Down: 700,
  42. }},
  43. }
  44. svc := InboundService{}
  45. if _, err := svc.setRemoteTrafficLocked(id, snap, true); err != nil {
  46. t.Fatalf("setRemoteTrafficLocked dirty: %v", err)
  47. }
  48. var got model.Inbound
  49. if err := db.First(&got, central.Id).Error; err != nil {
  50. t.Fatalf("reload inbound: %v", err)
  51. }
  52. if got.Settings != desiredSettings {
  53. t.Fatalf("dirty pull overwrote settings: want %q got %q", desiredSettings, got.Settings)
  54. }
  55. if got.Up != 500 || got.Down != 700 {
  56. t.Fatalf("traffic counters not applied while dirty: up=%d down=%d", got.Up, got.Down)
  57. }
  58. }
  59. // Deleting a *disabled* client attached to a node inbound must still propagate
  60. // to the node. The node's own DB carries the (disabled) client, so the central
  61. // panel has to mark the node dirty (→ reconcile) instead of dropping the delete
  62. // and letting the next traffic snapshot resurrect the client. Regression for
  63. // the enable-flag gate that used to skip the node path entirely (#5352).
  64. func TestDelInboundClientByEmail_DisabledNodeClientMarksDirty(t *testing.T) {
  65. setupConflictDB(t)
  66. db := database.GetDB()
  67. // Offline node so nodePushPlan reports dirty without needing a live runtime.
  68. node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "offline"}
  69. if err := db.Create(node).Error; err != nil {
  70. t.Fatalf("create node: %v", err)
  71. }
  72. id := node.Id
  73. central := &model.Inbound{
  74. UserId: 1,
  75. NodeID: &id,
  76. Tag: "in-443-tcp",
  77. Enable: true,
  78. Port: 443,
  79. Protocol: model.VLESS,
  80. Settings: `{"clients":[{"email":"a@x","enable":false}]}`,
  81. }
  82. if err := db.Create(central).Error; err != nil {
  83. t.Fatalf("create inbound: %v", err)
  84. }
  85. inboundSvc := &InboundService{}
  86. clientSvc := &ClientService{}
  87. if _, err := clientSvc.DelInboundClientByEmail(inboundSvc, central.Id, "a@x", false); err != nil {
  88. t.Fatalf("DelInboundClientByEmail: %v", err)
  89. }
  90. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(id); err != nil {
  91. t.Fatalf("NodeSyncState: %v", err)
  92. } else if !dirty {
  93. t.Fatal("deleting a disabled node client must mark the node dirty (#5352)")
  94. }
  95. }
  96. // ClearNodeDirty must be a compare-and-swap on config_dirty_at so a concurrent
  97. // edit that re-dirties the node during a reconcile is not silently cleared.
  98. func TestNodeDirty_ClearIsCASOnDirtyAt(t *testing.T) {
  99. setupConflictDB(t)
  100. db := database.GetDB()
  101. node := &model.Node{Name: "n2", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
  102. if err := db.Create(node).Error; err != nil {
  103. t.Fatalf("create node: %v", err)
  104. }
  105. nodeSvc := NodeService{}
  106. if err := nodeSvc.MarkNodeDirty(node.Id); err != nil {
  107. t.Fatalf("MarkNodeDirty: %v", err)
  108. }
  109. _, _, dirty, dirtyAt, err := nodeSvc.NodeSyncState(node.Id)
  110. if err != nil {
  111. t.Fatalf("NodeSyncState: %v", err)
  112. }
  113. if !dirty {
  114. t.Fatal("node should be dirty after MarkNodeDirty")
  115. }
  116. if err := nodeSvc.ClearNodeDirty(node.Id, dirtyAt-1); err != nil {
  117. t.Fatalf("ClearNodeDirty stale token: %v", err)
  118. }
  119. if _, _, stillDirty, _, _ := nodeSvc.NodeSyncState(node.Id); !stillDirty {
  120. t.Fatal("stale-token clear must not clear the dirty flag")
  121. }
  122. if err := nodeSvc.ClearNodeDirty(node.Id, dirtyAt); err != nil {
  123. t.Fatalf("ClearNodeDirty matching token: %v", err)
  124. }
  125. if _, _, stillDirty, _, _ := nodeSvc.NodeSyncState(node.Id); stillDirty {
  126. t.Fatal("matching-token clear must clear the dirty flag")
  127. }
  128. }