inbound_disable_flow_test.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. package service
  2. import (
  3. "encoding/json"
  4. "path/filepath"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "gorm.io/gorm"
  9. )
  10. const visionTest = "xtls-rprx-vision"
  11. func clientFlowsInSettings(t *testing.T, settings string) map[string]string {
  12. t.Helper()
  13. var parsed map[string]any
  14. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  15. t.Fatalf("parse settings: %v", err)
  16. }
  17. out := map[string]string{}
  18. clients, _ := parsed["clients"].([]any)
  19. for _, c := range clients {
  20. cm, ok := c.(map[string]any)
  21. if !ok {
  22. continue
  23. }
  24. email, _ := cm["email"].(string)
  25. flow, _ := cm["flow"].(string)
  26. out[email] = flow
  27. }
  28. return out
  29. }
  30. func TestStripClientFlows(t *testing.T) {
  31. cases := []struct {
  32. name string
  33. in string
  34. wantChanged bool
  35. wantFlows map[string]string
  36. }{
  37. {
  38. name: "clears vision on all clients",
  39. in: `{"clients":[{"email":"a","flow":"` + visionTest + `"},{"email":"b","flow":"` + visionTest + `"}]}`,
  40. wantChanged: true,
  41. wantFlows: map[string]string{"a": "", "b": ""},
  42. },
  43. {
  44. name: "mixed flows: clears only the non-empty",
  45. in: `{"clients":[{"email":"a","flow":"` + visionTest + `"},{"email":"b","flow":""}]}`,
  46. wantChanged: true,
  47. wantFlows: map[string]string{"a": "", "b": ""},
  48. },
  49. {
  50. name: "no flows: unchanged",
  51. in: `{"clients":[{"email":"a","flow":""},{"email":"b"}]}`,
  52. wantChanged: false,
  53. wantFlows: map[string]string{"a": "", "b": ""},
  54. },
  55. {
  56. name: "no clients: unchanged",
  57. in: `{"decryption":"none"}`,
  58. wantChanged: false,
  59. },
  60. {
  61. name: "malformed json: unchanged",
  62. in: `{not json`,
  63. wantChanged: false,
  64. },
  65. }
  66. for _, tc := range cases {
  67. t.Run(tc.name, func(t *testing.T) {
  68. out, changed := stripClientFlows(tc.in)
  69. if changed != tc.wantChanged {
  70. t.Fatalf("changed = %v, want %v", changed, tc.wantChanged)
  71. }
  72. if !changed {
  73. if out != tc.in {
  74. t.Fatalf("unchanged input must be returned verbatim, got %q", out)
  75. }
  76. return
  77. }
  78. got := clientFlowsInSettings(t, out)
  79. for email, want := range tc.wantFlows {
  80. if got[email] != want {
  81. t.Errorf("flow[%s] = %q, want %q", email, got[email], want)
  82. }
  83. }
  84. })
  85. }
  86. }
  87. func initFlowTestDB(t *testing.T) *gorm.DB {
  88. t.Helper()
  89. dbDir := t.TempDir()
  90. t.Setenv("XUI_DB_FOLDER", dbDir)
  91. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  92. t.Fatalf("InitDB: %v", err)
  93. }
  94. t.Cleanup(func() { _ = database.CloseDB() })
  95. return database.GetDB()
  96. }
  97. func TestAddInbound_DisableFlowClampsClientFlow(t *testing.T) {
  98. initFlowTestDB(t)
  99. ibSvc := &InboundService{}
  100. in := &model.Inbound{
  101. Tag: "dis-add", Enable: true, Port: 52001, Protocol: model.VLESS,
  102. StreamSettings: `{"network":"tcp","security":"reality"}`,
  103. Settings: `{"clients":[{"id":"u1","email":"a@x","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
  104. DisableFlow: true,
  105. }
  106. if _, _, err := ibSvc.AddInbound(in); err != nil {
  107. t.Fatalf("AddInbound: %v", err)
  108. }
  109. got, err := ibSvc.GetInbound(in.Id)
  110. if err != nil {
  111. t.Fatalf("GetInbound: %v", err)
  112. }
  113. if !got.DisableFlow {
  114. t.Error("DisableFlow not persisted on created inbound")
  115. }
  116. if f := clientFlowsInSettings(t, got.Settings)["a@x"]; f != "" {
  117. t.Errorf("settings flow = %q, want empty (clamped at creation)", f)
  118. }
  119. list, err := ibSvc.clientService.ListForInbound(nil, in.Id)
  120. if err != nil {
  121. t.Fatalf("ListForInbound: %v", err)
  122. }
  123. if len(list) != 1 || list[0].Flow != "" {
  124. t.Errorf("flow_override = %#v, want empty (xray must not expect Vision)", list)
  125. }
  126. }
  127. func TestUpdateInbound_DisableFlowPersistsStripsAndResistsRestore(t *testing.T) {
  128. db := initFlowTestDB(t)
  129. ibSvc := &InboundService{}
  130. cs := &ClientService{}
  131. const email = "shared@x"
  132. const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0d001"
  133. sibling := &model.Inbound{
  134. Tag: "sib", Enable: true, Port: 52101, Protocol: model.VLESS,
  135. StreamSettings: `{"network":"tcp","security":"reality"}`,
  136. Settings: `{"clients":[{"id":"` + uid + `","email":"` + email + `","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
  137. }
  138. if err := db.Create(sibling).Error; err != nil {
  139. t.Fatalf("create sibling: %v", err)
  140. }
  141. sc, _ := ibSvc.GetClients(sibling)
  142. if err := cs.SyncInbound(nil, sibling.Id, sc); err != nil {
  143. t.Fatalf("sync sibling: %v", err)
  144. }
  145. target := &model.Inbound{
  146. Tag: "tgt", Enable: true, Port: 52102, Protocol: model.VLESS,
  147. StreamSettings: `{"network":"tcp","security":"reality"}`,
  148. Settings: `{"clients":[{"id":"` + uid + `","email":"` + email + `","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
  149. }
  150. if err := db.Create(target).Error; err != nil {
  151. t.Fatalf("create target: %v", err)
  152. }
  153. tc, _ := ibSvc.GetClients(target)
  154. if err := cs.SyncInbound(nil, target.Id, tc); err != nil {
  155. t.Fatalf("sync target: %v", err)
  156. }
  157. upd := *target
  158. upd.DisableFlow = true
  159. if _, _, err := ibSvc.UpdateInbound(&upd); err != nil {
  160. t.Fatalf("UpdateInbound: %v", err)
  161. }
  162. reloaded, err := ibSvc.GetInbound(target.Id)
  163. if err != nil {
  164. t.Fatalf("GetInbound: %v", err)
  165. }
  166. if !reloaded.DisableFlow {
  167. t.Fatal("DisableFlow did not persist through UpdateInbound (blocking regression)")
  168. }
  169. if f := clientFlowsInSettings(t, reloaded.Settings)["shared@x"]; f != "" {
  170. t.Errorf("target settings flow = %q, want empty after disable", f)
  171. }
  172. list, err := cs.ListForInbound(nil, target.Id)
  173. if err != nil {
  174. t.Fatalf("ListForInbound(target): %v", err)
  175. }
  176. if len(list) != 1 || list[0].Flow != "" {
  177. t.Errorf("target flow_override = %#v, want empty", list)
  178. }
  179. ibSvc.MigrationRestoreVisionFlow()
  180. reloaded2, err := ibSvc.GetInbound(target.Id)
  181. if err != nil {
  182. t.Fatalf("GetInbound after restore: %v", err)
  183. }
  184. if f := clientFlowsInSettings(t, reloaded2.Settings)["shared@x"]; f != "" {
  185. t.Errorf("after MigrationRestoreVisionFlow target flow = %q, want empty (must not self-revert)", f)
  186. }
  187. sList, err := cs.ListForInbound(nil, sibling.Id)
  188. if err != nil {
  189. t.Fatalf("ListForInbound(sibling): %v", err)
  190. }
  191. if len(sList) != 1 || sList[0].Flow != visionTest {
  192. t.Errorf("sibling flow_override = %#v, want Vision preserved", sList)
  193. }
  194. }