@@ -2,55 +2,63 @@ package database
22
33import (
44 "context"
5+ "crypto/rand"
6+ "encoding/binary"
57 "fmt"
6- "sync"
78 "testing"
89 "time"
910
11+ "github.com/jackc/pgx/v5"
1012 "github.com/stretchr/testify/require"
1113)
1214
13- var (
14- // testDBMutex ensures only one test modifies the database at a time
15- // This prevents parallel tests from interfering with each other
16- testDBMutex sync.Mutex
17- )
18-
19- const (
20- // Advisory lock key for database schema initialization
21- // Using a fixed key ensures all test processes coordinate on the same lock
22- testSchemaLockKey = 123456789
23- )
24-
25- // NewTestDB creates a new PostgreSQL database connection for testing.
26- // It ensures the database schema is initialized once per test run, then just clears data per test.
15+ // NewTestDB creates an isolated PostgreSQL database for each test.
16+ // Each test gets its own database with a random name, eliminating all coordination issues.
2717// Requires PostgreSQL to be running on localhost:5432 (e.g., via docker-compose).
2818func NewTestDB (t * testing.T ) Database {
2919 t .Helper ()
3020
31- // Acquire mutex to prevent parallel test interference
32- testDBMutex .Lock ()
33- t .Cleanup (func () {
34- testDBMutex .Unlock ()
35- })
36-
37- // Create context with timeout for database operations
38- ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
21+ ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
3922 defer cancel ()
4023
41- // Connect to test database
42- connectionURI := "postgres://mcpregistry:mcpregistry@localhost:5432/mcp-registry?sslmode=disable"
43- db , err := NewPostgreSQL (ctx , connectionURI )
44- require .NoError (t , err , "Failed to connect to test PostgreSQL database. Make sure PostgreSQL is running via: docker-compose up -d postgres" )
24+ // Generate unique database name for this test
25+ var randomBytes [8 ]byte
26+ _ , err := rand .Read (randomBytes [:])
27+ require .NoError (t , err , "Failed to generate random database id" )
28+ randomInt := binary .BigEndian .Uint64 (randomBytes [:])
29+ dbName := fmt .Sprintf ("test_%d" , randomInt )
30+
31+ // Connect to postgres database to create test database
32+ adminURI := "postgres://mcpregistry:mcpregistry@localhost:5432/postgres?sslmode=disable"
33+ adminConn , err := pgx .Connect (ctx , adminURI )
34+ require .NoError (t , err , "Failed to connect to PostgreSQL. Make sure PostgreSQL is running via: docker-compose up -d postgres" )
35+ defer adminConn .Close (ctx )
4536
46- // Initialize schema once per test suite run using advisory locks for cross-process coordination
47- err = initializeTestSchemaWithLock ( db )
48- require .NoError (t , err , "Failed to initialize test database schema " )
37+ // Create test database
38+ _ , err = adminConn . Exec ( ctx , fmt . Sprintf ( "CREATE DATABASE %s" , dbName ) )
39+ require .NoError (t , err , "Failed to create test database" )
4940
50- // Clear data for this specific test
51- clearTestData (t , db )
41+ // Register cleanup to drop database
42+ t .Cleanup (func () {
43+ cleanupCtx , cleanupCancel := context .WithTimeout (context .Background (), 10 * time .Second )
44+ defer cleanupCancel ()
45+
46+ // Terminate any remaining connections
47+ _ , _ = adminConn .Exec (cleanupCtx , fmt .Sprintf (
48+ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()" ,
49+ dbName ,
50+ ))
51+
52+ // Drop database
53+ _ , _ = adminConn .Exec (cleanupCtx , fmt .Sprintf ("DROP DATABASE IF EXISTS %s" , dbName ))
54+ })
55+
56+ // Connect to test database
57+ testURI := fmt .Sprintf ("postgres://mcpregistry:mcpregistry@localhost:5432/%s?sslmode=disable" , dbName )
58+ db , err := NewPostgreSQL (ctx , testURI )
59+ require .NoError (t , err , "Failed to connect to test database" )
5260
53- // Register cleanup function to close database connection
61+ // Register cleanup to close connection
5462 t .Cleanup (func () {
5563 if err := db .Close (); err != nil {
5664 t .Logf ("Warning: failed to close test database connection: %v" , err )
@@ -59,92 +67,3 @@ func NewTestDB(t *testing.T) Database {
5967
6068 return db
6169}
62-
63- // initializeTestSchemaWithLock sets up a fresh database schema with all migrations applied
64- // Uses PostgreSQL advisory locks to ensure only one process initializes the schema
65- func initializeTestSchemaWithLock (db Database ) error {
66- // Cast to PostgreSQL to access the connection pool
67- pgDB , ok := db .(* PostgreSQL )
68- if ! ok {
69- return fmt .Errorf ("expected PostgreSQL database instance" )
70- }
71-
72- ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
73- defer cancel ()
74-
75- // Acquire advisory lock to coordinate schema initialization across processes
76- _ , err := pgDB .pool .Exec (ctx , "SELECT pg_advisory_lock($1)" , testSchemaLockKey )
77- if err != nil {
78- return fmt .Errorf ("failed to acquire advisory lock: %w" , err )
79- }
80- defer func () {
81- // Always release the advisory lock
82- _ , _ = pgDB .pool .Exec (context .Background (), "SELECT pg_advisory_unlock($1)" , testSchemaLockKey )
83- }()
84-
85- // Check if schema already exists (another process may have initialized it)
86- var tableCount int64
87- err = pgDB .pool .QueryRow (ctx , "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'servers'" ).Scan (& tableCount )
88- if err != nil {
89- return fmt .Errorf ("failed to check if schema exists: %w" , err )
90- }
91-
92- if tableCount > 0 {
93- // Schema already exists, nothing to do
94- return nil
95- }
96-
97- // Initialize the schema
98- return initializeTestSchema (db )
99- }
100-
101- // initializeTestSchema sets up a fresh database schema with all migrations applied
102- // This should only be called from initializeTestSchemaWithLock
103- func initializeTestSchema (db Database ) error {
104- // Cast to PostgreSQL to access the connection pool
105- pgDB , ok := db .(* PostgreSQL )
106- if ! ok {
107- return fmt .Errorf ("expected PostgreSQL database instance" )
108- }
109-
110- ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
111- defer cancel ()
112-
113- // Drop and recreate schema completely fresh
114- _ , err := pgDB .pool .Exec (ctx , "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" )
115- if err != nil {
116- return fmt .Errorf ("failed to reset database schema: %w" , err )
117- }
118-
119- // Apply all migrations from scratch
120- conn , err := pgDB .pool .Acquire (ctx )
121- if err != nil {
122- return fmt .Errorf ("failed to acquire connection for migration: %w" , err )
123- }
124- defer conn .Release ()
125-
126- migrator := NewMigrator (conn .Conn ())
127- err = migrator .Migrate (ctx )
128- if err != nil {
129- return fmt .Errorf ("failed to run database migrations: %w" , err )
130- }
131-
132- return nil
133- }
134-
135- // clearTestData removes all data from test tables while preserving schema
136- // This runs before each individual test
137- func clearTestData (t * testing.T , db Database ) {
138- t .Helper ()
139-
140- // Cast to PostgreSQL to access the connection pool
141- pgDB , ok := db .(* PostgreSQL )
142- require .True (t , ok , "Expected PostgreSQL database instance" )
143-
144- ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
145- defer cancel ()
146-
147- // Clear all data but keep schema intact
148- _ , err := pgDB .pool .Exec (ctx , "TRUNCATE TABLE servers RESTART IDENTITY CASCADE" )
149- require .NoError (t , err , "Failed to clear test data" )
150- }
0 commit comments