Added unit tests for CLI
This commit is contained in:
1 parent
169c6c6b64
commit
5ec4b65b91
7 files changed
+965
No files matched your search
@@ -0,0 +1,101 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
// The commands are tested through the real command tree in cmd/jobsearch.
|
||||
// These tests cover reporting a failed migration, which a working set of
|
||||
// migrations can't trigger.
|
||||
|
||||
func result(path, direction string) *goose.MigrationResult {
|
||||
return &goose.MigrationResult{
|
||||
Source: &goose.Source{Path: "migrations/" + path},
|
||||
Direction: direction,
|
||||
Duration: 1500 * time.Microsecond,
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailurePartial(t *testing.T) {
|
||||
cause := errors.New("no such table: missing")
|
||||
var out bytes.Buffer
|
||||
err := failure(&out, &goose.PartialError{
|
||||
Applied: []*goose.MigrationResult{result("00001_first.sql", "up")},
|
||||
Failed: result("00002_second.sql", "up"),
|
||||
Err: cause,
|
||||
})
|
||||
|
||||
if want := " applied 00001_first.sql (2ms)\n"; out.String() != want {
|
||||
t.Errorf("output = %q, want %q", out.String(), want)
|
||||
}
|
||||
if !errors.Is(err, cause) {
|
||||
t.Errorf("err = %v, want it to wrap the migration error", err)
|
||||
}
|
||||
for _, want := range []string{"00002_second.sql", "rolled back", "last version that succeeded"} {
|
||||
if err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("err = %v, want it to mention %q", err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailureWithoutFailedSource(t *testing.T) {
|
||||
err := failure(&bytes.Buffer{}, &goose.PartialError{Err: errors.New("boom")})
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown migration") {
|
||||
t.Errorf("err = %v, want unknown migration", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailureOtherError(t *testing.T) {
|
||||
cause := errors.New("database is locked")
|
||||
var out bytes.Buffer
|
||||
if err := failure(&out, cause); err != cause {
|
||||
t.Errorf("err = %v, want the error unchanged", err)
|
||||
}
|
||||
if out.Len() != 0 {
|
||||
t.Errorf("printed %q for a non-migration error", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintResults(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
err := printResults(&out, []*goose.MigrationResult{
|
||||
result("00001_first.sql", "up"),
|
||||
result("00002_second.sql", "down"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := " applied 00001_first.sql (2ms)\n reverted 00002_second.sql (2ms)\n"
|
||||
if out.String() != want {
|
||||
t.Errorf("output = %q, want %q", out.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// failingWriter fails every write.
|
||||
type failingWriter struct{}
|
||||
|
||||
var errWrite = errors.New("write failed")
|
||||
|
||||
func (failingWriter) Write([]byte) (int, error) { return 0, errWrite }
|
||||
|
||||
// A write error is returned, and the failed migration is still reported.
|
||||
func TestWriteErrors(t *testing.T) {
|
||||
results := []*goose.MigrationResult{result("00001_first.sql", "up")}
|
||||
if err := printResults(failingWriter{}, results); !errors.Is(err, errWrite) {
|
||||
t.Errorf("printResults: err = %v, want the write error", err)
|
||||
}
|
||||
|
||||
cause := errors.New("boom")
|
||||
err := failure(failingWriter{}, &goose.PartialError{
|
||||
Applied: results, Failed: result("00002_second.sql", "up"), Err: cause,
|
||||
})
|
||||
if !errors.Is(err, cause) || !errors.Is(err, errWrite) {
|
||||
t.Errorf("failure: err = %v, want both the migration and write errors", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// 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")
|
||||
}
|
||||
Reference in new issue
Block a user