✨ 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>
505 lines
14 KiB
Markdown
505 lines
14 KiB
Markdown
# 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`)
|
|
- `Config` struct: 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)
|
|
|
|
```mermaid
|
|
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)
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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)
|
|
```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)
|
|
1. Create `loadConfigFromFile()` function
|
|
2. Modify `ReloadConfig()` to use file for config feature
|
|
3. Add tests to verify config hot-reload works
|
|
|
|
### Phase 2: Clean Up Config Creation
|
|
1. Create `ConfigOptions` struct
|
|
2. Modify `createTestConfig()` to accept options
|
|
3. Update callers to pass explicit options
|
|
4. Remove env var reading from deep in config creation
|
|
|
|
### Phase 3: Document and Validate
|
|
1. Write comprehensive documentation (this file)
|
|
2. Add validation tests for all use cases
|
|
3. Create troubleshooting guide
|
|
|
|
### Phase 4: Consider Package Merge (Optional)
|
|
1. Evaluate merging testserver + testsetup
|
|
2. Design new `pkg/bdd/testing` package structure
|
|
3. Migrate code incrementally
|
|
|
|
## Rules for Adding New Configuration
|
|
|
|
1. **Prefer explicit parameters** over environment variables
|
|
2. **Read env vars at ONE layer only** (typically test entry point)
|
|
3. **Document all config sources** in this file
|
|
4. **Test config combinations** to prevent override bugs
|
|
5. **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](https://github.com/cucumber/godog)
|
|
- [pkg/config/config.go](../config/config.go) - Config struct definitions
|
|
- [pkg/bdd/testsetup/testsetup.go](../testsetup/testsetup.go) - Test suite creation
|
|
- [pkg/bdd/suite.go](../suite.go) - Test hooks
|
|
- [ADR-0008: BDD Testing](../adr/0008-bdd-testing.md)
|