bulk_clients_test.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. package service
  2. import (
  3. "encoding/json"
  4. "path/filepath"
  5. "sort"
  6. "testing"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. )
  10. func setupBulkDB(t *testing.T) {
  11. t.Helper()
  12. dbDir := t.TempDir()
  13. t.Setenv("XUI_DB_FOLDER", dbDir)
  14. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  15. t.Fatalf("InitDB: %v", err)
  16. }
  17. t.Cleanup(func() { _ = database.CloseDB() })
  18. }
  19. func clientsSettings(t *testing.T, clients []model.Client) string {
  20. t.Helper()
  21. b, err := json.Marshal(map[string][]model.Client{"clients": clients})
  22. if err != nil {
  23. t.Fatalf("marshal settings: %v", err)
  24. }
  25. var out map[string]any
  26. if err := json.Unmarshal(b, &out); err != nil {
  27. t.Fatalf("unmarshal settings: %v", err)
  28. }
  29. b2, err := json.MarshalIndent(out, "", " ")
  30. if err != nil {
  31. t.Fatalf("marshal settings again: %v", err)
  32. }
  33. return string(b2)
  34. }
  35. func emailsOf(clients []model.Client) []string {
  36. out := make([]string, 0, len(clients))
  37. for _, c := range clients {
  38. out = append(out, c.Email)
  39. }
  40. return out
  41. }
  42. func sortedEmails(list []model.Client) []string {
  43. out := emailsOf(list)
  44. sort.Strings(out)
  45. return out
  46. }
  47. func mkInbound(t *testing.T, port int, proto model.Protocol, settings string) *model.Inbound {
  48. t.Helper()
  49. ib := &model.Inbound{
  50. Tag: string(proto) + "-" + filepath.Base(t.TempDir()),
  51. Enable: true,
  52. Port: port,
  53. Protocol: proto,
  54. Settings: settings,
  55. }
  56. if err := database.GetDB().Create(ib).Error; err != nil {
  57. t.Fatalf("create inbound %d: %v", port, err)
  58. }
  59. return ib
  60. }
  61. // TestBulkAttachDetach_VLESS exercises the batched attach/detach round-trip on
  62. // VLESS inbounds: linkage, settings JSON, idempotency, skip, and record survival.
  63. func TestBulkAttachDetach_VLESS(t *testing.T) {
  64. setupBulkDB(t)
  65. svc := &ClientService{}
  66. inboundSvc := &InboundService{}
  67. source := []model.Client{
  68. {Email: "alice@x", ID: "11111111-1111-1111-1111-111111111111", SubID: "sa", Enable: true},
  69. {Email: "bob@x", ID: "22222222-2222-2222-2222-222222222222", SubID: "sb", Enable: true},
  70. {Email: "carol@x", ID: "33333333-3333-3333-3333-333333333333", SubID: "sc", Enable: true},
  71. }
  72. ib1 := mkInbound(t, 20001, model.VLESS, clientsSettings(t, source))
  73. ib2 := mkInbound(t, 20002, model.VLESS, `{"clients":[]}`)
  74. ib3 := mkInbound(t, 20003, model.VLESS, `{"clients":[]}`)
  75. if err := svc.SyncInbound(nil, ib1.Id, source); err != nil {
  76. t.Fatalf("seed source linkage: %v", err)
  77. }
  78. emails := emailsOf(source)
  79. res, _, err := svc.BulkAttach(inboundSvc, emails, []int{ib2.Id, ib3.Id})
  80. if err != nil {
  81. t.Fatalf("BulkAttach: %v", err)
  82. }
  83. if len(res.Errors) != 0 {
  84. t.Fatalf("BulkAttach errors: %v", res.Errors)
  85. }
  86. if len(res.Skipped) != 0 {
  87. t.Fatalf("BulkAttach skipped unexpectedly: %v", res.Skipped)
  88. }
  89. if len(res.Attached) != 6 {
  90. t.Fatalf("expected 6 attach entries (3 clients x 2 inbounds), got %d: %v", len(res.Attached), res.Attached)
  91. }
  92. for _, ib := range []*model.Inbound{ib2, ib3} {
  93. list, err := svc.ListForInbound(nil, ib.Id)
  94. if err != nil {
  95. t.Fatalf("ListForInbound(%d): %v", ib.Id, err)
  96. }
  97. if got := sortedEmails(list); len(got) != 3 {
  98. t.Fatalf("inbound %d: expected 3 linked clients, got %v", ib.Id, got)
  99. }
  100. reloaded, err := inboundSvc.GetInbound(ib.Id)
  101. if err != nil {
  102. t.Fatalf("GetInbound(%d): %v", ib.Id, err)
  103. }
  104. jsonClients, err := inboundSvc.GetClients(reloaded)
  105. if err != nil {
  106. t.Fatalf("GetClients(%d): %v", ib.Id, err)
  107. }
  108. if len(jsonClients) != 3 {
  109. t.Fatalf("inbound %d settings JSON: expected 3 clients, got %d", ib.Id, len(jsonClients))
  110. }
  111. }
  112. res2, _, err := svc.BulkAttach(inboundSvc, emails, []int{ib2.Id, ib3.Id})
  113. if err != nil {
  114. t.Fatalf("BulkAttach (idempotent): %v", err)
  115. }
  116. if len(res2.Attached) != 0 {
  117. t.Fatalf("re-attach should add nothing, got Attached=%v", res2.Attached)
  118. }
  119. if len(res2.Skipped) != 6 {
  120. t.Fatalf("re-attach should skip all 6, got Skipped=%v", res2.Skipped)
  121. }
  122. dres, _, err := svc.BulkDetach(inboundSvc, emails, []int{ib2.Id, ib3.Id})
  123. if err != nil {
  124. t.Fatalf("BulkDetach: %v", err)
  125. }
  126. if len(dres.Errors) != 0 {
  127. t.Fatalf("BulkDetach errors: %v", dres.Errors)
  128. }
  129. if len(dres.Detached) != 3 {
  130. t.Fatalf("expected 3 detached emails, got %v", dres.Detached)
  131. }
  132. for _, ib := range []*model.Inbound{ib2, ib3} {
  133. list, err := svc.ListForInbound(nil, ib.Id)
  134. if err != nil {
  135. t.Fatalf("ListForInbound after detach(%d): %v", ib.Id, err)
  136. }
  137. if len(list) != 0 {
  138. t.Fatalf("inbound %d should have no clients after detach, got %v", ib.Id, sortedEmails(list))
  139. }
  140. reloaded, _ := inboundSvc.GetInbound(ib.Id)
  141. jsonClients, _ := inboundSvc.GetClients(reloaded)
  142. if len(jsonClients) != 0 {
  143. t.Fatalf("inbound %d settings JSON should be empty after detach, got %d", ib.Id, len(jsonClients))
  144. }
  145. }
  146. for _, e := range emails {
  147. rec, err := svc.GetRecordByEmail(nil, e)
  148. if err != nil {
  149. t.Fatalf("record %q should survive detach: %v", e, err)
  150. }
  151. ids, err := svc.GetInboundIdsForRecord(rec.Id)
  152. if err != nil {
  153. t.Fatalf("GetInboundIdsForRecord(%q): %v", e, err)
  154. }
  155. if len(ids) != 1 || ids[0] != ib1.Id {
  156. t.Fatalf("record %q should remain attached only to source inbound %d, got %v", e, ib1.Id, ids)
  157. }
  158. }
  159. }
  160. // TestBulkDetach_SkipsUnattached verifies emails not on any requested inbound
  161. // land in Skipped, not Detached, and produce no error.
  162. func TestBulkDetach_SkipsUnattached(t *testing.T) {
  163. setupBulkDB(t)
  164. svc := &ClientService{}
  165. inboundSvc := &InboundService{}
  166. source := []model.Client{
  167. {Email: "only-on-1@x", ID: "44444444-4444-4444-4444-444444444444", SubID: "s1", Enable: true},
  168. }
  169. ib1 := mkInbound(t, 21001, model.VLESS, clientsSettings(t, source))
  170. ib2 := mkInbound(t, 21002, model.VLESS, `{"clients":[]}`)
  171. if err := svc.SyncInbound(nil, ib1.Id, source); err != nil {
  172. t.Fatalf("seed: %v", err)
  173. }
  174. dres, restart, err := svc.BulkDetach(inboundSvc, []string{"only-on-1@x"}, []int{ib2.Id})
  175. if err != nil {
  176. t.Fatalf("BulkDetach: %v", err)
  177. }
  178. if restart {
  179. t.Fatalf("no-op detach should not require restart")
  180. }
  181. if len(dres.Detached) != 0 {
  182. t.Fatalf("nothing should be detached, got %v", dres.Detached)
  183. }
  184. if len(dres.Skipped) != 1 || dres.Skipped[0] != "only-on-1@x" {
  185. t.Fatalf("expected the email in Skipped, got %v", dres.Skipped)
  186. }
  187. if len(dres.Errors) != 0 {
  188. t.Fatalf("unexpected errors: %v", dres.Errors)
  189. }
  190. }
  191. // TestBulkAttachDetach_Trojan checks the protocol-specific key matching in the
  192. // batched detach path (Trojan keys on password, not id).
  193. func TestBulkAttachDetach_Trojan(t *testing.T) {
  194. setupBulkDB(t)
  195. svc := &ClientService{}
  196. inboundSvc := &InboundService{}
  197. source := []model.Client{
  198. {Email: "t1@x", Password: "pw-t1", SubID: "t1", Enable: true},
  199. {Email: "t2@x", Password: "pw-t2", SubID: "t2", Enable: true},
  200. }
  201. ib1 := mkInbound(t, 22001, model.Trojan, clientsSettings(t, source))
  202. ib2 := mkInbound(t, 22002, model.Trojan, `{"clients":[]}`)
  203. if err := svc.SyncInbound(nil, ib1.Id, source); err != nil {
  204. t.Fatalf("seed: %v", err)
  205. }
  206. emails := emailsOf(source)
  207. if res, _, err := svc.BulkAttach(inboundSvc, emails, []int{ib2.Id}); err != nil {
  208. t.Fatalf("BulkAttach: %v", err)
  209. } else if len(res.Errors) != 0 || len(res.Attached) != 2 {
  210. t.Fatalf("attach result unexpected: attached=%v errors=%v", res.Attached, res.Errors)
  211. }
  212. list, _ := svc.ListForInbound(nil, ib2.Id)
  213. if len(list) != 2 {
  214. t.Fatalf("expected 2 trojan clients on ib2, got %v", sortedEmails(list))
  215. }
  216. dres, _, err := svc.BulkDetach(inboundSvc, emails, []int{ib2.Id})
  217. if err != nil {
  218. t.Fatalf("BulkDetach: %v", err)
  219. }
  220. if len(dres.Detached) != 2 || len(dres.Errors) != 0 {
  221. t.Fatalf("detach result unexpected: detached=%v errors=%v", dres.Detached, dres.Errors)
  222. }
  223. if list, _ := svc.ListForInbound(nil, ib2.Id); len(list) != 0 {
  224. t.Fatalf("trojan clients should be gone from ib2, got %v", sortedEmails(list))
  225. }
  226. }