402 lines
20 KiB
Go
402 lines
20 KiB
Go
package migrations_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"cmbuyer/admin/internal/migrations"
|
|
"cmbuyer/admin/internal/storage/sqlite"
|
|
|
|
"github.com/pressly/goose/v3"
|
|
)
|
|
|
|
const migrationTime = "2026-08-04T00:00:00Z"
|
|
|
|
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, 2)
|
|
assertTableExists(t, database, "tasks", 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, 2)
|
|
|
|
if err := migrations.Down(context, database, directory); err != nil {
|
|
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 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, name string }{
|
|
{"tasks", "max_total_price"},
|
|
{"order_authorizations", "total_price_cap"},
|
|
{"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")
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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() { _ = database.Close() })
|
|
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 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)
|
|
}
|
|
if strings.Contains(strings.ToUpper(string(contents)), "PRAGMA FOREIGN_KEYS = OFF") {
|
|
t.Fatal("migration disables foreign keys")
|
|
}
|
|
}
|