package migrations_test import ( "context" "database/sql" "path/filepath" "runtime" "testing" "cmbuyer/admin/internal/migrations" "cmbuyer/admin/internal/storage/sqlite" "github.com/pressly/goose/v3" ) func TestUpDownAndIdempotence(t *testing.T) { database := openTestDatabase(t) directory := migrationDirectory(t) context := context.Background() if err := migrations.Up(context, database, directory); err != nil { t.Fatalf("apply migrations: %v", err) } assertVersion(t, database, 1) assertTableExists(t, database, "tasks", true) assertTableExists(t, database, "spec_trials", true) assertTableExists(t, database, "order_authorizations", true) assertTableExists(t, database, "order_submissions", true) if err := migrations.Up(context, database, directory); err != nil { t.Fatalf("reapply migrations: %v", err) } assertVersion(t, database, 1) 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) } assertVersion(t, database, 1) } func TestSchemaConstraints(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 }{ {"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"}, } { 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") } 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") } 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") } 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") } 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") } 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") } 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") } 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") } 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") } 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") } 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 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) } }) return database } func migrationDirectory(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("locate migration test source") } return filepath.Join(filepath.Dir(file), "..", "..", "migrations") } func assertVersion(t *testing.T, database *sql.DB, want int64) { t.Helper() got, err := goose.GetDBVersion(database) if err != nil { t.Fatalf("read migration version: %v", err) } if got != want { t.Fatalf("migration version = %d, want %d", got, want) } } func assertTableExists(t *testing.T, database *sql.DB, table string, want bool) { t.Helper() var count int if err := database.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?`, table).Scan(&count); err != nil { t.Fatalf("look up table %s: %v", table, err) } if got := count == 1; got != want { t.Fatalf("table %s exists = %t, want %t", table, got, want) } } func assertColumnType(t *testing.T, database *sql.DB, table, column, want string) { t.Helper() var got string if err := database.QueryRow(`SELECT type FROM pragma_table_info(?) WHERE name = ?`, table, column).Scan(&got); err != nil { t.Fatalf("read %s.%s type: %v", table, column, err) } if got != want { t.Fatalf("%s.%s type = %s, want %s", table, column, got, want) } } 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 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) } }