1
0

bulk_clients_test.go 7.4 KB

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