service_dedup_test.go 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. package sub
  2. import (
  3. "fmt"
  4. "path/filepath"
  5. "strings"
  6. "testing"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. )
  10. // TestGetSubs_DuplicateSettingsClients_Deduped reproduces #5134: multi-node
  11. // sync/import drift can leave the same client twice inside an inbound's
  12. // legacy settings.clients JSON while the normalized client_inbounds table
  13. // stays clean. The subscription output must still contain one profile per
  14. // (inbound, client).
  15. func TestGetSubs_DuplicateSettingsClients_Deduped(t *testing.T) {
  16. dbDir := t.TempDir()
  17. t.Setenv("XUI_DB_FOLDER", dbDir)
  18. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  19. t.Fatalf("InitDB: %v", err)
  20. }
  21. t.Cleanup(func() { _ = database.CloseDB() })
  22. const subId = "sub-dup"
  23. const email = "[email protected]"
  24. const uuid = "f1b9265f-26a8-4b75-9be2-c64a94b15de1"
  25. db := database.GetDB()
  26. settings := fmt.Sprintf(`{"clients": [
  27. {"id": %q, "email": %q, "subId": %q, "enable": true},
  28. {"id": %q, "email": %q, "subId": %q, "enable": true}
  29. ]}`, uuid, email, subId, uuid, email, subId)
  30. ib := &model.Inbound{
  31. UserId: 1,
  32. Tag: "dup-in",
  33. Enable: true,
  34. Port: 42001,
  35. Protocol: model.VLESS,
  36. Settings: settings,
  37. StreamSettings: `{"network": "tcp", "security": "none"}`,
  38. }
  39. if err := db.Create(ib).Error; err != nil {
  40. t.Fatalf("seed inbound: %v", err)
  41. }
  42. client := &model.ClientRecord{Email: email, SubID: subId, UUID: uuid, Enable: true}
  43. if err := db.Create(client).Error; err != nil {
  44. t.Fatalf("seed client: %v", err)
  45. }
  46. if err := db.Create(&model.ClientInbound{ClientId: client.Id, InboundId: ib.Id}).Error; err != nil {
  47. t.Fatalf("seed client_inbound: %v", err)
  48. }
  49. s := NewSubService(false, "-ieo")
  50. links, emails, _, _, err := s.GetSubs(subId, "sub.example.com")
  51. if err != nil {
  52. t.Fatalf("GetSubs: %v", err)
  53. }
  54. if len(links) != 1 {
  55. t.Fatalf("links = %d, want 1 (duplicate settings.clients entries must collapse)", len(links))
  56. }
  57. if len(emails) != 1 {
  58. t.Fatalf("emails = %d, want 1, got %v", len(emails), emails)
  59. }
  60. // Identity, not just count: the single surviving link must be for this client.
  61. if !strings.Contains(links[0], uuid) {
  62. t.Fatalf("surviving link must carry the client uuid %q, got %q", uuid, links[0])
  63. }
  64. }
  65. // TestMatchingClients_DedupsCaseInsensitiveEmail pins the dedup KEY, not just the count:
  66. // the two entries differ only by email case, so dropping strings.ToLower (or keying on
  67. // another field) yields two clients. The byte-identical dupes above can't catch that.
  68. func TestMatchingClients_DedupsCaseInsensitiveEmail(t *testing.T) {
  69. const subId = "s1"
  70. const uuid = "11111111-2222-4333-8444-555555555555"
  71. ib := &model.Inbound{
  72. Protocol: model.VLESS,
  73. Settings: `{"clients":[
  74. {"id":"` + uuid + `","email":"[email protected]","subId":"` + subId + `","enable":true},
  75. {"id":"` + uuid + `","email":"[email protected]","subId":"` + subId + `","enable":true}
  76. ]}`,
  77. }
  78. s := &SubService{}
  79. got := s.matchingClients(ib, subId)
  80. if len(got) != 1 {
  81. t.Fatalf("case-differing duplicate emails must dedup to 1 client, got %d", len(got))
  82. }
  83. if got[0].Email != "[email protected]" {
  84. t.Fatalf("first occurrence must be kept, got %q", got[0].Email)
  85. }
  86. // A wrong subId must still be excluded (guards the subId filter at service.go:127).
  87. if other := s.matchingClients(ib, "nope"); len(other) != 0 {
  88. t.Fatalf("non-matching subId must yield 0 clients, got %d", len(other))
  89. }
  90. }