hyapp-server/pkg/logx/logx_test.go
2026-06-23 11:53:00 +08:00

181 lines
6.8 KiB
Go

package logx
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
userv1 "hyapp.local/api/proto/user/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestNormalizeConfigRejectsBadLevel(t *testing.T) {
_, err := NormalizeConfig(Config{Level: "verbose"})
if !errors.Is(err, ErrUnsupportedLevel) {
t.Fatalf("NormalizeConfig error=%v, want ErrUnsupportedLevel", err)
}
}
func TestSafePayloadRedactsSensitiveFieldsAndOmitsLargePayload(t *testing.T) {
payload := safePayload([]byte(`{"username":"tom","password":"pw","nested":{"user_sig":"sig"}}`), 2048, false)
values, ok := payload.(map[string]any)
if !ok {
t.Fatalf("payload type=%T, want map", payload)
}
if values["password"] != redactedValue {
t.Fatalf("password was not redacted: %+v", values)
}
nested := values["nested"].(map[string]any)
if nested["user_sig"] != redactedValue {
t.Fatalf("nested user_sig was not redacted: %+v", nested)
}
large := safePayload([]byte(`{"token":"secret-token","data":"abcdef"}`), 8, true)
largeMap, ok := large.(map[string]any)
if !ok || largeMap["truncated"] != true {
t.Fatalf("large payload=%+v, want truncated marker", large)
}
if encoded := toJSON(t, large); bytes.Contains(encoded, []byte("secret-token")) {
t.Fatalf("truncated payload leaked sensitive value: %s", encoded)
}
}
func TestHTTPMiddlewareUsesConfiguredBodyLoggingAndWarnLevel(t *testing.T) {
var output bytes.Buffer
if err := Init(Config{Service: "gateway-service", Env: "test", Level: "debug", Format: "json", IncludeRequestBody: true, IncludeResponseBody: true, MaxPayloadBytes: 2048, Output: &output}); err != nil {
t.Fatalf("Init failed: %v", err)
}
handler := HTTPMiddleware("gateway-service", func(context.Context) string { return "req-http-1" }, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte(`{"code":"INVALID_ARGUMENT","token":"server-token"}`))
}))
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login?debug=1", bytes.NewBufferString(`{"username":"tom","password":"client-password"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Forwarded-For", "203.0.113.10, 10.0.0.1")
handler.ServeHTTP(httptest.NewRecorder(), req)
entry := decodeOne(t, output.Bytes())
if entry["level"] != "WARN" || entry["msg"] != "http_access" || entry["service"] != "gateway-service" {
t.Fatalf("unexpected http log entry: %+v", entry)
}
if entry["request_id"] != "req-http-1" || entry["path"] != "/api/v1/auth/login" || entry["client_ip"] != "203.0.113.10" {
t.Fatalf("missing access fields: %+v", entry)
}
request := entry["request"].(map[string]any)
if request["password"] != redactedValue {
t.Fatalf("request body was not redacted: %+v", request)
}
response := entry["response"].(map[string]any)
if response["token"] != redactedValue {
t.Fatalf("response body was not redacted: %+v", response)
}
}
func TestHTTPMiddlewareOmitsBodiesByDefault(t *testing.T) {
var output bytes.Buffer
if err := Init(Config{Service: "gateway-service", Env: "test", Format: "json", Output: &output}); err != nil {
t.Fatalf("Init failed: %v", err)
}
handler := HTTPMiddleware("gateway-service", func(context.Context) string { return "req-http-2" }, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte(`{"ok":true}`))
}))
handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/api/v1/ping", bytes.NewBufferString(`{"password":"client-password"}`)))
entry := decodeOne(t, output.Bytes())
if _, exists := entry["request"]; exists {
t.Fatalf("request body should be omitted by default: %+v", entry)
}
if _, exists := entry["response"]; exists {
t.Fatalf("response body should be omitted by default: %+v", entry)
}
}
func TestUnaryServerInterceptorLevelsAndRequestMeta(t *testing.T) {
var output bytes.Buffer
if err := Init(Config{Service: "user-service", Env: "test", Format: "json", IncludeRequestBody: true, Output: &output}); err != nil {
t.Fatalf("Init failed: %v", err)
}
interceptor := UnaryServerInterceptor("user-service")
request := &userv1.GetUserRequest{Meta: &userv1.RequestMeta{RequestId: "req-grpc-1", AppCode: "lalu"}, UserId: 1001}
_, err := interceptor(context.Background(), request, &grpc.UnaryServerInfo{FullMethod: "/hyapp.user.v1.UserService/GetUser"}, func(ctx context.Context, req any) (any, error) {
return nil, status.Error(codes.InvalidArgument, "bad user id")
})
if err == nil {
t.Fatal("interceptor should return handler error")
}
entry := decodeOne(t, output.Bytes())
if entry["level"] != "WARN" || entry["msg"] != "grpc_access" || entry["request_id"] != "req-grpc-1" || entry["app_code"] != "lalu" {
t.Fatalf("unexpected grpc log entry: %+v", entry)
}
if entry["grpc_code"] != "InvalidArgument" {
t.Fatalf("grpc_code=%v, want InvalidArgument", entry["grpc_code"])
}
if entry["reason"] != "INVALID_ARGUMENT" || entry["error_message"] != "bad user id" {
t.Fatalf("grpc error fields=%+v, want stable reason and service message", entry)
}
}
func TestUnaryServerInterceptorExtractsTopLevelTraceFieldsWithoutBody(t *testing.T) {
var output bytes.Buffer
if err := Init(Config{Service: "wallet-service", Env: "test", Format: "json", Output: &output}); err != nil {
t.Fatalf("Init failed: %v", err)
}
interceptor := UnaryServerInterceptor("wallet-service")
request := &walletv1.AdminCreditAssetRequest{
CommandId: "cmd-credit-1",
TargetUserId: 42001,
AssetType: "COIN_SELLER_COIN",
Amount: 80000,
AppCode: "lalu",
}
_, err := interceptor(context.Background(), request, &grpc.UnaryServerInfo{FullMethod: "/hyapp.wallet.v1.WalletService/AdminCreditAsset"}, func(ctx context.Context, req any) (any, error) {
return &walletv1.AdminCreditAssetResponse{}, nil
})
if err != nil {
t.Fatalf("interceptor returned error: %v", err)
}
entry := decodeOne(t, output.Bytes())
if entry["app_code"] != "lalu" || entry["command_id"] != "cmd-credit-1" || entry["asset_type"] != "COIN_SELLER_COIN" {
t.Fatalf("missing top-level trace fields: %+v", entry)
}
if _, exists := entry["request"]; exists {
t.Fatalf("request body should remain omitted: %+v", entry)
}
}
func decodeOne(t *testing.T, raw []byte) map[string]any {
t.Helper()
lines := bytes.Split(bytes.TrimSpace(raw), []byte("\n"))
if len(lines) != 1 || len(lines[0]) == 0 {
t.Fatalf("log lines=%q, want exactly one", raw)
}
var entry map[string]any
if err := json.Unmarshal(lines[0], &entry); err != nil {
t.Fatalf("decode log JSON failed: %v raw=%s", err, lines[0])
}
return entry
}
func toJSON(t *testing.T, value any) []byte {
t.Helper()
encoded, err := json.Marshal(value)
if err != nil {
t.Fatalf("json marshal failed: %v", err)
}
return encoded
}