package middleware import ( "bytes" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "kra/internal/config" "github.com/gin-gonic/gin" ) func TestAccessLogRejectsOversizedRequestBody(t *testing.T) { gin.SetMode(gin.TestMode) engine := gin.New() called := false engine.Use(AccessLog(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, "test")) engine.POST("/payload", func(c *gin.Context) { called = true }) request := httptest.NewRequest(http.MethodPost, "/payload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1)))) request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() engine.ServeHTTP(response, request) if response.Code != http.StatusRequestEntityTooLarge || called { t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String()) } } func TestAccessLogRejectsOversizedMultipartOnOrdinaryRoute(t *testing.T) { gin.SetMode(gin.TestMode) engine := gin.New() called := false engine.Use(AccessLog(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, "test")) engine.POST("/login", func(c *gin.Context) { called = true }) request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1)))) request.ContentLength = -1 request.Header.Set("Content-Type", "multipart/form-data; boundary=test") response := httptest.NewRecorder() engine.ServeHTTP(response, request) if response.Code != http.StatusRequestEntityTooLarge || called { t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String()) } } func TestAccessLogAllowsMediaLimitOnlyOnUploadRoute(t *testing.T) { gin.SetMode(gin.TestMode) engine := gin.New() called := false engine.Use(AccessLog(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, "test")) engine.POST("/api/fileUploadAndDownload/upload", func(c *gin.Context) { called = true _, _ = io.Copy(io.Discard, c.Request.Body) }) request := httptest.NewRequest(http.MethodPost, "/api/fileUploadAndDownload/upload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1)))) request.Header.Set("Content-Type", "multipart/form-data; boundary=test") response := httptest.NewRecorder() engine.ServeHTTP(response, request) if response.Code != http.StatusOK || !called { t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String()) } } func TestAccessLogRedactsPaymentCallbackPayloadAndHeaders(t *testing.T) { gin.SetMode(gin.TestMode) var logs bytes.Buffer logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo})) runtime := config.NewStore(&config.Config{Admin: &config.Admin{Zap: &config.Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}}}) engine := gin.New() engine.Use(AccessLog(runtime, logger, "test")) engine.POST("/api/payment/callback/:provider", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"body": "callback-response-secret"}) }) request := httptest.NewRequest(http.MethodPost, "/api/payment/callback/alipay?signature=query-secret", strings.NewReader("payment-body-secret")) request.Header.Set("Content-Type", "application/json; boundary=credential-secret") request.Header.Set("Authorization", "Bearer header-secret") request.Header.Set("X-Alipay-Signature", "signature-secret") response := httptest.NewRecorder() engine.ServeHTTP(response, request) logText := logs.String() for _, secret := range []string{"payment-body-secret", "query-secret", "header-secret", "signature-secret", "callback-response-secret", "credential-secret"} { if strings.Contains(logText, secret) { t.Fatalf("payment callback secret leaked into access log: %q in %s", secret, logText) } } for _, marker := range []string{"payment_callback=true", "payment_provider=alipay", "body_sha256=", "http_status=200"} { if !strings.Contains(logText, marker) { t.Fatalf("payment callback access summary missing %q: %s", marker, logText) } } } func TestAccessLogOmitsPaymentIntegrationConfigBody(t *testing.T) { gin.SetMode(gin.TestMode) var logs bytes.Buffer logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo})) runtime := config.NewStore(&config.Config{Admin: &config.Admin{Zap: &config.Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}}}) engine := gin.New() engine.Use(AccessLog(runtime, logger, "test")) engine.PUT("/api/integration/configs/:kind/:provider", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0}) }) body := `{"enabled":true,"config":{"key":"saobei-secret","certificate_blob":"certificate-secret","unknown_credential":"credential-secret"}}` request := httptest.NewRequest(http.MethodPut, "/api/integration/configs/payment/saobei", strings.NewReader(body)) request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() engine.ServeHTTP(response, request) logText := logs.String() for _, secret := range []string{"saobei-secret", "certificate-secret", "credential-secret"} { if strings.Contains(logText, secret) { t.Fatalf("payment configuration secret leaked into access log: %q in %s", secret, logText) } } if !strings.Contains(logText, "支付配置正文已省略") || !strings.Contains(logText, "body_sha256=") { t.Fatalf("payment configuration summary missing: %s", logText) } } func TestAccessLogRedactsWebSocketQueryToken(t *testing.T) { gin.SetMode(gin.TestMode) var logs bytes.Buffer logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo})) runtime := config.NewStore(&config.Config{Admin: &config.Admin{}}) engine := gin.New() engine.Use(AccessLog(runtime, logger, "test")) engine.GET("/ws", func(c *gin.Context) { c.Status(http.StatusSwitchingProtocols) }) request := httptest.NewRequest(http.MethodGet, "/ws?token=jwt-secret&room=orders", nil) response := httptest.NewRecorder() engine.ServeHTTP(response, request) logText := logs.String() if strings.Contains(logText, "jwt-secret") { t.Fatalf("websocket token leaked into access log: %s", logText) } if !strings.Contains(logText, "token=%2A%2A%2A") || !strings.Contains(logText, "room=orders") { t.Fatalf("redacted query missing expected fields: %s", logText) } }