147 lines
6.1 KiB
Go
147 lines
6.1 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"kra/internal/conf"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func TestAccessLogRejectsOversizedRequestBody(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
engine := gin.New()
|
|
called := false
|
|
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), 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(conf.NewRuntime(nil, &conf.AdminBackend{}), 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(conf.NewRuntime(nil, &conf.AdminBackend{}), 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 := conf.NewRuntime(nil, &conf.AdminBackend{Zap: &conf.AdminBackend_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 := conf.NewRuntime(nil, &conf.AdminBackend{Zap: &conf.AdminBackend_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 := conf.NewRuntime(nil, &conf.AdminBackend{})
|
|
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)
|
|
}
|
|
}
|