102 lines
3 KiB
Go
102 lines
3 KiB
Go
package pgstore
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The migration SET is checkable without a database, and that check is worth having: a file whose
|
|
// name does not parse is not "skipped", it silently never runs.
|
|
func TestMigrationSetIsWellFormed(t *testing.T) {
|
|
names, err := fs.Glob(Migrations(), "*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(names) == 0 {
|
|
t.Fatal("no migrations embedded")
|
|
}
|
|
nameRe := regexp.MustCompile(`^(\d{5})_[a-z0-9_]+\.sql$`)
|
|
prev := 0
|
|
for _, name := range names {
|
|
m := nameRe.FindStringSubmatch(name)
|
|
if m == nil {
|
|
t.Fatalf("%s: goose expects NNNNN_name.sql", name)
|
|
}
|
|
version, _ := strconv.Atoi(m[1])
|
|
if version <= prev {
|
|
t.Fatalf("%s: versions must ascend and never repeat (previous %05d)", name, prev)
|
|
}
|
|
prev = version
|
|
|
|
body, err := fs.ReadFile(Migrations(), name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, marker := range []string{"-- +goose Up", "-- +goose Down"} {
|
|
if !strings.Contains(string(body), marker) {
|
|
// A missing Down is not cosmetic: a rollout that cannot be rolled back is a
|
|
// one-way door, and goose reports it only when someone tries to walk back.
|
|
t.Fatalf("%s: missing %q", name, marker)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A released migration is immutable, and this is the check that makes that true rather than
|
|
// intended. goose applies by NUMBER alone — no name, no checksum — so a file edited after it has
|
|
// run somewhere silently never runs again, and a number reused for different SQL leaves that
|
|
// database unable to roll back at all. Both were reproduced on a live PostgreSQL before this test
|
|
// existed; the prose rule that was supposed to prevent them did not.
|
|
func TestReleasedMigrationsAreUnchanged(t *testing.T) {
|
|
manifest, err := os.ReadFile("migrations.sha256")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recorded := map[string]string{}
|
|
for line := range strings.Lines(string(manifest)) {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
sum, name, ok := strings.Cut(line, " ")
|
|
if !ok {
|
|
t.Fatalf("migrations.sha256: cannot read %q", line)
|
|
}
|
|
recorded[name] = sum
|
|
}
|
|
|
|
names, err := fs.Glob(Migrations(), "*.sql")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
present := map[string]bool{}
|
|
for _, name := range names {
|
|
present[name] = true
|
|
body, err := fs.ReadFile(Migrations(), name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := fmt.Sprintf("%x", sha256.Sum256(body))
|
|
want, listed := recorded[name]
|
|
if !listed {
|
|
t.Errorf("%s is not in migrations.sha256: append its line when you add a migration", name)
|
|
continue
|
|
}
|
|
if got != want {
|
|
t.Errorf("%s changed after release (%s, recorded %s): a released migration is immutable — "+
|
|
"add a new one instead", name, got[:12], want[:12])
|
|
}
|
|
}
|
|
for name := range recorded {
|
|
if !present[name] {
|
|
t.Errorf("%s is listed in migrations.sha256 but gone: a released migration cannot be "+
|
|
"deleted, and its number cannot be reused", name)
|
|
}
|
|
}
|
|
}
|