Files
NianAIGC/backend/internal/postgres/payments_integration_test.go
T
2026-10-12 13:57:23 +08:00

117 lines
4.6 KiB
Go

package postgres
import (
"context"
"errors"
"os"
"sync"
"testing"
"time"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/payments"
"github.com/jackc/pgx/v5/pgxpool"
)
// Run only against a disposable PostgreSQL instance. The test never reads the
// application's DATABASE_URL or local credentials.
func TestPaymentSettlementInDisposablePostgres(t *testing.T) {
databaseURL := os.Getenv("NIAN_TEST_PAYMENT_DATABASE_URL")
if databaseURL == "" {
t.Skip("set NIAN_TEST_PAYMENT_DATABASE_URL to a disposable PostgreSQL database")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
db := NewDatabase(Config{Backend: BackendPostgres}, &pgxPoolAdapter{pool: pool})
if err := db.Readiness(ctx); err != nil {
t.Fatalf("migrations/readiness: %v", err)
}
_, err = pool.Exec(ctx, `INSERT INTO public.platform_organizations(id, name, archive_owner_id) VALUES ('pay_test_org', 'payment test org', 'pay_test_archive');
INSERT INTO public.platform_users(id, phone, display_name, role, organization_id, status, password_hash, password_salt)
VALUES ('pay_test_admin', '13900000123', 'Payment Test', 'organization_admin', 'pay_test_org', 'active', 'hash', 'salt')`)
if err != nil {
t.Fatal(err)
}
makeOrder := func(id, requestKey string, amount int64) payments.Order {
return payments.Order{ID: id, OrganizationID: "pay_test_org", AccountID: "pay_test_admin",
RequestKey: requestKey, MchID: "test_mch", AppID: "test_app", AmountFen: amount,
Currency: "CNY", ExpiresAt: time.Now().Add(15 * time.Minute)}
}
first, err := db.CreatePaymentOrder(ctx, makeOrder("pay_test_order_1", "pay_test_request_1", 5200))
if err != nil || first.Status != payments.StatusPending {
t.Fatalf("create first: %+v %v", first, err)
}
replayed, err := db.CreatePaymentOrder(ctx, makeOrder("pay_test_order_new", "pay_test_request_1", 5200))
if err != nil || replayed.ID != first.ID {
t.Fatalf("idempotent create: %+v %v", replayed, err)
}
if _, err := db.CreatePaymentOrder(ctx, makeOrder("pay_test_order_new", "pay_test_request_1", 5201)); !errors.Is(err, payments.ErrConflict) {
t.Fatalf("changed request payload: %v", err)
}
txn := payments.Transaction{OrderID: first.ID, MchID: "test_mch", AppID: "test_app",
TransactionID: "wx_test_transaction_1", TradeState: "SUCCESS", Currency: "CNY",
AmountFen: 5200, SuccessTime: time.Now().UTC()}
var wg sync.WaitGroup
errorsOut := make(chan error, 12)
for i := 0; i < 12; i++ {
wg.Add(1)
go func() {
defer wg.Done()
order, err := db.SettlePaymentOrder(ctx, txn)
if err == nil && order.Status != payments.StatusPaid {
err = payments.ErrConflict
}
errorsOut <- err
}()
}
wg.Wait()
close(errorsOut)
for err := range errorsOut {
if err != nil {
t.Fatalf("concurrent settlement: %v", err)
}
}
var balance, recharged int64
if err := pool.QueryRow(ctx, `SELECT balance_fen, total_recharged_fen FROM public.billing_wallets WHERE organization_id = 'pay_test_org'`).Scan(&balance, &recharged); err != nil {
t.Fatal(err)
}
var ledgerCount int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM public.billing_ledger WHERE organization_id = 'pay_test_org' AND idempotency_key = 'wechat-native:test_mch:pay_test_order_1'`).Scan(&ledgerCount); err != nil {
t.Fatal(err)
}
if balance != 5200 || recharged != 5200 || ledgerCount != 1 {
t.Fatalf("after 12 deliveries: balance=%d recharged=%d ledger=%d", balance, recharged, ledgerCount)
}
second, err := db.CreatePaymentOrder(ctx, makeOrder("pay_test_order_2", "pay_test_request_2", 300))
if err != nil {
t.Fatal(err)
}
if _, err := db.UpdatePaymentOrder(ctx, second.ID, payments.StatusClosed, ""); err != nil {
t.Fatal(err)
}
collision := txn
collision.OrderID = second.ID
collision.AmountFen = 300
if _, err := db.SettlePaymentOrder(ctx, collision); !errors.Is(err, payments.ErrConflict) {
t.Fatalf("reused WeChat transaction: %v", err)
}
stillClosed, err := db.GetPaymentOrder(ctx, second.ID)
if err != nil || stillClosed.Status != payments.StatusClosed {
t.Fatalf("transaction collision changed order: %+v %v", stillClosed, err)
}
late := collision
late.TransactionID = "wx_test_transaction_2"
if _, err := db.SettlePaymentOrder(ctx, late); err != nil {
t.Fatalf("valid late success: %v", err)
}
if err := pool.QueryRow(ctx, `SELECT balance_fen, total_recharged_fen FROM public.billing_wallets WHERE organization_id = 'pay_test_org'`).Scan(&balance, &recharged); err != nil {
t.Fatal(err)
}
if balance != 5500 || recharged != 5500 {
t.Fatalf("after late success: balance=%d recharged=%d", balance, recharged)
}
}