Files
junhong_cmp_fiber/pkg/logger/middleware_test.go
break ff44305d0e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m19s
实现审计覆盖门禁与外部集成日志闭环
2026-07-23 21:31:22 +09:00

243 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logger
import (
"bytes"
"io"
"net/http/httptest"
"strings"
"testing"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func TestSanitizeJSONRecursivelyMasksRequestAndResponseSecrets(t *testing.T) {
t.Parallel()
raw := []byte(`{"Password":"p@ss","items":[{"access_token":"token-value","count":3}],"profile":{"private_url":"https://secret.example/a"}}`)
result := sanitizeBody(raw, BodyPolicyJSON)
if strings.Contains(result.Content, "p@ss") || strings.Contains(result.Content, "token-value") || strings.Contains(result.Content, "secret.example") {
t.Fatalf("访问日志仍包含敏感值:%s", result.Content)
}
if !strings.Contains(result.Content, redactedValue) {
t.Fatalf("访问日志未输出统一脱敏占位:%s", result.Content)
}
if result.Truncated {
t.Fatal("短载荷不应标记为截断")
}
}
func TestFiberAccessLogSanitizesFinalRequestAndResponseJSON(t *testing.T) {
var output bytes.Buffer
encoderConfig := zap.NewProductionEncoderConfig()
log := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig), zapcore.AddSync(&output), zapcore.InfoLevel,
))
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("requestid", "request-test-1")
return c.Next()
})
app.Use(MiddlewareWithLogger(log))
app.Post("/tokens", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"access_token": "response-token", "data": fiber.Map{"ok": true}})
})
request := httptest.NewRequest("POST", "/tokens?Authorization=query-secret&page=1", strings.NewReader(`{"password":"request-secret"}`))
request.Header.Set("Content-Type", "application/json")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行 Fiber 请求失败:%v", err)
}
_, _ = io.Copy(io.Discard, response.Body)
_ = response.Body.Close()
logged := output.String()
for _, secret := range []string{"request-secret", "response-token", "query-secret"} {
if strings.Contains(logged, secret) {
t.Fatalf("最终 JSON 访问日志泄露测试凭证 %q%s", secret, logged)
}
}
for _, field := range []string{"request_id", "status", "duration_ms", "request_body_truncated", "response_body_truncated"} {
if !strings.Contains(logged, field) {
t.Fatalf("最终 JSON 访问日志缺少字段 %q%s", field, logged)
}
}
}
func TestSensitiveRouteMatrixNeverLogsRawPayload(t *testing.T) {
cases := []struct {
name string
path string
contentType string
body string
secret string
}{
{name: "登录JSON", path: "/api/auth/login", contentType: "application/json", body: `{"username":"safe-user","password":"json-secret"}`, secret: "json-secret"},
{name: "企微XML", path: "/api/callback/wecom", contentType: "application/xml", body: `<xml><Encrypt>xml-secret</Encrypt></xml>`, secret: "xml-secret"},
{name: "支付表单", path: "/api/callback/alipay", contentType: "application/x-www-form-urlencoded", body: "order_no=SAFE-1&sign=form-secret", secret: "form-secret"},
{name: "文件multipart", path: "/api/admin/storage/upload", contentType: "multipart/form-data; boundary=x", body: "--x\r\nContent-Disposition: form-data; name=\"file\"; filename=\"secret.bin\"\r\nContent-Type: application/octet-stream\r\n\r\nfile-bytes-secret\r\n--x--\r\n", secret: "file-bytes-secret"},
{name: "导出二进制", path: "/api/admin/export-tasks/1/download", contentType: "application/octet-stream", body: "binary-secret", secret: "binary-secret"},
{name: "支付不可解析", path: "/api/callback/wechat-pay", contentType: "application/json", body: `{"sign":"broken-secret"`, secret: "broken-secret"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var output bytes.Buffer
log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel))
app := fiber.New()
app.Use(MiddlewareWithLogger(log))
app.Post(tc.path, func(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).Type("json").SendString(`{"access_token":"response-secret","status":"ok"}`)
})
request := httptest.NewRequest("POST", tc.path+"?signature=query-secret", strings.NewReader(tc.body))
request.Header.Set("Content-Type", tc.contentType)
request.Header.Set("Authorization", "Bearer header-secret")
request.Header.Set("Cookie", "session=cookie-secret")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行敏感路由请求失败:%v", err)
}
_, _ = io.Copy(io.Discard, response.Body)
_ = response.Body.Close()
logged := output.String()
for _, secret := range []string{tc.secret, "query-secret", "response-secret", "header-secret", "cookie-secret"} {
if strings.Contains(logged, secret) {
t.Fatalf("敏感路由日志泄露 %q%s", secret, logged)
}
}
if !strings.Contains(logged, "body_policy") || !strings.Contains(logged, "sha256") {
t.Fatalf("敏感路由日志缺少策略或摘要:%s", logged)
}
})
}
}
func TestSensitiveRouteLongBodyRecordsTruncationWithoutRawSecret(t *testing.T) {
var output bytes.Buffer
log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel))
app := fiber.New()
app.Use(MiddlewareWithLogger(log))
app.Post("/api/admin/storage/upload", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) })
secret := strings.Repeat("long-file-secret", MaxBodyLogSize)
request := httptest.NewRequest("POST", "/api/admin/storage/upload", strings.NewReader(secret))
request.Header.Set("Content-Type", "application/octet-stream")
response, err := app.Test(request, 10000)
if err != nil {
t.Fatalf("执行超长敏感请求失败:%v", err)
}
_ = response.Body.Close()
logged := output.String()
if strings.Contains(logged, "long-file-secret") || !strings.Contains(logged, `"request_body_truncated":true`) {
t.Fatalf("超长敏感载荷未安全摘要或未标记截断:%s", logged)
}
}
func TestRealConfigurationRoutesOnlyLogSafeSummary(t *testing.T) {
for _, path := range []string{
"/api/admin/system-configs/payment.private_key",
"/api/admin/wechat-configs/1",
} {
t.Run(path, func(t *testing.T) {
var output bytes.Buffer
log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel))
app := fiber.New()
app.Use(MiddlewareWithLogger(log))
app.Put(path, func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"value": "response-private-material", "status": "ok"})
})
request := httptest.NewRequest("PUT", path, strings.NewReader(`{"value":"request-private-material","config_key":"payment.private_key"}`))
request.Header.Set("Content-Type", "application/json")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行配置路由请求失败:%v", err)
}
_ = response.Body.Close()
logged := output.String()
for _, secret := range []string{"request-private-material", "response-private-material"} {
if strings.Contains(logged, secret) {
t.Fatalf("配置路由日志泄露 %q%s", secret, logged)
}
}
if !strings.Contains(logged, `"body_policy":"sensitive_config"`) {
t.Fatalf("配置路由未应用安全摘要策略:%s", logged)
}
if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"length\":`) {
t.Fatalf("配置路由未记录字段存在性和长度:%s", logged)
}
})
}
}
func TestLoginRouteDoesNotLogUsernameAndOnlyKeepsPresenceLength(t *testing.T) {
var output bytes.Buffer
log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel))
app := fiber.New()
app.Use(MiddlewareWithLogger(log))
app.Post("/api/auth/login", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"username": "visible-user", "success": true})
})
request := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(`{"username":"visible-user","password":"login-secret"}`))
request.Header.Set("Content-Type", "application/json")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行登录请求失败:%v", err)
}
_ = response.Body.Close()
logged := output.String()
if strings.Contains(logged, "visible-user") || strings.Contains(logged, "login-secret") {
t.Fatalf("登录路由泄露账号或凭证:%s", logged)
}
if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"length\":`) {
t.Fatalf("登录路由未记录字段存在性和长度:%s", logged)
}
}
func TestPaymentRouteOnlyLogsPresenceLengthAndSafeResult(t *testing.T) {
var output bytes.Buffer
log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel))
app := fiber.New()
app.Use(MiddlewareWithLogger(log))
app.Post("/api/callback/alipay", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"payment_no": "PAY-SECRET-1", "amount": 12345, "status": "success"})
})
request := httptest.NewRequest("POST", "/api/callback/alipay", strings.NewReader("order_no=ORDER-SECRET-1&amount=12345&sign=signature-secret"))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行支付路由请求失败:%v", err)
}
_ = response.Body.Close()
logged := output.String()
for _, value := range []string{"PAY-SECRET-1", "ORDER-SECRET-1", "signature-secret"} {
if strings.Contains(logged, value) {
t.Fatalf("支付路由泄露原值 %q%s", value, logged)
}
}
if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"status\":\"success\"`) {
t.Fatalf("支付路由缺少存在性摘要或安全结果:%s", logged)
}
}
func TestSanitizeInvalidJSONNeverFallsBackToRawBody(t *testing.T) {
t.Parallel()
raw := []byte(`{"password":"should-never-appear"`)
result := sanitizeBody(raw, BodyPolicyJSON)
if strings.Contains(result.Content, "should-never-appear") {
t.Fatalf("解析失败时不得回退原文:%s", result.Content)
}
if result.SHA256 == "" || result.Size != len(raw) {
t.Fatalf("解析失败摘要缺少大小或哈希:%+v", result)
}
}
func TestSanitizeQueryUsesSameSensitiveFieldRules(t *testing.T) {
t.Parallel()
query := sanitizeQuery("page=1&Authorization=Bearer-secret&nonce=abc")
if strings.Contains(query.Content, "Bearer-secret") || strings.Contains(query.Content, "abc") {
t.Fatalf("query 敏感值未脱敏:%s", query.Content)
}
}