425 lines
17 KiB
Go
425 lines
17 KiB
Go
package tasks
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"database/sql"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"math"
|
||
|
|
"reflect"
|
||
|
|
"sort"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"cmbuyer/admin/internal/migrations"
|
||
|
|
)
|
||
|
|
|
||
|
|
var fixedStartTime = time.Date(2026, 8, 4, 9, 2, 3, 456000000, time.FixedZone("UTC+8", 8*60*60))
|
||
|
|
|
||
|
|
func TestStartPurchasesPersistsCompleteSnapshotsForOneAndHundredTasks(t *testing.T) {
|
||
|
|
for _, count := range []int{1, 100} {
|
||
|
|
t.Run(fmt.Sprintf("%d tasks", count), func(t *testing.T) {
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
store.now = func() time.Time { return fixedStartTime }
|
||
|
|
items := make([]StartItem, 0, count)
|
||
|
|
wantDrafts := make(map[string]Draft, count)
|
||
|
|
for index := 1; index <= count; index++ {
|
||
|
|
id := startTestUUID(index)
|
||
|
|
draft := Draft{
|
||
|
|
ID: id,
|
||
|
|
Title: fmt.Sprintf("task-%03d", index),
|
||
|
|
GoodsID: fmt.Sprintf("937122%06d", index),
|
||
|
|
SKUColor: fmt.Sprintf("color-%03d", index),
|
||
|
|
SKUSize: fmt.Sprintf("size-%03d", index),
|
||
|
|
Quantity: index%10 + 1,
|
||
|
|
MaxTotalPrice: fmt.Sprintf("%d.%02d", index+10, index%100),
|
||
|
|
}
|
||
|
|
if _, err := store.CreateDraft(context.Background(), draft); err != nil {
|
||
|
|
t.Fatalf("create draft %d: %v", index, err)
|
||
|
|
}
|
||
|
|
items = append(items, StartItem{TaskID: id, ExpectedTaskVersion: 1})
|
||
|
|
wantDrafts[id] = draft
|
||
|
|
}
|
||
|
|
sort.Slice(items, func(i, j int) bool { return items[i].TaskID > items[j].TaskID })
|
||
|
|
command := StartCommand{StartKey: startTestUUID(1001 + count), Tasks: items}
|
||
|
|
|
||
|
|
result, err := store.StartPurchases(context.Background(), command, "authenticated-admin")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("StartPurchases: %v", err)
|
||
|
|
}
|
||
|
|
if result.StartKey != command.StartKey || result.AuthorizedCount != count || result.PaymentAutomated || len(result.Tasks) != count {
|
||
|
|
t.Fatalf("result = %#v", result)
|
||
|
|
}
|
||
|
|
wantCreated := fixedStartTime.UTC()
|
||
|
|
wantExpires := wantCreated.Add(15 * time.Minute)
|
||
|
|
seenAuthorizationIDs := map[string]bool{}
|
||
|
|
for index, authorized := range result.Tasks {
|
||
|
|
if index > 0 && result.Tasks[index-1].TaskID >= authorized.TaskID {
|
||
|
|
t.Fatalf("result is not in canonical task order: %#v", result.Tasks)
|
||
|
|
}
|
||
|
|
if authorized.TaskVersion != 2 || !authorized.ExpiresAt.Equal(wantExpires) || !validUUID(authorized.AuthorizationID) || seenAuthorizationIDs[authorized.AuthorizationID] {
|
||
|
|
t.Fatalf("authorized task = %#v", authorized)
|
||
|
|
}
|
||
|
|
seenAuthorizationIDs[authorized.AuthorizationID] = true
|
||
|
|
want := wantDrafts[authorized.TaskID]
|
||
|
|
var taskStatus, taskUpdated, authTaskID, authStartKey, goodsID, color, size, priceCap, authStatus, createdBy, createdAt, expiresAt string
|
||
|
|
var taskVersion, authTaskVersion, quantity int
|
||
|
|
err := database.QueryRow(`
|
||
|
|
SELECT t.status,t.version,t.updated_at,
|
||
|
|
a.task_id,a.task_version,a.start_key,a.goods_id,a.sku_color,a.sku_size,a.quantity,a.total_price_cap,a.status,a.created_by,a.created_at,a.expires_at
|
||
|
|
FROM tasks t JOIN order_authorizations a ON a.task_id=t.id WHERE a.id=?`, authorized.AuthorizationID).
|
||
|
|
Scan(&taskStatus, &taskVersion, &taskUpdated, &authTaskID, &authTaskVersion, &authStartKey, &goodsID, &color, &size, &quantity, &priceCap, &authStatus, &createdBy, &createdAt, &expiresAt)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("read authorization snapshot: %v", err)
|
||
|
|
}
|
||
|
|
if taskStatus != "PENDING" || taskVersion != 2 || taskUpdated != wantCreated.Format(time.RFC3339Nano) ||
|
||
|
|
authTaskID != want.ID || authTaskVersion != 2 || authStartKey != command.StartKey ||
|
||
|
|
goodsID != want.GoodsID || color != want.SKUColor || size != want.SKUSize || quantity != want.Quantity || priceCap != want.MaxTotalPrice ||
|
||
|
|
authStatus != "ACTIVE" || createdBy != "authenticated-admin" || createdAt != wantCreated.Format(time.RFC3339Nano) || expiresAt != wantExpires.Format(time.RFC3339Nano) {
|
||
|
|
t.Fatalf("stored task/authorization mismatch for %s", want.ID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var distinctCreated, distinctExpires int
|
||
|
|
if err := database.QueryRow(`SELECT COUNT(DISTINCT created_at), COUNT(DISTINCT expires_at) FROM order_authorizations WHERE start_key=?`, command.StartKey).Scan(&distinctCreated, &distinctExpires); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if distinctCreated != 1 || distinctExpires != 1 {
|
||
|
|
t.Fatalf("batch timestamps are not shared: created=%d expires=%d", distinctCreated, distinctExpires)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStartPurchasesRejectsInvalidCommandsAndPolicyWithoutWrites(t *testing.T) {
|
||
|
|
validItem := StartItem{TaskID: startTestUUID(1), ExpectedTaskVersion: 1}
|
||
|
|
hundredOne := make([]StartItem, 101)
|
||
|
|
for index := range hundredOne {
|
||
|
|
hundredOne[index] = StartItem{TaskID: startTestUUID(index + 1), ExpectedTaskVersion: 1}
|
||
|
|
}
|
||
|
|
for name, command := range map[string]StartCommand{
|
||
|
|
"invalid start key": {StartKey: "not-a-uuid", Tasks: []StartItem{validItem}},
|
||
|
|
"empty tasks": {StartKey: startTestUUID(1001)},
|
||
|
|
"over batch limit": {StartKey: startTestUUID(1001), Tasks: hundredOne},
|
||
|
|
"invalid task id": {StartKey: startTestUUID(1001), Tasks: []StartItem{{TaskID: "1", ExpectedTaskVersion: 1}}},
|
||
|
|
"duplicate task": {StartKey: startTestUUID(1001), Tasks: []StartItem{validItem, validItem}},
|
||
|
|
"zero version": {StartKey: startTestUUID(1001), Tasks: []StartItem{{TaskID: validItem.TaskID}}},
|
||
|
|
"overflow version": {StartKey: startTestUUID(1001), Tasks: []StartItem{{TaskID: validItem.TaskID, ExpectedTaskVersion: math.MaxInt}}},
|
||
|
|
} {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
_, err := store.StartPurchases(context.Background(), command, "admin")
|
||
|
|
if !errors.Is(err, ErrInvalidStart) {
|
||
|
|
t.Fatalf("error = %v, want ErrInvalidStart", err)
|
||
|
|
}
|
||
|
|
assertAuthorizationCount(t, database, 0)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
for name, mutate := range map[string]func(*SQLiteStore){
|
||
|
|
"zero ttl": func(store *SQLiteStore) { store.policy.AuthorizationTTL = 0 },
|
||
|
|
"zero quantity": func(store *SQLiteStore) { store.policy.MaxQuantity = 0 },
|
||
|
|
"bad max price": func(store *SQLiteStore) { store.policy.MaxTotalPrice = "999" },
|
||
|
|
} {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
createStartDraft(t, store, validItem.TaskID)
|
||
|
|
mutate(store)
|
||
|
|
_, err := store.StartPurchases(context.Background(), StartCommand{StartKey: startTestUUID(1001), Tasks: []StartItem{validItem}}, "admin")
|
||
|
|
if !errors.Is(err, ErrInvalidStart) {
|
||
|
|
t.Fatalf("error = %v, want ErrInvalidStart", err)
|
||
|
|
}
|
||
|
|
assertDraftUnchanged(t, database, validItem.TaskID)
|
||
|
|
assertAuthorizationCount(t, database, 0)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
createStartDraft(t, store, validItem.TaskID)
|
||
|
|
_, err := store.StartPurchases(context.Background(), StartCommand{StartKey: startTestUUID(1001), Tasks: []StartItem{validItem}}, "")
|
||
|
|
if !errors.Is(err, ErrInvalidStart) {
|
||
|
|
t.Fatalf("empty created_by error = %v", err)
|
||
|
|
}
|
||
|
|
assertDraftUnchanged(t, database, validItem.TaskID)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStartPurchasesRejectsEveryTaskConflictWithoutAuthorization(t *testing.T) {
|
||
|
|
for name, mutate := range map[string]func(*testing.T, *SQLiteStore, string, *StartItem){
|
||
|
|
"missing": func(_ *testing.T, _ *SQLiteStore, _ string, item *StartItem) {
|
||
|
|
item.TaskID = startTestUUID(99)
|
||
|
|
},
|
||
|
|
"not draft": func(t *testing.T, store *SQLiteStore, id string, _ *StartItem) {
|
||
|
|
execTestSQL(t, store.database, `UPDATE tasks SET status='PENDING' WHERE id=?`, id)
|
||
|
|
},
|
||
|
|
"version mismatch": func(_ *testing.T, _ *SQLiteStore, _ string, item *StartItem) {
|
||
|
|
item.ExpectedTaskVersion = 2
|
||
|
|
},
|
||
|
|
"empty goods id": func(t *testing.T, store *SQLiteStore, id string, _ *StartItem) {
|
||
|
|
execTestSQL(t, store.database, `UPDATE tasks SET goods_id='' WHERE id=?`, id)
|
||
|
|
},
|
||
|
|
"nondigit goods id": func(t *testing.T, store *SQLiteStore, id string, _ *StartItem) {
|
||
|
|
execTestSQL(t, store.database, `UPDATE tasks SET goods_id='937x' WHERE id=?`, id)
|
||
|
|
},
|
||
|
|
"empty color": func(t *testing.T, store *SQLiteStore, id string, _ *StartItem) {
|
||
|
|
execTestSQL(t, store.database, `UPDATE tasks SET sku_color='' WHERE id=?`, id)
|
||
|
|
},
|
||
|
|
"empty size": func(t *testing.T, store *SQLiteStore, id string, _ *StartItem) {
|
||
|
|
execTestSQL(t, store.database, `UPDATE tasks SET sku_size='' WHERE id=?`, id)
|
||
|
|
},
|
||
|
|
"quantity over policy": func(_ *testing.T, store *SQLiteStore, _ string, _ *StartItem) {
|
||
|
|
store.policy.MaxQuantity = 1
|
||
|
|
},
|
||
|
|
"noncanonical price one decimal": func(t *testing.T, store *SQLiteStore, id string, _ *StartItem) {
|
||
|
|
execTestSQL(t, store.database, `UPDATE tasks SET max_total_price='12.8' WHERE id=?`, id)
|
||
|
|
},
|
||
|
|
"noncanonical leading zero": func(t *testing.T, store *SQLiteStore, id string, _ *StartItem) {
|
||
|
|
execTestSQL(t, store.database, `UPDATE tasks SET max_total_price='012.80' WHERE id=?`, id)
|
||
|
|
},
|
||
|
|
"price over policy": func(_ *testing.T, store *SQLiteStore, _ string, _ *StartItem) {
|
||
|
|
store.policy.MaxTotalPrice = "12.79"
|
||
|
|
},
|
||
|
|
} {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
id := startTestUUID(1)
|
||
|
|
createStartDraft(t, store, id)
|
||
|
|
item := StartItem{TaskID: id, ExpectedTaskVersion: 1}
|
||
|
|
mutate(t, store, id, &item)
|
||
|
|
_, err := store.StartPurchases(context.Background(), StartCommand{StartKey: startTestUUID(1001), Tasks: []StartItem{item}}, "admin")
|
||
|
|
if !errors.Is(err, ErrStartConflict) {
|
||
|
|
t.Fatalf("error = %v, want ErrStartConflict", err)
|
||
|
|
}
|
||
|
|
assertAuthorizationCount(t, database, 0)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStartPurchasesRollsBackWholeBatchForLateConflictAndSQLFailure(t *testing.T) {
|
||
|
|
for _, test := range []struct {
|
||
|
|
name string
|
||
|
|
breakBatch func(*testing.T, *SQLiteStore, string)
|
||
|
|
}{
|
||
|
|
{name: "late validation conflict", breakBatch: func(t *testing.T, store *SQLiteStore, secondID string) {
|
||
|
|
execTestSQL(t, store.database, `UPDATE tasks SET sku_size='' WHERE id=?`, secondID)
|
||
|
|
}},
|
||
|
|
{name: "late SQL failure", breakBatch: func(t *testing.T, store *SQLiteStore, secondID string) {
|
||
|
|
statement := fmt.Sprintf(`CREATE TRIGGER reject_second_authorization BEFORE INSERT ON order_authorizations WHEN NEW.task_id='%s' BEGIN SELECT RAISE(ABORT, 'test failure'); END`, secondID)
|
||
|
|
execTestSQL(t, store.database, statement)
|
||
|
|
}},
|
||
|
|
} {
|
||
|
|
t.Run(test.name, func(t *testing.T) {
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
firstID, secondID := startTestUUID(1), startTestUUID(2)
|
||
|
|
createStartDraft(t, store, firstID)
|
||
|
|
createStartDraft(t, store, secondID)
|
||
|
|
test.breakBatch(t, store, secondID)
|
||
|
|
_, err := store.StartPurchases(context.Background(), StartCommand{StartKey: startTestUUID(1001), Tasks: []StartItem{{TaskID: firstID, ExpectedTaskVersion: 1}, {TaskID: secondID, ExpectedTaskVersion: 1}}}, "admin")
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("StartPurchases unexpectedly succeeded")
|
||
|
|
}
|
||
|
|
assertDraftUnchanged(t, database, firstID)
|
||
|
|
var secondStatus string
|
||
|
|
var secondVersion int
|
||
|
|
if err := database.QueryRow(`SELECT status,version FROM tasks WHERE id=?`, secondID).Scan(&secondStatus, &secondVersion); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if secondStatus != "DRAFT" || secondVersion != 1 {
|
||
|
|
t.Fatalf("second task = %s/v%d, want DRAFT/v1", secondStatus, secondVersion)
|
||
|
|
}
|
||
|
|
assertAuthorizationCount(t, database, 0)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStartPurchasesReplayIsStableAndRejectsDifferentOrIncompleteSets(t *testing.T) {
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
firstID, secondID, thirdID := startTestUUID(1), startTestUUID(2), startTestUUID(3)
|
||
|
|
for _, id := range []string{firstID, secondID, thirdID} {
|
||
|
|
createStartDraft(t, store, id)
|
||
|
|
}
|
||
|
|
command := StartCommand{StartKey: startTestUUID(1001), Tasks: []StartItem{{TaskID: secondID, ExpectedTaskVersion: 1}, {TaskID: firstID, ExpectedTaskVersion: 1}}}
|
||
|
|
first, err := store.StartPurchases(context.Background(), command, "admin")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
command.Tasks[0], command.Tasks[1] = command.Tasks[1], command.Tasks[0]
|
||
|
|
replay, err := store.StartPurchases(context.Background(), command, "admin")
|
||
|
|
if err != nil || !reflect.DeepEqual(replay, first) {
|
||
|
|
t.Fatalf("replay = (%#v, %v), want %#v", replay, err, first)
|
||
|
|
}
|
||
|
|
assertAuthorizationCount(t, database, 2)
|
||
|
|
|
||
|
|
conflicting := []StartCommand{
|
||
|
|
{StartKey: command.StartKey, Tasks: command.Tasks[:1]},
|
||
|
|
{StartKey: command.StartKey, Tasks: []StartItem{{TaskID: firstID, ExpectedTaskVersion: 2}, {TaskID: secondID, ExpectedTaskVersion: 1}}},
|
||
|
|
{StartKey: command.StartKey, Tasks: []StartItem{{TaskID: firstID, ExpectedTaskVersion: 1}, {TaskID: secondID, ExpectedTaskVersion: 1}, {TaskID: thirdID, ExpectedTaskVersion: 1}}},
|
||
|
|
}
|
||
|
|
for _, changed := range conflicting {
|
||
|
|
if _, err := store.StartPurchases(context.Background(), changed, "admin"); !errors.Is(err, ErrStartConflict) {
|
||
|
|
t.Fatalf("different payload error = %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assertAuthorizationCount(t, database, 2)
|
||
|
|
assertDraftUnchanged(t, database, thirdID)
|
||
|
|
|
||
|
|
execTestSQL(t, database, `DELETE FROM order_authorizations WHERE task_id=?`, secondID)
|
||
|
|
if _, err := store.StartPurchases(context.Background(), command, "admin"); !errors.Is(err, ErrStartConflict) {
|
||
|
|
t.Fatalf("incomplete replay error = %v", err)
|
||
|
|
}
|
||
|
|
assertAuthorizationCount(t, database, 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStartPurchasesConcurrentReplayAndVersionRace(t *testing.T) {
|
||
|
|
t.Run("same key replays one stable result", func(t *testing.T) {
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
id := startTestUUID(1)
|
||
|
|
createStartDraft(t, store, id)
|
||
|
|
command := StartCommand{StartKey: startTestUUID(1001), Tasks: []StartItem{{TaskID: id, ExpectedTaskVersion: 1}}}
|
||
|
|
const callers = 16
|
||
|
|
start := make(chan struct{})
|
||
|
|
results := make(chan StartResult, callers)
|
||
|
|
errorsChannel := make(chan error, callers)
|
||
|
|
var group sync.WaitGroup
|
||
|
|
for range callers {
|
||
|
|
group.Add(1)
|
||
|
|
go func() {
|
||
|
|
defer group.Done()
|
||
|
|
<-start
|
||
|
|
result, err := store.StartPurchases(context.Background(), command, "admin")
|
||
|
|
if err != nil {
|
||
|
|
errorsChannel <- err
|
||
|
|
return
|
||
|
|
}
|
||
|
|
results <- result
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
close(start)
|
||
|
|
group.Wait()
|
||
|
|
close(results)
|
||
|
|
close(errorsChannel)
|
||
|
|
for err := range errorsChannel {
|
||
|
|
t.Fatalf("concurrent replay: %v", err)
|
||
|
|
}
|
||
|
|
var want StartResult
|
||
|
|
for result := range results {
|
||
|
|
if want.StartKey == "" {
|
||
|
|
want = result
|
||
|
|
} else if !reflect.DeepEqual(result, want) {
|
||
|
|
t.Fatalf("unstable replay: %#v != %#v", result, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assertAuthorizationCount(t, database, 1)
|
||
|
|
var version int
|
||
|
|
if err := database.QueryRow(`SELECT version FROM tasks WHERE id=?`, id).Scan(&version); err != nil || version != 2 {
|
||
|
|
t.Fatalf("task version = %d, err=%v", version, err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("different keys race one expected version", func(t *testing.T) {
|
||
|
|
database := migratedDatabase(t)
|
||
|
|
store := configuredStartStore(t, database)
|
||
|
|
id := startTestUUID(1)
|
||
|
|
createStartDraft(t, store, id)
|
||
|
|
start := make(chan struct{})
|
||
|
|
errorsChannel := make(chan error, 2)
|
||
|
|
var group sync.WaitGroup
|
||
|
|
for _, key := range []string{startTestUUID(1001), startTestUUID(1002)} {
|
||
|
|
group.Add(1)
|
||
|
|
go func(startKey string) {
|
||
|
|
defer group.Done()
|
||
|
|
<-start
|
||
|
|
_, err := store.StartPurchases(context.Background(), StartCommand{StartKey: startKey, Tasks: []StartItem{{TaskID: id, ExpectedTaskVersion: 1}}}, "admin")
|
||
|
|
errorsChannel <- err
|
||
|
|
}(key)
|
||
|
|
}
|
||
|
|
close(start)
|
||
|
|
group.Wait()
|
||
|
|
close(errorsChannel)
|
||
|
|
successes, conflicts := 0, 0
|
||
|
|
for err := range errorsChannel {
|
||
|
|
switch {
|
||
|
|
case err == nil:
|
||
|
|
successes++
|
||
|
|
case errors.Is(err, ErrStartConflict):
|
||
|
|
conflicts++
|
||
|
|
default:
|
||
|
|
t.Fatalf("unexpected race error: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if successes != 1 || conflicts != 1 {
|
||
|
|
t.Fatalf("success/conflict = %d/%d, want 1/1", successes, conflicts)
|
||
|
|
}
|
||
|
|
assertAuthorizationCount(t, database, 1)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSQLiteStoreRejectsV1SchemaAtStartup(t *testing.T) {
|
||
|
|
database := openDatabase(t)
|
||
|
|
if err := migrations.Run(context.Background(), database, migrationDirectory(t), "up-by-one"); err != nil {
|
||
|
|
t.Fatalf("migrate to v1: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := NewSQLiteStore(database); err == nil {
|
||
|
|
t.Fatal("NewSQLiteStore accepted the v1 two-pass schema")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func configuredStartStore(t *testing.T, database *sql.DB) *SQLiteStore {
|
||
|
|
t.Helper()
|
||
|
|
store, err := NewSQLiteStore(database)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewSQLiteStore: %v", err)
|
||
|
|
}
|
||
|
|
store.SetStartPolicy(StartPolicy{AuthorizationTTL: 15 * time.Minute, MaxQuantity: 10, MaxTotalPrice: "999.99"})
|
||
|
|
return store
|
||
|
|
}
|
||
|
|
|
||
|
|
func createStartDraft(t *testing.T, store *SQLiteStore, id string) {
|
||
|
|
t.Helper()
|
||
|
|
draft := Draft{ID: id, Title: "test", GoodsID: "937122477375", SKUColor: "黑色", SKUSize: "M", Quantity: 2, MaxTotalPrice: "12.80"}
|
||
|
|
if _, err := store.CreateDraft(context.Background(), draft); err != nil {
|
||
|
|
t.Fatalf("CreateDraft: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func startTestUUID(number int) string {
|
||
|
|
return fmt.Sprintf("%08x-1234-4abc-a123-%012x", number, number)
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertAuthorizationCount(t *testing.T, database *sql.DB, want int) {
|
||
|
|
t.Helper()
|
||
|
|
var got int
|
||
|
|
if err := database.QueryRow(`SELECT COUNT(*) FROM order_authorizations`).Scan(&got); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if got != want {
|
||
|
|
t.Fatalf("authorization count = %d, want %d", got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertDraftUnchanged(t *testing.T, database *sql.DB, id string) {
|
||
|
|
t.Helper()
|
||
|
|
var status string
|
||
|
|
var version int
|
||
|
|
if err := database.QueryRow(`SELECT status,version FROM tasks WHERE id=?`, id).Scan(&status, &version); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if status != "DRAFT" || version != 1 {
|
||
|
|
t.Fatalf("task %s = %s/v%d, want DRAFT/v1", id, status, version)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func execTestSQL(t *testing.T, database *sql.DB, statement string, arguments ...any) {
|
||
|
|
t.Helper()
|
||
|
|
if _, err := database.Exec(statement, arguments...); err != nil {
|
||
|
|
t.Fatalf("execute test SQL: %v", err)
|
||
|
|
}
|
||
|
|
}
|