inbound_dedupe_test.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. package database
  2. import (
  3. "encoding/json"
  4. "path/filepath"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. )
  8. // TestDedupeInboundSettingsClients_CollapsesDuplicateEmails covers the #5770
  9. // repair: settings.clients arrays written by older builds can carry the same
  10. // email several times; startup must collapse them to the first occurrence and
  11. // leave clean inbounds byte-for-byte untouched.
  12. func TestDedupeInboundSettingsClients_CollapsesDuplicateEmails(t *testing.T) {
  13. dbDir := t.TempDir()
  14. t.Setenv("XUI_DB_FOLDER", dbDir)
  15. if err := InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  16. t.Fatalf("InitDB failed: %v", err)
  17. }
  18. t.Cleanup(func() { _ = CloseDB() })
  19. dupSettings := `{"clients": [` +
  20. `{"id": "u1", "email": "dup@x", "subId": "s1", "enable": true},` +
  21. `{"id": "u2", "email": "keep@x", "subId": "s2", "enable": true},` +
  22. `{"id": "u1", "email": "dup@x", "subId": "s1", "enable": true},` +
  23. `{"id": "u1", "email": "dup@x", "subId": "s1", "enable": true}]}`
  24. dirty := model.Inbound{UserId: 1, Port: 21001, Protocol: model.VLESS, Tag: "dedupe-dirty", Settings: dupSettings}
  25. if err := db.Create(&dirty).Error; err != nil {
  26. t.Fatalf("create dirty inbound: %v", err)
  27. }
  28. cleanSettings := `{"clients": [{"id": "u3", "email": "solo@x", "subId": "s3", "enable": true}]}`
  29. clean := model.Inbound{UserId: 1, Port: 21002, Protocol: model.VLESS, Tag: "dedupe-clean", Settings: cleanSettings}
  30. if err := db.Create(&clean).Error; err != nil {
  31. t.Fatalf("create clean inbound: %v", err)
  32. }
  33. if err := dedupeInboundSettingsClients(); err != nil {
  34. t.Fatalf("dedupeInboundSettingsClients: %v", err)
  35. }
  36. var gotDirty model.Inbound
  37. if err := db.First(&gotDirty, dirty.Id).Error; err != nil {
  38. t.Fatalf("reload dirty inbound: %v", err)
  39. }
  40. var parsed struct {
  41. Clients []map[string]any `json:"clients"`
  42. }
  43. if err := json.Unmarshal([]byte(gotDirty.Settings), &parsed); err != nil {
  44. t.Fatalf("parse repaired settings: %v", err)
  45. }
  46. if len(parsed.Clients) != 2 {
  47. t.Fatalf("expected 2 clients after dedupe, got %d: %s", len(parsed.Clients), gotDirty.Settings)
  48. }
  49. if parsed.Clients[0]["email"] != "dup@x" || parsed.Clients[1]["email"] != "keep@x" {
  50. t.Fatalf("expected first occurrences [dup@x keep@x], got %v", parsed.Clients)
  51. }
  52. var gotClean model.Inbound
  53. if err := db.First(&gotClean, clean.Id).Error; err != nil {
  54. t.Fatalf("reload clean inbound: %v", err)
  55. }
  56. if gotClean.Settings != cleanSettings {
  57. t.Fatalf("clean inbound settings were rewritten:\nbefore: %s\nafter: %s", cleanSettings, gotClean.Settings)
  58. }
  59. if err := dedupeInboundSettingsClients(); err != nil {
  60. t.Fatalf("second dedupe run: %v", err)
  61. }
  62. var again model.Inbound
  63. if err := db.First(&again, dirty.Id).Error; err != nil {
  64. t.Fatalf("reload after second run: %v", err)
  65. }
  66. if again.Settings != gotDirty.Settings {
  67. t.Fatal("dedupe is not idempotent: settings changed on the second run")
  68. }
  69. }