client_edit_node_dirty_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. package service
  2. import (
  3. "context"
  4. "sync/atomic"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. )
  9. // dirtyProbeRuntime reads a node's config_dirty flag at the moment an inbound
  10. // pushes, i.e. while the edit's other inbounds are still unapplied.
  11. type dirtyProbeRuntime struct {
  12. fakeNodeRuntime
  13. watch int
  14. sawSet atomic.Bool
  15. probed atomic.Bool
  16. }
  17. func (d *dirtyProbeRuntime) UpdateUser(ctx context.Context, ib *model.Inbound, oldEmail string, c model.Client) error {
  18. var node model.Node
  19. if err := database.GetDB().Where("id = ?", d.watch).First(&node).Error; err == nil {
  20. d.sawSet.Store(node.ConfigDirty)
  21. d.probed.Store(true)
  22. }
  23. return d.fakeNodeRuntime.UpdateUser(ctx, ib, oldEmail, c)
  24. }
  25. // TestEditFlagsOtherNodesBeforeApplying pins the ordering: every node is flagged
  26. // BEFORE any inbound applies, so no merge can slot into the fanout gap.
  27. func TestEditFlagsOtherNodesBeforeApplying(t *testing.T) {
  28. setupBulkDB(t)
  29. startSerializedWriter(t)
  30. mgr := useTestRuntimeManager(t)
  31. db := database.GetDB()
  32. const uuid = "eeeeeeee-1111-2222-3333-444444444444"
  33. probe := &dirtyProbeRuntime{}
  34. ids := fanoutNodeInbounds(t, mgr, probe, 2, 47200)
  35. // The watched node is the one whose apply is made to fail, so nothing but
  36. // the up-front marking can have flagged it when the other inbound pushes.
  37. var victim model.Inbound
  38. if err := db.Where("id = ?", ids[1]).First(&victim).Error; err != nil {
  39. t.Fatalf("read inbound %d: %v", ids[1], err)
  40. }
  41. probe.watch = *victim.NodeID
  42. if _, err := (&ClientService{}).Create(&InboundService{}, &ClientCreatePayload{
  43. Client: model.Client{Email: "carol", ID: uuid, SubID: "sub-carol", Enable: true},
  44. InboundIds: ids,
  45. }); err != nil {
  46. t.Fatalf("seed Create: %v", err)
  47. }
  48. if err := db.Model(&model.Inbound{}).Where("id = ?", ids[1]).
  49. Update("settings", `{"clients":`).Error; err != nil {
  50. t.Fatalf("corrupt inbound %d: %v", ids[1], err)
  51. }
  52. if err := db.Model(model.Node{}).Where("1 = 1").
  53. Update("config_dirty", false).Error; err != nil {
  54. t.Fatalf("clear config_dirty: %v", err)
  55. }
  56. rec := lookupClientRecord(t, "carol")
  57. if _, err := (&ClientService{}).Update(&InboundService{}, rec.Id, model.Client{
  58. Email: "carol-renamed", ID: uuid, SubID: "sub-carol", Enable: true,
  59. }, 0); err == nil {
  60. t.Fatal("Update on a corrupted inbound should report the failure")
  61. }
  62. if !probe.probed.Load() {
  63. t.Fatal("the healthy inbound never pushed, so the ordering was never observed")
  64. }
  65. if !probe.sawSet.Load() {
  66. t.Fatal("a node was still clean while another inbound of the same edit was applying: a snapshot merge in that gap would resurrect the pre-edit email")
  67. }
  68. }
  69. // TestEditFlagsNodesOutsideTheInboundFilter pins that the marking covers the
  70. // client's whole attachment set, not just the inbounds the filter applies.
  71. func TestEditFlagsNodesOutsideTheInboundFilter(t *testing.T) {
  72. setupBulkDB(t)
  73. startSerializedWriter(t)
  74. mgr := useTestRuntimeManager(t)
  75. db := database.GetDB()
  76. const uuid = "ffffffff-1111-2222-3333-444444444444"
  77. ids := fanoutNodeInbounds(t, mgr, &fakeNodeRuntime{}, 2, 47300)
  78. if _, err := (&ClientService{}).Create(&InboundService{}, &ClientCreatePayload{
  79. Client: model.Client{Email: "dave", ID: uuid, SubID: "sub-dave", Enable: true},
  80. InboundIds: ids,
  81. }); err != nil {
  82. t.Fatalf("seed Create: %v", err)
  83. }
  84. if err := db.Model(model.Node{}).Where("1 = 1").
  85. Update("config_dirty", false).Error; err != nil {
  86. t.Fatalf("clear config_dirty: %v", err)
  87. }
  88. // Edit scoped to the first inbound only; the second one's node still holds
  89. // the old email once the shared record is renamed.
  90. rec := lookupClientRecord(t, "dave")
  91. if _, err := (&ClientService{}).Update(&InboundService{}, rec.Id, model.Client{
  92. Email: "dave-renamed", ID: uuid, SubID: "sub-dave", Enable: true,
  93. }, 0, ids[0]); err != nil {
  94. t.Fatalf("filtered Update: %v", err)
  95. }
  96. var excluded model.Inbound
  97. if err := db.Where("id = ?", ids[1]).First(&excluded).Error; err != nil {
  98. t.Fatalf("read inbound %d: %v", ids[1], err)
  99. }
  100. var node model.Node
  101. if err := db.Where("id = ?", *excluded.NodeID).First(&node).Error; err != nil {
  102. t.Fatalf("read node %d: %v", *excluded.NodeID, err)
  103. }
  104. if !node.ConfigDirty {
  105. t.Fatal("a node left out of the inboundIds filter stayed clean after the shared record was renamed, so its stale snapshot would be merged")
  106. }
  107. }