package migrations import ( "bytes" "context" "crypto/sha256" "encoding/hex" "os" "path/filepath" "strings" "testing" "testing/fstest" ) func TestEmbeddedBundleMatchesCanonicalSQL(t *testing.T) { all, err := discover(sqlFiles) if err != nil { t.Fatal(err) } canonical, err := filepath.Glob("../../../database/migrations/*.sql") if err != nil { t.Fatal(err) } if len(canonical) != len(all) { t.Fatalf("canonical count %d differs from embedded count %d", len(canonical), len(all)) } for i, path := range canonical { data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } version := strings.TrimSuffix(filepath.Base(path), ".sql") if all[i].version != version || !bytes.Equal(data, []byte(all[i].sql)) { t.Fatalf("embedded migration %s differs from canonical SQL", version) } checksum := sha256.Sum256(data) if all[i].checksum != hex.EncodeToString(checksum[:]) { t.Fatalf("checksum differs for %s", version) } } } func TestDiscoverMigrations(t *testing.T) { fsys := fstest.MapFS{ "sql/0002.sql": {Data: []byte("select 2;\n")}, "sql/0001.sql": {Data: []byte("select 1;\n")}, } all, err := discover(fsys) if err != nil || len(all) != 2 || all[0].version != "0001" || all[1].version != "0002" { t.Fatalf("discover: %+v, %v", all, err) } } func TestOuterTransactionRemoval(t *testing.T) { for _, tc := range []struct { in, want string }{ {"-- title\n\nBEGIN;\nselect 1;\nCOMMIT;\n", "-- title\n\nselect 1;\n"}, {"do $$ begin raise notice 'x'; end $$;", "do $$ begin raise notice 'x'; end $$;"}, } { got, err := withoutOuterTransaction(tc.in) if err != nil || got != tc.want { t.Fatalf("got %q, %v; want %q", got, err, tc.want) } } for _, sql := range []string{"begin;\nselect 1;", "select 1;\ncommit;"} { if _, err := withoutOuterTransaction(sql); err == nil { t.Fatalf("accepted unmatched transaction: %q", sql) } } } func TestRunDisabledAndConfiguration(t *testing.T) { getter := func(mapper map[string]string) func(string) string { return func(key string) string { return mapper[key] } } if err := Run(context.Background(), getter(nil)); err != nil { t.Fatal(err) } for _, tc := range []struct { env map[string]string want string }{ {map[string]string{"DATABASE_AUTO_MIGRATE": "yes"}, "must be true or false"}, {map[string]string{"DATABASE_AUTO_MIGRATE": "true", "ZHINIAN_DATA_BACKEND": "local"}, "requires ZHINIAN_DATA_BACKEND=postgres"}, {map[string]string{"DATABASE_AUTO_MIGRATE": "true", "ZHINIAN_DATA_BACKEND": "postgres", "DATABASE_MIGRATION_TIMEOUT_SECONDS": "0"}, "must be an integer"}, {map[string]string{"DATABASE_AUTO_MIGRATE": "true", "ZHINIAN_DATA_BACKEND": "postgres", "NODE_ENV": "production"}, "DATABASE_APP_ROLE is required"}, {map[string]string{"DATABASE_AUTO_MIGRATE": "true", "ZHINIAN_DATA_BACKEND": "postgres"}, "DATABASE_MIGRATION_URL or DATABASE_URL is required"}, {map[string]string{"DATABASE_AUTO_MIGRATE": "true", "ZHINIAN_DATA_BACKEND": "postgres", "DATABASE_APP_ROLE": "bad;role"}, "DATABASE_APP_ROLE must match"}, {map[string]string{"DATABASE_AUTO_MIGRATE": "true", "ZHINIAN_DATA_BACKEND": "postgres", "DATABASE_MIGRATION_URL": "postgres://admin:secret@localhost/db1", "DATABASE_URL": "postgres://app:secret@localhost/db2"}, "must target the same"}, } { err := Run(context.Background(), getter(tc.env)) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("env %+v: got %v, want %q", tc.env, err, tc.want) } } }