info_endpoint_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. package sub
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "testing"
  8. "github.com/gin-gonic/gin"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. )
  12. func seedInfoEndpointSub(t *testing.T, subId, email string) {
  13. t.Helper()
  14. db := database.GetDB()
  15. rec := &model.ClientRecord{Email: email, SubID: subId, UUID: "info-uuid", Enable: true}
  16. if err := db.Create(rec).Error; err != nil {
  17. t.Fatalf("seed client: %v", err)
  18. }
  19. link := "vless://[email protected]:443?type=tcp&security=reality&pbk=abc&sid=12&fp=chrome#orig"
  20. if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: link, Remark: "DE-Provider", SortIndex: 1}).Error; err != nil {
  21. t.Fatalf("seed external link: %v", err)
  22. }
  23. }
  24. func TestSubInfoEndpoint_ServesStatusJSONEvenForBrowsers(t *testing.T) {
  25. gin.SetMode(gin.TestMode)
  26. initSubDB(t)
  27. seedInfoEndpointSub(t, "info-sub", "info@x")
  28. router := gin.New()
  29. NewSUBController(router.Group("/"))
  30. req := httptest.NewRequest(http.MethodGet, "/sub/info-sub?format=info", nil)
  31. req.Host = "sub.example.com"
  32. req.Header.Set("Accept", "text/html")
  33. w := httptest.NewRecorder()
  34. router.ServeHTTP(w, req)
  35. if w.Code != http.StatusOK {
  36. t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
  37. }
  38. if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
  39. t.Fatalf("Content-Type = %q, want application/json", ct)
  40. }
  41. if cc := w.Header().Get("Cache-Control"); !strings.Contains(cc, "no-store") {
  42. t.Fatalf("Cache-Control = %q, want a no-store directive", cc)
  43. }
  44. var info map[string]any
  45. if err := json.Unmarshal(w.Body.Bytes(), &info); err != nil {
  46. t.Fatalf("body is not valid JSON: %v; body=%s", err, w.Body.String())
  47. }
  48. if info["sId"] != "info-sub" {
  49. t.Fatalf("sId = %v, want %q", info["sId"], "info-sub")
  50. }
  51. if _, hasLinks := info["links"]; hasLinks {
  52. t.Fatal("info payload must not include the links list")
  53. }
  54. if isOnline, ok := info["isOnline"].(bool); !ok || isOnline {
  55. t.Fatalf("isOnline = %v, want false with no live xray", info["isOnline"])
  56. }
  57. emails, ok := info["emails"].([]any)
  58. if !ok || len(emails) != 1 || emails[0] != "info@x" {
  59. t.Fatalf("emails = %v, want [info@x]", info["emails"])
  60. }
  61. subUrl, _ := info["subUrl"].(string)
  62. if !strings.HasSuffix(subUrl, "/sub/info-sub") {
  63. t.Fatalf("subUrl = %q, want a /sub/info-sub suffix", subUrl)
  64. }
  65. for _, key := range []string{"enabled", "used", "remained", "expire", "lastOnline", "datepicker", "announce"} {
  66. if _, present := info[key]; !present {
  67. t.Fatalf("info payload missing %q; body=%s", key, w.Body.String())
  68. }
  69. }
  70. if info["subUpdates"] != float64(12) {
  71. t.Fatalf("subUpdates = %v, want the default 12-hour interval", info["subUpdates"])
  72. }
  73. }
  74. func TestSubInfoEndpoint_UnknownSubIs404(t *testing.T) {
  75. gin.SetMode(gin.TestMode)
  76. initSubDB(t)
  77. router := gin.New()
  78. NewSUBController(router.Group("/"))
  79. req := httptest.NewRequest(http.MethodGet, "/sub/does-not-exist?format=info", nil)
  80. req.Host = "sub.example.com"
  81. w := httptest.NewRecorder()
  82. router.ServeHTTP(w, req)
  83. if w.Code != http.StatusNotFound {
  84. t.Fatalf("status = %d, want 404", w.Code)
  85. }
  86. }
  87. func TestSubInfoEndpoint_HTMLPageStillWinsWithoutFormatParam(t *testing.T) {
  88. gin.SetMode(gin.TestMode)
  89. initSubDB(t)
  90. seedInfoEndpointSub(t, "html-sub", "html@x")
  91. oldDistFS := distFS
  92. distFS = testDistFS
  93. t.Cleanup(func() { distFS = oldDistFS })
  94. router := gin.New()
  95. NewSUBController(router.Group("/"))
  96. req := httptest.NewRequest(http.MethodGet, "/sub/html-sub", nil)
  97. req.Host = "sub.example.com"
  98. req.Header.Set("Accept", "text/html")
  99. w := httptest.NewRecorder()
  100. router.ServeHTTP(w, req)
  101. if w.Code != http.StatusOK {
  102. t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
  103. }
  104. if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
  105. t.Fatalf("Content-Type = %q, want text/html for a browser request", ct)
  106. }
  107. if !strings.Contains(w.Body.String(), "__SUB_PAGE_DATA__") {
  108. t.Fatal("browser request must still get the SPA page with injected page data")
  109. }
  110. if !strings.Contains(w.Body.String(), `"isOnline":false`) {
  111. t.Fatalf("injected page data must carry isOnline; body=%s", w.Body.String())
  112. }
  113. }