140 lines
4.6 KiB
Go
140 lines
4.6 KiB
Go
package store_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/wangjia/pay/internal/model"
|
|
"github.com/wangjia/pay/internal/store"
|
|
)
|
|
|
|
func TestWebhookOutboxEnqueueIdempotent(t *testing.T) {
|
|
ws := store.NewWebhookStore(model.OpenTestDB(t))
|
|
|
|
if err := ws.EnqueueDelivery("PAY-1", "pangolin", "payment.succeeded", "", `{"a":1}`); err != nil {
|
|
t.Fatalf("enqueue#1: %v", err)
|
|
}
|
|
// 幂等:同 (out_trade_no,event_type,refund_id) 再入队不新增行、不报错。
|
|
if err := ws.EnqueueDelivery("PAY-1", "pangolin", "payment.succeeded", "", `{"a":1}`); err != nil {
|
|
t.Fatalf("enqueue#2: %v", err)
|
|
}
|
|
list, _ := ws.ListUndelivered(10)
|
|
if len(list) != 1 {
|
|
t.Fatalf("应恰 1 行待投递, got %d", len(list))
|
|
}
|
|
|
|
if err := ws.MarkDelivered(list[0].ID); err != nil {
|
|
t.Fatalf("markDelivered: %v", err)
|
|
}
|
|
if again, _ := ws.ListUndelivered(10); len(again) != 0 {
|
|
t.Fatalf("投递后应 0 待投递, got %d", len(again))
|
|
}
|
|
}
|
|
|
|
func TestWebhookMarkFailed(t *testing.T) {
|
|
ws := store.NewWebhookStore(model.OpenTestDB(t))
|
|
_ = ws.EnqueueDelivery("PAY-2", "jiu", "payment.succeeded", "", `{}`)
|
|
list, _ := ws.ListUndelivered(10)
|
|
if err := ws.MarkFailed(list[0].ID, "boom"); err != nil {
|
|
t.Fatalf("markFailed: %v", err)
|
|
}
|
|
again, _ := ws.ListUndelivered(10)
|
|
if len(again) != 1 || again[0].Attempts != 1 || again[0].LastError != "boom" {
|
|
t.Fatalf("失败后应仍待投递且 attempts=1, got %+v", again)
|
|
}
|
|
}
|
|
|
|
func TestWebhookMarkFailedUTF8Truncation(t *testing.T) {
|
|
ws := store.NewWebhookStore(model.OpenTestDB(t))
|
|
_ = ws.EnqueueDelivery("PAY-3", "pangolin", "payment.succeeded", "", `{}`)
|
|
list, _ := ws.ListUndelivered(10)
|
|
|
|
// Create a long Chinese error message that exceeds 255 bytes
|
|
// Each Chinese character is typically 3 bytes in UTF-8
|
|
longChinese := "错误信息: " // "error message: " in Chinese (each char ~3 bytes)
|
|
for i := 0; i < 100; i++ {
|
|
longChinese += "中"
|
|
}
|
|
// longChinese is now > 300 bytes
|
|
|
|
if err := ws.MarkFailed(list[0].ID, longChinese); err != nil {
|
|
t.Fatalf("markFailed with long Chinese: %v", err)
|
|
}
|
|
|
|
again, _ := ws.ListUndelivered(10)
|
|
if len(again) != 1 {
|
|
t.Fatalf("expected 1 pending row, got %d", len(again))
|
|
}
|
|
|
|
stored := again[0].LastError
|
|
// Verify last_error is valid UTF-8
|
|
if !utf8.ValidString(stored) {
|
|
t.Fatalf("last_error is not valid UTF-8: %q", stored)
|
|
}
|
|
|
|
// Verify last_error is <= 255 bytes
|
|
if len(stored) > 255 {
|
|
t.Fatalf("last_error exceeds 255 bytes: %d", len(stored))
|
|
}
|
|
|
|
t.Logf("UTF-8 truncated error (%d bytes): %q", len(stored), stored)
|
|
}
|
|
|
|
func TestEnqueueDeliveryRefundIDUnique(t *testing.T) {
|
|
ws := store.NewWebhookStore(model.OpenTestDB(t))
|
|
// payment:refund_id="" —— 同单同事件第二次幂等丢弃
|
|
must := func(err error) {
|
|
if err != nil {
|
|
t.Fatalf("enqueue: %v", err)
|
|
}
|
|
}
|
|
must(ws.EnqueueDelivery("PAY-1", "pangolin", "payment.succeeded", "", `{"a":1}`))
|
|
must(ws.EnqueueDelivery("PAY-1", "pangolin", "payment.succeeded", "", `{"a":2}`)) // 幂等 no-op
|
|
// refund:同单同事件、不同 refund_id —— 各自成行(部分退多次)
|
|
must(ws.EnqueueDelivery("PAY-1", "pangolin", "refund.succeeded", "rf-A", `{"r":"A"}`))
|
|
must(ws.EnqueueDelivery("PAY-1", "pangolin", "refund.succeeded", "rf-B", `{"r":"B"}`))
|
|
must(ws.EnqueueDelivery("PAY-1", "pangolin", "refund.succeeded", "rf-A", `{"r":"A2"}`)) // 同 refund_id 幂等
|
|
|
|
rows, err := ws.ListUndelivered(50)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 期望 3 行:1 payment + 2 refund(rf-A / rf-B)
|
|
if len(rows) != 3 {
|
|
t.Fatalf("undelivered rows = %d want 3: %+v", len(rows), rows)
|
|
}
|
|
}
|
|
|
|
func TestWebhookScheduleRetryAndDead(t *testing.T) {
|
|
ws := store.NewWebhookStore(model.OpenTestDB(t))
|
|
now := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC)
|
|
_ = ws.EnqueueDelivery("PAY-1", "pangolin", "payment.succeeded", "", `{"x":1}`)
|
|
|
|
// 刚入队:next_attempt_at NULL → 立即可投。
|
|
rows, _ := ws.ListDeliverable(now, 10)
|
|
if len(rows) != 1 {
|
|
t.Fatalf("新单应可投, got %d", len(rows))
|
|
}
|
|
id := rows[0].ID
|
|
|
|
// 排下一次重试到 now+30s:此刻不可投,过点可投。
|
|
if err := ws.ScheduleRetry(id, "http 500", now.Add(30*time.Second)); err != nil {
|
|
t.Fatalf("schedule: %v", err)
|
|
}
|
|
if r, _ := ws.ListDeliverable(now, 10); len(r) != 0 {
|
|
t.Fatalf("退避窗内不应可投, got %d", len(r))
|
|
}
|
|
if r, _ := ws.ListDeliverable(now.Add(31*time.Second), 10); len(r) != 1 || r[0].Attempts != 1 {
|
|
t.Fatalf("过退避点应可投且 attempts=1, got %+v", r)
|
|
}
|
|
|
|
// 标死信:不再出现在可投集。
|
|
if err := ws.MarkDead(id, "gave up"); err != nil {
|
|
t.Fatalf("markdead: %v", err)
|
|
}
|
|
if r, _ := ws.ListDeliverable(now.Add(time.Hour), 10); len(r) != 0 {
|
|
t.Fatalf("死信不应可投, got %d", len(r))
|
|
}
|
|
}
|