inbound_migration_test.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package service
  2. import (
  3. "path/filepath"
  4. "strings"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  9. )
  10. // TestMigrationRequirements_BackfillsClientTrafficsWithMultiDomainInbound guards the
  11. // PostgreSQL fix where the externalProxy detection query (executed via .Scan) errored on
  12. // json_extract and rolled back the whole transaction — including the client_traffics
  13. // backfill at inbound.go:3093-3106, leaving clients with no traffic rows. A MultiDomain
  14. // inbound is present so that query returns rows and the function runs to completion; both
  15. // the backfill and the MultiDomain→ExternalProxy migration must then commit.
  16. func TestMigrationRequirements_BackfillsClientTrafficsWithMultiDomainInbound(t *testing.T) {
  17. dbDir := t.TempDir()
  18. t.Setenv("XUI_DB_FOLDER", dbDir)
  19. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  20. t.Fatalf("InitDB: %v", err)
  21. }
  22. t.Cleanup(func() { _ = database.CloseDB() })
  23. db := database.GetDB()
  24. const backfillEmail = "[email protected]"
  25. const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c010"
  26. // Inbound A: a client present only in settings.clients, with no client_traffics row.
  27. clientInbound := &model.Inbound{
  28. UserId: 1,
  29. Tag: "a-tag",
  30. Enable: true,
  31. Port: 30001,
  32. Protocol: model.VLESS,
  33. Settings: `{"clients":[{"email":"` + backfillEmail + `","id":"` + uid + `","enable":true}]}`,
  34. StreamSettings: `{"network":"tcp","security":"none"}`,
  35. }
  36. if err := db.Create(clientInbound).Error; err != nil {
  37. t.Fatalf("create client inbound: %v", err)
  38. }
  39. // Inbound B: a legacy MultiDomain inbound whose tag carries the 0.0.0.0: prefix.
  40. // Its presence makes the externalProxy query return rows, so the function does not
  41. // early-return and reaches the tag-cleanup statement.
  42. multiDomainInbound := &model.Inbound{
  43. UserId: 1,
  44. Tag: "inbound-0.0.0.0:30002",
  45. Enable: true,
  46. Port: 30002,
  47. Protocol: model.VLESS,
  48. Settings: `{"clients":[]}`,
  49. StreamSettings: `{"security":"tls","tlsSettings":{"settings":{"domains":[{"domain":"example.com"}]}}}`,
  50. }
  51. if err := db.Create(multiDomainInbound).Error; err != nil {
  52. t.Fatalf("create multidomain inbound: %v", err)
  53. }
  54. var before int64
  55. if err := db.Model(xray.ClientTraffic{}).Count(&before).Error; err != nil {
  56. t.Fatalf("count client_traffics before: %v", err)
  57. }
  58. if before != 0 {
  59. t.Fatalf("expected no client_traffics before migration, got %d", before)
  60. }
  61. svc := InboundService{}
  62. svc.MigrationRequirements()
  63. // The backfill must have committed: the settings-only client now owns a row.
  64. // Before the fix this was rolled back whenever the externalProxy detection query
  65. // errored (it does on Postgres via json_extract), so the MultiDomain inbound below
  66. // is deliberately present to make that query return rows and run to completion.
  67. var ct xray.ClientTraffic
  68. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", backfillEmail).First(&ct).Error; err != nil {
  69. t.Fatalf("client_traffics row not backfilled for %s: %v", backfillEmail, err)
  70. }
  71. // The MultiDomain→ExternalProxy migration must have committed too: the detection
  72. // query ran (.Scan executes it) and the loop rewrote the inbound's streamSettings.
  73. var refreshed model.Inbound
  74. if err := db.First(&refreshed, multiDomainInbound.Id).Error; err != nil {
  75. t.Fatalf("reload multidomain inbound: %v", err)
  76. }
  77. if !strings.Contains(refreshed.StreamSettings, "externalProxy") {
  78. t.Errorf("MultiDomain migration did not commit; streamSettings = %q", refreshed.StreamSettings)
  79. }
  80. }
  81. // TestMigrationRequirements_CleansLegacyZeroAddrTag guards the legacy tag cleanup that
  82. // strips the auto-generated "0.0.0.0:" prefix. The inbound is MultiDomain TLS so the
  83. // externalProxy detection query returns rows and the cleanup is reached (it early-returns
  84. // at len(externalProxy)==0 otherwise). The cleanup must use tx.Exec, not tx.Raw, which
  85. // only builds a non-SELECT statement without running it.
  86. func TestMigrationRequirements_CleansLegacyZeroAddrTag(t *testing.T) {
  87. dbDir := t.TempDir()
  88. t.Setenv("XUI_DB_FOLDER", dbDir)
  89. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  90. t.Fatalf("InitDB: %v", err)
  91. }
  92. t.Cleanup(func() { _ = database.CloseDB() })
  93. db := database.GetDB()
  94. legacy := &model.Inbound{
  95. UserId: 1,
  96. Tag: "inbound-0.0.0.0:30002",
  97. Enable: true,
  98. Port: 30002,
  99. Protocol: model.VLESS,
  100. Settings: `{"clients":[]}`,
  101. StreamSettings: `{"security":"tls","tlsSettings":{"settings":{"domains":[{"domain":"example.com"}]}}}`,
  102. }
  103. if err := db.Create(legacy).Error; err != nil {
  104. t.Fatalf("create legacy inbound: %v", err)
  105. }
  106. svc := InboundService{}
  107. svc.MigrationRequirements()
  108. var got model.Inbound
  109. if err := db.First(&got, legacy.Id).Error; err != nil {
  110. t.Fatalf("reload inbound: %v", err)
  111. }
  112. if got.Tag != "inbound-30002" {
  113. t.Fatalf("legacy 0.0.0.0: tag not stripped: got %q, want %q", got.Tag, "inbound-30002")
  114. }
  115. }
  116. func TestMigrationRequirements_NormalizesShareAddressFields(t *testing.T) {
  117. setupConflictDB(t)
  118. db := database.GetDB()
  119. invalidStrategy := &model.Inbound{
  120. UserId: 1,
  121. Tag: "invalid-share-strategy",
  122. Enable: true,
  123. Port: 31001,
  124. Protocol: model.VLESS,
  125. Settings: `{"clients":[]}`,
  126. StreamSettings: `{"network":"tcp","security":"none"}`,
  127. }
  128. paddedStrategy := &model.Inbound{
  129. UserId: 1,
  130. Tag: "padded-share-strategy",
  131. Enable: true,
  132. Port: 31002,
  133. Protocol: model.VLESS,
  134. Settings: `{"clients":[]}`,
  135. StreamSettings: `{"network":"tcp","security":"none"}`,
  136. }
  137. invalidAddress := &model.Inbound{
  138. UserId: 1,
  139. Tag: "invalid-share-address",
  140. Enable: true,
  141. Port: 31003,
  142. Protocol: model.VLESS,
  143. Settings: `{"clients":[]}`,
  144. StreamSettings: `{"network":"tcp","security":"none"}`,
  145. }
  146. if err := db.Create(invalidStrategy).Error; err != nil {
  147. t.Fatalf("create invalid strategy inbound: %v", err)
  148. }
  149. if err := db.Create(paddedStrategy).Error; err != nil {
  150. t.Fatalf("create padded strategy inbound: %v", err)
  151. }
  152. if err := db.Create(invalidAddress).Error; err != nil {
  153. t.Fatalf("create invalid address inbound: %v", err)
  154. }
  155. if err := db.Model(&model.Inbound{}).Where("id = ?", invalidStrategy.Id).Updates(map[string]any{
  156. "share_addr_strategy": " auto ",
  157. "share_addr": " edge.example.com ",
  158. }).Error; err != nil {
  159. t.Fatalf("seed invalid share fields: %v", err)
  160. }
  161. if err := db.Model(&model.Inbound{}).Where("id = ?", paddedStrategy.Id).Updates(map[string]any{
  162. "share_addr_strategy": " listen ",
  163. "share_addr": " 10.0.0.1 ",
  164. }).Error; err != nil {
  165. t.Fatalf("seed padded share fields: %v", err)
  166. }
  167. if err := db.Model(&model.Inbound{}).Where("id = ?", invalidAddress.Id).Updates(map[string]any{
  168. "share_addr_strategy": "custom",
  169. "share_addr": "edge.example.com:8443",
  170. }).Error; err != nil {
  171. t.Fatalf("seed invalid address share fields: %v", err)
  172. }
  173. svc := InboundService{}
  174. svc.MigrationRequirements()
  175. var gotInvalid model.Inbound
  176. if err := db.First(&gotInvalid, invalidStrategy.Id).Error; err != nil {
  177. t.Fatalf("reload invalid strategy inbound: %v", err)
  178. }
  179. if gotInvalid.ShareAddrStrategy != "node" || gotInvalid.ShareAddr != "edge.example.com" {
  180. t.Fatalf("invalid share fields = (%q, %q), want (node, edge.example.com)", gotInvalid.ShareAddrStrategy, gotInvalid.ShareAddr)
  181. }
  182. var gotPadded model.Inbound
  183. if err := db.First(&gotPadded, paddedStrategy.Id).Error; err != nil {
  184. t.Fatalf("reload padded strategy inbound: %v", err)
  185. }
  186. if gotPadded.ShareAddrStrategy != "listen" || gotPadded.ShareAddr != "10.0.0.1" {
  187. t.Fatalf("padded share fields = (%q, %q), want (listen, 10.0.0.1)", gotPadded.ShareAddrStrategy, gotPadded.ShareAddr)
  188. }
  189. var gotInvalidAddress model.Inbound
  190. if err := db.First(&gotInvalidAddress, invalidAddress.Id).Error; err != nil {
  191. t.Fatalf("reload invalid address inbound: %v", err)
  192. }
  193. if gotInvalidAddress.ShareAddrStrategy != "node" || gotInvalidAddress.ShareAddr != "" {
  194. t.Fatalf("invalid address share fields = (%q, %q), want (node, empty)", gotInvalidAddress.ShareAddrStrategy, gotInvalidAddress.ShareAddr)
  195. }
  196. }