✨ 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>
14 KiB
14 KiB
BDD Test Configuration Schema
Overview
This document describes the configuration architecture for BDD tests in the dance-lessons-coach project. It establishes a clear hierarchy and flow of configuration parameters to ensure predictable, maintainable, and isolated test execution.
Configuration Sources (Priority Order)
1. Explicit Parameters (Highest Priority)
Passed directly between components with no hidden behavior:
FEATURE: Which feature is being tested (greet,config,auth,health,jwt)GODOG_TAGS: Scenario tag filters (e.g.,@v2,~@flaky,~@todo)Configstruct: Passed explicitly to server initialization
2. Feature-Specific Configuration Files
Loaded from filesystem when testing specific features:
- Path:
features/{FEATURE}/{FEATURE}-test-config.yaml - Used by: Config hot-reload tests only
- Monitored by:
testserver.monitorConfigFile() - Example:
features/config/config-test-config.yaml
3. Environment Variables (External Control Only)
Set by test scripts and CI/CD, NOT read deep in implementation code:
| Variable | Purpose | Default | Set By |
|---|---|---|---|
DLC_API_V2_ENABLED |
Enable v2 API globally | false |
Test scripts |
BDD_SCHEMA_ISOLATION |
Enable per-scenario database schema isolation | false |
Test scripts, validate-test-suite.sh |
BDD_ENABLE_CLEANUP_LOGS |
Enable detailed cleanup logging | false |
Test scripts |
BDD_TRACE_STATE |
Enable state tracing | false |
Test scripts |
FIXED_TEST_PORT |
Use fixed port instead of random | false |
Test scripts |
FEATURE |
Current feature under test | "" |
testsetup.CreateTestSuite |
GODOG_TAGS |
Tag filter for scenario selection | "~@flaky && ~@todo && ~@skip" |
CreateTestSuite |
4. Hardcoded Defaults (Fallback)
Used when no other source provides a value:
- Port: Random in range 10000-19999 (or 9191 if FIXED_TEST_PORT=true)
- JWT Secret:
test-secret-key-for-bdd-tests - Database: localhost:5432, postgres/postgres, dance_lessons_coach
- Logging Level: debug
- v2_enabled: false
Configuration Layers (Mermaid Diagram)
flowchart TB
subgraph TestExecutionControl["Test Execution Control
(Shell/Script Layer)"]
A1[Environment Variables]
A2[DLC_API_V2_ENABLED]
A3[BDD_SCHEMA_ISOLATION]
A4[BDD_ENABLE_CLEANUP_LOGS]
A5[FEATURE]
A6[GODOG_TAGS]
end
subgraph TestSuiteSetup["Test Suite Setup
(pkg/bdd/testsetup)"]
B1[CreateTestSuite]
B2[Set FEATURE]
B3[Set GODOG_TAGS]
B4[Configure godog.Options]
end
subgraph ServerSetup["Server Setup
(pkg/bdd/suite)"]
C1[InitializeTestSuite]
C2[Create sharedServer]
C3[InitializeScenario]
end
subgraph ServerConfiguration["Server Configuration
(pkg/bdd/testserver)"]
D1[Server.Start]
D2[shouldEnableV2]
D3[createTestConfig]
D4[monitorConfigFile]
D5[ReloadConfig]
D6[loadConfigFromFile]
end
subgraph ScenarioExecution["Scenario Execution
(pkg/bdd/steps)"]
E1[BeforeScenario]
E2[SetScenarioKey]
E3[Execute Steps]
E4[AfterScenario]
E5[ClearScenarioState]
end
A1 --> B1
A2 --> D2
A3 --> D1
A4 --> D1
A5 --> B2
A5 --> D2
A6 --> B3
A6 --> D2
B1 --> C1
B2 --> C1
B3 --> C1
B4 --> C1
C1 --> D1
C2 --> D1
C3 --> E1
D1 --> D4
D2 --> D3
D3 --> D1
D4 --> D5
D5 --> D1
D5 --> D6
D6 --> D3
D1 --> E1
E1 --> E2
E2 --> E3
E3 --> E4
E4 --> E5
classDef external fill:#09f,stroke:#333
classDef setup fill:#08f,stroke:#333
classDef server fill:#090,stroke:#333
classDef scenario fill:#000,stroke:#333
class A1,A2,A3,A4,A5,A6 external
class B1,B2,B3,B4 setup
class C1,C2,C3 setup
class D1,D2,D3,D4,D5,D6 server
class E1,E2,E3,E4,E5 scenario
Configuration Flow (Mermaid Sequence Diagram)
sequenceDiagram
participant Script as Test Script
participant TestSetup as testsetup
participant Suite as suite.go
participant Server as testserver
participant ConfigFile as Config File
participant Steps as Step Definitions
Script->>Script: Set env vars (BDD_*, DLC_*)
Script->>TestSetup: Run go test ./features/{feature}
TestSetup->>TestSetup: Read FEATURE from env
TestSetup->>TestSetup: Read GODOG_TAGS from env
TestSetup->>Suite: CreateTestSuite(FEATURE, tags)
Suite->>Server: InitializeTestSuite -> NewServer()
Server->>Server: shouldEnableV2() checks FEATURE+GODOG_TAGS
Server->>Server: createTestConfig(port, v2Enabled)
Server->>Server: Start()
Server->>Server: Start monitorConfigFile() goroutine
Suite->>Suite: InitializeScenario
Suite->>Steps: Create step context
loop Each Scenario
Suite->>Server: BeforeScenario: SetupSchemaIsolation
Suite->>Steps: SetScenarioKeyForAllSteps
Steps->>Steps: Clear scenario state
Steps->>Server: Execute step requests
alt Config Feature + File Modified
ConfigFile->>Server: File modification detected
Server->>Server: ReloadConfig()
Server->>ConfigFile: loadConfigFromFile()
Server->>Server: Restart with new config
end
Suite->>Server: AfterScenario: Cleanup
Suite->>Steps: ClearScenarioState
end
Use Cases
UC-1: Default Test Run (No v2, No Config File)
Input: go test ./features/greet
FEATURE: greet
GODOG_TAGS: ~@flaky && ~@todo && ~@skip
Config Source: createTestConfig(port)
v2_enabled: false
Result: v1 scenarios pass, v2 scenarios skipped by tag filter
UC-2: v2 API Tests (Split Test Suite)
Input: go test ./features/greet (with GODOG_TAGS="@v2" in v2 subtest)
FEATURE: greet
GODOG_TAGS: @v2 && ~@skip
Config Source: createTestConfig(port) with v2 check
v2_enabled: true (because FEATURE=greet AND tags contain @v2)
Result: v2 scenarios execute with v2 API available
Flow:
1. TestGreetBDD runs v1 subtest with tags="~@v2"
2. TestGreetBDD runs v2 subtest with tags="@v2"
3. Each subtest starts its own server
4. Server in v2 subtest has v2_enabled=true
5. v2 scenarios pass
UC-3: Config Hot Reload Tests
Input: go test ./features/config
FEATURE: config
GODOG_TAGS: ~@flaky && ~@todo && ~@skip
Config File: features/config/config-test-config.yaml
Config Monitor: Watches config file for changes
When config file is modified:
1. monitorConfigFile() detects file change via mod time
2. Calls ReloadConfig()
3. ReloadConfig() for FEATURE=config: loads from config file
4. Server restarts with new config
5. Subsequent scenarios see new configuration
Note: This is the ONLY feature that uses config file hot-reload.
All other features use hardcoded/test defaults.
UC-4: Config Hot Reload with v2 Enable
Scenario: Hot reloading feature flags
Steps:
1. Server starts with default config (v2_enabled: false)
2. Test sets v2_enabled: true in config file
3. Config monitor detects change
4. ReloadConfig() called
5. Server loads from config file (NOT createTestConfig)
6. Server restarts with v2_enabled: true
7. Test verifies v2 API works
Current Bug: ReloadConfig() calls createTestConfig() which:
- Reads FEATURE=config
- Reads GODOG_TAGS (doesn't contain @v2)
- Sets v2_enabled: false
- Overrides the config file setting!
Fix: ReloadConfig() must load from file for config feature.
Implementation Details
Config Creation Flow
// pkg/bdd/testserver/server.go
func NewServer() *Server {
port := getRandomPort() // 10000-19999
return &Server{port: port}
}
func (s *Server) Start() error {
cfg := createTestConfig(s.port)
// ... start server with cfg
go s.monitorConfigFile()
}
// CURRENT - BAD
func createTestConfig(port int) *config.Config {
feature := os.Getenv("FEATURE")
tags := os.Getenv("GODOG_TAGS")
enableV2 := false
if feature == "greet" && strings.Contains(tags, "@v2") {
enableV2 = true
}
// ...
return &config.Config{
API: config.APIConfig{V2Enabled: enableV2},
// ...
}
}
// PROPOSED - GOOD
func createTestConfig(port int, opts ConfigOptions) *config.Config {
defaults := &config.Config{
Server: config.ServerConfig{Host: "0.0.0.0", Port: port},
// ... all hardcoded defaults
}
// Apply explicit options (passed from caller)
if opts.V2Enabled {
defaults.API.V2Enabled = true
}
return defaults
}
// ConfigOptions passed from testsuite
type ConfigOptions struct {
V2Enabled bool
UseConfigFile bool
ConfigFilePath string
}
Reload Flow Fix
// pkg/bdd/testserver/server.go
func (s *Server) ReloadConfig() error {
feature := os.Getenv("FEATURE")
if feature == "config" && s.configFilePath != "" {
// For config tests: load from monitored file
cfg, err := loadConfigFromFile(s.configFilePath)
if err != nil {
return err
}
return s.applyConfig(cfg)
}
// For all other features: use defaults
// (hot reload not supported for non-config features)
cfg := createDefaultConfig(s.port)
return s.applyConfig(cfg)
}
func loadConfigFromFile(path string) (*config.Config, error) {
v := viper.New()
v.SetConfigFile(path)
v.SetConfigType("yaml")
if err := v.ReadInConfig(); err != nil {
return nil, err
}
var cfg config.Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, err
}
// Apply hardcoded values that should NOT come from file
// (database connection for BDD tests, etc.)
cfg.Database.Host = getDatabaseHost()
cfg.Database.Port = getDatabasePort()
cfg.Database.User = "postgres"
cfg.Database.Password = "postgres"
cfg.Database.Name = "dance_lessons_coach"
return &cfg, nil
}
Configuration File Format
Config Test File (features/config/config-test-config.yaml)
server:
host: "127.0.0.1"
port: 9191
logging:
level: "info"
json: false
api:
v2_enabled: false # Will be toggled by tests
telemetry:
enabled: true
sampler:
type: "parentbased_always_on"
ratio: 1.0
auth:
jwt:
ttl: 1h
database:
# These are OVERRIDDEN by BDD test infrastructure
host: "localhost"
port: 5432
user: "postgres"
password: "postgres"
name: "dance_lessons_coach_bdd_test"
ssl_mode: "disable"
State Isolation
Per-Scenario State
- Managed by:
pkg/bdd/steps/scenario_state.go - Key: SHA256 hash of scenario URI + name
- State includes: LastToken, FirstToken, LastUserID, LastSecret, LastError
- Cleared: At start of each scenario in BeforeScenario hook
Database Schema Isolation
- Enabled by:
BDD_SCHEMA_ISOLATION=true - Mechanism: Creates unique schema per scenario
- Schema name:
test_{sha256(scenarioKey)[:8]} - Search path: Set via
SET search_path TO ... - Cleanup: Schema dropped after scenario
Server-Level State Reset
- JWT secrets: Reset after every scenario via
ResetJWTSecrets() - Database: Cleaned up after every scenario
- Auth state: Per-scenario via state manager
Package Responsibilities
pkg/bdd/testserver
- Purpose: Test HTTP server management
- Responsibilities:
- Server lifecycle (Start, Stop)
- Configuration loading and reloading
- Database cleanup
- Schema isolation
- JWT secret management
- Config file monitoring (config feature only)
pkg/bdd/testsetup
- Purpose: Godog test suite setup
- Responsibilities:
- Feature test file discovery
- Test suite configuration
- Tag filtering
- godog options setup
pkg/bdd/suite
- Purpose: Test suite initialization hooks
- Responsibilities:
- BeforeSuite/AfterSuite hooks
- BeforeScenario/AfterScenario hooks
- Step context creation
- State isolation setup
pkg/bdd/steps
- Purpose: Step definitions
- Responsibilities:
- All Gherkin step implementations
- Per-scenario state management
- Per-feature step organization
Migration Plan
Phase 1: Fix Config Reload (Urgent)
- Create
loadConfigFromFile()function - Modify
ReloadConfig()to use file for config feature - Add tests to verify config hot-reload works
Phase 2: Clean Up Config Creation
- Create
ConfigOptionsstruct - Modify
createTestConfig()to accept options - Update callers to pass explicit options
- Remove env var reading from deep in config creation
Phase 3: Document and Validate
- Write comprehensive documentation (this file)
- Add validation tests for all use cases
- Create troubleshooting guide
Phase 4: Consider Package Merge (Optional)
- Evaluate merging testserver + testsetup
- Design new
pkg/bdd/testingpackage structure - Migrate code incrementally
Rules for Adding New Configuration
- Prefer explicit parameters over environment variables
- Read env vars at ONE layer only (typically test entry point)
- Document all config sources in this file
- Test config combinations to prevent override bugs
- Never read env vars in hot paths (scenario steps, server handlers)
Troubleshooting
Symptom: Config file changes not applied
- Check: Is FEATURE=config?
- Check: Does config file exist at
features/config/config-test-config.yaml? - Check: Does monitorConfigFile() detect the change?
- Fix: ReloadConfig() must load from file, not createTestConfig()
Symptom: v2 tests fail with 404
- Check: Is FEATURE=greet?
- Check: Does GODOG_TAGS contain @v2?
- Check: Does createTestConfig() see the tags?
- Fix: Ensure tags are set before server creation
Symptom: State pollution between scenarios
- Check: Is schema isolation enabled?
- Check: Are step definitions using per-scenario state?
- Fix: Use ScenarioState for all mutable state
References
- Godog Documentation
- pkg/config/config.go - Config struct definitions
- pkg/bdd/testsetup/testsetup.go - Test suite creation
- pkg/bdd/suite.go - Test hooks
- ADR-0008: BDD Testing