httpx_test.go 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. package httpx
  2. import (
  3. "errors"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "testing"
  8. "github.com/gin-gonic/gin"
  9. "github.com/kairo/haven/internal/auth"
  10. )
  11. func contains(s, sub string) bool { return strings.Contains(s, sub) }
  12. func init() { gin.SetMode(gin.TestMode) }
  13. func run(fn func(c *gin.Context)) *httptest.ResponseRecorder {
  14. r := gin.New()
  15. r.GET("/x", fn)
  16. w := httptest.NewRecorder()
  17. r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/x", nil))
  18. return w
  19. }
  20. func TestFailMapping(t *testing.T) {
  21. cases := []struct {
  22. name string
  23. err error
  24. wantCode int
  25. wantBody string
  26. }{
  27. {name: "凭据错误", err: auth.ErrInvalidCredentials, wantCode: 401, wantBody: "unauthorized"},
  28. {name: "临时token过期", err: auth.ErrPendingExpired, wantCode: 401, wantBody: "unauthorized"},
  29. {name: "动态码错误", err: auth.ErrBadTotp, wantCode: 401, wantBody: "unauthorized"},
  30. {name: "TOTP已启用", err: auth.ErrTotpAlreadyEnabled, wantCode: 409, wantBody: "conflict"},
  31. {name: "TOTP未启用", err: auth.ErrTotpNotEnabled, wantCode: 422, wantBody: "validation_failed"},
  32. {name: "找不到", err: auth.ErrNotFound, wantCode: 404, wantBody: "not_found"},
  33. {name: "未知错误", err: errors.New("boom"), wantCode: 500, wantBody: "internal_error"},
  34. }
  35. for _, tc := range cases {
  36. t.Run(tc.name, func(t *testing.T) {
  37. w := run(func(c *gin.Context) { Fail(c, tc.err) })
  38. if w.Code != tc.wantCode {
  39. t.Errorf("状态码 = %d,期望 %d", w.Code, tc.wantCode)
  40. }
  41. if body := w.Body.String(); !contains(body, tc.wantBody) {
  42. t.Errorf("响应体应含 %q,实际 %s", tc.wantBody, body)
  43. }
  44. // 内部错误细节不能泄漏给客户端
  45. if tc.wantCode == 500 && contains(w.Body.String(), "boom") {
  46. t.Error("500 响应不应包含内部错误细节")
  47. }
  48. })
  49. }
  50. }
  51. func TestFailValidationFields(t *testing.T) {
  52. w := run(func(c *gin.Context) {
  53. FailValidation(c, map[string]string{"title": "不能为空"})
  54. })
  55. if w.Code != http.StatusUnprocessableEntity {
  56. t.Errorf("状态码 = %d", w.Code)
  57. }
  58. if body := w.Body.String(); !contains(body, `"fields":{"title":"不能为空"}`) {
  59. t.Errorf("应带 fields,实际 %s", body)
  60. }
  61. }
  62. func TestInstallAuthErrorHandlers(t *testing.T) {
  63. InstallAuthErrorHandlers()
  64. // 用 RequireLogin 触发 401,不需要数据库
  65. r := gin.New()
  66. r.Use(auth.Authenticate(nil), auth.RequireLogin())
  67. r.GET("/p", func(c *gin.Context) { c.Status(http.StatusOK) })
  68. w := httptest.NewRecorder()
  69. r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/p", nil))
  70. if w.Code != http.StatusUnauthorized {
  71. t.Fatalf("状态码 = %d", w.Code)
  72. }
  73. if body := w.Body.String(); !contains(body, `"error":"unauthorized"`) {
  74. t.Errorf("应走 httpx 的统一错误体,实际 %s", body)
  75. }
  76. }
苏ICP备2026082406号
苏公网安备 待审核