1
0

validate_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. package middleware
  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/web/entity"
  10. )
  11. type sampleBody struct {
  12. Port int `json:"port" form:"port" validate:"gte=1,lte=65535"`
  13. Protocol string `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan"`
  14. Tag string `json:"tag" form:"tag"`
  15. }
  16. func newRouter(handler gin.HandlerFunc) *gin.Engine {
  17. gin.SetMode(gin.TestMode)
  18. r := gin.New()
  19. r.POST("/submit", handler)
  20. return r
  21. }
  22. func decodeMsg(t *testing.T, body string) entity.Msg {
  23. t.Helper()
  24. var msg entity.Msg
  25. if err := json.Unmarshal([]byte(body), &msg); err != nil {
  26. t.Fatalf("decode msg: %v (body=%q)", err, body)
  27. }
  28. return msg
  29. }
  30. func TestBindAndValidate_ValidPayloadPassesThrough(t *testing.T) {
  31. r := newRouter(func(c *gin.Context) {
  32. got, ok := BindAndValidate[sampleBody](c)
  33. if !ok {
  34. t.Fatalf("expected ok=true, got false (body should be valid)")
  35. }
  36. if got.Port != 443 || got.Protocol != "vless" || got.Tag != "inbound-443" {
  37. t.Fatalf("decoded payload mismatch: %+v", got)
  38. }
  39. c.JSON(http.StatusOK, entity.Msg{Success: true, Msg: "ok"})
  40. })
  41. rec := httptest.NewRecorder()
  42. req := httptest.NewRequest(http.MethodPost, "/submit",
  43. strings.NewReader(`{"port":443,"protocol":"vless","tag":"inbound-443"}`))
  44. req.Header.Set("Content-Type", "application/json")
  45. r.ServeHTTP(rec, req)
  46. if rec.Code != http.StatusOK {
  47. t.Fatalf("status = %d, want %d (body=%s)", rec.Code, http.StatusOK, rec.Body.String())
  48. }
  49. if msg := decodeMsg(t, rec.Body.String()); !msg.Success {
  50. t.Fatalf("expected Success=true; got %+v", msg)
  51. }
  52. }
  53. func TestBindAndValidate_PortOutOfRangeIsRejected(t *testing.T) {
  54. r := newRouter(func(c *gin.Context) {
  55. if _, ok := BindAndValidate[sampleBody](c); ok {
  56. t.Fatal("expected ok=false on invalid port; got true")
  57. }
  58. })
  59. rec := httptest.NewRecorder()
  60. req := httptest.NewRequest(http.MethodPost, "/submit",
  61. strings.NewReader(`{"port":70000,"protocol":"vless"}`))
  62. req.Header.Set("Content-Type", "application/json")
  63. r.ServeHTTP(rec, req)
  64. msg := decodeMsg(t, rec.Body.String())
  65. if msg.Success {
  66. t.Fatalf("expected Success=false; got %+v", msg)
  67. }
  68. payload, err := payloadFromObj(msg.Obj)
  69. if err != nil {
  70. t.Fatalf("payload extraction: %v", err)
  71. }
  72. found := false
  73. for _, issue := range payload.Issues {
  74. if issue.Field == "port" && issue.Rule == "lte" {
  75. found = true
  76. break
  77. }
  78. }
  79. if !found {
  80. t.Fatalf("expected an Issue for field=port rule=lte; got %+v", payload.Issues)
  81. }
  82. }
  83. func TestBindAndValidate_ProtocolEnumIsRejected(t *testing.T) {
  84. r := newRouter(func(c *gin.Context) {
  85. if _, ok := BindAndValidate[sampleBody](c); ok {
  86. t.Fatal("expected ok=false on invalid protocol; got true")
  87. }
  88. })
  89. rec := httptest.NewRecorder()
  90. req := httptest.NewRequest(http.MethodPost, "/submit",
  91. strings.NewReader(`{"port":443,"protocol":"unknown"}`))
  92. req.Header.Set("Content-Type", "application/json")
  93. r.ServeHTTP(rec, req)
  94. msg := decodeMsg(t, rec.Body.String())
  95. payload, err := payloadFromObj(msg.Obj)
  96. if err != nil {
  97. t.Fatalf("payload extraction: %v", err)
  98. }
  99. found := false
  100. for _, issue := range payload.Issues {
  101. if issue.Field == "protocol" && issue.Rule == "oneof" {
  102. found = true
  103. }
  104. }
  105. if !found {
  106. t.Fatalf("expected an Issue for field=protocol rule=oneof; got %+v", payload.Issues)
  107. }
  108. }
  109. func TestBindAndValidate_MalformedJSONReturnsMessageButNoIssues(t *testing.T) {
  110. r := newRouter(func(c *gin.Context) {
  111. if _, ok := BindAndValidate[sampleBody](c); ok {
  112. t.Fatal("expected ok=false on malformed JSON; got true")
  113. }
  114. })
  115. rec := httptest.NewRecorder()
  116. req := httptest.NewRequest(http.MethodPost, "/submit",
  117. strings.NewReader(`{"port":}`))
  118. req.Header.Set("Content-Type", "application/json")
  119. r.ServeHTTP(rec, req)
  120. msg := decodeMsg(t, rec.Body.String())
  121. if msg.Success {
  122. t.Fatal("expected Success=false on malformed JSON")
  123. }
  124. payload, err := payloadFromObj(msg.Obj)
  125. if err != nil {
  126. t.Fatalf("payload extraction: %v", err)
  127. }
  128. if len(payload.Issues) != 0 {
  129. t.Fatalf("expected empty Issues for parse error; got %+v", payload.Issues)
  130. }
  131. if payload.Message == "" {
  132. t.Fatal("expected non-empty Message describing the parse error")
  133. }
  134. }
  135. func TestBindAndValidateInto_PreservesPrePopulatedFields(t *testing.T) {
  136. r := newRouter(func(c *gin.Context) {
  137. dst := &sampleBody{Tag: "preset"}
  138. if !BindAndValidateInto(c, dst) {
  139. t.Fatal("expected ok=true; got false")
  140. }
  141. if dst.Tag != "inbound-443" {
  142. t.Fatalf("expected payload Tag to overwrite preset; got %q", dst.Tag)
  143. }
  144. if dst.Port != 443 {
  145. t.Fatalf("expected Port=443; got %d", dst.Port)
  146. }
  147. })
  148. rec := httptest.NewRecorder()
  149. req := httptest.NewRequest(http.MethodPost, "/submit",
  150. strings.NewReader(`{"port":443,"protocol":"trojan","tag":"inbound-443"}`))
  151. req.Header.Set("Content-Type", "application/json")
  152. r.ServeHTTP(rec, req)
  153. if rec.Code != http.StatusOK {
  154. t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
  155. }
  156. }
  157. func TestBindJSONAndValidate_RejectsFormEncodedBody(t *testing.T) {
  158. r := newRouter(func(c *gin.Context) {
  159. if _, ok := BindJSONAndValidate[sampleBody](c); ok {
  160. t.Fatal("expected ok=false for form-encoded request to a JSON-only endpoint")
  161. }
  162. })
  163. rec := httptest.NewRecorder()
  164. req := httptest.NewRequest(http.MethodPost, "/submit",
  165. strings.NewReader("port=443&protocol=vless"))
  166. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  167. r.ServeHTTP(rec, req)
  168. if msg := decodeMsg(t, rec.Body.String()); msg.Success {
  169. t.Fatalf("expected Success=false; got %+v", msg)
  170. }
  171. }
  172. func payloadFromObj(obj any) (ValidationPayload, error) {
  173. raw, err := json.Marshal(obj)
  174. if err != nil {
  175. return ValidationPayload{}, err
  176. }
  177. var payload ValidationPayload
  178. if err := json.Unmarshal(raw, &payload); err != nil {
  179. return ValidationPayload{}, err
  180. }
  181. return payload, nil
  182. }