client_portable_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  8. )
  9. // TestExportImportPreservesDisabledEnable covers #6478: ExportAll keeps the
  10. // real enable flag; ImportClients must not force enable=true.
  11. func TestExportImportPreservesDisabledEnable(t *testing.T) {
  12. setupBulkDB(t)
  13. svc := &ClientService{}
  14. inboundSvc := &InboundService{}
  15. ib := mkInbound(t, 26001, model.VLESS, `{"clients":[]}`)
  16. const email = "portable@disabled"
  17. const subID = "sub-portable-disabled"
  18. if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
  19. Client: model.Client{
  20. Email: email, SubID: subID, Enable: true,
  21. ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
  22. },
  23. InboundIds: []int{ib.Id},
  24. }); err != nil {
  25. t.Fatalf("Create: %v", err)
  26. }
  27. rec := lookupClientRecord(t, email)
  28. updated := rec.ToClient()
  29. updated.Enable = false
  30. if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
  31. t.Fatalf("Update disable: %v", err)
  32. }
  33. assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
  34. exported, err := svc.ExportAll()
  35. if err != nil {
  36. t.Fatalf("ExportAll: %v", err)
  37. }
  38. if len(exported) != 1 {
  39. t.Fatalf("ExportAll len=%d, want 1", len(exported))
  40. }
  41. if exported[0].Client.Enable {
  42. t.Fatal("ExportAll should carry enable=false for a disabled client")
  43. }
  44. raw, err := json.Marshal(exported)
  45. if err != nil {
  46. t.Fatalf("marshal export: %v", err)
  47. }
  48. var roundTrip []ClientCreatePayload
  49. if err := json.Unmarshal(raw, &roundTrip); err != nil {
  50. t.Fatalf("unmarshal export: %v", err)
  51. }
  52. if roundTrip[0].Client.Enable {
  53. t.Fatal("JSON round-trip lost enable=false")
  54. }
  55. if _, err := svc.Delete(inboundSvc, rec.Id, false); err != nil {
  56. t.Fatalf("Delete: %v", err)
  57. }
  58. res, _, err := svc.ImportClients(inboundSvc, roundTrip)
  59. if err != nil {
  60. t.Fatalf("ImportClients: %v", err)
  61. }
  62. if res.Created != 1 || len(res.Skipped) != 0 {
  63. t.Fatalf("ImportClients result=%+v", res)
  64. }
  65. assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
  66. }
  67. func TestImportClientsPreservesOrphanDisabledEnable(t *testing.T) {
  68. setupBulkDB(t)
  69. svc := &ClientService{}
  70. items := []ClientCreatePayload{{
  71. Client: model.Client{
  72. Email: "orphan@disabled", SubID: "sub-orphan-disabled", Enable: false,
  73. ID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
  74. },
  75. InboundIds: nil,
  76. }}
  77. res, _, err := svc.ImportClients(&InboundService{}, items)
  78. if err != nil {
  79. t.Fatalf("ImportClients orphan: %v", err)
  80. }
  81. if res.Created != 1 {
  82. t.Fatalf("created=%d, want 1; skipped=%v", res.Created, res.Skipped)
  83. }
  84. if got := recordEnableOf(t, svc, "orphan@disabled"); got {
  85. t.Fatal("orphan import forced enable=true; want false")
  86. }
  87. }
  88. func TestBulkCreatePreservesExplicitDisable(t *testing.T) {
  89. setupBulkDB(t)
  90. svc := &ClientService{}
  91. inboundSvc := &InboundService{}
  92. ib := mkInbound(t, 26002, model.VLESS, `{"clients":[]}`)
  93. const email = "bulk@disabled"
  94. res, _, err := svc.BulkCreate(inboundSvc, []ClientCreatePayload{{
  95. Client: model.Client{
  96. Email: email, SubID: "sub-bulk-disabled", Enable: false,
  97. ID: "cccccccc-cccc-cccc-cccc-cccccccccccc",
  98. },
  99. InboundIds: []int{ib.Id},
  100. }})
  101. if err != nil {
  102. t.Fatalf("BulkCreate: %v", err)
  103. }
  104. if res.Created != 1 {
  105. t.Fatalf("BulkCreate result=%+v", res)
  106. }
  107. assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
  108. }
  109. func TestClientCreatePayload_OmitEnableDefaultsTrue(t *testing.T) {
  110. raw := []byte(`{"client":{"email":"omit@x","id":"dddddddd-dddd-dddd-dddd-dddddddddddd","subId":"sub-omit"},"inboundIds":[1]}`)
  111. var p ClientCreatePayload
  112. if err := json.Unmarshal(raw, &p); err != nil {
  113. t.Fatalf("unmarshal: %v", err)
  114. }
  115. if !p.Client.Enable {
  116. t.Fatal("omitted enable must default to true")
  117. }
  118. rawFalse := []byte(`{"client":{"email":"off@x","id":"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee","subId":"sub-off","enable":false},"inboundIds":[1]}`)
  119. var pFalse ClientCreatePayload
  120. if err := json.Unmarshal(rawFalse, &pFalse); err != nil {
  121. t.Fatalf("unmarshal false: %v", err)
  122. }
  123. if pFalse.Client.Enable {
  124. t.Fatal("explicit enable:false must stay false")
  125. }
  126. }
  127. func TestBulkCreate_DisabledOnNodeSkipsAddClient(t *testing.T) {
  128. setupBulkDB(t)
  129. nodeID, fake := setupNodeRuntime(t)
  130. ib := nodeInbound(t, nodeID, 26003, nil)
  131. svc := &ClientService{}
  132. inboundSvc := &InboundService{}
  133. const email = "node@disabled"
  134. res, _, err := svc.BulkCreate(inboundSvc, []ClientCreatePayload{{
  135. Client: model.Client{
  136. Email: email, SubID: "sub-node-disabled", Enable: false,
  137. ID: "ffffffff-ffff-ffff-ffff-ffffffffffff",
  138. },
  139. InboundIds: []int{ib.Id},
  140. }})
  141. if err != nil {
  142. t.Fatalf("BulkCreate: %v", err)
  143. }
  144. if res.Created != 1 {
  145. t.Fatalf("BulkCreate result=%+v", res)
  146. }
  147. if got := fake.addClient.Load(); got != 0 {
  148. t.Fatalf("AddClient RPCs = %d, want 0 for enable=false", got)
  149. }
  150. assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
  151. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  152. t.Fatalf("NodeSyncState: %v", err)
  153. } else if !dirty {
  154. t.Fatal("disabled node create must leave node dirty for reconcile")
  155. }
  156. }
  157. func TestExportImportPreservesTrafficCounters(t *testing.T) {
  158. setupBulkDB(t)
  159. svc := &ClientService{}
  160. inboundSvc := &InboundService{}
  161. ib := mkInbound(t, 25001, model.VLESS, `{"clients":[]}`)
  162. const email = "portable@traffic"
  163. const subID = "sub-portable-traffic"
  164. if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
  165. Client: model.Client{
  166. Email: email, SubID: subID, Enable: true,
  167. TotalGB: 10 << 30, ExpiryTime: 1_700_000_000_000,
  168. },
  169. InboundIds: []int{ib.Id},
  170. }); err != nil {
  171. t.Fatalf("Create: %v", err)
  172. }
  173. db := database.GetDB()
  174. if err := db.Model(&xray.ClientTraffic{}).Where("email = ?", email).Updates(map[string]any{
  175. "up": 111, "down": 222, "reset_count": 3, "last_online": 999,
  176. }).Error; err != nil {
  177. t.Fatalf("seed traffic: %v", err)
  178. }
  179. exported, err := svc.ExportAll()
  180. if err != nil {
  181. t.Fatalf("ExportAll: %v", err)
  182. }
  183. if len(exported) != 1 {
  184. t.Fatalf("ExportAll len=%d, want 1", len(exported))
  185. }
  186. if exported[0].Traffic == nil {
  187. t.Fatal("ExportAll missing traffic snapshot")
  188. }
  189. if exported[0].Traffic.Up != 111 || exported[0].Traffic.Down != 222 || exported[0].Traffic.ResetCount != 3 {
  190. t.Fatalf("exported traffic = %+v, want up=111 down=222 resetCount=3", exported[0].Traffic)
  191. }
  192. raw, err := json.Marshal(exported)
  193. if err != nil {
  194. t.Fatalf("marshal export: %v", err)
  195. }
  196. var roundTrip []ClientCreatePayload
  197. if err := json.Unmarshal(raw, &roundTrip); err != nil {
  198. t.Fatalf("unmarshal export: %v", err)
  199. }
  200. if roundTrip[0].Traffic == nil || roundTrip[0].Traffic.Up != 111 {
  201. t.Fatalf("JSON round-trip lost traffic: %+v", roundTrip[0].Traffic)
  202. }
  203. rec := lookupClientRecord(t, email)
  204. if _, err := svc.Delete(inboundSvc, rec.Id, false); err != nil {
  205. t.Fatalf("Delete: %v", err)
  206. }
  207. var gone int64
  208. if err := db.Model(&xray.ClientTraffic{}).Where("email = ?", email).Count(&gone).Error; err != nil {
  209. t.Fatalf("count after delete: %v", err)
  210. }
  211. if gone != 0 {
  212. t.Fatalf("client_traffics still present after delete: %d", gone)
  213. }
  214. res, _, err := svc.ImportClients(inboundSvc, roundTrip)
  215. if err != nil {
  216. t.Fatalf("ImportClients: %v", err)
  217. }
  218. if res.Created != 1 || len(res.Skipped) != 0 {
  219. t.Fatalf("ImportClients result=%+v", res)
  220. }
  221. var restored xray.ClientTraffic
  222. if err := db.Where("email = ?", email).First(&restored).Error; err != nil {
  223. t.Fatalf("lookup restored traffic: %v", err)
  224. }
  225. if restored.Up != 111 || restored.Down != 222 || restored.ResetCount != 3 || restored.LastOnline != 999 {
  226. t.Fatalf("restored traffic = %+v, want up=111 down=222 resetCount=3 lastOnline=999", restored)
  227. }
  228. if err := db.Model(&xray.ClientTraffic{}).Where("email = ?", email).Updates(map[string]any{
  229. "up": 5000, "down": 6000,
  230. }).Error; err != nil {
  231. t.Fatalf("bump live traffic: %v", err)
  232. }
  233. // Same email+subId is a BulkCreate reuse (may count as Created), not a hard
  234. // skip — traffic apply must still refuse to overwrite the live counters.
  235. if _, _, err := svc.ImportClients(inboundSvc, roundTrip); err != nil {
  236. t.Fatalf("second ImportClients: %v", err)
  237. }
  238. var live xray.ClientTraffic
  239. if err := db.Where("email = ?", email).First(&live).Error; err != nil {
  240. t.Fatalf("lookup live traffic: %v", err)
  241. }
  242. if live.Up != 5000 || live.Down != 6000 {
  243. t.Fatalf("re-import of existing email must leave live traffic alone, got up=%d down=%d", live.Up, live.Down)
  244. }
  245. }
  246. func TestImportClientsAppliesTrafficForOrphans(t *testing.T) {
  247. setupBulkDB(t)
  248. svc := &ClientService{}
  249. items := []ClientCreatePayload{{
  250. Client: model.Client{
  251. Email: "orphan@traffic", SubID: "sub-orphan-traffic", Enable: true,
  252. TotalGB: 1 << 30,
  253. },
  254. InboundIds: nil,
  255. Traffic: &ClientPortableTraffic{
  256. Up: 7, Down: 8, ResetCount: 1,
  257. },
  258. }}
  259. res, _, err := svc.ImportClients(&InboundService{}, items)
  260. if err != nil {
  261. t.Fatalf("ImportClients orphan: %v", err)
  262. }
  263. if res.Created != 1 {
  264. t.Fatalf("created=%d, want 1", res.Created)
  265. }
  266. var traf xray.ClientTraffic
  267. if err := database.GetDB().Where("email = ?", "orphan@traffic").First(&traf).Error; err != nil {
  268. t.Fatalf("orphan traffic row missing: %v", err)
  269. }
  270. if traf.Up != 7 || traf.Down != 8 || traf.ResetCount != 1 {
  271. t.Fatalf("orphan traffic = %+v", traf)
  272. }
  273. }
  274. // An orphan's restored row must carry its weekly schedule, or depletedClientsClause
  275. // treats the over-quota renewing client as depleted and DelDepleted deletes it.
  276. func TestImportClientsOrphanTrafficKeepsWeeklyRenewal(t *testing.T) {
  277. const email = "weekly@orphan"
  278. cases := []struct {
  279. name string
  280. seed func(t *testing.T, svc *ClientService, inboundSvc *InboundService)
  281. }{
  282. {name: "no prior row", seed: func(*testing.T, *ClientService, *InboundService) {}},
  283. {
  284. name: "row kept by keepTraffic delete",
  285. seed: func(t *testing.T, svc *ClientService, inboundSvc *InboundService) {
  286. ib := mkInbound(t, 25003, model.VLESS, `{"clients":[]}`)
  287. if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
  288. Client: model.Client{Email: email, SubID: "sub-weekly-old", Enable: true, TotalGB: 1 << 30},
  289. InboundIds: []int{ib.Id},
  290. }); err != nil {
  291. t.Fatalf("Create: %v", err)
  292. }
  293. if _, err := svc.Delete(inboundSvc, lookupClientRecord(t, email).Id, true); err != nil {
  294. t.Fatalf("Delete keepTraffic: %v", err)
  295. }
  296. },
  297. },
  298. }
  299. for _, tc := range cases {
  300. t.Run(tc.name, func(t *testing.T) {
  301. setupBulkDB(t)
  302. svc := &ClientService{}
  303. inboundSvc := &InboundService{}
  304. tc.seed(t, svc, inboundSvc)
  305. items := []ClientCreatePayload{{
  306. Client: model.Client{
  307. Email: email, SubID: "sub-weekly-orphan", Enable: true,
  308. TotalGB: 1 << 30, ResetWeekday: 3,
  309. },
  310. Traffic: &ClientPortableTraffic{Up: 1 << 30, Down: 1},
  311. }}
  312. if res, _, err := svc.ImportClients(inboundSvc, items); err != nil || res.Created != 1 {
  313. t.Fatalf("ImportClients result=%+v err=%v, want 1 created", res, err)
  314. }
  315. deleted, _, err := svc.DelDepleted(inboundSvc)
  316. if err != nil {
  317. t.Fatalf("DelDepleted: %v", err)
  318. }
  319. if deleted != 0 {
  320. t.Fatalf("DelDepleted deleted %d weekly-renewing client(s), want 0", deleted)
  321. }
  322. lookupClientRecord(t, email)
  323. })
  324. }
  325. }
  326. // Restored counters are usage from before the import, so they must not move the
  327. // group total at import time: a delete+re-import would otherwise count them twice.
  328. func TestImportClientsTrafficLeavesGroupTotalUnchanged(t *testing.T) {
  329. t.Run("re-import after delete on the same panel", func(t *testing.T) {
  330. setupBulkDB(t)
  331. svc := &ClientService{}
  332. inboundSvc := &InboundService{}
  333. ib := mkInbound(t, 25004, model.VLESS, `{"clients":[]}`)
  334. const email = "grouped@traffic"
  335. if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
  336. Client: model.Client{Email: email, SubID: "sub-grouped", Enable: true, Group: "g"},
  337. InboundIds: []int{ib.Id},
  338. }); err != nil {
  339. t.Fatalf("Create: %v", err)
  340. }
  341. if err := database.GetDB().Model(&xray.ClientTraffic{}).Where("email = ?", email).
  342. Updates(map[string]any{"up": 100, "down": 200}).Error; err != nil {
  343. t.Fatalf("seed traffic: %v", err)
  344. }
  345. exported, err := svc.ExportAll()
  346. if err != nil {
  347. t.Fatalf("ExportAll: %v", err)
  348. }
  349. if _, err := svc.Delete(inboundSvc, lookupClientRecord(t, email).Id, false); err != nil {
  350. t.Fatalf("Delete: %v", err)
  351. }
  352. if g := groupByName(t, svc, "g"); g.TrafficUsed != 300 {
  353. t.Fatalf("group after delete = %d, want the kept 300", g.TrafficUsed)
  354. }
  355. if res, _, err := svc.ImportClients(inboundSvc, exported); err != nil || res.Created != 1 {
  356. t.Fatalf("ImportClients result=%+v err=%v, want 1 created", res, err)
  357. }
  358. if g := groupByName(t, svc, "g"); g.Up != 100 || g.Down != 200 {
  359. t.Fatalf("group after re-import up=%d down=%d, want unchanged 100/200", g.Up, g.Down)
  360. }
  361. })
  362. t.Run("new panel starts the group at zero", func(t *testing.T) {
  363. setupBulkDB(t)
  364. svc := &ClientService{}
  365. ib := mkInbound(t, 25005, model.VLESS, `{"clients":[]}`)
  366. items := []ClientCreatePayload{
  367. {Client: model.Client{Email: "attached@g", SubID: "sub-attached-g", Enable: true, Group: "g"}, InboundIds: []int{ib.Id}, Traffic: &ClientPortableTraffic{Up: 100, Down: 200}},
  368. {Client: model.Client{Email: "orphan@g", SubID: "sub-orphan-g", Enable: true, Group: "g"}, Traffic: &ClientPortableTraffic{Up: 10, Down: 20}},
  369. }
  370. if res, _, err := svc.ImportClients(&InboundService{}, items); err != nil || res.Created != 2 {
  371. t.Fatalf("ImportClients result=%+v err=%v, want 2 created", res, err)
  372. }
  373. if g := groupByName(t, svc, "g"); g.TrafficUsed != 0 {
  374. t.Fatalf("group after import = %d (up=%d down=%d), want 0", g.TrafficUsed, g.Up, g.Down)
  375. }
  376. })
  377. }
  378. // A duplicate email in the file is skipped, but the copy that was created must
  379. // still get its own counters rather than none or the skipped copy's.
  380. func TestImportClientsDuplicateEmailRestoresCreatedCopy(t *testing.T) {
  381. cases := []struct {
  382. name string
  383. items func(ibID int) []ClientCreatePayload
  384. }{
  385. {
  386. name: "second attached copy skipped",
  387. items: func(ibID int) []ClientCreatePayload {
  388. return []ClientCreatePayload{
  389. {Client: model.Client{Email: "dup@traffic", SubID: "sub-dup-a", Enable: true}, InboundIds: []int{ibID}, Traffic: &ClientPortableTraffic{Up: 11, Down: 12}},
  390. {Client: model.Client{Email: "dup@traffic", SubID: "sub-dup-b", Enable: true}, InboundIds: []int{ibID}, Traffic: &ClientPortableTraffic{Up: 99, Down: 99}},
  391. }
  392. },
  393. },
  394. {
  395. name: "earlier orphan copy skipped",
  396. items: func(ibID int) []ClientCreatePayload {
  397. return []ClientCreatePayload{
  398. {Client: model.Client{Email: "dup@traffic", SubID: "sub-dup-b", Enable: true}, Traffic: &ClientPortableTraffic{Up: 99, Down: 99}},
  399. {Client: model.Client{Email: "dup@traffic", SubID: "sub-dup-a", Enable: true}, InboundIds: []int{ibID}, Traffic: &ClientPortableTraffic{Up: 11, Down: 12}},
  400. }
  401. },
  402. },
  403. }
  404. for _, tc := range cases {
  405. t.Run(tc.name, func(t *testing.T) {
  406. setupBulkDB(t)
  407. svc := &ClientService{}
  408. ib := mkInbound(t, 25002, model.VLESS, `{"clients":[]}`)
  409. res, _, err := svc.ImportClients(&InboundService{}, tc.items(ib.Id))
  410. if err != nil {
  411. t.Fatalf("ImportClients: %v", err)
  412. }
  413. if res.Created != 1 || len(res.Skipped) != 1 {
  414. t.Fatalf("ImportClients result=%+v, want 1 created and 1 skipped", res)
  415. }
  416. var row xray.ClientTraffic
  417. if err := database.GetDB().Where("email = ?", "dup@traffic").First(&row).Error; err != nil {
  418. t.Fatalf("lookup traffic: %v", err)
  419. }
  420. if row.Up != 11 || row.Down != 12 {
  421. t.Fatalf("traffic up=%d down=%d, want the created copy's 11/12", row.Up, row.Down)
  422. }
  423. })
  424. }
  425. }