service_info_node_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. package sub
  2. import (
  3. "fmt"
  4. "net/url"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  11. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  12. )
  13. func setupInfoNodeTestDB(t *testing.T) {
  14. t.Helper()
  15. if err := database.InitDB(t.TempDir() + "/test_infonode.db"); err != nil {
  16. t.Fatalf("InitDB: %v", err)
  17. }
  18. t.Cleanup(func() {
  19. _ = database.CloseDB()
  20. })
  21. db := database.GetDB()
  22. if err := db.AutoMigrate(
  23. &model.Inbound{},
  24. &model.ClientRecord{},
  25. &model.ClientInbound{},
  26. &xray.ClientTraffic{},
  27. &model.Setting{},
  28. ); err != nil {
  29. t.Fatalf("AutoMigrate: %v", err)
  30. }
  31. }
  32. func TestSubService_InfoNode_Active(t *testing.T) {
  33. setupInfoNodeTestDB(t)
  34. db := database.GetDB()
  35. ib := &model.Inbound{
  36. Id: 1,
  37. UserId: 1,
  38. Up: 0,
  39. Down: 0,
  40. Total: 0,
  41. Remark: "Germany-VLESS",
  42. Enable: true,
  43. Port: 443,
  44. Protocol: model.VLESS,
  45. Settings: `{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-active","enable":true,"totalGB":10737418240,"expiryTime":0}]}`,
  46. StreamSettings: `{"network":"tcp","security":"none"}`,
  47. }
  48. if err := db.Create(ib).Error; err != nil {
  49. t.Fatal(err)
  50. }
  51. rec := &model.ClientRecord{
  52. Id: 1,
  53. Email: "[email protected]",
  54. SubID: "sub-active",
  55. UUID: "c1-uuid",
  56. Enable: true,
  57. TotalGB: 10737418240, // 10 GB
  58. }
  59. if err := db.Create(rec).Error; err != nil {
  60. t.Fatal(err)
  61. }
  62. if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
  63. t.Fatal(err)
  64. }
  65. if err := db.Create(&xray.ClientTraffic{
  66. InboundId: 1,
  67. Email: "[email protected]",
  68. Up: 1073741824, // 1 GB
  69. Down: 1073741824, // 1 GB
  70. Total: 10737418240,
  71. Enable: true,
  72. }).Error; err != nil {
  73. t.Fatal(err)
  74. }
  75. svc := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}")
  76. svc.subInfoNodeEnable = true
  77. svc.subscriptionBody = true
  78. links, emails, _, traffic, err := svc.GetSubs("sub-active", "sub.example.com")
  79. if err != nil {
  80. t.Fatalf("GetSubs error: %v", err)
  81. }
  82. if len(links) != 2 {
  83. t.Fatalf("expected 2 links (dummy info node + 1 vless link), got %d: %v", len(links), links)
  84. }
  85. if len(emails) != 1 || emails[0] != "[email protected]" {
  86. t.Fatalf("emails = %v, want [[email protected]]", emails)
  87. }
  88. if !traffic.Enable {
  89. t.Fatalf("traffic should be enabled")
  90. }
  91. // First link must be the dummy socks node
  92. if !strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
  93. t.Fatalf("first link must be dummy socks node, got: %q", links[0])
  94. }
  95. decodedRemark, _ := url.QueryUnescape(strings.TrimPrefix(links[0], "socks://127.0.0.1:1080#"))
  96. if !strings.Contains(decodedRemark, "[email protected]") || !strings.Contains(decodedRemark, "8.00GB") {
  97. t.Fatalf("expected dummy remark to contain [email protected] and 8.00GB, got: %q", decodedRemark)
  98. }
  99. // Second link must be the clean vless link without traffic left tokens
  100. if !strings.HasPrefix(links[1], "vless://") {
  101. t.Fatalf("second link must be vless, got: %q", links[1])
  102. }
  103. if strings.Contains(links[1], "8.00GB") {
  104. t.Fatalf("server link must have clean remark without usage stats, got: %q", links[1])
  105. }
  106. }
  107. func TestSubService_InfoNode_Expired(t *testing.T) {
  108. setupInfoNodeTestDB(t)
  109. db := database.GetDB()
  110. expiredTime := time.Now().Add(-24 * time.Hour).UnixMilli()
  111. ib := &model.Inbound{
  112. Id: 1,
  113. UserId: 1,
  114. Remark: "Germany-VLESS",
  115. Enable: true,
  116. Port: 443,
  117. Protocol: model.VLESS,
  118. Settings: fmt.Sprintf(`{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-exp","enable":true,"expiryTime":%d}]}`, expiredTime),
  119. StreamSettings: `{"network":"tcp","security":"none"}`,
  120. }
  121. if err := db.Create(ib).Error; err != nil {
  122. t.Fatal(err)
  123. }
  124. rec := &model.ClientRecord{
  125. Id: 1,
  126. Email: "[email protected]",
  127. SubID: "sub-exp",
  128. UUID: "c1-uuid",
  129. Enable: true,
  130. ExpiryTime: expiredTime,
  131. }
  132. if err := db.Create(rec).Error; err != nil {
  133. t.Fatal(err)
  134. }
  135. if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
  136. t.Fatal(err)
  137. }
  138. if err := db.Create(&xray.ClientTraffic{
  139. InboundId: 1,
  140. Email: "[email protected]",
  141. ExpiryTime: expiredTime,
  142. Enable: true,
  143. }).Error; err != nil {
  144. t.Fatal(err)
  145. }
  146. svc := NewSubService("{{INBOUND}}|{{EMAIL}}")
  147. svc.subInfoNodeEnable = true
  148. svc.subExpiredTemplate = service.DefaultSubExpiredTemplate
  149. svc.subscriptionBody = true
  150. links, _, _, _, err := svc.GetSubs("sub-exp", "sub.example.com")
  151. if err != nil {
  152. t.Fatalf("GetSubs error: %v", err)
  153. }
  154. if len(links) != 1 {
  155. t.Fatalf("expected ONLY 1 link (dummy expired node), got %d: %v", len(links), links)
  156. }
  157. if !strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
  158. t.Fatalf("expired link must be dummy socks node, got: %q", links[0])
  159. }
  160. decodedRemark, _ := url.QueryUnescape(strings.TrimPrefix(links[0], "socks://127.0.0.1:1080#"))
  161. if !strings.Contains(decodedRemark, "Expired") || !strings.Contains(decodedRemark, "[email protected]") {
  162. t.Fatalf("expected expired remark, got: %q", decodedRemark)
  163. }
  164. }
  165. func TestSubService_InfoNode_TrafficDepleted(t *testing.T) {
  166. setupInfoNodeTestDB(t)
  167. db := database.GetDB()
  168. ib := &model.Inbound{
  169. Id: 1,
  170. UserId: 1,
  171. Remark: "Germany-VLESS",
  172. Enable: true,
  173. Port: 443,
  174. Protocol: model.VLESS,
  175. Settings: `{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-dep","enable":true,"totalGB":5368709120}]}`,
  176. StreamSettings: `{"network":"tcp","security":"none"}`,
  177. }
  178. if err := db.Create(ib).Error; err != nil {
  179. t.Fatal(err)
  180. }
  181. rec := &model.ClientRecord{
  182. Id: 1,
  183. Email: "[email protected]",
  184. SubID: "sub-dep",
  185. UUID: "c1-uuid",
  186. Enable: true,
  187. TotalGB: 5368709120, // 5 GB
  188. }
  189. if err := db.Create(rec).Error; err != nil {
  190. t.Fatal(err)
  191. }
  192. if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
  193. t.Fatal(err)
  194. }
  195. if err := db.Create(&xray.ClientTraffic{
  196. InboundId: 1,
  197. Email: "[email protected]",
  198. Up: 3221225472, // 3 GB
  199. Down: 2147483648, // 2 GB (total 5 GB used = depleted)
  200. Total: 5368709120,
  201. Enable: true,
  202. }).Error; err != nil {
  203. t.Fatal(err)
  204. }
  205. svc := NewSubService("{{INBOUND}}|{{EMAIL}}")
  206. svc.subInfoNodeEnable = true
  207. svc.subTrafficDepletedTemplate = service.DefaultSubTrafficDepletedTemplate
  208. svc.subscriptionBody = true
  209. links, _, _, _, err := svc.GetSubs("sub-dep", "sub.example.com")
  210. if err != nil {
  211. t.Fatalf("GetSubs error: %v", err)
  212. }
  213. if len(links) != 1 {
  214. t.Fatalf("expected ONLY 1 link (dummy depleted node), got %d: %v", len(links), links)
  215. }
  216. if !strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
  217. t.Fatalf("depleted link must be dummy socks node, got: %q", links[0])
  218. }
  219. decodedRemark, _ := url.QueryUnescape(strings.TrimPrefix(links[0], "socks://127.0.0.1:1080#"))
  220. if !strings.Contains(decodedRemark, "Traffic Depleted") || !strings.Contains(decodedRemark, "[email protected]") {
  221. t.Fatalf("expected depleted remark, got: %q", decodedRemark)
  222. }
  223. }
  224. func TestSubService_GetSubs_NonSubscriptionBody_NoInfoNode(t *testing.T) {
  225. setupInfoNodeTestDB(t)
  226. db := database.GetDB()
  227. ib := &model.Inbound{
  228. Id: 1,
  229. UserId: 1,
  230. Remark: "US-VLESS",
  231. Enable: true,
  232. Port: 443,
  233. Protocol: model.VLESS,
  234. Settings: `{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-panel","enable":true,"totalGB":10737418240}]}`,
  235. StreamSettings: `{"network":"tcp","security":"none"}`,
  236. }
  237. if err := db.Create(ib).Error; err != nil {
  238. t.Fatal(err)
  239. }
  240. rec := &model.ClientRecord{
  241. Id: 1,
  242. Email: "[email protected]",
  243. SubID: "sub-panel",
  244. UUID: "c1-uuid",
  245. Enable: true,
  246. TotalGB: 10737418240,
  247. }
  248. if err := db.Create(rec).Error; err != nil {
  249. t.Fatal(err)
  250. }
  251. if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
  252. t.Fatal(err)
  253. }
  254. if err := db.Create(&xray.ClientTraffic{
  255. InboundId: 1,
  256. Email: "[email protected]",
  257. Up: 1073741824,
  258. Down: 1073741824,
  259. Total: 10737418240,
  260. Enable: true,
  261. }).Error; err != nil {
  262. t.Fatal(err)
  263. }
  264. // When called via GetSubs (e.g. from LinkProvider for admin QR / copy modals),
  265. // subscriptionBody is false, so it should render the clean link and not add dummy nodes.
  266. svc := NewSubService("{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D")
  267. svc.subInfoNodeEnable = true
  268. links, _, _, _, err := svc.GetSubs("sub-panel", "sub.example.com")
  269. if err != nil {
  270. t.Fatalf("GetSubs error: %v", err)
  271. }
  272. if len(links) != 1 {
  273. t.Fatalf("expected 1 clean link, got %d: %v", len(links), links)
  274. }
  275. if strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
  276. t.Fatalf("GetSubs must NOT return dummy socks node when subscriptionBody is false: %v", links[0])
  277. }
  278. if strings.Contains(links[0], "8.00GB") {
  279. t.Fatalf("GetSubs must NOT contain snapshot usage stats in non-subscriptionBody context: %v", links[0])
  280. }
  281. }
  282. func TestSubService_InfoNode_MultiClient_DeterministicPrimaryEmail(t *testing.T) {
  283. setupInfoNodeTestDB(t)
  284. db := database.GetDB()
  285. ib := &model.Inbound{
  286. Id: 1,
  287. UserId: 1,
  288. Remark: "Germany-VLESS",
  289. Enable: true,
  290. Port: 443,
  291. Protocol: model.VLESS,
  292. Settings: `{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-multi","enable":true,"totalGB":10737418240},{"id":"c2-uuid","email":"[email protected]","subId":"sub-multi","enable":true,"totalGB":10737418240}]}`,
  293. StreamSettings: `{"network":"tcp","security":"none"}`,
  294. }
  295. if err := db.Create(ib).Error; err != nil {
  296. t.Fatal(err)
  297. }
  298. if err := db.Create(&model.ClientRecord{
  299. Id: 1, Email: "[email protected]", SubID: "sub-multi", UUID: "c1-uuid", Enable: true, TotalGB: 10737418240,
  300. }).Error; err != nil {
  301. t.Fatal(err)
  302. }
  303. if err := db.Create(&model.ClientRecord{
  304. Id: 2, Email: "[email protected]", SubID: "sub-multi", UUID: "c2-uuid", Enable: true, TotalGB: 10737418240,
  305. }).Error; err != nil {
  306. t.Fatal(err)
  307. }
  308. if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
  309. t.Fatal(err)
  310. }
  311. if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 2}).Error; err != nil {
  312. t.Fatal(err)
  313. }
  314. if err := db.Create(&xray.ClientTraffic{
  315. InboundId: 1, Email: "[email protected]", Up: 100, Down: 100, Total: 10737418240, Enable: true,
  316. }).Error; err != nil {
  317. t.Fatal(err)
  318. }
  319. if err := db.Create(&xray.ClientTraffic{
  320. InboundId: 1, Email: "[email protected]", Up: 100, Down: 100, Total: 10737418240, Enable: true,
  321. }).Error; err != nil {
  322. t.Fatal(err)
  323. }
  324. svc := NewSubService("{{EMAIL}}")
  325. svc.subInfoNodeEnable = true
  326. svc.subscriptionBody = true
  327. for i := 0; i < 5; i++ {
  328. links, _, _, _, err := svc.GetSubs("sub-multi", "sub.example.com")
  329. if err != nil {
  330. t.Fatalf("GetSubs error: %v", err)
  331. }
  332. if len(links) < 1 || !strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
  333. t.Fatalf("expected dummy socks node, got: %v", links)
  334. }
  335. decodedRemark, _ := url.QueryUnescape(strings.TrimPrefix(links[0], "socks://127.0.0.1:1080#"))
  336. if decodedRemark != "[email protected]" {
  337. t.Fatalf("expected primaryEmail '[email protected]' (sorted alphabetically), got %q", decodedRemark)
  338. }
  339. }
  340. }