Files
dance-lessons-coach/pkg/bdd/suite.go
Gabriel Radureau 0aedf829de
All checks were successful
CI/CD Pipeline / Build Docker Cache (push) Successful in 14s
CI/CD Pipeline / CI Pipeline (push) Successful in 3m52s
feat: implement schema-per-scenario isolation for BDD tests
- Add BDD_SCHEMA_ISOLATION env var to enable schema-per-scenario mode
- Generate unique schema names using SHA256 hash of feature+scenario
- Implement SetupScenarioSchema() and TeardownScenarioSchema() methods
- Handle search_path configuration for schema isolation
- Use CASCADE drop to clean up all scenario-created DB objects
- Add isSchemaIsolationEnabled() helper to suite.go
- Update cleanup flow: skip table clearing when schema isolation is active
- Add ADR 0025 documenting isolation strategies and decision rationale

Activation: Set BDD_SCHEMA_ISOLATION=true to enable
Debug: Set BDD_ENABLE_CLEANUP_LOGS=true for verbose isolation logging

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-10 22:46:02 +02:00

114 lines
3.4 KiB
Go

package bdd
import (
"fmt"
"os"
"strings"
"time"
"dance-lessons-coach/pkg/bdd/steps"
"dance-lessons-coach/pkg/bdd/testserver"
"github.com/cucumber/godog"
"github.com/rs/zerolog/log"
)
var sharedServer *testserver.Server
// isCleanupLoggingEnabled returns true if BDD_ENABLE_CLEANUP_LOGS environment variable is set to "true"
func isCleanupLoggingEnabled() bool {
return os.Getenv("BDD_ENABLE_CLEANUP_LOGS") == "true"
}
// isSchemaIsolationEnabled returns true if BDD_SCHEMA_ISOLATION environment variable is set to "true"
func isSchemaIsolationEnabled() bool {
return os.Getenv("BDD_SCHEMA_ISOLATION") == "true"
}
func InitializeTestSuite(ctx *godog.TestSuiteContext) {
ctx.BeforeSuite(func() {
// Small delay to ensure any previous server instances are fully cleaned up
time.Sleep(50 * time.Millisecond)
sharedServer = testserver.NewServer()
if err := sharedServer.Start(); err != nil {
// Improved error message for port conflicts
if strings.Contains(err.Error(), "address already in use") {
panic(fmt.Sprintf("Port conflict: %v. Try running 'lsof -i :9191' and 'kill -9 <PID>' to free the port", err))
}
panic(fmt.Sprintf("Failed to start test server: %v", err))
}
})
sc := ctx.ScenarioContext()
sc.BeforeScenario(func(s *godog.Scenario) {
// Get feature name from context or environment
feature := os.Getenv("FEATURE")
if feature == "" {
// Try to extract feature from scenario tags or path
feature = "unknown"
}
if isCleanupLoggingEnabled() {
log.Info().Str("feature", feature).Str("scenario", s.Name).Msg("CLEANUP: Scenario starting")
}
// Setup schema isolation if enabled
if sharedServer != nil {
if err := sharedServer.SetupScenarioSchema(feature, s.Name); err != nil {
if isCleanupLoggingEnabled() {
log.Warn().Err(err).Msg("ISOLATION: Failed to setup scenario schema")
}
}
}
})
sc.AfterScenario(func(s *godog.Scenario, err error) {
if isCleanupLoggingEnabled() {
log.Info().Str("scenario", s.Name).Str("status", "completed").Err(err).Msg("CLEANUP: Scenario completed")
}
if sharedServer != nil {
// Teardown schema isolation if enabled
if teardownErr := sharedServer.TeardownScenarioSchema(); teardownErr != nil {
if isCleanupLoggingEnabled() {
log.Warn().Err(teardownErr).Msg("ISOLATION: Failed to teardown scenario schema")
}
}
// Reset JWT secrets after every scenario to prevent pollution
// Note: This is still needed for in-memory state even with schema isolation
if resetErr := sharedServer.ResetJWTSecrets(); resetErr != nil {
if isCleanupLoggingEnabled() {
log.Warn().Err(resetErr).Msg("CLEANUP: Failed to reset JWT secrets after scenario")
}
}
// Clean database after every scenario (only if schema isolation is disabled)
if !isSchemaIsolationEnabled() {
if cleanupErr := sharedServer.CleanupDatabase(); cleanupErr != nil {
if isCleanupLoggingEnabled() {
log.Warn().Err(cleanupErr).Msg("CLEANUP: Failed to cleanup database after scenario")
}
}
}
}
})
ctx.AfterSuite(func() {
if sharedServer != nil {
// Final cleanup
if err := sharedServer.Stop(); err != nil {
log.Warn().Err(err).Msg("Failed to shutdown HTTP server")
}
time.Sleep(100 * time.Millisecond)
}
steps.CleanupAllTestConfigFiles()
})
}
func InitializeScenario(ctx *godog.ScenarioContext) {
client := testserver.NewClient(sharedServer)
steps.InitializeAllSteps(ctx, client)
}