270 lines
9.9 KiB
Go
270 lines
9.9 KiB
Go
package purchase
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go-admin/app/goauto/aimatching"
|
|
"go-admin/app/goauto/models"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type countingRetryMatcher struct{ calls int }
|
|
|
|
func (matcher *countingRetryMatcher) Resolve(context.Context, aimatching.MatchRequest) (aimatching.MatchResult, error) {
|
|
matcher.calls++
|
|
return aimatching.MatchResult{}, &aimatching.Error{Code: aimatching.CodeNoMatch, Message: "no match"}
|
|
}
|
|
|
|
func failedLiveTask(t *testing.T, db *gorm.DB, service *Service, f fixture) models.PurchaseTask {
|
|
t.Helper()
|
|
task, err := createLive(t, service, f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = task.SetStatus(models.PurchaseTaskStatusFailed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, message := "TEST_FAILURE", "测试失败"
|
|
task.ErrorCode, task.ErrorMessage = &code, &message
|
|
if err = db.Save(&task).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return task
|
|
}
|
|
|
|
func TestBatchRetryCreatesNewTaskPreservesFailureAndReplays(t *testing.T) {
|
|
db := testDB(t)
|
|
f := seed(t, db, liveCaps(), true)
|
|
setCollectedPDDPrice(t, db, f.pdd.ID)
|
|
service := testService(db)
|
|
failed := failedLiveTask(t, db, service, f)
|
|
if err := db.Model(&models.SYBProduct{}).Where("id = ?", f.syb.ID).Update("order_code", "SYB-CHANGED").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := BatchRetryRequest{RequestID: uuid.NewString(), TaskIDs: []uint64{failed.ID}}
|
|
|
|
first, err := service.BatchRetry(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.CreatedCount != 1 || first.FailedCount != 0 || len(first.Items) != 1 || !first.Items[0].Created || first.Items[0].TaskID == nil {
|
|
t.Fatalf("unexpected retry result: %+v", first)
|
|
}
|
|
if *first.Items[0].TaskID == failed.ID {
|
|
t.Fatalf("retry reused failed task: %+v", first.Items[0])
|
|
}
|
|
var oldTask, newTask models.PurchaseTask
|
|
if err = db.First(&oldTask, failed.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.First(&newTask, *first.Items[0].TaskID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if oldTask.Status != models.PurchaseTaskStatusFailed || newTask.Status != models.PurchaseTaskStatusPending || newTask.AddressSuffix == failed.AddressSuffix || newTask.DeviceID == nil || *newTask.DeviceID != f.device.ID {
|
|
t.Fatalf("retry facts mismatch: old=%+v new=%+v", oldTask, newTask)
|
|
}
|
|
if newTask.ShopeeOrderNoSnapshot != failed.ShopeeOrderNoSnapshot {
|
|
t.Fatalf("retry changed order snapshot: old=%q new=%q", failed.ShopeeOrderNoSnapshot, newTask.ShopeeOrderNoSnapshot)
|
|
}
|
|
|
|
replay, err := service.BatchRetry(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if replay.CreatedCount != 1 || !replay.Items[0].Replayed || replay.Items[0].TaskID == nil || *replay.Items[0].TaskID != newTask.ID {
|
|
t.Fatalf("retry replay was not stable: first=%+v replay=%+v", first, replay)
|
|
}
|
|
var count int64
|
|
if err = db.Model(&models.PurchaseTask{}).Where("syb_product_id = ?", f.syb.ID).Count(&count).Error; err != nil || count != 2 {
|
|
t.Fatalf("retry created duplicate tasks: count=%d err=%v", count, err)
|
|
}
|
|
}
|
|
|
|
func TestStockTaskIsExcludedFromBatchRetry(t *testing.T) {
|
|
db := testDB(t)
|
|
f := seed(t, db, liveCaps(), true)
|
|
setCollectedPDDPrice(t, db, f.pdd.ID)
|
|
service := testService(db)
|
|
task, _, err := service.CreateStock(context.Background(), StockCreateRequest{
|
|
RequestID: uuid.NewString(), ExecutionMode: models.PurchaseExecutionModeLive,
|
|
PDDProductID: f.pdd.ID, DeviceID: &f.device.ID, Color: "黑色", Size: "XL",
|
|
Quantity: 1, MinUnitPriceCent: 100, MaxUnitPriceCent: 3000,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = task.SetStatus(models.PurchaseTaskStatusFailed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Save(&task).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := service.BatchRetry(context.Background(), BatchRetryRequest{RequestID: uuid.NewString(), TaskIDs: []uint64{task.ID}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.CreatedCount != 0 || result.FailedCount != 1 || result.Items[0].ReasonCode != CodeRetryNotAllowed || !strings.Contains(result.Items[0].Reason, "重新创建备货采购") {
|
|
t.Fatalf("stock retry was not rejected clearly: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestBatchRetryKeepsPartialSuccessAndRejectsUnsafeTask(t *testing.T) {
|
|
db := testDB(t)
|
|
f := seed(t, db, liveCaps(), true)
|
|
setCollectedPDDPrice(t, db, f.pdd.ID)
|
|
secondSYB := f.syb
|
|
secondSYB.ID = 0
|
|
secondSYB.OrderCode = "SYB-2"
|
|
secondSYB.DetailID++
|
|
if err := db.Create(&secondSYB).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := testService(db)
|
|
firstFailed := failedLiveTask(t, db, service, f)
|
|
secondFixture := f
|
|
secondFixture.syb = secondSYB
|
|
unsafe := failedLiveTask(t, db, service, secondFixture)
|
|
irreversible := service.Now()
|
|
unsafe.IrreversibleAt = &irreversible
|
|
if err := db.Save(&unsafe).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := service.BatchRetry(context.Background(), BatchRetryRequest{
|
|
RequestID: uuid.NewString(), TaskIDs: []uint64{firstFailed.ID, unsafe.ID},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.CreatedCount != 1 || result.FailedCount != 1 || !result.Items[0].Created || result.Items[1].ReasonCode != CodeRetryUnsafe {
|
|
t.Fatalf("partial retry result mismatch: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestAdminListRetryEligibilityRejectsStaleAndOfflineFailures(t *testing.T) {
|
|
db := testDB(t)
|
|
f := seed(t, db, liveCaps(), true)
|
|
setCollectedPDDPrice(t, db, f.pdd.ID)
|
|
service := testService(db)
|
|
failed := failedLiveTask(t, db, service, f)
|
|
|
|
list, err := service.AdminList(context.Background(), AdminListRequest{Page: 1, PageSize: 20, TaskID: failed.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(list.Items) != 1 || !list.Items[0].Retryable || list.Items[0].RetryDisabledReason != "" {
|
|
t.Fatalf("safe failed task was not retryable: %+v", list.Items)
|
|
}
|
|
|
|
if err = db.Model(&models.AgentDevice{}).Where("id = ?", f.device.ID).Update("status", models.DeviceStatusOffline).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
list, err = service.AdminList(context.Background(), AdminListRequest{Page: 1, PageSize: 20, TaskID: failed.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if list.Items[0].Retryable || !strings.Contains(list.Items[0].RetryDisabledReason, "原设备") {
|
|
t.Fatalf("offline failed task was selectable: %+v", list.Items[0])
|
|
}
|
|
if err = db.Model(&models.AgentDevice{}).Where("id = ?", f.device.ID).Update("status", models.DeviceStatusOnline).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
created, err := service.BatchRetry(context.Background(), BatchRetryRequest{RequestID: uuid.NewString(), TaskIDs: []uint64{failed.ID}})
|
|
if err != nil || created.CreatedCount != 1 {
|
|
t.Fatalf("create newer task: %+v %v", created, err)
|
|
}
|
|
list, err = service.AdminList(context.Background(), AdminListRequest{Page: 1, PageSize: 20, TaskID: failed.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if list.Items[0].Retryable || list.Items[0].RetryDisabledCode != CodeRetryStale || !strings.Contains(list.Items[0].RetryDisabledReason, "更新任务") {
|
|
t.Fatalf("stale failed task was selectable: %+v", list.Items[0])
|
|
}
|
|
}
|
|
|
|
func TestAdminListRetryEligibilityRejectsBusyOriginalDevice(t *testing.T) {
|
|
db := testDB(t)
|
|
f := seed(t, db, liveCaps(), true)
|
|
setCollectedPDDPrice(t, db, f.pdd.ID)
|
|
service := testService(db)
|
|
failed := failedLiveTask(t, db, service, f)
|
|
|
|
otherSYB := f.syb
|
|
otherSYB.ID = 0
|
|
otherSYB.OrderCode = "SYB-BUSY"
|
|
otherSYB.DetailID++
|
|
if err := db.Create(&otherSYB).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
otherFixture := f
|
|
otherFixture.syb = otherSYB
|
|
busyTask, err := createLive(t, service, otherFixture)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = service.Claim(context.Background(), busyTask.ID, ActionRequest{RequestID: uuid.NewString()}, f.token); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = service.Start(context.Background(), busyTask.ID, ActionRequest{RequestID: uuid.NewString()}, f.token); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
list, err := service.AdminList(context.Background(), AdminListRequest{Page: 1, PageSize: 20, TaskID: failed.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(list.Items) != 1 || list.Items[0].Retryable || !strings.Contains(list.Items[0].RetryDisabledReason, "原设备已有其他任务") {
|
|
t.Fatalf("busy device failure was selectable: %+v", list.Items)
|
|
}
|
|
}
|
|
|
|
func TestAdminQueriesAndBatchRetryDoNotResolveArchivedSpecs(t *testing.T) {
|
|
db := testDB(t)
|
|
f := seed(t, db, liveCaps(), false)
|
|
service := testService(db)
|
|
failed := failedLiveTask(t, db, service, f)
|
|
if err := db.Model(&models.PDDProduct{}).Where("id = ?", f.pdd.ID).Update("specs_json", `[{"name":"颜色","role":"color","values":[{"name":"白色","selectable":true,"priceCent":2000}]},{"name":"尺码","role":"size","values":[{"name":"L","selectable":true}]}]`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
matcher := &countingRetryMatcher{}
|
|
service.Matcher = matcher
|
|
|
|
list, err := service.AdminList(context.Background(), AdminListRequest{Page: 1, PageSize: 20, TaskID: failed.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(list.Items) != 1 || !list.Items[0].Retryable || matcher.calls != 0 {
|
|
t.Fatalf("admin list resolved specs: items=%+v matcherCalls=%d", list.Items, matcher.calls)
|
|
}
|
|
detail, err := service.AdminDetail(context.Background(), failed.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !detail.Task.Retryable || matcher.calls != 0 {
|
|
t.Fatalf("admin detail resolved specs: task=%+v matcherCalls=%d", detail.Task, matcher.calls)
|
|
}
|
|
|
|
retry, err := service.BatchRetry(context.Background(), BatchRetryRequest{RequestID: uuid.NewString(), TaskIDs: []uint64{failed.ID}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if matcher.calls != 0 || retry.CreatedCount != 1 || retry.FailedCount != 0 {
|
|
t.Fatalf("batch retry did not create a mandatory probe task: result=%+v matcherCalls=%d", retry, matcher.calls)
|
|
}
|
|
}
|
|
|
|
func TestFailedTaskNoLongerNeedsRepurchaseAuthorization(t *testing.T) {
|
|
db := testDB(t)
|
|
f := seed(t, db, liveCaps(), true)
|
|
task := failedLiveTask(t, db, testService(db), f)
|
|
_, _, err := testService(db).AuthorizeRePurchase(context.Background(), task.ID, ManualRequest{RequestID: uuid.NewString(), OperatorID: 1})
|
|
if code(err) != CodeStateConflict {
|
|
t.Fatalf("failed task accepted redundant authorization: %v", err)
|
|
}
|
|
}
|