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) } }