118 lines
4.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|