Files
kra-oa/internal/biz/payment/payment_log_test.go
T
2026-09-24 17:44:17 +08:00

118 lines
4.5 KiB
Go

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)
}
}
}