Files
Gabriel Radureau 5eec64e5e8
All checks were successful
CI/CD Pipeline / Build Docker Cache (push) Successful in 9s
CI/CD Pipeline / CI Pipeline (push) Successful in 4m15s
CI/CD Pipeline / Trigger Docker Push (push) Has been skipped
🧪 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>
2026-04-11 17:56:45 +02:00
..

BDD Steps Organization

This folder contains the step definitions for the BDD tests, organized by domain for better maintainability and scalability.

Structure

pkg/bdd/steps/
├── steps.go                  # Main registration file that ties everything together
├── scenario_state.go         # Per-scenario state isolation manager
├── common_steps.go           # Shared steps used across multiple domains
├── auth_steps.go             # Authentication and user management steps
├── config_steps.go           # Configuration and hot-reloading steps
├── greet_steps.go            # Greet-related steps (v1 and v2 API)
├── health_steps.go           # Health check and server status steps
├── jwt_retention_steps.go    # JWT secret retention policy steps
└── README.md                 # This file

Design Principles

  1. Domain Separation: Steps are grouped by functional domain
  2. Single Responsibility: Each file focuses on a specific area of functionality
  3. Reusability: Common steps are shared via common_steps.go
  4. Scalability: Easy to add new domains as the application grows
  5. State Isolation: Use per-scenario state to prevent pollution between test scenarios

Adding New Steps

  1. For new domains: Create a new *_steps.go file following the existing pattern
  2. For existing domains: Add to the appropriate domain file
  3. For shared functionality: Add to common_steps.go
  4. Register all steps: Update steps.go to include the new steps

Step Naming Convention

  • Use descriptive, action-oriented names
  • Follow the pattern: i[Action][Object] or the[Object][State]
  • Example: iRequestAGreetingFor, theAuthenticationShouldBeSuccessful
  • Use present tense for actions: "I authenticate", "the server reloads"

State Isolation Pattern

Problem: Step definition structs (AuthSteps, GreetSteps, etc.) maintain state in their fields (e.g., lastToken, lastUserID). This state persists across all scenarios in a test process, causing pollution even with database schema isolation.

Solution: Use the ScenarioState manager for per-scenario state isolation.

How It Works

The scenario_state.go provides a thread-safe mechanism to store and retrieve state that is isolated per scenario:

// Get scenario-specific state
state := steps.GetScenarioState(scenarioName)

// Store scenario-specific data
state.LastToken = token
state.LastUserID = userID

// Retrieve scenario-specific data
token := state.LastToken

Usage in Step Definitions

Instead of storing state in struct fields:

// ❌ NOT RECOMMENDED - state shared across all scenarios
type AuthSteps struct {
    client     *testserver.Client
    lastToken  string  // Shared across all scenarios!
    lastUserID uint    // Shared across all scenarios!
}

func (s *AuthSteps) iShouldReceiveAValidJWTToken() error {
    s.lastToken = extractedToken  // Pollutes other scenarios
    return nil
}

Use per-scenario state:

// ✅ RECOMMENDED - state isolated per scenario
type AuthSteps struct {
    client     *testserver.Client
    scenarioName string  // Track current scenario for state isolation
}

func (s *AuthSteps) iShouldReceiveAValidJWTToken() error {
    state := steps.GetScenarioState(s.scenarioName)
    state.LastToken = extractedToken  // Isolated to this scenario
    return nil
}

Integration with Suite Hooks

Clear state in AfterScenario to prevent memory growth:

sc.AfterScenario(func(s *godog.Scenario, err error) {
    scenarioKey := s.Name
    if s.Uri != "" {
        scenarioKey = fmt.Sprintf("%s:%s", s.Uri, s.Name)
    }
    steps.ClearScenarioState(scenarioKey)
})

ScenarioState Structure

The ScenarioState struct contains common fields needed across step definitions:

type ScenarioState struct {
    LastToken  string
    FirstToken string
    LastUserID uint
    // Add more fields as needed for other step types
}

If you need additional scenario-scoped fields, add them to the ScenarioState struct.

Testing the Steps

Run BDD tests with:

# Run all features
go test ./features/... -v

# Run specific feature
go test ./features/auth -v

# Run with state tracing enabled
BDD_TRACE_STATE=1 go test ./features/auth -v

# Validate full test suite
./scripts/validate-test-suite.sh 1

State Cleanup Strategy

Cleanup Level When What Implementation
Per-Scenario After each scenario Step struct fields ClearScenarioState()
Per-Scenario After each scenario Database state CleanupDatabase() (if no schema isolation)
Per-Scenario After each scenario Schema DROP SCHEMA (if schema isolation enabled)
Per-Process After each feature test Server-level state ResetJWTSecrets()
Per-Suite After all scenarios All state Server restart

Best Practices

1. Use Per-Scenario State for Shared Data

Any data that:

  • Is modified during scenario execution
  • Affects subsequent steps in the same scenario
  • Should NOT affect other scenarios

Use: GetScenarioState(scenarioName).Field

2. Keep Step Definitions Stateless Where Possible

If a step doesn't need to store intermediate state, don't store it:

// ✅ Good - stateless
func (s *GreetSteps) iRequestAGreetingFor(name string) error {
    return s.client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
}

// ❌ Avoid - unnecessary state
func (s *GreetSteps) iRequestAGreetingFor(name string) error {
    s.lastGreetedName = name  // Unnecessary unless used later
    return s.client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
}

3. Prefix Config Files Per-Scenario

If your scenario modifies config files, use scenario-specific paths:

configPath := fmt.Sprintf("features/%s/%s-scenario-%s.yaml", 
    feature, feature, scenarioKey)

4. Document Dependencies

If a step depends on state set by another step, document it:

// Step: The user should have a valid JWT token
// Requires: iAuthenticateWithUsernameAndPassword to have been called first
func (s *AuthSteps) theUserShouldHaveAValidJWTToken() error {
    state := steps.GetScenarioState(s.scenarioName)
    if state.LastToken == "" {
        return fmt.Errorf("no token found - did you authenticate first?")
    }
    // Verify token is valid...
}

Future Domains

As the application grows, consider adding:

  • payment_steps.go - Payment processing steps
  • notification_steps.go - Notification and email steps
  • admin_steps.go - Admin-specific functionality steps
  • api_steps.go - General API interaction patterns
  • user_steps.go - User profile and management steps (if auth gets complex)

Troubleshooting

State Pollution Between Scenarios

Symptom: Tests pass individually but fail when run together

Check:

  1. Are you using struct fields to store state? → Use ScenarioState instead
  2. Are database tables being cleaned up? → Verify CleanupDatabase() or schema isolation
  3. Are JWT secrets being reset? → Verify ResetJWTSecrets() is called

Debug: Enable state tracing:

BDD_TRACE_STATE=1 go test ./features/auth -v

Timeout or Delay Issues

Symptom: Config reloading tests fail intermittently

Cause: Server monitors config files every 1 second

Fix: Add delays >1100ms after config file changes:

time.Sleep(1100 * time.Millisecond)  // Wait for monitoring cycle

Missing Step Definitions

Symptom: undefined step error

Check:

  1. Step is defined in the appropriate *_steps.go file
  2. Step is registered in steps.go
  3. Step regex matches the feature file text exactly
  4. No typos in the step name

Tip: Run with -v to see which step is undefined