| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- package sub
- import (
- "fmt"
- "net/url"
- "strings"
- "testing"
- "time"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "github.com/mhsanaei/3x-ui/v3/internal/web/service"
- "github.com/mhsanaei/3x-ui/v3/internal/xray"
- )
- func setupInfoNodeTestDB(t *testing.T) {
- t.Helper()
- if err := database.InitDB(t.TempDir() + "/test_infonode.db"); err != nil {
- t.Fatalf("InitDB: %v", err)
- }
- t.Cleanup(func() {
- _ = database.CloseDB()
- })
- db := database.GetDB()
- if err := db.AutoMigrate(
- &model.Inbound{},
- &model.ClientRecord{},
- &model.ClientInbound{},
- &xray.ClientTraffic{},
- &model.Setting{},
- ); err != nil {
- t.Fatalf("AutoMigrate: %v", err)
- }
- }
- func TestSubService_InfoNode_Active(t *testing.T) {
- setupInfoNodeTestDB(t)
- db := database.GetDB()
- ib := &model.Inbound{
- Id: 1,
- UserId: 1,
- Up: 0,
- Down: 0,
- Total: 0,
- Remark: "Germany-VLESS",
- Enable: true,
- Port: 443,
- Protocol: model.VLESS,
- Settings: `{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-active","enable":true,"totalGB":10737418240,"expiryTime":0}]}`,
- StreamSettings: `{"network":"tcp","security":"none"}`,
- }
- if err := db.Create(ib).Error; err != nil {
- t.Fatal(err)
- }
- rec := &model.ClientRecord{
- Id: 1,
- Email: "[email protected]",
- SubID: "sub-active",
- UUID: "c1-uuid",
- Enable: true,
- TotalGB: 10737418240, // 10 GB
- }
- if err := db.Create(rec).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&xray.ClientTraffic{
- InboundId: 1,
- Email: "[email protected]",
- Up: 1073741824, // 1 GB
- Down: 1073741824, // 1 GB
- Total: 10737418240,
- Enable: true,
- }).Error; err != nil {
- t.Fatal(err)
- }
- svc := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}")
- svc.subInfoNodeEnable = true
- svc.subscriptionBody = true
- links, emails, _, traffic, err := svc.GetSubs("sub-active", "sub.example.com")
- if err != nil {
- t.Fatalf("GetSubs error: %v", err)
- }
- if len(links) != 2 {
- t.Fatalf("expected 2 links (dummy info node + 1 vless link), got %d: %v", len(links), links)
- }
- if len(emails) != 1 || emails[0] != "[email protected]" {
- t.Fatalf("emails = %v, want [[email protected]]", emails)
- }
- if !traffic.Enable {
- t.Fatalf("traffic should be enabled")
- }
- // First link must be the dummy socks node
- if !strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
- t.Fatalf("first link must be dummy socks node, got: %q", links[0])
- }
- decodedRemark, _ := url.QueryUnescape(strings.TrimPrefix(links[0], "socks://127.0.0.1:1080#"))
- if !strings.Contains(decodedRemark, "[email protected]") || !strings.Contains(decodedRemark, "8.00GB") {
- t.Fatalf("expected dummy remark to contain [email protected] and 8.00GB, got: %q", decodedRemark)
- }
- // Second link must be the clean vless link without traffic left tokens
- if !strings.HasPrefix(links[1], "vless://") {
- t.Fatalf("second link must be vless, got: %q", links[1])
- }
- if strings.Contains(links[1], "8.00GB") {
- t.Fatalf("server link must have clean remark without usage stats, got: %q", links[1])
- }
- }
- func TestSubService_InfoNode_Expired(t *testing.T) {
- setupInfoNodeTestDB(t)
- db := database.GetDB()
- expiredTime := time.Now().Add(-24 * time.Hour).UnixMilli()
- ib := &model.Inbound{
- Id: 1,
- UserId: 1,
- Remark: "Germany-VLESS",
- Enable: true,
- Port: 443,
- Protocol: model.VLESS,
- Settings: fmt.Sprintf(`{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-exp","enable":true,"expiryTime":%d}]}`, expiredTime),
- StreamSettings: `{"network":"tcp","security":"none"}`,
- }
- if err := db.Create(ib).Error; err != nil {
- t.Fatal(err)
- }
- rec := &model.ClientRecord{
- Id: 1,
- Email: "[email protected]",
- SubID: "sub-exp",
- UUID: "c1-uuid",
- Enable: true,
- ExpiryTime: expiredTime,
- }
- if err := db.Create(rec).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&xray.ClientTraffic{
- InboundId: 1,
- Email: "[email protected]",
- ExpiryTime: expiredTime,
- Enable: true,
- }).Error; err != nil {
- t.Fatal(err)
- }
- svc := NewSubService("{{INBOUND}}|{{EMAIL}}")
- svc.subInfoNodeEnable = true
- svc.subExpiredTemplate = service.DefaultSubExpiredTemplate
- svc.subscriptionBody = true
- links, _, _, _, err := svc.GetSubs("sub-exp", "sub.example.com")
- if err != nil {
- t.Fatalf("GetSubs error: %v", err)
- }
- if len(links) != 1 {
- t.Fatalf("expected ONLY 1 link (dummy expired node), got %d: %v", len(links), links)
- }
- if !strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
- t.Fatalf("expired link must be dummy socks node, got: %q", links[0])
- }
- decodedRemark, _ := url.QueryUnescape(strings.TrimPrefix(links[0], "socks://127.0.0.1:1080#"))
- if !strings.Contains(decodedRemark, "Expired") || !strings.Contains(decodedRemark, "[email protected]") {
- t.Fatalf("expected expired remark, got: %q", decodedRemark)
- }
- }
- func TestSubService_InfoNode_TrafficDepleted(t *testing.T) {
- setupInfoNodeTestDB(t)
- db := database.GetDB()
- ib := &model.Inbound{
- Id: 1,
- UserId: 1,
- Remark: "Germany-VLESS",
- Enable: true,
- Port: 443,
- Protocol: model.VLESS,
- Settings: `{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-dep","enable":true,"totalGB":5368709120}]}`,
- StreamSettings: `{"network":"tcp","security":"none"}`,
- }
- if err := db.Create(ib).Error; err != nil {
- t.Fatal(err)
- }
- rec := &model.ClientRecord{
- Id: 1,
- Email: "[email protected]",
- SubID: "sub-dep",
- UUID: "c1-uuid",
- Enable: true,
- TotalGB: 5368709120, // 5 GB
- }
- if err := db.Create(rec).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&xray.ClientTraffic{
- InboundId: 1,
- Email: "[email protected]",
- Up: 3221225472, // 3 GB
- Down: 2147483648, // 2 GB (total 5 GB used = depleted)
- Total: 5368709120,
- Enable: true,
- }).Error; err != nil {
- t.Fatal(err)
- }
- svc := NewSubService("{{INBOUND}}|{{EMAIL}}")
- svc.subInfoNodeEnable = true
- svc.subTrafficDepletedTemplate = service.DefaultSubTrafficDepletedTemplate
- svc.subscriptionBody = true
- links, _, _, _, err := svc.GetSubs("sub-dep", "sub.example.com")
- if err != nil {
- t.Fatalf("GetSubs error: %v", err)
- }
- if len(links) != 1 {
- t.Fatalf("expected ONLY 1 link (dummy depleted node), got %d: %v", len(links), links)
- }
- if !strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
- t.Fatalf("depleted link must be dummy socks node, got: %q", links[0])
- }
- decodedRemark, _ := url.QueryUnescape(strings.TrimPrefix(links[0], "socks://127.0.0.1:1080#"))
- if !strings.Contains(decodedRemark, "Traffic Depleted") || !strings.Contains(decodedRemark, "[email protected]") {
- t.Fatalf("expected depleted remark, got: %q", decodedRemark)
- }
- }
- func TestSubService_GetSubs_NonSubscriptionBody_NoInfoNode(t *testing.T) {
- setupInfoNodeTestDB(t)
- db := database.GetDB()
- ib := &model.Inbound{
- Id: 1,
- UserId: 1,
- Remark: "US-VLESS",
- Enable: true,
- Port: 443,
- Protocol: model.VLESS,
- Settings: `{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-panel","enable":true,"totalGB":10737418240}]}`,
- StreamSettings: `{"network":"tcp","security":"none"}`,
- }
- if err := db.Create(ib).Error; err != nil {
- t.Fatal(err)
- }
- rec := &model.ClientRecord{
- Id: 1,
- Email: "[email protected]",
- SubID: "sub-panel",
- UUID: "c1-uuid",
- Enable: true,
- TotalGB: 10737418240,
- }
- if err := db.Create(rec).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&xray.ClientTraffic{
- InboundId: 1,
- Email: "[email protected]",
- Up: 1073741824,
- Down: 1073741824,
- Total: 10737418240,
- Enable: true,
- }).Error; err != nil {
- t.Fatal(err)
- }
- // When called via GetSubs (e.g. from LinkProvider for admin QR / copy modals),
- // subscriptionBody is false, so it should render the clean link and not add dummy nodes.
- svc := NewSubService("{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D")
- svc.subInfoNodeEnable = true
- links, _, _, _, err := svc.GetSubs("sub-panel", "sub.example.com")
- if err != nil {
- t.Fatalf("GetSubs error: %v", err)
- }
- if len(links) != 1 {
- t.Fatalf("expected 1 clean link, got %d: %v", len(links), links)
- }
- if strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
- t.Fatalf("GetSubs must NOT return dummy socks node when subscriptionBody is false: %v", links[0])
- }
- if strings.Contains(links[0], "8.00GB") {
- t.Fatalf("GetSubs must NOT contain snapshot usage stats in non-subscriptionBody context: %v", links[0])
- }
- }
- func TestSubService_InfoNode_MultiClient_DeterministicPrimaryEmail(t *testing.T) {
- setupInfoNodeTestDB(t)
- db := database.GetDB()
- ib := &model.Inbound{
- Id: 1,
- UserId: 1,
- Remark: "Germany-VLESS",
- Enable: true,
- Port: 443,
- Protocol: model.VLESS,
- 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}]}`,
- StreamSettings: `{"network":"tcp","security":"none"}`,
- }
- if err := db.Create(ib).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientRecord{
- Id: 1, Email: "[email protected]", SubID: "sub-multi", UUID: "c1-uuid", Enable: true, TotalGB: 10737418240,
- }).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientRecord{
- Id: 2, Email: "[email protected]", SubID: "sub-multi", UUID: "c2-uuid", Enable: true, TotalGB: 10737418240,
- }).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 2}).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&xray.ClientTraffic{
- InboundId: 1, Email: "[email protected]", Up: 100, Down: 100, Total: 10737418240, Enable: true,
- }).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&xray.ClientTraffic{
- InboundId: 1, Email: "[email protected]", Up: 100, Down: 100, Total: 10737418240, Enable: true,
- }).Error; err != nil {
- t.Fatal(err)
- }
- svc := NewSubService("{{EMAIL}}")
- svc.subInfoNodeEnable = true
- svc.subscriptionBody = true
- for i := 0; i < 5; i++ {
- links, _, _, _, err := svc.GetSubs("sub-multi", "sub.example.com")
- if err != nil {
- t.Fatalf("GetSubs error: %v", err)
- }
- if len(links) < 1 || !strings.HasPrefix(links[0], "socks://127.0.0.1:1080#") {
- t.Fatalf("expected dummy socks node, got: %v", links)
- }
- decodedRemark, _ := url.QueryUnescape(strings.TrimPrefix(links[0], "socks://127.0.0.1:1080#"))
- if decodedRemark != "[email protected]" {
- t.Fatalf("expected primaryEmail '[email protected]' (sorted alphabetically), got %q", decodedRemark)
- }
- }
- }
|