Adds a Kubernetes-style healthz endpoint returning status, version, uptime_seconds and timestamp. Non-breaking — /api/health is preserved. - New route: GET /api/healthz - New handler: handleHealthz with HealthzResponse struct - New unit test: pkg/server/healthz_test.go (passes locally) - New BDD scenario: features/health/health.feature - BDD steps: pkg/bdd/steps/health_steps.go, common_steps.go Note: BDD tests require Postgres and will be validated by CI. 🤖 Co-Authored-By: Mistral Vibe (devstral-2 / mistral-medium-3.5) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
- Domain Separation: Steps are grouped by functional domain
- Single Responsibility: Each file focuses on a specific area of functionality
- Reusability: Common steps are shared via
common_steps.go - Scalability: Easy to add new domains as the application grows
- State Isolation: Use per-scenario state to prevent pollution between test scenarios
Adding New Steps
- For new domains: Create a new
*_steps.gofile following the existing pattern - For existing domains: Add to the appropriate domain file
- For shared functionality: Add to
common_steps.go - Register all steps: Update
steps.goto include the new steps
Step Naming Convention
- Use descriptive, action-oriented names
- Follow the pattern:
i[Action][Object]orthe[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 stepsnotification_steps.go- Notification and email stepsadmin_steps.go- Admin-specific functionality stepsapi_steps.go- General API interaction patternsuser_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:
- Are you using struct fields to store state? → Use
ScenarioStateinstead - Are database tables being cleaned up? → Verify
CleanupDatabase()or schema isolation - 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:
- Step is defined in the appropriate
*_steps.gofile - Step is registered in
steps.go - Step regex matches the feature file text exactly
- No typos in the step name
Tip: Run with -v to see which step is undefined