181 lines
6.8 KiB
Go
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
|
|
}
|