Files
pay/internal/store/order_test.go
T
wangjia af15f4736e fix(v2): MarkAttemptPaid 按单scope+校验attempt命中 + OpenTestDB 唯一库名
- attempt UPDATE 加 out_trade_no 过滤并检查 RowsAffected,防止 order 已翻
  paid 但 providerRef 错配导致没有对应 attempt 被标记的账本背离(未命中
  时返回 error 触发事务回滚,order 保持 pending)
- 补测试覆盖 providerRef 错配场景:断言返回 error 且回滚后订单仍 pending
- OpenTestDB 改为每次调用生成唯一内存库名(atomic 计数器),避免同包多
  测试共享 file::memory:?cache=shared 导致数据串扰

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:35:22 +08:00

86 lines
2.4 KiB
Go

package store_test
import (
"testing"
"time"
"github.com/wangjia/pay/internal/model"
"github.com/wangjia/pay/internal/store"
)
func seedOrder(t *testing.T, s *store.OrderStore, no string) {
t.Helper()
if err := s.CreateOrder(&model.OrderV2{
OutTradeNo: no, BizSystem: "pangolin", BizRef: "u-1", ProductID: 3,
AmountMinor: 29990000, Currency: "USDT", Status: model.OrderPendingV2,
}); err != nil {
t.Fatalf("create order: %v", err)
}
if err := s.CreateAttempt(&model.Attempt{
OutTradeNo: no, Channel: "crypto", AccountID: "usdt-a1", ProviderRef: "P-" + no,
RenderType: "crypto_address", AmountMinor: 29990000, Currency: "USDT",
Status: model.AttemptPending, ExpiresAt: ptr(time.Now().Add(15 * time.Minute)),
}); err != nil {
t.Fatalf("create attempt: %v", err)
}
}
func ptr(t time.Time) *time.Time { return &t }
func TestMarkPaidIdempotentAndCancel(t *testing.T) {
s := store.NewOrderStore(model.OpenTestDB(t))
seedOrder(t, s, "PAY-1")
ok, err := s.MarkAttemptPaid("PAY-1", "crypto", "P-PAY-1", time.Now())
if err != nil || !ok {
t.Fatalf("mark#1 ok=%v err=%v", ok, err)
}
// 幂等:再标 → false
ok2, _ := s.MarkAttemptPaid("PAY-1", "crypto", "P-PAY-1", time.Now())
if ok2 {
t.Fatalf("第二次 MarkAttemptPaid 应 false")
}
// paid 后不能取消
if okc, _ := s.CancelOrder("PAY-1"); okc {
t.Fatalf("paid 单不应可取消")
}
// 另一单可取消,且列表可见
seedOrder(t, s, "PAY-2")
if okc, err := s.CancelOrder("PAY-2"); err != nil || !okc {
t.Fatalf("cancel PAY-2 ok=%v err=%v", okc, err)
}
list, _ := s.ListOrders("pangolin", "u-1", 20)
var sawCanceled bool
for _, o := range list {
if o.OutTradeNo == "PAY-2" && o.Status == model.OrderCanceledV2 {
sawCanceled = true
}
}
if !sawCanceled {
t.Fatalf("canceled 单应出现在列表")
}
}
func TestMarkAttemptPaidProviderRefMismatchRollsBack(t *testing.T) {
db := model.OpenTestDB(t)
s := store.NewOrderStore(db)
seedOrder(t, s, "PAY-X")
ok, err := s.MarkAttemptPaid("PAY-X", "crypto", "P-WRONG", time.Now())
if err == nil {
t.Fatalf("providerRef 错配应返回 error, got ok=%v err=%v", ok, err)
}
if ok {
t.Fatalf("providerRef 错配不应返回 ok=true")
}
var o model.OrderV2
if err := db.Where("out_trade_no = ?", "PAY-X").First(&o).Error; err != nil {
t.Fatalf("query order: %v", err)
}
if o.Status != model.OrderPendingV2 {
t.Fatalf("事务应回滚, order status 应仍为 pending, got %v", o.Status)
}
}