bulk_clients_test.go 7.6 KB

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