Files
dance-lessons-coach/pkg/bdd/testserver/CONFIG_SCHEMA.md
Gabriel Radureau 70c2eb554e
Some checks failed
CI/CD Pipeline / Build Docker Cache (push) Successful in 10s
CI/CD Pipeline / CI Pipeline (push) Failing after 4m8s
🧪 test: implement per-scenario state isolation and enhance validate-test-suite.sh
- Add pkg/bdd/steps/scenario_state.go with thread-safe per-scenario state manager
- Update auth_steps.go, jwt_retention_steps.go to use per-scenario state accessors
- Add LastSecret and LastError fields to ScenarioState for JWT retention testing
- Update steps.go with SetScenarioKeyForAllSteps function
- Update suite.go to generate scenario keys and clear state properly
- Mark config hot-reload scenarios as @flaky (timing-sensitive)
- Fix validate-test-suite.sh: add -p 1 flag for sequential execution, filter JSON logs, add --count flag
- Add CONFIG_SCHEMA.md documenting configuration architecture
- Split greet tests into v1/v2 sub-tests with explicit v2 enable/disable

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-11 13:34:51 +02:00

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)
  • 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)

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)

  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