🧪 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:
228
pkg/bdd/testsetup/testsetup.go
Normal file
228
pkg/bdd/testsetup/testsetup.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package testsetup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"dance-lessons-coach/pkg/bdd"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
// getWorkingDir returns the current working directory
|
||||
func getWorkingDir() string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// FeatureConfig holds configuration for a feature test
|
||||
type FeatureConfig struct {
|
||||
FeatureName string
|
||||
Format string
|
||||
StopOnFailure bool
|
||||
}
|
||||
|
||||
// MultiFeatureConfig holds configuration for multi-feature tests
|
||||
type MultiFeatureConfig struct {
|
||||
Paths []string
|
||||
Format string
|
||||
StopOnFailure bool
|
||||
}
|
||||
|
||||
// NewFeatureConfig creates a new feature configuration
|
||||
func NewFeatureConfig(featureName, format string, stopOnFailure bool) *FeatureConfig {
|
||||
return &FeatureConfig{
|
||||
FeatureName: featureName,
|
||||
Format: format,
|
||||
StopOnFailure: stopOnFailure,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMultiFeatureConfig creates a new multi-feature configuration
|
||||
func NewMultiFeatureConfig(paths []string, format string, stopOnFailure bool) *MultiFeatureConfig {
|
||||
return &MultiFeatureConfig{
|
||||
Paths: paths,
|
||||
Format: format,
|
||||
StopOnFailure: stopOnFailure,
|
||||
}
|
||||
}
|
||||
|
||||
// GetFeatureFromEnv gets the feature name from environment variable
|
||||
func GetFeatureFromEnv() string {
|
||||
return os.Getenv("FEATURE")
|
||||
}
|
||||
|
||||
// GetAllFeaturePaths returns paths for all features by scanning the filesystem
|
||||
func GetAllFeaturePaths() []string {
|
||||
// Get the project root directory
|
||||
projectRoot, err := getProjectRoot()
|
||||
if err != nil {
|
||||
// Fallback to hardcoded list if we can't determine project root
|
||||
return []string{
|
||||
"auth",
|
||||
"config",
|
||||
"greet",
|
||||
"health",
|
||||
"jwt",
|
||||
}
|
||||
}
|
||||
|
||||
// Read the features directory from project root
|
||||
featuresPath := filepath.Join(projectRoot, "features")
|
||||
entries, err := os.ReadDir(featuresPath)
|
||||
if err != nil {
|
||||
// Fallback to hardcoded list if filesystem access fails
|
||||
return []string{
|
||||
"auth",
|
||||
"config",
|
||||
"greet",
|
||||
"health",
|
||||
"jwt",
|
||||
}
|
||||
}
|
||||
|
||||
var paths []string
|
||||
for _, entry := range entries {
|
||||
// Only include directories (features) that are not hidden and not test files
|
||||
if entry.IsDir() && !strings.HasPrefix(entry.Name(), ".") {
|
||||
paths = append(paths, entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// Sort paths for consistent ordering
|
||||
sort.Strings(paths)
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// getProjectRoot finds the project root directory by looking for go.mod
|
||||
func getProjectRoot() (string, error) {
|
||||
// Start from current directory and walk up the tree
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Walk up the directory tree until we find go.mod or reach root
|
||||
for {
|
||||
// Check if go.mod exists in current directory
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// Move up one directory
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
// Reached root directory
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
// If we get here, we didn't find go.mod - return original working directory
|
||||
return "", fmt.Errorf("could not find project root (go.mod not found)")
|
||||
}
|
||||
|
||||
// CreateTestSuite creates a configured godog test suite
|
||||
func CreateTestSuite(t *testing.T, config *FeatureConfig, suiteName string) godog.TestSuite {
|
||||
// Set FEATURE environment variable for feature-specific configuration
|
||||
os.Setenv("FEATURE", config.FeatureName)
|
||||
|
||||
// Allow tag override via environment variable
|
||||
tags := os.Getenv("GODOG_TAGS")
|
||||
if tags == "" {
|
||||
// Default tags if not overridden
|
||||
tags = "~@flaky && ~@todo && ~@skip"
|
||||
}
|
||||
|
||||
// Allow stop on failure override via environment variable
|
||||
stopOnFailure := config.StopOnFailure
|
||||
if envStop := os.Getenv("GODOG_STOP_ON_FAILURE"); envStop != "" {
|
||||
// Support various boolean formats
|
||||
stopOnFailure, _ = strconv.ParseBool(envStop)
|
||||
}
|
||||
|
||||
// Allow randomization seed override via environment variable
|
||||
randomize := int64(-1) // Default: randomize test order
|
||||
if envSeed := os.Getenv("GODOG_RANDOM_SEED"); envSeed != "" {
|
||||
if parsedSeed, err := strconv.ParseInt(envSeed, 10, 64); err == nil {
|
||||
randomize = parsedSeed
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the correct path for feature files
|
||||
// When running from within a feature directory, use "." to find feature files in current dir
|
||||
// When running from outside, use the feature name as a relative path
|
||||
featurePath := "."
|
||||
if workingDir := getWorkingDir(); !strings.HasSuffix(workingDir, "/"+config.FeatureName) && !strings.HasSuffix(workingDir, "\\"+config.FeatureName) {
|
||||
// Not running from within the feature directory, use feature name
|
||||
featurePath = config.FeatureName
|
||||
}
|
||||
|
||||
return godog.TestSuite{
|
||||
Name: suiteName,
|
||||
TestSuiteInitializer: bdd.InitializeTestSuite,
|
||||
ScenarioInitializer: bdd.InitializeScenario,
|
||||
Options: &godog.Options{
|
||||
Format: config.Format,
|
||||
Paths: []string{featurePath},
|
||||
TestingT: t,
|
||||
Strict: true,
|
||||
Randomize: randomize,
|
||||
StopOnFailure: stopOnFailure,
|
||||
Tags: tags,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMultiFeatureTestSuite creates a configured godog test suite for multiple features
|
||||
func CreateMultiFeatureTestSuite(t *testing.T, config *MultiFeatureConfig, suiteName string) godog.TestSuite {
|
||||
// Set FEATURE environment variable for feature-specific configuration
|
||||
// For multi-feature tests, we don't set a specific feature
|
||||
os.Setenv("FEATURE", "")
|
||||
|
||||
// Allow tag override via environment variable
|
||||
tags := os.Getenv("GODOG_TAGS")
|
||||
if tags == "" {
|
||||
// Default tags if not overridden
|
||||
tags = "~@flaky && ~@todo && ~@skip"
|
||||
}
|
||||
|
||||
// Allow stop on failure override via environment variable
|
||||
stopOnFailure := config.StopOnFailure
|
||||
if envStop := os.Getenv("GODOG_STOP_ON_FAILURE"); envStop != "" {
|
||||
// Support various boolean formats
|
||||
stopOnFailure, _ = strconv.ParseBool(envStop)
|
||||
}
|
||||
|
||||
// Allow randomization seed override via environment variable
|
||||
randomize := int64(-1) // Default: randomize test order
|
||||
if envSeed := os.Getenv("GODOG_RANDOM_SEED"); envSeed != "" {
|
||||
if parsedSeed, err := strconv.ParseInt(envSeed, 10, 64); err == nil {
|
||||
randomize = parsedSeed
|
||||
}
|
||||
}
|
||||
|
||||
return godog.TestSuite{
|
||||
Name: suiteName,
|
||||
TestSuiteInitializer: bdd.InitializeTestSuite,
|
||||
ScenarioInitializer: bdd.InitializeScenario,
|
||||
Options: &godog.Options{
|
||||
Format: config.Format,
|
||||
Paths: config.Paths,
|
||||
TestingT: t,
|
||||
Strict: true,
|
||||
Randomize: randomize,
|
||||
StopOnFailure: stopOnFailure,
|
||||
Tags: tags,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user