✨ feat(user): NewPostgresRepositoryFromDSN factory + integration test (T12 stage 1/2)
First building block for parallel-safe BDD scenario isolation (T12 plan, ADR-0025 follow-up). PR #28 had to revert BDD_SCHEMA_ISOLATION because SetupScenarioSchema created an empty schema without migrations -- the production server's repo never saw it. This PR adds the missing piece: a factory that opens a *PostgresRepository connected via an arbitrary DSN AND runs AutoMigrate against it, so a per-scenario schema actually gets the users table. Public API additions in pkg/user/postgres_repository.go: - NewPostgresRepositoryFromDSN(cfg, dsn) (*PostgresRepository, error) Opens the repo from an explicit DSN (overrides cfg's host/port/etc), runs AutoMigrate -- creates tables in whatever schema the DSN's search_path points to. - BuildSchemaIsolatedDSN(cfg, schemaName) string Builds a DSN with `search_path=<schemaName>` from a base config. The existing NewPostgresRepository(cfg) is unchanged. Existing Close() method is reused. Integration test in postgres_repository_isolated_test.go proves: - AutoMigrate creates `users` table in the per-scenario schema (not public) - A CreateUser through the isolated repo writes into the per-scenario schema - public.users sees ZERO rows for the test username - The per-scenario schema users table sees exactly 1 row Test skips gracefully when DLC_DATABASE_HOST is not set. Out of scope (T12 stage 2/2 next): - Wiring this factory into pkg/bdd/testserver/SetupScenarioSchema - Spawning a fresh server.Server per scenario (requires NewServerWithUserRepo) - Removing -p 1 from scripts/run-bdd-tests.sh after parallel safety is achieved Per code-reviewer skill SOLID/DDD section : - SRP : factory has single responsibility (open + migrate, no business logic) - OCP : the new factory extends the package without changing existing callers - Cognitive load : 1 file, 50 lines added, 1 dedicated test file 🤖 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,67 @@ func NewPostgresRepository(cfg *config.Config) (*PostgresRepository, error) {
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// NewPostgresRepositoryFromDSN creates a PostgresRepository connected via the given DSN
|
||||
// and runs AutoMigrate against it. Used by BDD test infra to create a per-scenario
|
||||
// repository pointing at an isolated schema (the DSN typically includes search_path=<schema>).
|
||||
//
|
||||
// Pass the same cfg used elsewhere (it is required by methods that read pool settings),
|
||||
// but the DSN passed here OVERRIDES the host/port/dbname/etc that cfg would have built.
|
||||
func NewPostgresRepositoryFromDSN(cfg *config.Config, dsn string) (*PostgresRepository, error) {
|
||||
repo := &PostgresRepository{
|
||||
config: cfg,
|
||||
spanPrefix: "user.repo.",
|
||||
}
|
||||
|
||||
gormLogger := logger.New(
|
||||
log.New(os.Stderr, "\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second,
|
||||
LogLevel: logger.Warn,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: false,
|
||||
},
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: gormLogger})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to PostgreSQL with custom DSN: %w", err)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get sql.DB from gorm: %w", err)
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(cfg.GetDatabaseMaxOpenConns())
|
||||
sqlDB.SetMaxIdleConns(cfg.GetDatabaseMaxIdleConns())
|
||||
sqlDB.SetConnMaxLifetime(cfg.GetDatabaseConnMaxLifetime())
|
||||
|
||||
if err := db.AutoMigrate(&User{}); err != nil {
|
||||
return nil, fmt.Errorf("failed to auto-migrate via custom DSN: %w", err)
|
||||
}
|
||||
|
||||
repo.db = db
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// BuildSchemaIsolatedDSN returns a Postgres DSN that targets the given schema via
|
||||
// the search_path connection parameter. Use with NewPostgresRepositoryFromDSN to
|
||||
// get a repository whose connection only sees the per-scenario schema.
|
||||
func BuildSchemaIsolatedDSN(cfg *config.Config, schemaName string) string {
|
||||
return fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s search_path=%s",
|
||||
cfg.GetDatabaseHost(),
|
||||
cfg.GetDatabasePort(),
|
||||
cfg.GetDatabaseUser(),
|
||||
cfg.GetDatabasePassword(),
|
||||
cfg.GetDatabaseName(),
|
||||
cfg.GetDatabaseSSLMode(),
|
||||
schemaName,
|
||||
)
|
||||
}
|
||||
|
||||
// (Close already exists below; we reuse it.)
|
||||
|
||||
// initializeDatabase sets up the PostgreSQL database connection and runs migrations
|
||||
func (r *PostgresRepository) initializeDatabase() error {
|
||||
// Configure GORM logger based on config
|
||||
|
||||
Reference in New Issue
Block a user