196 lines
7.2 KiB
Go
196 lines
7.2 KiB
Go
package tasks
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestListTasksTreatsLikeMetacharactersLiterally(t *testing.T) {
|
|
database := migratedDatabase(t)
|
|
store, err := NewSQLiteStore(database)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
created := "2026-08-04T01:00:00Z"
|
|
insertTaskRow(t, database, "percent", "100%纯棉", "100", "DRAFT", created)
|
|
insertTaskRow(t, database, "underscore", "尺码_A", "101", "DRAFT", created)
|
|
insertTaskRow(t, database, "backslash", `路径\名称`, "102", "DRAFT", created)
|
|
insertTaskRow(t, database, "plain", "普通商品", "103", "DRAFT", created)
|
|
|
|
for _, test := range []struct {
|
|
keyword string
|
|
wantID string
|
|
}{
|
|
{keyword: "%", wantID: "percent"},
|
|
{keyword: "_", wantID: "underscore"},
|
|
{keyword: `\`, wantID: "backslash"},
|
|
} {
|
|
t.Run(test.wantID, func(t *testing.T) {
|
|
rows, err := store.ListTasks(context.Background(), TaskFilter{Keyword: test.keyword})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 1 || rows[0].ID != test.wantID {
|
|
t.Fatalf("keyword %q rows = %#v, want only %q", test.keyword, rows, test.wantID)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListTasksSupportsEveryStatusAndEmptyMeansAll(t *testing.T) {
|
|
database := migratedDatabase(t)
|
|
store, err := NewSQLiteStore(database)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
statuses := []string{"DRAFT", "PENDING", "CLAIMED", "ORDERING", "NEEDS_MANUAL", "WAITING_PAYMENT", "RECONCILIATION_REQUIRED", "SUCCEEDED", "FAILED", "CANCELED"}
|
|
for index, status := range statuses {
|
|
insertTaskRow(t, database, status, status, "200", status, time.Date(2026, 8, 4, 1, 0, index, 0, time.UTC).Format(time.RFC3339Nano))
|
|
}
|
|
|
|
all, err := store.ListTasks(context.Background(), TaskFilter{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(all) != len(statuses) {
|
|
t.Fatalf("all-status rows = %d, want %d", len(all), len(statuses))
|
|
}
|
|
for _, status := range statuses {
|
|
rows, err := store.ListTasks(context.Background(), TaskFilter{Status: status})
|
|
if err != nil {
|
|
t.Fatalf("status %s: %v", status, err)
|
|
}
|
|
if len(rows) != 1 || rows[0].Status != status {
|
|
t.Fatalf("status %s rows = %#v", status, rows)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListTasksUsesShanghaiHalfOpenDateRange(t *testing.T) {
|
|
database := migratedDatabase(t)
|
|
store, err := NewSQLiteStore(database)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
insertTaskRow(t, database, "before", "before", "300", "DRAFT", "2026-08-03T15:59:59Z")
|
|
insertTaskRow(t, database, "at-start", "at-start", "301", "DRAFT", "2026-08-03T16:00:00Z")
|
|
insertTaskRow(t, database, "before-end", "before-end", "302", "DRAFT", "2026-08-04T15:59:59Z")
|
|
insertTaskRow(t, database, "at-end", "at-end", "303", "DRAFT", "2026-08-04T16:00:00Z")
|
|
|
|
rows, err := store.ListTasks(context.Background(), TaskFilter{CreatedFrom: "2026-08-04", CreatedTo: "2026-08-04"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 2 || rows[0].ID != "before-end" || rows[1].ID != "at-start" {
|
|
t.Fatalf("Shanghai day rows = %#v, want [before-end at-start]", rows)
|
|
}
|
|
}
|
|
|
|
func TestListTasksBreaksEqualTimestampsByDescendingRowID(t *testing.T) {
|
|
database := migratedDatabase(t)
|
|
store, err := NewSQLiteStore(database)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
created := "2026-08-04T01:02:03Z"
|
|
insertTaskRow(t, database, "first", "first", "400", "DRAFT", created)
|
|
insertTaskRow(t, database, "second", "second", "401", "DRAFT", created)
|
|
|
|
rows, err := store.ListTasks(context.Background(), TaskFilter{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 2 || rows[0].ID != "second" || rows[1].ID != "first" {
|
|
t.Fatalf("equal-time rows = %#v, want descending rowid", rows)
|
|
}
|
|
}
|
|
|
|
func TestListTasksRejectsInvalidStatusAndDates(t *testing.T) {
|
|
store, err := NewSQLiteStore(migratedDatabase(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, filter := range map[string]TaskFilter{
|
|
"status": {Status: "UNKNOWN"},
|
|
"from date": {CreatedFrom: "2026-02-30"},
|
|
"to date": {CreatedTo: "04/08/2026"},
|
|
"reverse range": {CreatedFrom: "2026-08-05", CreatedTo: "2026-08-04"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
rows, err := store.ListTasks(context.Background(), filter)
|
|
if !errors.Is(err, ErrInvalidFilter) || rows != nil {
|
|
t.Fatalf("ListTasks(%#v) = (%#v, %v), want ErrInvalidFilter", filter, rows, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStartPurchasesIsAtomicAndReplaysSameSet(t *testing.T) {
|
|
database := migratedDatabase(t)
|
|
store, err := NewSQLiteStore(database)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store.SetStartPolicy(StartPolicy{AuthorizationTTL: time.Hour, MaxQuantity: 10, MaxTotalPrice: "999.99"})
|
|
store.now = func() time.Time { return time.Date(2026, 8, 4, 1, 2, 3, 0, time.UTC) }
|
|
for _, draft := range []Draft{testDraft(testKey, "one"), testDraft("b3c9f507-7473-4fa6-8d71-8786c34c6301", "two")} {
|
|
if _, err := store.CreateDraft(context.Background(), draft); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
command := StartCommand{StartKey: "c3c9f507-7473-4fa6-8d71-8786c34c6301", Tasks: []StartItem{{TaskID: "b3c9f507-7473-4fa6-8d71-8786c34c6301", ExpectedTaskVersion: 1}, {TaskID: testKey, ExpectedTaskVersion: 1}}}
|
|
first, err := store.StartPurchases(context.Background(), command, "admin")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.AuthorizedCount != 2 || first.PaymentAutomated {
|
|
t.Fatalf("start result=%#v", first)
|
|
}
|
|
command.Tasks[0], command.Tasks[1] = command.Tasks[1], command.Tasks[0]
|
|
replay, err := store.StartPurchases(context.Background(), command, "admin")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if replay.Tasks[0].AuthorizationID != first.Tasks[0].AuthorizationID || replay.Tasks[1].AuthorizationID != first.Tasks[1].AuthorizationID {
|
|
t.Fatalf("replay=%#v first=%#v", replay, first)
|
|
}
|
|
var pending, auths int
|
|
if err := database.QueryRow(`SELECT COUNT(*) FROM tasks WHERE status='PENDING' AND version=2`).Scan(&pending); err != nil || pending != 2 {
|
|
t.Fatalf("pending=%d err=%v", pending, err)
|
|
}
|
|
if err := database.QueryRow(`SELECT COUNT(*) FROM order_authorizations WHERE status='ACTIVE' AND created_by='admin'`).Scan(&auths); err != nil || auths != 2 {
|
|
t.Fatalf("auths=%d err=%v", auths, err)
|
|
}
|
|
_, err = store.StartPurchases(context.Background(), StartCommand{StartKey: command.StartKey, Tasks: command.Tasks[:1]}, "admin")
|
|
if !errors.Is(err, ErrStartConflict) {
|
|
t.Fatalf("subset err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestShanghaiRangeAndMoneyAreFailClosed(t *testing.T) {
|
|
start, end, err := ShanghaiRange("2026-08-04", "2026-08-04")
|
|
if err != nil || start.Format(time.RFC3339) != "2026-08-03T16:00:00Z" || end.Format(time.RFC3339) != "2026-08-04T16:00:00Z" {
|
|
t.Fatalf("range=(%s,%s,%v)", start, end, err)
|
|
}
|
|
for _, value := range []string{"0.01", "12.80", "999999999999999999999999.99"} {
|
|
if _, _, ok := normalizeCents(value); !ok {
|
|
t.Fatalf("money %q rejected", value)
|
|
}
|
|
}
|
|
for _, value := range []string{"1", "01.20", "0.00", "1.234", "1.", " 1.00", "1e2"} {
|
|
if _, _, ok := normalizeCents(value); ok {
|
|
t.Fatalf("money %q accepted", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func insertTaskRow(t *testing.T, database *sql.DB, id, title, goodsID, status, createdAt string) {
|
|
t.Helper()
|
|
if _, err := database.Exec(`INSERT INTO tasks (id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price, status, version, created_at, updated_at) VALUES (?, 'MANUAL', ?, ?, '黑色', 'M', 2, '12.80', ?, 1, ?, ?)`, id, title, goodsID, status, createdAt, createdAt); err != nil {
|
|
t.Fatalf("insert task %s: %v", id, err)
|
|
}
|
|
}
|