placeholders_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. package sub
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. "github.com/gin-gonic/gin"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  10. )
  11. func TestRenderSubPlaceholders(t *testing.T) {
  12. data := subPlaceholderData{
  13. SubID: "sub-123",
  14. Context: remarkContext{client: model.Client{
  15. Email: "Ilnur",
  16. ID: "abcdef12-3456-7890-abcd-ef1234567890",
  17. SubID: "sub-123",
  18. TgID: 42,
  19. Enable: true,
  20. }},
  21. HasCtx: true,
  22. }
  23. tests := []struct {
  24. name string
  25. tmpl string
  26. data subPlaceholderData
  27. want string
  28. }{
  29. {
  30. name: "identity tokens",
  31. tmpl: "{{EMAIL}}/{{ID}}/{{SHORT_ID}}/{{SUB_ID}}/{{TELEGRAM_ID}}",
  32. data: data,
  33. want: "Ilnur/abcdef12-3456-7890-abcd-ef1234567890/abcdef12/sub-123/42",
  34. },
  35. {
  36. name: "no template",
  37. tmpl: "isVPN",
  38. data: subPlaceholderData{SubID: "sub-123"},
  39. want: "isVPN",
  40. },
  41. {
  42. name: "unsupported tokens stay literal",
  43. tmpl: "{{SUB_ID}}/{{INBOUND}}/{{TRAFFIC_LEFT}}/{{PROTOCOL}}/{EMAIL}",
  44. data: subPlaceholderData{SubID: "sub-123"},
  45. want: "sub-123/{{INBOUND}}/{{TRAFFIC_LEFT}}/{{PROTOCOL}}/{EMAIL}",
  46. },
  47. {
  48. name: "URL values are escaped",
  49. tmpl: "https://support.example/?email={{EMAIL}}&sub={{SUB_ID}}",
  50. data: subPlaceholderData{
  51. SubID: "sub id",
  52. Context: remarkContext{client: model.Client{
  53. Email: "john [email protected]",
  54. SubID: "sub id",
  55. }},
  56. HasCtx: true,
  57. Escape: true,
  58. },
  59. want: "https://support.example/?email=john+doe%40example.com&sub=sub+id",
  60. },
  61. }
  62. for _, tt := range tests {
  63. t.Run(tt.name, func(t *testing.T) {
  64. if got := renderSubPlaceholders(tt.tmpl, tt.data); got != tt.want {
  65. t.Fatalf("renderSubPlaceholders() = %q, want %q", got, tt.want)
  66. }
  67. })
  68. }
  69. }
  70. func TestMetadataForSubRequestOmitsUnconfiguredProfileURL(t *testing.T) {
  71. a := &SUBController{
  72. subTitle: "isVPN",
  73. subSupportUrl: "https://support.example/",
  74. }
  75. metadata := a.metadataForSubRequest(func() *SubService {
  76. t.Fatal("metadataForSubRequest loaded a subscription context without configured placeholders")
  77. return nil
  78. }, "sub-123", "https://sub.example/sub-123")
  79. if metadata.ProfileURL != "" {
  80. t.Fatalf("ProfileURL = %q, want no link when unconfigured", metadata.ProfileURL)
  81. }
  82. }
  83. func TestSubscriptionProfileURLRequiresExplicitConfiguration(t *testing.T) {
  84. gin.SetMode(gin.TestMode)
  85. initSubDB(t)
  86. seedInfoEndpointSub(t, "profile-sub", "[email protected]")
  87. for _, config := range []struct {
  88. name, profileURL, want string
  89. }{
  90. {name: "empty"},
  91. {name: "whitespace", profileURL: " "},
  92. {name: "explicit", profileURL: "https://portal.example.com/account", want: "https://portal.example.com/account"},
  93. {name: "template", profileURL: "https://portal.example.com/account?sub={{SUB_ID}}", want: "https://portal.example.com/account?sub=profile-sub"},
  94. } {
  95. t.Run(config.name, func(t *testing.T) {
  96. for _, client := range []struct {
  97. name, userAgent string
  98. happAutoDetect bool
  99. }{
  100. {name: "Happ", userAgent: "Happ/3.22.0 (Android)", happAutoDetect: true},
  101. {name: "Happ without auto-detection", userAgent: "Happ/3.22.0 (Android)"},
  102. {name: "standard", userAgent: "v2rayNG/1.8.5", happAutoDetect: true},
  103. } {
  104. t.Run(client.name, func(t *testing.T) {
  105. // All formats must honor the opt-in, independently of Happ customization.
  106. router := gin.New()
  107. NewSUBController(router.Group("/"),
  108. WithSUBJsonEnabled(true), WithSUBClashEnabled(true),
  109. WithSUBProfileURL(config.profileURL),
  110. WithSUBProfileMode(service.SubProfileModeCustom),
  111. WithSUBHappConfig(HappConfig{AutoDetect: client.happAutoDetect}),
  112. )
  113. for _, path := range []string{"/sub/profile-sub", "/json/profile-sub", "/clash/profile-sub"} {
  114. t.Run(path, func(t *testing.T) {
  115. req := httptest.NewRequest(http.MethodGet, path, nil)
  116. req.Host = "sub.example.com"
  117. req.Header.Set("User-Agent", client.userAgent)
  118. resp := httptest.NewRecorder()
  119. router.ServeHTTP(resp, req)
  120. if resp.Code != http.StatusOK {
  121. t.Fatalf("status = %d, want 200; body=%s", resp.Code, resp.Body.String())
  122. }
  123. if got := resp.Header().Get("Profile-Web-Page-Url"); got != config.want {
  124. t.Fatalf("Profile-Web-Page-Url = %q, want %q", got, config.want)
  125. }
  126. if config.want == "" {
  127. if _, present := resp.Header()["Profile-Web-Page-Url"]; present {
  128. t.Fatal("unconfigured profile header must be omitted")
  129. }
  130. }
  131. })
  132. }
  133. })
  134. }
  135. })
  136. }
  137. }
  138. func TestMetadataForSubRequestUsesStableClientIdentity(t *testing.T) {
  139. initSubDB(t)
  140. db := database.GetDB()
  141. first := model.ClientRecord{
  142. Email: "john [email protected]",
  143. SubID: "sub-123",
  144. UUID: "abcdef12-3456-7890-abcd-ef1234567890",
  145. TgID: 42,
  146. Enable: true,
  147. }
  148. second := model.ClientRecord{
  149. Email: "[email protected]",
  150. SubID: "sub-123",
  151. UUID: "fedcba98-3456-7890-abcd-ef1234567890",
  152. TgID: 99,
  153. Enable: true,
  154. }
  155. if err := db.Create(&first).Error; err != nil {
  156. t.Fatalf("seed first client: %v", err)
  157. }
  158. if err := db.Create(&second).Error; err != nil {
  159. t.Fatalf("seed second client: %v", err)
  160. }
  161. a := &SUBController{
  162. subTitle: "isVPN — {{EMAIL}}",
  163. subSupportUrl: "https://support.example/?email={{EMAIL}}&tg={{TELEGRAM_ID}}",
  164. subProfileUrl: "https://profile.example/account/{{ID}}",
  165. subProfileMode: service.SubProfileModeCustom,
  166. subAnnounce: "Subscription {{SUB_ID}}",
  167. }
  168. metadata := a.metadataForSubRequest(func() *SubService { return &SubService{} }, "sub-123", "https://sub.example/sub-123")
  169. if metadata.Title != "isVPN — john [email protected]" {
  170. t.Fatalf("Title = %q", metadata.Title)
  171. }
  172. if metadata.SupportURL != "https://support.example/?email=john+doe%40example.com&tg=42" {
  173. t.Fatalf("SupportURL = %q", metadata.SupportURL)
  174. }
  175. if metadata.ProfileURL != "https://profile.example/account/abcdef12-3456-7890-abcd-ef1234567890" {
  176. t.Fatalf("ProfileURL = %q", metadata.ProfileURL)
  177. }
  178. if metadata.Announce != "Subscription sub-123" {
  179. t.Fatalf("Announce = %q", metadata.Announce)
  180. }
  181. }