node_dirty_test.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/database"
  5. "github.com/mhsanaei/3x-ui/v3/database/model"
  6. "github.com/mhsanaei/3x-ui/v3/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. // ClearNodeDirty must be a compare-and-swap on config_dirty_at so a concurrent
  60. // edit that re-dirties the node during a reconcile is not silently cleared.
  61. func TestNodeDirty_ClearIsCASOnDirtyAt(t *testing.T) {
  62. setupConflictDB(t)
  63. db := database.GetDB()
  64. node := &model.Node{Name: "n2", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
  65. if err := db.Create(node).Error; err != nil {
  66. t.Fatalf("create node: %v", err)
  67. }
  68. nodeSvc := NodeService{}
  69. if err := nodeSvc.MarkNodeDirty(node.Id); err != nil {
  70. t.Fatalf("MarkNodeDirty: %v", err)
  71. }
  72. _, _, dirty, dirtyAt, err := nodeSvc.NodeSyncState(node.Id)
  73. if err != nil {
  74. t.Fatalf("NodeSyncState: %v", err)
  75. }
  76. if !dirty {
  77. t.Fatal("node should be dirty after MarkNodeDirty")
  78. }
  79. if err := nodeSvc.ClearNodeDirty(node.Id, dirtyAt-1); err != nil {
  80. t.Fatalf("ClearNodeDirty stale token: %v", err)
  81. }
  82. if _, _, stillDirty, _, _ := nodeSvc.NodeSyncState(node.Id); !stillDirty {
  83. t.Fatal("stale-token clear must not clear the dirty flag")
  84. }
  85. if err := nodeSvc.ClearNodeDirty(node.Id, dirtyAt); err != nil {
  86. t.Fatalf("ClearNodeDirty matching token: %v", err)
  87. }
  88. if _, _, stillDirty, _, _ := nodeSvc.NodeSyncState(node.Id); stillDirty {
  89. t.Fatal("matching-token clear must clear the dirty flag")
  90. }
  91. }