Files
job-search/internal/cli/migrate/migrate_test.go
T
bwilliams e19c8c2b69 Imports split into three groups: standard library, third-party, then this project's own packages
gci is configured with the sections standard, default and localmodule, and
goimports with the module path as its local prefix, so both formatters
agree.
2026-10-14 17:40:06 +01:00

169 lines
4.7 KiB
Go

package migrate
import (
"fmt"
"path/filepath"
"testing"
"github.com/spf13/cobra"
"git.avsdev.uk/Go/job-search/internal/cli"
"git.avsdev.uk/Go/job-search/internal/cli/cmdtest"
"git.avsdev.uk/Go/job-search/internal/store"
)
// Run with: go test -tags sqlite_fts5 ./...
//
// These tests run the migrate commands as a user would, through a root
// command like job-search's (see cmdtest).
// dbConfig is a config.yaml with a database in the test's directory.
const dbConfig = "logging:\n level: info\ndatabase:\n file: ./test.db\n"
func newRoot() *cobra.Command {
opts := &cli.Options{}
return cmdtest.NewRoot(opts, NewCommand(opts))
}
func run(t *testing.T, stdin string, args ...string) (string, error) {
t.Helper()
return cmdtest.Run(t, newRoot, stdin, args...)
}
func mustRun(t *testing.T, args ...string) string {
t.Helper()
return cmdtest.MustRun(t, newRoot, args...)
}
// latestVersion is the schema version of this build's newest migration.
func latestVersion(t *testing.T) int64 {
t.Helper()
db, err := store.OpenDB(filepath.Join(t.TempDir(), "version.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
m, err := store.NewMigrator(db)
if err != nil {
t.Fatal(err)
}
return store.LatestVersion(m)
}
func TestMigrateUpCreateThenStatus(t *testing.T) {
cmdtest.Setup(t, dbConfig)
latest := latestVersion(t)
out := mustRun(t, "migrate", "up", "--create")
cmdtest.Contains(t, out,
"applied 00001_create_opportunities_table.sql",
fmt.Sprintf("Database migrated to version %d", latest),
)
if !cmdtest.Exists("test.db") {
t.Fatal("test.db was not created")
}
out = mustRun(t, "migrate", "status")
cmdtest.Contains(t, out,
fmt.Sprintf("Database version %d of %d (up-to-date)", latest, latest),
"00001_create_opportunities_table.sql",
)
out = mustRun(t, "migrate", "up")
cmdtest.Contains(t, out, "No changes made to database (up-to-date)")
}
// Plain "migrate" is "migrate up" with every pending migration.
func TestMigrateWithoutSubcommand(t *testing.T) {
cmdtest.Setup(t, dbConfig)
cmdtest.WriteFile(t, "test.db", "") // exists, so no question is asked
out := mustRun(t, "migrate")
cmdtest.Contains(t, out, fmt.Sprintf("Database migrated to version %d", latestVersion(t)))
}
func TestMigrateUpAsksBeforeCreating(t *testing.T) {
cmdtest.Setup(t, dbConfig)
out, err := run(t, "n\n", "migrate", "up")
cmdtest.ErrContains(t, err, "no database file")
cmdtest.Contains(t, out,
"Database file './test.db' does not exist. Would you like to create it?")
if cmdtest.Exists("test.db") {
t.Error("test.db was created after answering no")
}
out, err = run(t, "y\n", "migrate", "up")
if err != nil {
t.Fatalf("answering yes: %v\n%s", err, out)
}
if !cmdtest.Exists("test.db") {
t.Error("test.db was not created after answering yes")
}
}
func TestMigrateUpBySteps(t *testing.T) {
cmdtest.Setup(t, dbConfig)
latest := latestVersion(t)
out := mustRun(t, "migrate", "up", "--create", "--steps", "2")
cmdtest.Contains(t, out, "Database migrated to version 2")
out = mustRun(t, "migrate", "status")
cmdtest.Contains(t, out,
fmt.Sprintf("Database version 2 of %d (%d pending)", latest, latest-2))
}
func TestMigrateDown(t *testing.T) {
cmdtest.Setup(t, dbConfig)
latest := latestVersion(t)
mustRun(t, "migrate", "up", "--create")
out := mustRun(t, "migrate", "down", "--steps", "1")
cmdtest.Contains(t, out, "reverted", fmt.Sprintf("Database migrated to version %d", latest-1))
// Rolling back everything deletes all data, so it must be confirmed.
for _, args := range [][]string{
{"migrate", "down", "--steps", "0"},
{"migrate", "down", "--steps", "99"},
} {
_, err := run(t, "", args...)
cmdtest.ErrContains(t, err, "pass the --yes flag")
}
cmdtest.Contains(t, mustRun(t, "migrate", "status"),
fmt.Sprintf("Database version %d of %d", latest-1, latest))
out = mustRun(t, "migrate", "down", "--steps", "0", "--yes")
cmdtest.Contains(t, out, "Database cleared")
out = mustRun(t, "migrate", "down", "--steps", "1")
cmdtest.Contains(t, out, "No changes made to database (empty)")
}
func TestMigrateDownRequiresSteps(t *testing.T) {
cmdtest.Setup(t, dbConfig)
mustRun(t, "migrate", "up", "--create")
_, err := run(t, "", "migrate", "down")
cmdtest.ErrContains(t, err, "steps")
}
func TestMigrateMissingDatabase(t *testing.T) {
cmdtest.Setup(t, dbConfig)
for _, args := range [][]string{
{"migrate", "status"},
{"migrate", "down", "--steps", "1"},
} {
_, err := run(t, "", args...)
cmdtest.ErrContains(t, err, "does not exist")
}
if cmdtest.Exists("test.db") {
t.Error("status or down created the database")
}
}
func TestMigrateWithoutDatabaseConfigured(t *testing.T) {
cmdtest.Setup(t, "logging:\n level: info\n")
_, err := run(t, "", "migrate", "status")
cmdtest.ErrContains(t, err, "no database file specified")
}