inbound_disable_flow_test.go 6.1 KB

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