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) } }