feat(admin): migrate core schema to single-pass model
This commit is contained in:
@@ -3,8 +3,11 @@ package migrations_test
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cmbuyer/admin/internal/migrations"
|
||||
@@ -13,6 +16,8 @@ import (
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
const migrationTime = "2026-08-04T00:00:00Z"
|
||||
|
||||
func TestUpDownAndIdempotence(t *testing.T) {
|
||||
database := openTestDatabase(t)
|
||||
directory := migrationDirectory(t)
|
||||
@@ -21,173 +26,325 @@ func TestUpDownAndIdempotence(t *testing.T) {
|
||||
if err := migrations.Up(context, database, directory); err != nil {
|
||||
t.Fatalf("apply migrations: %v", err)
|
||||
}
|
||||
assertVersion(t, database, 1)
|
||||
assertVersion(t, database, 2)
|
||||
assertTableExists(t, database, "tasks", true)
|
||||
assertTableExists(t, database, "spec_trials", true)
|
||||
assertTableExists(t, database, "spec_trials", false)
|
||||
assertTableExists(t, database, "order_authorizations", true)
|
||||
assertTableExists(t, database, "purchase_attempts", true)
|
||||
assertTableExists(t, database, "order_submissions", true)
|
||||
assertTableExists(t, database, "single_pass_upgrade_guard", false)
|
||||
|
||||
if err := migrations.Up(context, database, directory); err != nil {
|
||||
t.Fatalf("reapply migrations: %v", err)
|
||||
}
|
||||
assertVersion(t, database, 1)
|
||||
assertVersion(t, database, 2)
|
||||
|
||||
if err := migrations.Down(context, database, directory); err != nil {
|
||||
t.Fatalf("roll back migration: %v", err)
|
||||
}
|
||||
assertVersion(t, database, 0)
|
||||
assertTableExists(t, database, "tasks", false)
|
||||
assertTableExists(t, database, "spec_trials", false)
|
||||
assertTableExists(t, database, "order_authorizations", false)
|
||||
assertTableExists(t, database, "order_submissions", false)
|
||||
|
||||
if err := migrations.Up(context, database, directory); err != nil {
|
||||
t.Fatalf("apply migration after rollback: %v", err)
|
||||
t.Fatalf("roll back v2 migration: %v", err)
|
||||
}
|
||||
assertVersion(t, database, 1)
|
||||
assertTableExists(t, database, "spec_trials", true)
|
||||
assertTableExists(t, database, "purchase_attempts", false)
|
||||
assertTableExists(t, database, "single_pass_downgrade_guard", false)
|
||||
|
||||
if err := migrations.Up(context, database, directory); err != nil {
|
||||
t.Fatalf("reapply v2 after rollback: %v", err)
|
||||
}
|
||||
assertVersion(t, database, 2)
|
||||
}
|
||||
|
||||
func TestSchemaConstraints(t *testing.T) {
|
||||
func TestUpgradePreservesManualDraftLosslessly(t *testing.T) {
|
||||
database := openTestDatabase(t)
|
||||
migrateToV1(t, database)
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO tasks (
|
||||
id, source, source_ref, title, goods_id, sku_color, sku_size, quantity, max_total_price,
|
||||
reference_asset_id, status, version, created_at, updated_at
|
||||
) VALUES ('draft-one', 'MANUAL', 'source-ref', 'title', 'goods', 'white', 'XL', 2, '80.50',
|
||||
'asset-id', 'DRAFT', 7, '2026-08-03T00:00:00Z', '2026-08-03T01:00:00Z')
|
||||
`); err != nil {
|
||||
t.Fatalf("insert v1 draft: %v", err)
|
||||
}
|
||||
|
||||
if err := migrations.Up(context.Background(), database, migrationDirectory(t)); err != nil {
|
||||
t.Fatalf("upgrade v1 draft: %v", err)
|
||||
}
|
||||
assertVersion(t, database, 2)
|
||||
var got struct {
|
||||
id, source, sourceRef, title, goodsID, color, size, maxPrice, assetID, status, created, updated string
|
||||
quantity, version int
|
||||
}
|
||||
if err := database.QueryRow(`SELECT id, source, source_ref, title, goods_id, sku_color, sku_size, quantity, max_total_price, reference_asset_id, status, version, created_at, updated_at FROM tasks WHERE id = 'draft-one'`).Scan(
|
||||
&got.id, &got.source, &got.sourceRef, &got.title, &got.goodsID, &got.color, &got.size, &got.quantity, &got.maxPrice, &got.assetID, &got.status, &got.version, &got.created, &got.updated,
|
||||
); err != nil {
|
||||
t.Fatalf("read upgraded draft: %v", err)
|
||||
}
|
||||
if got != (struct {
|
||||
id, source, sourceRef, title, goodsID, color, size, maxPrice, assetID, status, created, updated string
|
||||
quantity, version int
|
||||
}{"draft-one", "MANUAL", "source-ref", "title", "goods", "white", "XL", "80.50", "asset-id", "DRAFT", "2026-08-03T00:00:00Z", "2026-08-03T01:00:00Z", 2, 7}) {
|
||||
t.Fatalf("upgraded draft changed: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeRejectsLegacyExecutionDataAtomically(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*testing.T, *sql.DB)
|
||||
}{
|
||||
{"non-draft task", func(t *testing.T, database *sql.DB) {
|
||||
insertV1Task(t, database, "pending", "MANUAL", "PENDING", "1.00")
|
||||
}},
|
||||
{"non-manual task", func(t *testing.T, database *sql.DB) { insertV1Task(t, database, "excel", "EXCEL", "DRAFT", "1.00") }},
|
||||
{"invalid v2 money", func(t *testing.T, database *sql.DB) { insertV1Task(t, database, "zero", "MANUAL", "DRAFT", "0.00") }},
|
||||
{"third decimal place", func(t *testing.T, database *sql.DB) {
|
||||
insertV1Task(t, database, "third-decimal", "MANUAL", "DRAFT", "1.234")
|
||||
}},
|
||||
{"spec trial", func(t *testing.T, database *sql.DB) {
|
||||
insertV1Task(t, database, "task", "MANUAL", "DRAFT", "1.00")
|
||||
insertV1SpecTrial(t, database, "trial", "task")
|
||||
}},
|
||||
{"authorization", func(t *testing.T, database *sql.DB) {
|
||||
insertV1Task(t, database, "task", "MANUAL", "DRAFT", "1.00")
|
||||
insertV1SpecTrial(t, database, "trial", "task")
|
||||
insertV1Authorization(t, database, "auth", "task", "trial")
|
||||
}},
|
||||
{"submission", func(t *testing.T, database *sql.DB) {
|
||||
insertV1Task(t, database, "task", "MANUAL", "DRAFT", "1.00")
|
||||
insertV1SpecTrial(t, database, "trial", "task")
|
||||
insertV1Authorization(t, database, "auth", "task", "trial")
|
||||
if _, err := database.Exec(`INSERT INTO order_submissions (id, task_id, authorization_id, command_id, dry_run_id, status, verified_unit_price, quantity_read, confirm_page_amount, created_at) VALUES ('submission', 'task', 'auth', 'command', 'dry-run', 'FENCED', '1.00', 1, '1.00', ? )`, migrationTime); err != nil {
|
||||
t.Fatalf("insert v1 submission: %v", err)
|
||||
}
|
||||
}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
database := openTestDatabase(t)
|
||||
migrateToV1(t, database)
|
||||
test.setup(t, database)
|
||||
before := v1RowCount(t, database)
|
||||
if err := migrations.Up(context.Background(), database, migrationDirectory(t)); err == nil {
|
||||
t.Fatal("unsafe legacy data upgraded successfully")
|
||||
}
|
||||
assertVersion(t, database, 1)
|
||||
assertTableExists(t, database, "spec_trials", true)
|
||||
assertTableExists(t, database, "purchase_attempts", false)
|
||||
assertTableExists(t, database, "single_pass_upgrade_guard", false)
|
||||
if after := v1RowCount(t, database); after != before {
|
||||
t.Fatalf("v1 data changed after rejection: before=%d after=%d", before, after)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2SchemaConstraintsAndRelationships(t *testing.T) {
|
||||
database := openTestDatabase(t)
|
||||
if err := migrations.Up(context.Background(), database, migrationDirectory(t)); err != nil {
|
||||
t.Fatalf("apply migrations: %v", err)
|
||||
}
|
||||
|
||||
for _, column := range []struct {
|
||||
table string
|
||||
name string
|
||||
}{
|
||||
for _, column := range []struct{ table, name string }{
|
||||
{"tasks", "max_total_price"},
|
||||
{"spec_trials", "unit_price"},
|
||||
{"spec_trials", "total_price"},
|
||||
{"order_authorizations", "authorized_unit_price"},
|
||||
{"order_authorizations", "total_price_cap"},
|
||||
{"order_submissions", "verified_unit_price"},
|
||||
{"order_submissions", "confirm_page_amount"},
|
||||
{"purchase_attempts", "gate1_unit_price"},
|
||||
{"purchase_attempts", "gate2_unit_price"},
|
||||
{"purchase_attempts", "confirm_amount"},
|
||||
{"order_submissions", "gate1_unit_price"},
|
||||
{"order_submissions", "gate2_unit_price"},
|
||||
{"order_submissions", "confirm_amount"},
|
||||
} {
|
||||
assertColumnType(t, database, column.table, column.name, "TEXT")
|
||||
}
|
||||
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO tasks (
|
||||
id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price,
|
||||
status, created_at, updated_at
|
||||
) VALUES ('bad-quantity', 'MANUAL', 'title', 'goods', 'white', 'XL', 0, '80.00', 'DRAFT', '2026-08-03T00:00:00Z', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert task with quantity 0 succeeded")
|
||||
for _, legacy := range []string{"spec_trials", "authorized_unit_price", "spec_trial_id", "command_id", "dry_run_id"} {
|
||||
var count int
|
||||
if err := database.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE sql LIKE '%' || ? || '%'`, legacy).Scan(&count); err != nil {
|
||||
t.Fatalf("search schema for %s: %v", legacy, err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("legacy identifier %q remains in v2 schema", legacy)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO tasks (
|
||||
id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price,
|
||||
status, created_at, updated_at
|
||||
) VALUES ('bad-price', 'MANUAL', 'title', 'goods', 'white', 'XL', 1, '80..00', 'DRAFT', '2026-08-03T00:00:00Z', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert task with malformed decimal price succeeded")
|
||||
insertV2Task(t, database, "task-one", "MANUAL", "DRAFT")
|
||||
insertV2Task(t, database, "task-two", "MANUAL", "DRAFT")
|
||||
for index, value := range []string{"", "0", "0.00", "-1.00", "1e2", "1.", "1.234", " 1.00", "one"} {
|
||||
if _, err := database.Exec(`INSERT INTO tasks (id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price, status, created_at, updated_at) VALUES (?, 'MANUAL', 'title', 'goods', 'white', 'XL', 1, ?, 'DRAFT', ?, ?)`, "bad-price-"+strconv.Itoa(index), value, migrationTime, migrationTime); err == nil {
|
||||
t.Fatalf("invalid total price %q succeeded", value)
|
||||
}
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO tasks (id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price, status, created_at, updated_at) VALUES ('bad-status', 'MANUAL', 'title', 'goods', 'white', 'XL', 1, '1.00', 'UNKNOWN', ?, ?)`, migrationTime, migrationTime); err == nil {
|
||||
t.Fatal("unknown task status succeeded")
|
||||
}
|
||||
insertV2Authorization(t, database, "auth-one", "task-one", 1, "start-one")
|
||||
insertV2Authorization(t, database, "auth-two", "task-two", 1, "start-two")
|
||||
if _, err := database.Exec(`INSERT INTO order_authorizations (id, task_id, task_version, start_key, goods_id, sku_color, sku_size, quantity, total_price_cap, status, created_by, created_at, expires_at) VALUES ('bad-auth-price', 'task-one', 2, 'bad-price', 'goods', 'white', 'XL', 1, '1.234', 'ACTIVE', 'admin', ?, ?)`, migrationTime, migrationTime); err == nil {
|
||||
t.Fatal("third decimal authorization cap succeeded")
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO order_authorizations (id, task_id, task_version, start_key, goods_id, sku_color, sku_size, quantity, total_price_cap, status, created_by, created_at, expires_at) VALUES ('bad-auth-status', 'task-one', 2, 'bad-status', 'goods', 'white', 'XL', 1, '1.00', 'UNKNOWN', 'admin', ?, ?)`, migrationTime, migrationTime); err == nil {
|
||||
t.Fatal("unknown authorization status succeeded")
|
||||
}
|
||||
insertV2Authorization(t, database, "auth-one-b", "task-one", 2, "start-one-b")
|
||||
if _, err := database.Exec(`INSERT INTO order_authorizations (id, task_id, task_version, start_key, goods_id, sku_color, sku_size, quantity, total_price_cap, status, created_by, created_at, expires_at) VALUES ('duplicate-version', 'task-one', 1, 'different-start', 'goods', 'white', 'XL', 1, '1.00', 'ACTIVE', 'admin', ?, ?)`, migrationTime, migrationTime); err == nil {
|
||||
t.Fatal("duplicate task version authorization succeeded")
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO purchase_attempts (id, task_id, authorization_id, claim_generation, status, started_at) VALUES ('cross-attempt', 'task-one', 'auth-two', 1, 'CLAIMED', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("attempt using another task authorization succeeded")
|
||||
}
|
||||
insertV2Attempt(t, database, "attempt-one", "task-one", "auth-one", 1)
|
||||
if _, err := database.Exec(`INSERT INTO purchase_attempts (id, task_id, authorization_id, claim_generation, status, gate1_unit_price, started_at) VALUES ('bad-attempt-price', 'task-one', 'auth-one', 2, 'ORDERING', '1.234', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("third decimal gate price succeeded")
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO purchase_attempts (id, task_id, authorization_id, claim_generation, status, started_at) VALUES ('bad-attempt-status', 'task-one', 'auth-one', 2, 'UNKNOWN', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("unknown attempt status succeeded")
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO purchase_attempts (id, task_id, authorization_id, claim_generation, status, failure_code, started_at) VALUES ('bad-code', 'task-one', 'auth-one', 2, 'FAILED', 'FREE_TEXT', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("unknown failure code succeeded")
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO order_submissions (id, task_id, authorization_id, attempt_id, status, gate1_unit_price, gate2_unit_price, quantity_read, confirm_amount, created_at) VALUES ('cross-submission', 'task-one', 'auth-two', 'attempt-one', 'FENCED', '1.00', '1.00', 1, '1.00', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("submission using another task authorization succeeded")
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO order_submissions (id, task_id, authorization_id, attempt_id, status, gate1_unit_price, gate2_unit_price, quantity_read, confirm_amount, created_at) VALUES ('cross-authorization-submission', 'task-one', 'auth-one-b', 'attempt-one', 'FENCED', '1.00', '1.00', 1, '1.00', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("submission combining another same-task authorization and attempt succeeded")
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO order_submissions (id, task_id, authorization_id, attempt_id, status, gate1_unit_price, gate2_unit_price, quantity_read, confirm_amount, created_at) VALUES ('bad-submission-status', 'task-one', 'auth-one', 'attempt-one', 'UNKNOWN', '1.00', '1.00', 1, '1.00', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("unknown submission status succeeded")
|
||||
}
|
||||
if _, err := database.Exec(`INSERT INTO order_submissions (id, task_id, authorization_id, attempt_id, status, gate1_unit_price, gate2_unit_price, quantity_read, confirm_amount, created_at) VALUES ('bad-submission-price', 'task-one', 'auth-one', 'attempt-one', 'FENCED', '1.234', '1.00', 1, '1.00', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("third decimal submission price succeeded")
|
||||
}
|
||||
insertV2Submission(t, database, "submission-one", "task-one", "auth-one", "attempt-one")
|
||||
if _, err := database.Exec(`INSERT INTO order_submissions (id, task_id, authorization_id, attempt_id, status, gate1_unit_price, gate2_unit_price, quantity_read, confirm_amount, created_at) VALUES ('duplicate-auth', 'task-one', 'auth-one', 'attempt-one', 'FENCED', '1.00', '1.00', 1, '1.00', ?)`, migrationTime); err == nil {
|
||||
t.Fatal("second submission for fenced authorization succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO tasks (
|
||||
id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price,
|
||||
status, created_at, updated_at
|
||||
) VALUES ('fractional-quantity', 'MANUAL', 'title', 'goods', 'white', 'XL', 1.5, '80.00', 'DRAFT', '2026-08-03T00:00:00Z', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert task with fractional quantity succeeded")
|
||||
func TestDowngradeRejectsV2BusinessDataAtomically(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*testing.T, *sql.DB)
|
||||
}{
|
||||
{"authorization", func(t *testing.T, database *sql.DB) {
|
||||
insertV2Task(t, database, "task", "MANUAL", "DRAFT")
|
||||
insertV2Authorization(t, database, "auth", "task", 1, "start")
|
||||
}},
|
||||
{"attempt", func(t *testing.T, database *sql.DB) {
|
||||
insertV2Task(t, database, "task", "MANUAL", "DRAFT")
|
||||
insertV2Authorization(t, database, "auth", "task", 1, "start")
|
||||
insertV2Attempt(t, database, "attempt", "task", "auth", 1)
|
||||
}},
|
||||
{"submission", func(t *testing.T, database *sql.DB) {
|
||||
insertV2Task(t, database, "task", "MANUAL", "DRAFT")
|
||||
insertV2Authorization(t, database, "auth", "task", 1, "start")
|
||||
insertV2Attempt(t, database, "attempt", "task", "auth", 1)
|
||||
insertV2Submission(t, database, "submission", "task", "auth", "attempt")
|
||||
}},
|
||||
{"non-draft task", func(t *testing.T, database *sql.DB) { insertV2Task(t, database, "pending", "MANUAL", "PENDING") }},
|
||||
{"non-manual task", func(t *testing.T, database *sql.DB) { insertV2Task(t, database, "excel", "EXCEL", "DRAFT") }},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
database := openTestDatabase(t)
|
||||
if err := migrations.Up(context.Background(), database, migrationDirectory(t)); err != nil {
|
||||
t.Fatalf("apply migrations: %v", err)
|
||||
}
|
||||
test.setup(t, database)
|
||||
before := v2RowCount(t, database)
|
||||
if err := migrations.Down(context.Background(), database, migrationDirectory(t)); err == nil {
|
||||
t.Fatal("unsafe v2 data downgraded successfully")
|
||||
}
|
||||
assertVersion(t, database, 2)
|
||||
assertTableExists(t, database, "purchase_attempts", true)
|
||||
assertTableExists(t, database, "spec_trials", false)
|
||||
assertTableExists(t, database, "single_pass_downgrade_guard", false)
|
||||
if after := v2RowCount(t, database); after != before {
|
||||
t.Fatalf("v2 data changed after rejected downgrade: before=%d after=%d", before, after)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO tasks (
|
||||
id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price,
|
||||
status, created_at, updated_at
|
||||
) VALUES ('trailing-decimal', 'MANUAL', 'title', 'goods', 'white', 'XL', 1, '80.', 'DRAFT', '2026-08-03T00:00:00Z', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert task with trailing decimal point succeeded")
|
||||
func migrateToV1(t *testing.T, database *sql.DB) {
|
||||
t.Helper()
|
||||
if err := migrations.Run(context.Background(), database, migrationDirectory(t), "up-by-one"); err != nil {
|
||||
t.Fatalf("apply v1: %v", err)
|
||||
}
|
||||
assertVersion(t, database, 1)
|
||||
}
|
||||
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO tasks (
|
||||
id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price,
|
||||
status, created_at, updated_at
|
||||
) VALUES ('bad-status', 'MANUAL', 'title', 'goods', 'white', 'XL', 1, '80.00', 'UNKNOWN', '2026-08-03T00:00:00Z', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert task with invalid status succeeded")
|
||||
func insertV1Task(t *testing.T, database *sql.DB, id, source, status, price string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`INSERT INTO tasks (id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price, status, created_at, updated_at) VALUES (?, ?, 'title', 'goods', 'white', 'XL', 1, ?, ?, ?, ?)`, id, source, price, status, migrationTime, migrationTime); err != nil {
|
||||
t.Fatalf("insert v1 task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
insertTask(t, database, "task-one")
|
||||
insertTask(t, database, "task-two")
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO spec_trials (
|
||||
id, task_id, attempt, product_title, selected_color, selected_size, unit_price,
|
||||
total_price, evidence_sha256, created_at
|
||||
) VALUES ('orphan-trial', 'missing-task', 1, 'title', 'white', 'XL', '32.50', '65.00', 'hash', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert spec trial without task succeeded")
|
||||
func insertV1SpecTrial(t *testing.T, database *sql.DB, id, taskID string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`INSERT INTO spec_trials (id, task_id, attempt, product_title, selected_color, selected_size, unit_price, total_price, evidence_sha256, created_at) VALUES (?, ?, 1, 'title', 'white', 'XL', '1.00', '1.00', 'hash', ?)`, id, taskID, migrationTime); err != nil {
|
||||
t.Fatalf("insert v1 spec trial: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
insertSpecTrial(t, database, "trial-one", "task-one")
|
||||
insertSpecTrial(t, database, "trial-two", "task-two")
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO order_authorizations (
|
||||
id, task_id, spec_trial_id, version, goods_id, sku_color, sku_size, quantity,
|
||||
authorized_unit_price, total_price_cap, status, created_by, created_at, expires_at
|
||||
) VALUES ('authorization-cross-task', 'task-one', 'trial-two', 1, 'goods', 'white', 'XL', 2, '32.50', '80.00', 'PENDING_DELIVERY', 'admin-one', '2026-08-03T00:00:00Z', '2026-08-03T01:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert authorization with a spec trial from another task succeeded")
|
||||
func insertV1Authorization(t *testing.T, database *sql.DB, id, taskID, trialID string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`INSERT INTO order_authorizations (id, task_id, spec_trial_id, version, goods_id, sku_color, sku_size, quantity, authorized_unit_price, total_price_cap, status, created_by, created_at, expires_at) VALUES (?, ?, ?, 1, 'goods', 'white', 'XL', 1, '1.00', '1.00', 'PENDING_DELIVERY', 'admin', ?, ?)`, id, taskID, trialID, migrationTime, migrationTime); err != nil {
|
||||
t.Fatalf("insert v1 authorization: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
insertAuthorization(t, database, "authorization-one", "task-one", "trial-one", 1)
|
||||
insertAuthorization(t, database, "authorization-task-two", "task-two", "trial-two", 1)
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO order_submissions (
|
||||
id, task_id, authorization_id, command_id, dry_run_id, status, verified_unit_price,
|
||||
quantity_read, confirm_page_amount, created_at
|
||||
) VALUES ('submission-cross-task', 'task-one', 'authorization-task-two', 'command-cross-task', 'dry-run-cross-task', 'FENCED', '32.50', 2, '65.00', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert submission with an authorization from another task succeeded")
|
||||
func insertV2Task(t *testing.T, database *sql.DB, id, source, status string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`INSERT INTO tasks (id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price, status, created_at, updated_at) VALUES (?, ?, 'title', 'goods', 'white', 'XL', 1, '1.00', ?, ?, ?)`, id, source, status, migrationTime, migrationTime); err != nil {
|
||||
t.Fatalf("insert v2 task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO order_authorizations (
|
||||
id, task_id, spec_trial_id, version, goods_id, sku_color, sku_size, quantity,
|
||||
authorized_unit_price, total_price_cap, status, created_by, created_at, expires_at
|
||||
) VALUES ('authorization-duplicate', 'task-one', 'trial-one', 1, 'goods', 'white', 'XL', 2, '32.50', '80.00', 'PENDING_DELIVERY', 'admin-one', '2026-08-03T00:00:00Z', '2026-08-03T01:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert authorization with duplicate task version succeeded")
|
||||
func insertV2Authorization(t *testing.T, database *sql.DB, id, taskID string, version int, startKey string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`INSERT INTO order_authorizations (id, task_id, task_version, start_key, goods_id, sku_color, sku_size, quantity, total_price_cap, status, created_by, created_at, expires_at) VALUES (?, ?, ?, ?, 'goods', 'white', 'XL', 1, '1.00', 'ACTIVE', 'admin', ?, ?)`, id, taskID, version, startKey, migrationTime, migrationTime); err != nil {
|
||||
t.Fatalf("insert v2 authorization: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
insertSubmission(t, database, "submission-one", "authorization-one", "command-one")
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO order_submissions (
|
||||
id, task_id, authorization_id, command_id, dry_run_id, status, verified_unit_price,
|
||||
quantity_read, confirm_page_amount, created_at
|
||||
) VALUES ('submission-duplicate-auth', 'task-one', 'authorization-one', 'command-two', 'dry-run-two', 'FENCED', '32.50', 2, '65.00', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert submission with duplicate authorization succeeded")
|
||||
func insertV2Attempt(t *testing.T, database *sql.DB, id, taskID, authorizationID string, generation int) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`INSERT INTO purchase_attempts (id, task_id, authorization_id, claim_generation, status, started_at) VALUES (?, ?, ?, ?, 'CLAIMED', ?)`, id, taskID, authorizationID, generation, migrationTime); err != nil {
|
||||
t.Fatalf("insert v2 attempt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
insertAuthorization(t, database, "authorization-two", "task-one", "trial-one", 2)
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO order_submissions (
|
||||
id, task_id, authorization_id, command_id, dry_run_id, status, verified_unit_price,
|
||||
quantity_read, confirm_page_amount, created_at
|
||||
) VALUES ('submission-duplicate-command', 'task-one', 'authorization-two', 'command-one', 'dry-run-three', 'FENCED', '32.50', 2, '65.00', '2026-08-03T00:00:00Z')
|
||||
`); err == nil {
|
||||
t.Fatal("insert submission with duplicate command succeeded")
|
||||
func insertV2Submission(t *testing.T, database *sql.DB, id, taskID, authorizationID, attemptID string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`INSERT INTO order_submissions (id, task_id, authorization_id, attempt_id, status, gate1_unit_price, gate2_unit_price, quantity_read, confirm_amount, created_at) VALUES (?, ?, ?, ?, 'FENCED', '1.00', '1.00', 1, '1.00', ?)`, id, taskID, authorizationID, attemptID, migrationTime); err != nil {
|
||||
t.Fatalf("insert v2 submission: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func v1RowCount(t *testing.T, database *sql.DB) int {
|
||||
t.Helper()
|
||||
var count int
|
||||
if err := database.QueryRow(`SELECT (SELECT COUNT(*) FROM tasks) + (SELECT COUNT(*) FROM spec_trials) + (SELECT COUNT(*) FROM order_authorizations) + (SELECT COUNT(*) FROM order_submissions)`).Scan(&count); err != nil {
|
||||
t.Fatalf("count v1 rows: %v", err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func v2RowCount(t *testing.T, database *sql.DB) int {
|
||||
t.Helper()
|
||||
var count int
|
||||
if err := database.QueryRow(`SELECT (SELECT COUNT(*) FROM tasks) + (SELECT COUNT(*) FROM order_authorizations) + (SELECT COUNT(*) FROM purchase_attempts) + (SELECT COUNT(*) FROM order_submissions)`).Scan(&count); err != nil {
|
||||
t.Fatalf("count v2 rows: %v", err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func openTestDatabase(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
database, err := sqlite.Open(filepath.Join(t.TempDir(), "migrations.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open test database: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := database.Close(); err != nil {
|
||||
t.Errorf("close test database: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
return database
|
||||
}
|
||||
|
||||
@@ -197,7 +354,6 @@ func migrationDirectory(t *testing.T) string {
|
||||
if !ok {
|
||||
t.Fatal("locate migration test source")
|
||||
}
|
||||
|
||||
return filepath.Join(filepath.Dir(file), "..", "..", "migrations")
|
||||
}
|
||||
|
||||
@@ -234,50 +390,12 @@ func assertColumnType(t *testing.T, database *sql.DB, table, column, want string
|
||||
}
|
||||
}
|
||||
|
||||
func insertTask(t *testing.T, database *sql.DB, id string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO tasks (
|
||||
id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price,
|
||||
status, created_at, updated_at
|
||||
) VALUES (?, 'MANUAL', 'title', 'goods', 'white', 'XL', 2, '80.00', 'DRAFT', '2026-08-03T00:00:00Z', '2026-08-03T00:00:00Z')
|
||||
`, id); err != nil {
|
||||
t.Fatalf("insert task: %v", err)
|
||||
func TestV2MigrationSQLDoesNotDisableForeignKeys(t *testing.T) {
|
||||
contents, err := os.ReadFile(filepath.Join(migrationDirectory(t), "00002_single_pass_model.sql"))
|
||||
if err != nil {
|
||||
t.Fatalf("read migration: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func insertSpecTrial(t *testing.T, database *sql.DB, id, taskID string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO spec_trials (
|
||||
id, task_id, attempt, product_title, selected_color, selected_size, unit_price,
|
||||
total_price, evidence_sha256, created_at
|
||||
) VALUES (?, ?, 1, 'title', 'white', 'XL', '32.50', '65.00', 'hash', '2026-08-03T00:00:00Z')
|
||||
`, id, taskID); err != nil {
|
||||
t.Fatalf("insert spec trial: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func insertAuthorization(t *testing.T, database *sql.DB, id, taskID, specTrialID string, version int) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO order_authorizations (
|
||||
id, task_id, spec_trial_id, version, goods_id, sku_color, sku_size, quantity,
|
||||
authorized_unit_price, total_price_cap, status, created_by, created_at, expires_at
|
||||
) VALUES (?, ?, ?, ?, 'goods', 'white', 'XL', 2, '32.50', '80.00', 'PENDING_DELIVERY', 'admin-one', '2026-08-03T00:00:00Z', '2026-08-03T01:00:00Z')
|
||||
`, id, taskID, specTrialID, version); err != nil {
|
||||
t.Fatalf("insert authorization: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func insertSubmission(t *testing.T, database *sql.DB, id, authorizationID, commandID string) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`
|
||||
INSERT INTO order_submissions (
|
||||
id, task_id, authorization_id, command_id, dry_run_id, status, verified_unit_price,
|
||||
quantity_read, confirm_page_amount, created_at
|
||||
) VALUES (?, 'task-one', ?, ?, 'dry-run-one', 'FENCED', '32.50', 2, '65.00', '2026-08-03T00:00:00Z')
|
||||
`, id, authorizationID, commandID); err != nil {
|
||||
t.Fatalf("insert submission: %v", err)
|
||||
if strings.Contains(strings.ToUpper(string(contents)), "PRAGMA FOREIGN_KEYS = OFF") {
|
||||
t.Fatal("migration disables foreign keys")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user