Files
platforms/backend/api/internal/logic/client/user/cart_test.go

181 lines
9.6 KiB
Go
Raw Normal View History

// 功能描述购物车并发覆盖、数量边界、归档重试及账户隔离回归版本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)
}
}