client_delete_tombstone_test.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. package service
  2. import (
  3. "fmt"
  4. "testing"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  8. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  9. )
  10. // Delete tombstones up front and keeps the record when an inbound fails. A
  11. // surviving tombstone lets the next node merge finish the refused deletion.
  12. func TestFailedDeleteWithdrawsTombstone(t *testing.T) {
  13. setupBulkDB(t)
  14. svc := &ClientService{}
  15. inboundSvc := &InboundService{}
  16. db := database.GetDB()
  17. const email = "retry@x"
  18. broken := mkInbound(t, 30401, model.VLESS, `{"clients": [ THIS IS NOT JSON`)
  19. rec := &model.ClientRecord{Email: email, Enable: true, UUID: "33333333-3333-3333-3333-333333333333"}
  20. if err := db.Create(rec).Error; err != nil {
  21. t.Fatalf("create client record: %v", err)
  22. }
  23. if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: broken.Id}).Error; err != nil {
  24. t.Fatalf("attach client: %v", err)
  25. }
  26. t.Cleanup(func() { withdrawClientTombstones(email) })
  27. if _, err := svc.Delete(inboundSvc, rec.Id, false); err == nil {
  28. t.Fatal("setup: delete was expected to fail on the unparseable inbound settings")
  29. }
  30. var surviving int64
  31. if err := db.Model(&model.ClientRecord{}).Where("email = ?", email).Count(&surviving).Error; err != nil {
  32. t.Fatalf("count clients: %v", err)
  33. }
  34. if surviving != 1 {
  35. t.Fatalf("failed delete must keep the record for a retry, got %d rows", surviving)
  36. }
  37. if isClientEmailTombstoned(email) {
  38. t.Fatal("delete kept the record but left a live tombstone: the next node sync would finish the deletion it refused")
  39. }
  40. }
  41. // A client re-created under a just-deleted email is a live identity again. If
  42. // the tombstone outlives it, the node merge prunes the new client's link.
  43. func TestRecreatedClientSurvivesNodeMerge(t *testing.T) {
  44. db := initTrafficTestDB(t)
  45. svc := &ClientService{}
  46. inboundSvc := &InboundService{}
  47. const email = "reborn@x"
  48. seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
  49. createNodeInboundWithClient(t, db, 1, "n1-in", 41501, email)
  50. nodeSettings := fmt.Sprintf(`{"clients": [{"email": %q, "enable": true}]}`, email)
  51. syncNodeWithSettings(t, inboundSvc, 1, "n1-in", nodeSettings, xray.ClientTraffic{Email: email, Enable: true})
  52. var ib model.Inbound
  53. if err := db.Where("tag = ?", "n1-in").First(&ib).Error; err != nil {
  54. t.Fatalf("load inbound: %v", err)
  55. }
  56. rec := &model.ClientRecord{}
  57. if err := db.Where("email = ?", email).First(rec).Error; err != nil {
  58. t.Fatalf("load adopted client record: %v", err)
  59. }
  60. t.Cleanup(func() { withdrawClientTombstones(email) })
  61. if _, err := svc.Delete(inboundSvc, rec.Id, false); err != nil {
  62. t.Fatalf("delete client: %v", err)
  63. }
  64. if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
  65. Client: model.Client{Email: email, Enable: true, ID: "44444444-4444-4444-4444-444444444444"},
  66. InboundIds: []int{ib.Id},
  67. }); err != nil {
  68. t.Fatalf("re-create client: %v", err)
  69. }
  70. // The create marks the node config-dirty, which parks the client merge; the
  71. // reconcile clears it a tick later, well inside the 90s tombstone window.
  72. if err := db.Model(&model.Node{}).Where("id = ?", 1).Update("config_dirty", false).Error; err != nil {
  73. t.Fatalf("clear config_dirty: %v", err)
  74. }
  75. snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{{
  76. Tag: "n1-in", Protocol: model.VLESS, Settings: nodeSettings,
  77. ClientStats: []xray.ClientTraffic{{Email: email, Enable: true}},
  78. }}}
  79. if _, err := inboundSvc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
  80. t.Fatalf("node merge: %v", err)
  81. }
  82. var links int64
  83. if err := db.Model(&model.ClientInbound{}).
  84. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  85. Where("clients.email = ? AND client_inbounds.inbound_id = ?", email, ib.Id).
  86. Count(&links).Error; err != nil {
  87. t.Fatalf("count client links: %v", err)
  88. }
  89. if links != 1 {
  90. t.Fatalf("re-created client has %d inbound links after the node merge, want 1: the delete tombstone outlived the email and the merge pruned it", links)
  91. }
  92. }