package user import ( "context" "fmt" "os" "testing" "dance-lessons-coach/pkg/config" _ "github.com/lib/pq" ) // TestNewPostgresRepositoryFromDSN_SchemaIsolation verifies that the factory // + BuildSchemaIsolatedDSN combo produces a repository whose AutoMigrate // creates the users table inside a per-scenario schema (NOT public). // // This is the foundation block for parallel-safe BDD tests (T12). // Wiring it into the BDD testserver's SetupScenarioSchema is a follow-up. // // Skipped if Postgres is not available (no env vars / connection refused). func TestNewPostgresRepositoryFromDSN_SchemaIsolation(t *testing.T) { host := os.Getenv("DLC_DATABASE_HOST") if host == "" { t.Skip("DLC_DATABASE_HOST not set, skipping integration test") } cfg := &config.Config{} cfg.Database.Host = host cfg.Database.Port = parsePortOrDefault(os.Getenv("DLC_DATABASE_PORT"), 5432) cfg.Database.User = envOr("DLC_DATABASE_USER", "postgres") cfg.Database.Password = envOr("DLC_DATABASE_PASSWORD", "postgres") cfg.Database.Name = envOr("DLC_DATABASE_NAME", "dance_lessons_coach_bdd_test") cfg.Database.SSLMode = envOr("DLC_DATABASE_SSL_MODE", "disable") schemaName := "test_isolated_dsn_factory" // Open default repo (public schema) just to manage the schema lifecycle defaultRepo, err := NewPostgresRepository(cfg) if err != nil { t.Skipf("Postgres unavailable: %v", err) } defer defaultRepo.Close() // Drop schema if it exists from a previous run if err := defaultRepo.db.Exec(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName)).Error; err != nil { t.Fatalf("DROP SCHEMA setup failed: %v", err) } defer func() { _ = defaultRepo.db.Exec(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName)).Error }() // CREATE SCHEMA if err := defaultRepo.db.Exec(fmt.Sprintf("CREATE SCHEMA %s", schemaName)).Error; err != nil { t.Fatalf("CREATE SCHEMA failed: %v", err) } // Now use the factory to open a repo whose connection has search_path=schemaName dsn := BuildSchemaIsolatedDSN(cfg, schemaName) isolatedRepo, err := NewPostgresRepositoryFromDSN(cfg, dsn) if err != nil { t.Fatalf("NewPostgresRepositoryFromDSN failed: %v", err) } defer isolatedRepo.Close() // Verify the users table now exists in our schema (not just in public) var count int64 q := fmt.Sprintf("SELECT count(*) FROM information_schema.tables WHERE table_schema='%s' AND table_name='users'", schemaName) if err := isolatedRepo.db.Raw(q).Scan(&count).Error; err != nil { t.Fatalf("information_schema query failed: %v", err) } if count != 1 { t.Fatalf("expected users table in schema %s after AutoMigrate, count=%d", schemaName, count) } // Verify a CreateUser via the isolated repo writes into the new schema, NOT public u := &User{Username: "isolated_factory_user", PasswordHash: "x"} if err := isolatedRepo.CreateUser(context.Background(), u); err != nil { t.Fatalf("CreateUser via isolated repo failed: %v", err) } var publicCount int64 if err := defaultRepo.db.Raw(fmt.Sprintf("SELECT count(*) FROM public.users WHERE username='%s'", u.Username)).Scan(&publicCount).Error; err != nil { t.Fatalf("query public.users failed: %v", err) } if publicCount != 0 { t.Fatalf("isolation leak: expected 0 rows in public.users for username=%s, got %d", u.Username, publicCount) } var schemaCount int64 if err := isolatedRepo.db.Raw(fmt.Sprintf("SELECT count(*) FROM %s.users WHERE username='%s'", schemaName, u.Username)).Scan(&schemaCount).Error; err != nil { t.Fatalf("query schema users failed: %v", err) } if schemaCount != 1 { t.Fatalf("expected 1 row in %s.users, got %d", schemaName, schemaCount) } } // envOr returns the env var value or the fallback if empty. func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // parsePortOrDefault parses a port string or returns the fallback. func parsePortOrDefault(s string, fallback int) int { if s == "" { return fallback } var n int _, err := fmt.Sscanf(s, "%d", &n) if err != nil || n <= 0 { return fallback } return n }