181 lines
9.6 KiB
Go
181 lines
9.6 KiB
Go
// 功能描述:购物车并发覆盖、数量边界、归档重试及账户隔离回归;版本:1.0.0。
|
||
package user
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||
"github.com/DATA-DOG/go-sqlmock"
|
||
)
|
||
|
||
func TestCartConditionalMutation(t *testing.T) {
|
||
stamp := time.Date(2026, 9, 8, 1, 2, 3, 123456000, time.UTC)
|
||
for _, tc := range []struct {
|
||
name string
|
||
quantity, current, status, stock, code int
|
||
selected bool
|
||
revision string
|
||
write bool
|
||
}{
|
||
{"相同目标重试不重复加量", 3, 3, 1, 10, 0, true, "old", false},
|
||
{"过期版本不可覆盖", 4, 3, 1, 10, 2403, true, "old", false},
|
||
{"增加超过库存被拒绝", 4, 3, 1, 3, 2402, true, "cart:2026-09-08T01:02:03.123456Z", false},
|
||
{"减少数量保留真实结果", 2, 3, 1, 10, 0, true, "cart:2026-09-08T01:02:03.123456Z", true},
|
||
{"取消勾选即使库存不足", 3, 3, 1, 0, 0, false, "cart:2026-09-08T01:02:03.123456Z", true},
|
||
{"删除归档不物理删除", 0, 3, 1, 0, 0, false, "cart:2026-09-08T01:02:03.123456Z", true},
|
||
{"重复删除无副作用", 0, 3, common.StatusArchived, 0, 0, false, "old", false},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
mock := primaryTestDB(t)
|
||
expectAddressAccount(mock)
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery(`SELECT .* FROM "user_account".*FOR UPDATE`).WithArgs(1, 1).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_product"`).WithArgs("product", 1).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "name", "status", "price_amount", "stock_quantity"}).AddRow(3, "product", "报警器", 1, 16900, tc.stock))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_cart".*user_account_id = \$1 AND ec_product_id = \$2`).WithArgs(1, 3).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "quantity", "selected", "updated_at"}).AddRow(4, "cart", tc.status, tc.current, true, stamp))
|
||
if tc.write {
|
||
mock.ExpectExec(`UPDATE "ec_cart" SET .*`).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
}
|
||
if tc.code == 0 {
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_product_image"`).WithArgs(3, 1).WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||
mock.ExpectCommit()
|
||
} else {
|
||
mock.ExpectRollback()
|
||
}
|
||
ctx, response := addressContext(fmt.Sprintf(`{"quantity":%d,"selected":%t,"revision":%q}`, tc.quantity, tc.selected, tc.revision), "product")
|
||
SetCartItem(ctx)
|
||
if !strings.Contains(response.Body.String(), fmt.Sprintf(`"code":%d`, tc.code)) {
|
||
t.Fatal(response.Body.String())
|
||
}
|
||
if tc.code == 0 && !strings.Contains(response.Body.String(), fmt.Sprintf(`"quantity":%d`, tc.quantity)) {
|
||
t.Fatal("数量返回错误", response.Body.String())
|
||
}
|
||
if strings.Contains(response.Body.String(), `"user_account_id"`) || strings.Contains(response.Body.String(), `"ec_product_id"`) {
|
||
t.Fatal("泄漏内部标识")
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestCartQuantityValidation(t *testing.T) {
|
||
for _, body := range []string{`{"quantity":-1,"selected":true}`, `{"quantity":1000,"selected":true}`, `{"quantity":1}`, `{"selected":true}`, `{"quantity":1.5,"selected":true}`} {
|
||
mock := primaryTestDB(t)
|
||
expectAddressAccount(mock)
|
||
ctx, response := addressContext(body, "product")
|
||
SetCartItem(ctx)
|
||
if !strings.Contains(response.Body.String(), `"code":1704`) {
|
||
t.Fatal(response.Body.String())
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 首次建行需要兼容 GORM 的 true 默认值,同时保留客户端明确取消勾选。
|
||
func TestCartFirstAddUnselected(t *testing.T) {
|
||
mock := primaryTestDB(t)
|
||
expectAddressAccount(mock)
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery(`SELECT .* FROM "user_account".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_product"`).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "stock_quantity"}).AddRow(3, "product", 1, 10))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_cart"`).WithArgs(1, 3).WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||
mock.ExpectQuery(`SELECT count\(\*\) FROM "ec_cart"`).WithArgs(1, common.StatusArchived).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||
mock.ExpectQuery(`INSERT INTO "ec_cart"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(4))
|
||
mock.ExpectExec(`UPDATE "ec_cart" SET .*`).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_product_image"`).WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||
mock.ExpectCommit()
|
||
ctx, response := addressContext(`{"quantity":2,"selected":false,"revision":""}`, "product")
|
||
SetCartItem(ctx)
|
||
if !strings.Contains(response.Body.String(), `"selected":false`) || !strings.Contains(response.Body.String(), `"quantity":2`) {
|
||
t.Fatal(response.Body.String())
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func TestCheckoutRejectsInvalidItemQuantityAndDuplicates(t *testing.T) {
|
||
for _, items := range []string{`[{"product_identity":"p1","quantity":-1}]`, `[{"product_identity":"p1","quantity":0}]`, `[{"product_identity":"p1","quantity":1000}]`, `[{"product_identity":"p1","quantity":1},{"product_identity":"p1","quantity":2}]`} {
|
||
mock := primaryTestDB(t)
|
||
expectAddressAccount(mock)
|
||
ctx, response := addressContext(fmt.Sprintf(`{"request_no":"request","address_identity":"a1","contact_name":"用户","contact_phone":"13800000001","items":%s}`, items), "")
|
||
CreateShopOrder(ctx)
|
||
if !strings.Contains(response.Body.String(), `"code":1704`) {
|
||
t.Fatal(response.Body.String())
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCartRevisionDatabasePrecision(t *testing.T) {
|
||
item := models.EcCart{Entity: models.Entity{ID: 1, Identity: "cart", UpdatedAt: time.Unix(10, 123456789)}}
|
||
before := cartRevision(item)
|
||
item.UpdatedAt = item.UpdatedAt.Truncate(time.Microsecond)
|
||
if before != cartRevision(item) {
|
||
t.Fatal("写入响应和数据库微秒时间戳必须产生相同版本")
|
||
}
|
||
}
|
||
|
||
func TestCartCheckoutRollbackOnStaleVersion(t *testing.T) {
|
||
mock := primaryTestDB(t)
|
||
expectAddressAccount(mock)
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_order"`).WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||
mock.ExpectQuery(`SELECT .* FROM "user_address"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(2))
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery(`SELECT .* FROM "user_account".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_product".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "price_amount", "stock_quantity"}).AddRow(3, "p1", 12800, 30))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_cart".*user_account_id = \$1 AND ec_product_id = \$2`).WithArgs(1, 3).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "quantity", "selected"}).AddRow(4, "cart", 1, 2, true))
|
||
mock.ExpectRollback()
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_order"`).WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||
ctx, response := addressContext(`{"request_no":"cart-order","address_identity":"owned","contact_name":"收货人","contact_phone":"13800000001","items":[{"product_identity":"p1","quantity":2,"cart_revision":"old"}]}`, "")
|
||
CreateShopOrder(ctx)
|
||
if !strings.Contains(response.Body.String(), `"code":2403`) {
|
||
t.Fatal(response.Body.String())
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func TestCartCheckoutTwoProductsAtomic(t *testing.T) {
|
||
mock := primaryTestDB(t)
|
||
expectAddressAccount(mock)
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_order"`).WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||
mock.ExpectQuery(`SELECT .* FROM "user_address"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(2))
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery(`SELECT .* FROM "user_account".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||
for i := 1; i <= 2; i++ {
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_product".*FOR UPDATE`).WithArgs(fmt.Sprintf("p%d", i), 1, i, 1).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "price_amount", "stock_quantity"}).AddRow(i, fmt.Sprintf("p%d", i), 10000, 30))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_cart"`).WithArgs(1, i).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "quantity", "selected"}).AddRow(i, fmt.Sprintf("c%d", i), 1, i, true))
|
||
mock.ExpectExec(`UPDATE "ec_cart" SET "status"`).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectExec(`UPDATE "ec_product" SET "stock_quantity"`).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectQuery(`SELECT .* FROM "ec_product_image"`).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "image_uri"}).AddRow(i, fmt.Sprintf("/p%d.png", i)))
|
||
}
|
||
mock.ExpectQuery(`INSERT INTO "ec_order"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(5))
|
||
for i := 1; i <= 2; i++ {
|
||
mock.ExpectQuery(`INSERT INTO "ec_order_item"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(i))
|
||
}
|
||
mock.ExpectCommit()
|
||
ctx, response := addressContext(`{"request_no":"cart-order","address_identity":"owned","contact_name":"收货人","contact_phone":"13800000001","expected_payable_amount":30000,"items":[{"product_identity":"p2","quantity":2,"cart_revision":"c2:0001-01-01T00:00:00Z"},{"product_identity":"p1","quantity":1,"cart_revision":"c1:0001-01-01T00:00:00Z"}]}`, "")
|
||
CreateShopOrder(ctx)
|
||
if !strings.Contains(response.Body.String(), `"payable_amount":30000`) {
|
||
t.Fatal(response.Body.String())
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|