intelligent_test_handler_test.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. package admin
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. "github.com/stretchr/testify/require"
  13. )
  14. type intelligentHandlerRepo struct {
  15. service.IntelligentTestRepository
  16. admin bool
  17. record *service.IntelligentTestRecord
  18. calls int
  19. }
  20. func (r *intelligentHandlerRepo) IsAdmin(context.Context, int64) (bool, error) { return r.admin, nil }
  21. func (r *intelligentHandlerRepo) Get(context.Context, int64) (*service.IntelligentTestRecord, error) {
  22. r.calls++
  23. return r.record, nil
  24. }
  25. func (r *intelligentHandlerRepo) Accounts(context.Context, service.IntelligentTestFilter) (*service.IntelligentTestAccounts, error) {
  26. r.calls++
  27. return &service.IntelligentTestAccounts{}, nil
  28. }
  29. func intelligentHandlerEngine(repo *intelligentHandlerRepo, role string) *gin.Engine {
  30. gin.SetMode(gin.TestMode)
  31. engine := gin.New()
  32. h := NewIntelligentTestHandler(service.NewIntelligentTestService(repo, nil))
  33. engine.Use(func(c *gin.Context) {
  34. if role != "" {
  35. c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{UserID: 1})
  36. c.Set(string(middleware.ContextKeyUserRole), role)
  37. }
  38. })
  39. engine.GET("/accounts", h.Accounts)
  40. engine.GET("/records/:id", h.Get)
  41. engine.GET("/records/:id/image", h.Image)
  42. engine.POST("/run", h.Run)
  43. engine.POST("/evaluate-preview", h.PreviewEvaluation)
  44. engine.POST("/records/:id/cancel", h.Cancel)
  45. engine.POST("/records/:id/reevaluate", h.Reevaluate)
  46. return engine
  47. }
  48. func TestIntelligentHandlerAdminAuthorization(t *testing.T) {
  49. for _, tc := range []struct {
  50. role string
  51. dbAdmin bool
  52. want int
  53. }{{"", false, 401}, {"user", false, 403}, {"admin", false, 403}, {"admin", true, 200}, {"super_admin", true, 200}} {
  54. t.Run(tc.role, func(t *testing.T) {
  55. repo := &intelligentHandlerRepo{admin: tc.dbAdmin}
  56. engine := intelligentHandlerEngine(repo, tc.role)
  57. w := httptest.NewRecorder()
  58. engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/accounts", nil))
  59. require.Equal(t, tc.want, w.Code)
  60. if tc.want != 200 {
  61. require.Zero(t, repo.calls)
  62. }
  63. })
  64. }
  65. }
  66. func TestIntelligentHandlerRejectsMalformedInputs(t *testing.T) {
  67. repo := &intelligentHandlerRepo{admin: true}
  68. engine := intelligentHandlerEngine(repo, "admin")
  69. for _, path := range []string{"/records/-1", "/accounts?anti_degradation=maybe", "/accounts?from=tomorrow", "/accounts?page=0", "/accounts?group_id=-1"} {
  70. w := httptest.NewRecorder()
  71. engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
  72. require.Equal(t, 400, w.Code, path)
  73. }
  74. w := httptest.NewRecorder()
  75. engine.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/run", strings.NewReader(`{"account_ids":[1,1],"test_types":["candy"],"idempotency_key":"repeat-operation-key"}`)))
  76. require.Equal(t, 400, w.Code)
  77. require.Zero(t, repo.calls)
  78. }
  79. func TestIntelligentHandlerImageSandboxAndValidation(t *testing.T) {
  80. repo := &intelligentHandlerRepo{admin: true, record: &service.IntelligentTestRecord{ResultImage: `<svg viewBox="0 0 10 10"><circle r="2"/></svg>`}}
  81. engine := intelligentHandlerEngine(repo, "admin")
  82. w := httptest.NewRecorder()
  83. engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/records/1/image", nil))
  84. require.Equal(t, 200, w.Code)
  85. require.Contains(t, w.Header().Get("Content-Type"), "image/svg+xml")
  86. require.Contains(t, w.Header().Get("Content-Security-Policy"), "sandbox")
  87. require.Contains(t, w.Header().Get("Cache-Control"), "no-store")
  88. repo.record.ResultImage = `<svg onload="alert(1)"><circle r="2"/></svg>`
  89. w = httptest.NewRecorder()
  90. engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/records/1/image", nil))
  91. require.Equal(t, 404, w.Code)
  92. require.NotContains(t, w.Body.String(), "onload")
  93. }
  94. func TestIntelligentHandlerHistoricalImageAndNewActionsAuthorization(t *testing.T) {
  95. repo := &intelligentHandlerRepo{admin: true, record: &service.IntelligentTestRecord{Result: `<svg viewBox="0 0 100 100"><style>.bird{fill:orange}</style><circle class="bird" cx="50" cy="50" r="20"/><script>alert(1)</script></svg>`}}
  96. engine := intelligentHandlerEngine(repo, "admin")
  97. w := httptest.NewRecorder()
  98. engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/records/1/image", nil))
  99. require.Equal(t, 200, w.Code)
  100. require.Contains(t, w.Body.String(), `fill="orange"`)
  101. require.NotContains(t, w.Body.String(), "script")
  102. require.NotContains(t, w.Body.String(), "<style")
  103. require.Equal(t, "nosniff", w.Header().Get("X-Content-Type-Options"))
  104. for _, role := range []string{"", "user", "admin"} {
  105. forbidden := &intelligentHandlerRepo{admin: false}
  106. restricted := intelligentHandlerEngine(forbidden, role)
  107. for _, path := range []string{"/evaluate-preview", "/records/1/cancel", "/records/1/reevaluate"} {
  108. w := httptest.NewRecorder()
  109. restricted.ServeHTTP(w, httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`)))
  110. require.Contains(t, []int{401, 403}, w.Code)
  111. require.Zero(t, forbidden.calls)
  112. }
  113. }
  114. }
  115. func TestIntelligentHandlerDraftPreviewSeparatesVerdictsWithoutRunner(t *testing.T) {
  116. repo := &intelligentHandlerRepo{admin: true}
  117. engine := intelligentHandlerEngine(repo, "admin") // nil runner: local rule only
  118. body, err := json.Marshal(map[string]any{"output": "**ANSWER: 12颗**", "config": service.IntelligentTestConfig{Prompt: strings.Repeat("中", 6000), Evaluator: "exact_answer", ExpectedAnswer: "12", AnswerType: "number", AnswerUnit: "颗", TimeoutSeconds: 120}})
  119. require.NoError(t, err)
  120. w := httptest.NewRecorder()
  121. req := httptest.NewRequest(http.MethodPost, "/evaluate-preview", strings.NewReader(string(body)))
  122. req.Header.Set("Content-Type", "application/json")
  123. engine.ServeHTTP(w, req)
  124. require.Equal(t, 200, w.Code, w.Body.String())
  125. require.Contains(t, w.Body.String(), `"answer_verdict":"correct"`)
  126. require.Contains(t, w.Body.String(), `"format_verdict":"non_compliant"`)
  127. require.Contains(t, w.Body.String(), `"status":"completed"`)
  128. require.Zero(t, repo.calls)
  129. }