🧪 test: add JWT secret rotation BDD scenarios and step implementations (#12)
✨ merge: implement JWT secret rotation with BDD scenario isolation - Implement JWT secret rotation mechanism (closes #8) - Add per-scenario state isolation for BDD tests (closes #14) - Validate password reset workflow via BDD tests (closes #7) - Fix port conflicts in test validation - Add state tracer for debugging test execution - Document BDD isolation strategies in ADR 0025 - Fix PostgreSQL configuration environment variables Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai> Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #12.
This commit is contained in:
131
pkg/bdd/suite.go
131
pkg/bdd/suite.go
@@ -1,6 +1,11 @@
|
||||
package bdd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dance-lessons-coach/pkg/bdd/steps"
|
||||
"dance-lessons-coach/pkg/bdd/testserver"
|
||||
|
||||
@@ -9,31 +14,137 @@ import (
|
||||
)
|
||||
|
||||
var sharedServer *testserver.Server
|
||||
var sharedStepContext *steps.StepContext
|
||||
|
||||
// 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 {
|
||||
panic(err)
|
||||
// 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 environment - falls back to "bdd" for multi-feature tests
|
||||
feature := os.Getenv("FEATURE")
|
||||
if feature == "" {
|
||||
feature = "bdd"
|
||||
}
|
||||
|
||||
// Generate scenario key for state isolation
|
||||
scenarioKey := s.Name
|
||||
if s.Uri != "" {
|
||||
scenarioKey = fmt.Sprintf("%s:%s", s.Uri, s.Name)
|
||||
}
|
||||
|
||||
// Set scenario key on all step instances for state isolation
|
||||
if sharedStepContext != nil {
|
||||
steps.SetScenarioKeyForAllSteps(sharedStepContext, scenarioKey)
|
||||
// Also clear state for this scenario to ensure clean start
|
||||
steps.ClearScenarioState(scenarioKey)
|
||||
}
|
||||
|
||||
if isCleanupLoggingEnabled() {
|
||||
log.Info().Str("feature", feature).Str("scenario", s.Name).Msg("CLEANUP: Scenario starting")
|
||||
}
|
||||
|
||||
// Trace scenario start
|
||||
testserver.TraceStateScenarioStart(feature, scenarioKey)
|
||||
|
||||
// Setup schema isolation if enabled
|
||||
if sharedServer != nil {
|
||||
if err := sharedServer.SetupScenarioSchema(feature, scenarioKey); err != nil {
|
||||
if isCleanupLoggingEnabled() {
|
||||
log.Warn().Err(err).Str("feature", feature).Str("scenario", scenarioKey).Msg("ISOLATION: Failed to setup scenario schema")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
sc.AfterScenario(func(s *godog.Scenario, err error) {
|
||||
// Get feature name from environment - falls back to "bdd" for multi-feature tests
|
||||
feature := os.Getenv("FEATURE")
|
||||
if feature == "" {
|
||||
feature = "bdd"
|
||||
}
|
||||
|
||||
if isCleanupLoggingEnabled() {
|
||||
log.Info().Str("scenario", s.Name).Str("status", "completed").Err(err).Msg("CLEANUP: Scenario completed")
|
||||
}
|
||||
|
||||
// Trace scenario end
|
||||
scenarioKey := s.Name
|
||||
if s.Uri != "" {
|
||||
scenarioKey = fmt.Sprintf("%s:%s", s.Uri, s.Name)
|
||||
}
|
||||
testserver.TraceStateScenarioEnd(feature, scenarioKey, err)
|
||||
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
testserver.TraceStateJWTSecretOperation(feature, scenarioKey, "RESET", "ok")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
} else {
|
||||
testserver.TraceStateDBCleanup(feature, scenarioKey, "all_tables")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ctx.AfterSuite(func() {
|
||||
if sharedServer != nil {
|
||||
// Cleanup database after all tests
|
||||
if err := sharedServer.CleanupDatabase(); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cleanup database after suite")
|
||||
// Final cleanup
|
||||
if err := sharedServer.Stop(); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to shutdown HTTP server")
|
||||
}
|
||||
// Close database connection
|
||||
if err := sharedServer.CloseDatabase(); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to close database connection")
|
||||
}
|
||||
sharedServer.Stop()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
// Clear all scenario states
|
||||
steps.ClearAllScenarioStates()
|
||||
steps.CleanupAllTestConfigFiles()
|
||||
})
|
||||
}
|
||||
|
||||
func InitializeScenario(ctx *godog.ScenarioContext) {
|
||||
client := testserver.NewClient(sharedServer)
|
||||
steps.InitializeAllSteps(ctx, client)
|
||||
// Create and store the step context for scenario isolation
|
||||
sharedStepContext = steps.NewStepContext(client)
|
||||
steps.InitializeAllSteps(ctx, client, sharedStepContext)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user