120 lines
2.9 KiB
Go
120 lines
2.9 KiB
Go
package orderstate
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
)
|
||
|
||
// 需要 PostgreSQL:TEST_DATABASE_URL=postgres://user:pass@localhost:5432/db?sslmode=disable go test ./...
|
||
func setup(t *testing.T) *pgxpool.Pool {
|
||
t.Helper()
|
||
url := os.Getenv("TEST_DATABASE_URL")
|
||
if url == "" {
|
||
t.Skip("未设置 TEST_DATABASE_URL,跳过集成测试")
|
||
}
|
||
ctx := context.Background()
|
||
schema := fmt.Sprintf("t_order_%d", time.Now().UnixNano())
|
||
admin, err := pgxpool.New(ctx, url)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := admin.Exec(ctx, "CREATE SCHEMA "+schema); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
cfg, _ := pgxpool.ParseConfig(url)
|
||
cfg.ConnConfig.RuntimeParams["search_path"] = schema // 每次测试用独立的 schema
|
||
db, err := pgxpool.NewWithConfig(ctx, cfg)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() {
|
||
db.Close()
|
||
_, _ = admin.Exec(ctx, "DROP SCHEMA "+schema+" CASCADE")
|
||
admin.Close()
|
||
})
|
||
if _, err := db.Exec(ctx, Schema); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return db
|
||
}
|
||
|
||
func newOrder(t *testing.T, db *pgxpool.Pool) Order {
|
||
t.Helper()
|
||
var id int64
|
||
if err := db.QueryRow(context.Background(), "INSERT INTO orders DEFAULT VALUES RETURNING id").Scan(&id); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
o, err := Get(context.Background(), db, id)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return o
|
||
}
|
||
|
||
func TestAllowed(t *testing.T) {
|
||
if !allowed("pending", "paid") || allowed("pending", "shipped") || allowed("completed", "paid") {
|
||
t.Fatal("状态流转表不对")
|
||
}
|
||
}
|
||
|
||
func TestTransitionAndAudit(t *testing.T) {
|
||
db := setup(t)
|
||
ctx := context.Background()
|
||
o := newOrder(t, db)
|
||
o, err := Transition(ctx, db, o, "paid", "payment-callback", "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := Transition(ctx, db, o, "completed", "admin", ""); !errors.Is(err, ErrIllegal) {
|
||
t.Fatalf("paid -> completed 应该不允许,得到 %v", err)
|
||
}
|
||
var n int
|
||
_ = db.QueryRow(ctx, "SELECT count(*) FROM order_events WHERE order_id = $1", o.ID).Scan(&n)
|
||
if n != 1 {
|
||
t.Fatalf("审计日志条数 = %d", n)
|
||
}
|
||
}
|
||
|
||
func TestConcurrentCancelAndPay(t *testing.T) {
|
||
db := setup(t)
|
||
ctx := context.Background()
|
||
o := newOrder(t, db) // 两个操作读到的是同一个版本
|
||
var wg sync.WaitGroup
|
||
errs := make([]error, 2)
|
||
for i, to := range []string{"paid", "cancelled"} {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
_, errs[i] = Transition(ctx, db, o, to, "test", "")
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
ok, conflict := 0, 0
|
||
for _, err := range errs {
|
||
switch {
|
||
case err == nil:
|
||
ok++
|
||
case errors.Is(err, ErrConflict):
|
||
conflict++
|
||
default:
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if ok != 1 || conflict != 1 {
|
||
t.Fatalf("应该一个成功一个冲突,得到 ok=%d conflict=%d", ok, conflict)
|
||
}
|
||
cur, _ := Get(ctx, db, o.ID)
|
||
var n int
|
||
_ = db.QueryRow(ctx, "SELECT count(*) FROM order_events WHERE order_id = $1", o.ID).Scan(&n)
|
||
if cur.Version != 2 || n != 1 {
|
||
t.Fatalf("版本 %d,审计 %d 条", cur.Version, n)
|
||
}
|
||
}
|