db_seed_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. package database
  2. import (
  3. "encoding/json"
  4. "path/filepath"
  5. "regexp"
  6. "testing"
  7. "github.com/mhsanaei/3x-ui/v3/database/model"
  8. )
  9. func TestSeedClientsFromInboundJSON_IsIdempotentAgainstExistingClients(t *testing.T) {
  10. dbDir := t.TempDir()
  11. t.Setenv("XUI_DB_FOLDER", dbDir)
  12. if err := InitDB(filepath.Join(dbDir, "3x-ui.db")); err != nil {
  13. t.Fatalf("InitDB failed: %v", err)
  14. }
  15. t.Cleanup(func() { _ = CloseDB() })
  16. settings, err := json.Marshal(map[string]any{
  17. "clients": []any{
  18. map[string]any{
  19. "id": "ce8d33df-3a64-4f10-8f9b-91c3a8e0c001",
  20. "email": "[email protected]",
  21. "enable": true,
  22. "flow": "",
  23. "subId": "alice-sub",
  24. "comment": "from-inbound-json",
  25. },
  26. },
  27. })
  28. if err != nil {
  29. t.Fatalf("marshal settings: %v", err)
  30. }
  31. inbound := model.Inbound{
  32. UserId: 1,
  33. Port: 12345,
  34. Protocol: model.VLESS,
  35. Settings: string(settings),
  36. Tag: "test-inbound",
  37. }
  38. if err := db.Create(&inbound).Error; err != nil {
  39. t.Fatalf("seed inbound: %v", err)
  40. }
  41. preExisting := &model.ClientRecord{
  42. Email: "[email protected]",
  43. UUID: "ce8d33df-3a64-4f10-8f9b-91c3a8e0c001",
  44. SubID: "alice-sub",
  45. Enable: true,
  46. Comment: "added-via-api",
  47. }
  48. if err := db.Create(preExisting).Error; err != nil {
  49. t.Fatalf("seed client row: %v", err)
  50. }
  51. if err := db.Where("seeder_name = ?", "ClientsTable").Delete(&model.HistoryOfSeeders{}).Error; err != nil {
  52. t.Fatalf("clear ClientsTable history: %v", err)
  53. }
  54. if err := seedClientsFromInboundJSON(); err != nil {
  55. t.Fatalf("seedClientsFromInboundJSON should be idempotent against existing rows, got: %v", err)
  56. }
  57. var count int64
  58. if err := db.Model(&model.ClientRecord{}).Where("email = ?", "[email protected]").Count(&count).Error; err != nil {
  59. t.Fatalf("count clients: %v", err)
  60. }
  61. if count != 1 {
  62. t.Fatalf("[email protected] should resolve to exactly one row, got %d", count)
  63. }
  64. }
  65. func TestNormalizeInboundClientSubId_FillsMissingAndPreservesExisting(t *testing.T) {
  66. dbDir := t.TempDir()
  67. t.Setenv("XUI_DB_FOLDER", dbDir)
  68. if err := InitDB(filepath.Join(dbDir, "3x-ui.db")); err != nil {
  69. t.Fatalf("InitDB failed: %v", err)
  70. }
  71. t.Cleanup(func() { _ = CloseDB() })
  72. settings, err := json.Marshal(map[string]any{
  73. "clients": []any{
  74. map[string]any{
  75. "id": "00000000-0000-0000-0000-000000000001",
  76. "email": "[email protected]",
  77. "subId": "",
  78. },
  79. map[string]any{
  80. "id": "00000000-0000-0000-0000-000000000002",
  81. "email": "[email protected]",
  82. },
  83. map[string]any{
  84. "id": "00000000-0000-0000-0000-000000000003",
  85. "email": "[email protected]",
  86. "subId": "keep-me-1234",
  87. },
  88. },
  89. })
  90. if err != nil {
  91. t.Fatalf("marshal settings: %v", err)
  92. }
  93. inbound := model.Inbound{
  94. UserId: 1,
  95. Port: 23456,
  96. Protocol: model.VLESS,
  97. Settings: string(settings),
  98. Tag: "subid-fix-inbound",
  99. }
  100. if err := db.Create(&inbound).Error; err != nil {
  101. t.Fatalf("seed inbound: %v", err)
  102. }
  103. if err := db.Where("seeder_name = ?", "InboundClientSubIdFix").Delete(&model.HistoryOfSeeders{}).Error; err != nil {
  104. t.Fatalf("clear seeder history: %v", err)
  105. }
  106. if err := normalizeInboundClientSubId(); err != nil {
  107. t.Fatalf("normalizeInboundClientSubId: %v", err)
  108. }
  109. var reloaded model.Inbound
  110. if err := db.First(&reloaded, inbound.Id).Error; err != nil {
  111. t.Fatalf("reload inbound: %v", err)
  112. }
  113. var parsed map[string]any
  114. if err := json.Unmarshal([]byte(reloaded.Settings), &parsed); err != nil {
  115. t.Fatalf("unmarshal settings: %v", err)
  116. }
  117. clients, ok := parsed["clients"].([]any)
  118. if !ok || len(clients) != 3 {
  119. t.Fatalf("expected 3 clients, got %v", parsed["clients"])
  120. }
  121. subIdPattern := regexp.MustCompile(`^[0-9a-z]{16}$`)
  122. for i := 0; i < 2; i++ {
  123. obj := clients[i].(map[string]any)
  124. sub, _ := obj["subId"].(string)
  125. if !subIdPattern.MatchString(sub) {
  126. t.Fatalf("client %d: expected 16-char [0-9a-z] subId, got %q", i, sub)
  127. }
  128. }
  129. preserved := clients[2].(map[string]any)["subId"].(string)
  130. if preserved != "keep-me-1234" {
  131. t.Fatalf("expected existing subId preserved, got %q", preserved)
  132. }
  133. var historyCount int64
  134. if err := db.Model(&model.HistoryOfSeeders{}).Where("seeder_name = ?", "InboundClientSubIdFix").Count(&historyCount).Error; err != nil {
  135. t.Fatalf("count seeder history: %v", err)
  136. }
  137. if historyCount != 1 {
  138. t.Fatalf("expected one InboundClientSubIdFix history row, got %d", historyCount)
  139. }
  140. }