service_info_node_test.go 11 KB

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