@@ -4,86 +4,178 @@ package database_test
44
55import (
66 "context"
7+ "fmt"
8+ "os"
79 "path/filepath"
810 "runtime"
11+ "sort"
12+ "strings"
913 "testing"
1014
11- "github.com/capyrpi/api/internal/database"
1215 "github.com/capyrpi/api/internal/testutils"
13- "github.com/jackc/pgx/v5/pgtype"
14- "github.com/stretchr/testify/assert"
16+ "github.com/jackc/pgx/v5/pgxpool"
1517 "github.com/stretchr/testify/require"
1618)
1719
18- func TestRunMigrations_AppliesInitAndIsIdempotent (t * testing.T ) {
19- connStr := testutils .SetupTestPostgres (t )
20- migrationsPath := testMigrationsPath (t )
20+ func TestMigrationsApplyAndRollback (t * testing.T ) {
21+ pool := testutils .SetupEmptyTestDB (t )
22+ defer pool .Close ()
23+
2124 ctx := context .Background ()
25+ migrationsDir := repoRoot (t , 2 )
26+ migrationsDir = filepath .Join (migrationsDir , "migrations" )
2227
23- require .NoError (t , database .RunMigrations (ctx , connStr , migrationsPath ))
24- require .NoError (t , database .RunMigrations (ctx , connStr , migrationsPath ))
28+ upFiles , downFiles := migrationFiles (t , migrationsDir )
29+ require .NotEmpty (t , upFiles , "no migration files found in %s" , migrationsDir )
30+ require .Equal (t , len (upFiles ), len (downFiles ), "up/down migration file count mismatch" )
2531
26- pool , err := database .NewPool (ctx , connStr )
27- require .NoError (t , err )
28- defer pool .Close ()
32+ beforeState := snapshotPublicSchema (t , ctx , pool )
2933
30- for _ , table := range []string {
31- "users" ,
32- "organizations" ,
33- "org_members" ,
34- "events" ,
35- "event_hosting" ,
36- "event_registrations" ,
37- "bot_tokens" ,
38- } {
39- var regclass pgtype.Text
40- err := pool .QueryRow (ctx , "SELECT to_regclass($1)::text" , "public." + table ).Scan (& regclass )
34+ for i , path := range upFiles {
35+ sqlBytes , err := os .ReadFile (path )
4136 require .NoError (t , err )
42- assert .Truef (t , regclass .Valid , "expected table %s to exist" , table )
37+ _ , err = pool .Exec (ctx , string (sqlBytes ))
38+ require .NoErrorf (t , err , "failed applying up migration %d: %s" , i , filepath .Base (path ))
4339 }
4440
45- var version int64
46- var dirty bool
47- err = pool .QueryRow (ctx , "SELECT version, dirty FROM schema_migrations" ).Scan (& version , & dirty )
41+ afterUpState := snapshotPublicSchema (t , ctx , pool )
42+ require .NotEqual (t , beforeState , afterUpState , "expected schema to change after applying up migrations" )
43+
44+ for i := len (downFiles ) - 1 ; i >= 0 ; i -- {
45+ path := downFiles [i ]
46+ sqlBytes , err := os .ReadFile (path )
47+ require .NoError (t , err )
48+ _ , err = pool .Exec (ctx , string (sqlBytes ))
49+ require .NoErrorf (t , err , "failed applying down migration %d: %s" , i , filepath .Base (path ))
50+ }
51+
52+ afterDownState := snapshotPublicSchema (t , ctx , pool )
53+ require .Equal (t , beforeState , afterDownState , "expected schema state after down migrations to match initial state" )
54+ }
55+
56+ func migrationFiles (t * testing.T , dir string ) ([]string , []string ) {
57+ t .Helper ()
58+
59+ entries , err := os .ReadDir (dir )
4860 require .NoError (t , err )
49- assert .Equal (t , int64 (1 ), version )
50- assert .False (t , dirty )
61+
62+ var upFiles []string
63+ var downFiles []string
64+
65+ for _ , entry := range entries {
66+ if entry .IsDir () {
67+ continue
68+ }
69+ name := entry .Name ()
70+ fullPath := filepath .Join (dir , name )
71+
72+ switch {
73+ case strings .HasSuffix (name , ".up.sql" ):
74+ upFiles = append (upFiles , fullPath )
75+ case strings .HasSuffix (name , ".down.sql" ):
76+ downFiles = append (downFiles , fullPath )
77+ }
78+ }
79+
80+ sort .Strings (upFiles )
81+ sort .Strings (downFiles )
82+ return upFiles , downFiles
5183}
5284
53- func TestRunMigrations_DownAndUp (t * testing.T ) {
54- connStr := testutils .SetupTestPostgres (t )
55- migrationsPath := testMigrationsPath (t )
56- ctx := context .Background ()
85+ func repoRoot (t * testing.T , upLevels int ) string {
86+ t .Helper ()
5787
58- require . NoError ( t , database . RunMigrations ( ctx , connStr , migrationsPath ) )
59- require .NoError (t , database . RunMigrationsDown ( ctx , connStr , migrationsPath , 1 ) )
88+ _ , filename , _ , ok := runtime . Caller ( 0 )
89+ require .True (t , ok )
6090
61- pool , err := database .NewPool (ctx , connStr )
62- require .NoError (t , err )
91+ root := filepath .Dir (filename )
92+ for range upLevels {
93+ root = filepath .Dir (root )
94+ }
95+ return root
96+ }
6397
64- var regclass pgtype.Text
65- err = pool .QueryRow (ctx , "SELECT to_regclass('public.users')::text" ).Scan (& regclass )
66- require .NoError (t , err )
67- assert .False (t , regclass .Valid , "users table should be removed after rolling back init migration" )
68- pool .Close ()
98+ type schemaSnapshot struct {
99+ Tables []string
100+ Columns []string
101+ Indexes []string
102+ Views []string
103+ Sequences []string
104+ Enums []string
105+ }
69106
70- require .NoError (t , database .RunMigrations (ctx , connStr , migrationsPath ))
107+ func snapshotPublicSchema (t * testing.T , ctx context.Context , pool * pgxpool.Pool ) schemaSnapshot {
108+ t .Helper ()
71109
72- pool , err = database .NewPool (ctx , connStr )
110+ return schemaSnapshot {
111+ Tables : querySingleColumn (t , ctx , pool , `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name` ),
112+ Columns : queryColumns (t , ctx , pool ),
113+ Indexes : querySingleColumn (t , ctx , pool , `SELECT indexname || ':' || indexdef FROM pg_indexes WHERE schemaname = 'public' ORDER BY indexname` ),
114+ Views : querySingleColumn (t , ctx , pool , `SELECT table_name FROM information_schema.views WHERE table_schema = 'public' ORDER BY table_name` ),
115+ Sequences : querySingleColumn (t , ctx , pool , `SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public' ORDER BY sequence_name` ),
116+ Enums : queryEnums (t , ctx , pool ),
117+ }
118+ }
119+
120+ func querySingleColumn (t * testing.T , ctx context.Context , pool * pgxpool.Pool , sql string ) []string {
121+ t .Helper ()
122+
123+ rows , err := pool .Query (ctx , sql )
73124 require .NoError (t , err )
74- defer pool .Close ()
125+ defer rows .Close ()
75126
76- err = pool .QueryRow (ctx , "SELECT to_regclass('public.users')::text" ).Scan (& regclass )
127+ var out []string
128+ for rows .Next () {
129+ var v string
130+ require .NoError (t , rows .Scan (& v ))
131+ out = append (out , v )
132+ }
133+ require .NoError (t , rows .Err ())
134+ return out
135+ }
136+
137+ func queryColumns (t * testing.T , ctx context.Context , pool * pgxpool.Pool ) []string {
138+ t .Helper ()
139+
140+ rows , err := pool .Query (ctx , `
141+ SELECT table_name, column_name, data_type, is_nullable, COALESCE(column_default, '')
142+ FROM information_schema.columns
143+ WHERE table_schema = 'public'
144+ ORDER BY table_name, ordinal_position
145+ ` )
77146 require .NoError (t , err )
78- assert .True (t , regclass .Valid , "users table should exist after re-applying migrations" )
147+ defer rows .Close ()
148+
149+ var out []string
150+ for rows .Next () {
151+ var tableName , columnName , dataType , isNullable , columnDefault string
152+ require .NoError (t , rows .Scan (& tableName , & columnName , & dataType , & isNullable , & columnDefault ))
153+ out = append (out , fmt .Sprintf ("%s.%s:%s:%s:%s" , tableName , columnName , dataType , isNullable , columnDefault ))
154+ }
155+ require .NoError (t , rows .Err ())
156+ return out
79157}
80158
81- func testMigrationsPath (t * testing.T ) string {
159+ func queryEnums (t * testing.T , ctx context. Context , pool * pgxpool. Pool ) [] string {
82160 t .Helper ()
83161
84- _ , filename , _ , ok := runtime .Caller (0 )
85- require .True (t , ok )
162+ rows , err := pool .Query (ctx , `
163+ SELECT t.typname, e.enumlabel
164+ FROM pg_type t
165+ JOIN pg_enum e ON e.enumtypid = t.oid
166+ JOIN pg_namespace n ON n.oid = t.typnamespace
167+ WHERE n.nspname = 'public'
168+ ORDER BY t.typname, e.enumsortorder
169+ ` )
170+ require .NoError (t , err )
171+ defer rows .Close ()
86172
87- projectRoot := filepath .Join (filepath .Dir (filename ), "../.." )
88- return filepath .Join (projectRoot , "migrations" )
173+ var out []string
174+ for rows .Next () {
175+ var typeName , label string
176+ require .NoError (t , rows .Scan (& typeName , & label ))
177+ out = append (out , fmt .Sprintf ("%s:%s" , typeName , label ))
178+ }
179+ require .NoError (t , rows .Err ())
180+ return out
89181}
0 commit comments