package payment import ( "bytes" "context" "encoding/json" "errors" "log/slog" "strings" "testing" ) func TestPaymentLifecycleLogsDoNotIncludeProviderErrorsOrPayloads(t *testing.T) { var output bytes.Buffer logger := slog.New(slog.NewJSONHandler(&output, nil)) repo := &paymentRepoStub{createErr: errors.New("provider response: secret-token"), callbackErr: errors.New("callback body: secret-token")} orders := &paymentOrderRepoStub{} sources := NewPaymentOrderSourceRegistry() if err := sources.Register(&paymentSourceStub{intent: &PaymentIntent{ Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "test item", Amount: 100, Currency: "CNY", }}); err != nil { t.Fatal(err) } uc := NewPaymentUsecase(repo, orders, nil, sources, nil, logger) _, _ = uc.Create(context.Background(), &PaymentRequest{ Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Extra: map[string]any{"api_key": "secret-token"}, }) _, _ = uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("secret-token")}) text := output.String() if strings.Contains(text, "secret-token") { t.Fatal("provider error or request payload leaked to payment log") } for _, stage := range []string{ "payment.create.started", "payment.order.prepared", "payment.order.persisted", "payment.provider.create.started", "payment.create.failed", "payment.callback.received", "payment.callback.verify_failed", "payment.callback.failed", } { if !strings.Contains(text, stage) { t.Errorf("missing lifecycle stage %s", stage) } } for _, line := range strings.Split(strings.TrimSpace(text), "\n") { var entry map[string]any if err := json.Unmarshal([]byte(line), &entry); err != nil { t.Fatalf("invalid structured log %q: %v", line, err) } if entry["mod"] != "payment" { t.Fatalf("payment log module = %v", entry["mod"]) } if entry["msg"] == "payment.create.failed" && entry["duration_ms"] == nil { t.Fatal("create failure did not record duration") } } } func TestPaymentLogErrorUsesStableReasonWithoutOriginalMessage(t *testing.T) { var output bytes.Buffer logger := NewPaymentLogger(slog.New(slog.NewJSONHandler(&output, nil))) logger.Error(context.Background(), "payment.test.failed", "error", PaymentRejected(errors.New("secret-token"))) if strings.Contains(output.String(), "secret-token") || !strings.Contains(output.String(), `"error_code":"provider_rejected"`) { t.Fatalf("unsafe or missing error code in %s", output.String()) } if got := paymentErrorCode(context.DeadlineExceeded); got != "timeout" { t.Fatalf("deadline error code = %q, want timeout", got) } } func TestPaymentLogCorrelatesQueryFulfillmentAndRefund(t *testing.T) { var output bytes.Buffer logger := slog.New(slog.NewJSONHandler(&output, nil)) pending := successPaymentResult() pending.Status = "pending" repo := &paymentRepoStub{query: pending, refund: &PaymentResult{ Provider: PaymentAlipay, TradeNo: "order-1", Status: "pending", Amount: 40, Currency: "CNY", }} orders := &paymentOrderRepoStub{order: testBizPaymentOrder()} registry := NewPaymentFulfillmentRegistry() if err := registry.Register(&paymentFulfillmentStub{}); err != nil { t.Fatal(err) } uc := NewPaymentUsecase(repo, orders, nil, nil, registry, logger) if _, err := uc.Query(context.Background(), PaymentAlipay, "order-1"); err != nil { t.Fatal(err) } orders.order.PaymentStatus = PaymentStatusPaid if _, err := uc.Fulfill(context.Background(), PaymentAlipay, "order-1"); err != nil { t.Fatal(err) } if _, err := uc.Refund(context.Background(), PaymentAlipay, "order-1", 40); err != nil { t.Fatal(err) } if _, err := uc.ConfirmRefund(context.Background(), PaymentAlipay, "order-1", "refund-1", 40, true, "confirmed"); err != nil { t.Fatal(err) } for _, stage := range []string{ "payment.provider.query.checked", "payment.order.reconciled", "payment.query.completed", "payment.fulfillment.started", "payment.fulfillment.completed", "payment.refund.reserved", "payment.provider.refund.checked", "payment.refund.completed", "payment.refund_confirmation.completed", } { if !strings.Contains(output.String(), stage) { t.Errorf("missing lifecycle stage %s", stage) } } for _, line := range strings.Split(strings.TrimSpace(output.String()), "\n") { var entry map[string]any if err := json.Unmarshal([]byte(line), &entry); err != nil { t.Fatal(err) } if entry["trade_no"] != "order-1" { t.Fatalf("uncorrelated payment log: %s", line) } } }