🧪 test: add JWT secret rotation BDD scenarios and step implementations (#12)
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

 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>
This commit was merged in pull request #12.
This commit is contained in:
2026-04-11 17:56:45 +02:00
committed by arcodange
parent 5de703468f
commit 5eec64e5e8
66 changed files with 10025 additions and 701 deletions

View File

@@ -4,31 +4,75 @@ import (
"dance-lessons-coach/pkg/bdd/testserver"
"github.com/cucumber/godog"
"github.com/rs/zerolog/log"
)
// StepContext holds the test client and implements all step definitions
type StepContext struct {
client *testserver.Client
greetSteps *GreetSteps
healthSteps *HealthSteps
authSteps *AuthSteps
commonSteps *CommonSteps
client *testserver.Client
greetSteps *GreetSteps
healthSteps *HealthSteps
authSteps *AuthSteps
commonSteps *CommonSteps
jwtRetentionSteps *JWTRetentionSteps
configSteps *ConfigSteps
}
// NewStepContext creates a new step context
func NewStepContext(client *testserver.Client) *StepContext {
return &StepContext{
client: client,
greetSteps: NewGreetSteps(client),
healthSteps: NewHealthSteps(client),
authSteps: NewAuthSteps(client),
commonSteps: NewCommonSteps(client),
client: client,
greetSteps: NewGreetSteps(client),
healthSteps: NewHealthSteps(client),
authSteps: NewAuthSteps(client),
commonSteps: NewCommonSteps(client),
jwtRetentionSteps: NewJWTRetentionSteps(client),
configSteps: NewConfigSteps(client),
}
}
// CleanupAllTestConfigFiles cleans up any test config files created during tests
func CleanupAllTestConfigFiles() error {
// Cleanup config hot reloading test file
configSteps := &ConfigSteps{configFilePath: "test-config.yaml"}
if err := configSteps.CleanupTestConfigFile(); err != nil {
log.Warn().Err(err).Msg("Failed to cleanup config test file")
}
return nil
}
// SetScenarioKeyForAllSteps sets the scenario key on all step instances for state isolation
func SetScenarioKeyForAllSteps(sc *StepContext, key string) {
if sc != nil {
if sc.authSteps != nil {
sc.authSteps.SetScenarioKey(key)
}
if sc.jwtRetentionSteps != nil {
sc.jwtRetentionSteps.SetScenarioKey(key)
}
if sc.configSteps != nil {
sc.configSteps.SetScenarioKey(key)
}
if sc.greetSteps != nil {
sc.greetSteps.SetScenarioKey(key)
}
if sc.healthSteps != nil {
sc.healthSteps.SetScenarioKey(key)
}
if sc.commonSteps != nil {
sc.commonSteps.SetScenarioKey(key)
}
}
}
// InitializeAllSteps registers all step definitions for the BDD tests
func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) {
sc := NewStepContext(client)
func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client, stepContext *StepContext) {
var sc *StepContext
if stepContext != nil {
sc = stepContext
} else {
sc = NewStepContext(client)
}
// Greet steps
ctx.Step(`^I request a greeting for "([^"]*)"$`, sc.greetSteps.iRequestAGreetingFor)
@@ -76,6 +120,179 @@ func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) {
ctx.Step(`^I should receive a different JWT token$`, sc.authSteps.iShouldReceiveADifferentJWTToken)
ctx.Step(`^I authenticate with username "([^"]*)" and password "([^"]*)" again$`, sc.authSteps.iAuthenticateWithUsernameAndPasswordAgain)
// JWT Secret Rotation steps
ctx.Step(`^the server is running with multiple JWT secrets$`, sc.authSteps.theServerIsRunningWithMultipleJWTSecrets)
ctx.Step(`^I should receive a valid JWT token signed with the primary secret$`, sc.authSteps.iShouldReceiveAValidJWTTokenSignedWithThePrimarySecret)
ctx.Step(`^I validate a JWT token signed with the secondary secret$`, sc.authSteps.iValidateAJWTTokenSignedWithTheSecondarySecret)
ctx.Step(`^I add a new secondary JWT secret to the server$`, sc.authSteps.iAddANewSecondaryJWTSecretToTheServer)
ctx.Step(`^I add a new secondary JWT secret and rotate to it$`, sc.authSteps.iAddANewSecondaryJWTSecretAndRotateToIt)
ctx.Step(`^I authenticate with username "([^"]*)" and password "([^"]*)" after rotation$`, sc.authSteps.iAuthenticateWithUsernameAndPasswordAfterRotation)
ctx.Step(`^I should receive a valid JWT token signed with the new secondary secret$`, sc.authSteps.iShouldReceiveAValidJWTTokenSignedWithTheNewSecondarySecret)
ctx.Step(`^the token should still be valid during retention period$`, sc.authSteps.theTokenShouldStillBeValidDuringRetentionPeriod)
ctx.Step(`^I use a JWT token signed with the expired secondary secret for authentication$`, sc.authSteps.iUseAJWTTokenSignedWithTheExpiredSecondarySecretForAuthentication)
ctx.Step(`^I use the old JWT token signed with primary secret$`, sc.authSteps.iUseTheOldJWTTokenSignedWithPrimarySecret)
ctx.Step(`^I validate the old JWT token signed with primary secret$`, sc.authSteps.iValidateTheOldJWTTokenSignedWithPrimarySecret)
ctx.Step(`^the server is running with primary JWT secret$`, sc.authSteps.theServerIsRunningWithPrimaryJWTSecret)
ctx.Step(`^the server is running with primary and expired secondary JWT secrets$`, sc.authSteps.theServerIsRunningWithPrimaryAndExpiredSecondaryJWTSecrets)
ctx.Step(`^the token should still be valid$`, sc.authSteps.theTokenShouldStillBeValid)
// JWT Retention steps
ctx.Step(`^the server is running with JWT secret retention configured$`, sc.jwtRetentionSteps.theServerIsRunningWithJWTSecretRetentionConfigured)
ctx.Step(`^the default JWT TTL is (\d+) hours$`, sc.jwtRetentionSteps.theDefaultJWTTTLIsHours)
ctx.Step(`^the retention factor is (\d+\.?\d*)$`, sc.jwtRetentionSteps.theRetentionFactorIs)
ctx.Step(`^the maximum retention is (\d+) hours$`, sc.jwtRetentionSteps.theMaximumRetentionIsHours)
ctx.Step(`^a primary JWT secret exists$`, sc.jwtRetentionSteps.aPrimaryJWTSecretExists)
ctx.Step(`^I add a secondary JWT secret with (\d+) hour expiration$`, sc.jwtRetentionSteps.iAddASecondaryJWTSecretWithHourExpiration)
ctx.Step(`^I wait for the retention period to elapse$`, sc.jwtRetentionSteps.iWaitForTheRetentionPeriodToElapse)
ctx.Step(`^the expired secondary secret should be automatically removed$`, sc.jwtRetentionSteps.theExpiredSecondarySecretShouldBeAutomaticallyRemoved)
ctx.Step(`^the primary secret should remain active$`, sc.jwtRetentionSteps.thePrimarySecretShouldRemainActive)
ctx.Step(`^I should see cleanup event in logs$`, sc.jwtRetentionSteps.iShouldSeeCleanupEventInLogs)
ctx.Step(`^the JWT TTL is set to (\d+) hours$`, sc.jwtRetentionSteps.theJWTTTLIsSetToHours)
ctx.Step(`^the retention period should be capped at (\d+) hours$`, sc.jwtRetentionSteps.theRetentionPeriodShouldBeCappedAtHours)
ctx.Step(`^the retention period should be (\d+) hours$`, sc.jwtRetentionSteps.theRetentionPeriodShouldBeHours)
ctx.Step(`^the cleanup interval is set to (\d+) minutes$`, sc.jwtRetentionSteps.theCleanupIntervalIsSetToMinutes)
ctx.Step(`^it should be removed within (\d+) minutes$`, sc.jwtRetentionSteps.itShouldBeRemovedWithinMinutes)
ctx.Step(`^I should see cleanup events every (\d+) minutes$`, sc.jwtRetentionSteps.iShouldSeeCleanupEventsEveryMinutes)
// Removed duplicate user creation and authentication steps - using authSteps versions from lines 60 and 61
ctx.Step(`^I receive a valid JWT token signed with current secret$`, sc.jwtRetentionSteps.iReceiveAValidJWTTokenSignedWithCurrentSecret)
ctx.Step(`^I wait for the secret to expire$`, sc.jwtRetentionSteps.iWaitForTheSecretToExpire)
ctx.Step(`^I try to validate the expired token$`, sc.jwtRetentionSteps.iTryToValidateTheExpiredToken)
ctx.Step(`^the token validation should fail$`, sc.jwtRetentionSteps.theTokenValidationShouldFail)
ctx.Step(`^I should receive "([^"]*)" error$`, sc.jwtRetentionSteps.iShouldReceiveInvalidTokenError)
ctx.Step(`^I set retention factor to (\d+\.?\d*)$`, sc.jwtRetentionSteps.iSetRetentionFactorTo)
ctx.Step(`^I try to start the server$`, sc.jwtRetentionSteps.iTryToStartTheServer)
ctx.Step(`^I should receive configuration validation error$`, sc.jwtRetentionSteps.iShouldReceiveConfigurationValidationError)
ctx.Step(`^the error should mention "([^"]*)"$`, sc.jwtRetentionSteps.theErrorShouldMention)
ctx.Step(`^I have enabled Prometheus metrics$`, sc.jwtRetentionSteps.iHaveEnabledPrometheusMetrics)
ctx.Step(`^I should see "([^"]*)" metric increment$`, sc.jwtRetentionSteps.iShouldSeeMetricIncrement)
ctx.Step(`^I should see "([^"]*)" metric decrease$`, sc.jwtRetentionSteps.iShouldSeeMetricDecrease)
ctx.Step(`^I should see "([^"]*)" histogram update$`, sc.jwtRetentionSteps.iShouldSeeHistogramUpdate)
ctx.Step(`^I add a new JWT secret "([^"]*)"$`, sc.jwtRetentionSteps.iAddANewJWTSecret)
ctx.Step(`^the logs should show masked secret "([^"]*)"$`, sc.jwtRetentionSteps.theLogsShouldShowMaskedSecret)
ctx.Step(`^the logs should not expose the full secret in logs$`, sc.jwtRetentionSteps.theLogsShouldNotExposeTheFullSecret)
ctx.Step(`^I have (\d+) JWT secrets$`, sc.jwtRetentionSteps.iHaveJWTSecrets)
ctx.Step(`^(\d+) of them are expired$`, sc.jwtRetentionSteps.ofThemAreExpired)
ctx.Step(`^it should complete within (\d+) milliseconds$`, sc.jwtRetentionSteps.itShouldCompleteWithinMilliseconds)
ctx.Step(`^and not impact server performance$`, sc.jwtRetentionSteps.andNotImpactServerPerformance)
ctx.Step(`^I set cleanup interval to (\d+) hours$`, sc.jwtRetentionSteps.iSetCleanupIntervalToHours)
ctx.Step(`^they should not be automatically removed$`, sc.jwtRetentionSteps.theyShouldNotBeAutomaticallyRemoved)
ctx.Step(`^and manual cleanup should still be possible$`, sc.jwtRetentionSteps.andManualCleanupShouldStillBePossible)
ctx.Step(`^the retention period should be (\d+) hour$`, sc.jwtRetentionSteps.theRetentionPeriodShouldBeHour)
ctx.Step(`^the secret should expire after (\d+) hour$`, sc.jwtRetentionSteps.theSecretShouldExpireAfterHour)
ctx.Step(`^I try to add an invalid JWT secret$`, sc.jwtRetentionSteps.iTryToAddAnInvalidJWTSecret)
ctx.Step(`^I should receive validation error$`, sc.jwtRetentionSteps.iShouldReceiveValidationError)
ctx.Step(`^the error should mention minimum (\d+) characters$`, sc.jwtRetentionSteps.theErrorShouldMentionMinimumCharacters)
ctx.Step(`^the cleanup job encounters an error$`, sc.jwtRetentionSteps.theCleanupJobEncountersAnError)
ctx.Step(`^it should log the error$`, sc.jwtRetentionSteps.itShouldLogTheError)
ctx.Step(`^and continue with remaining secrets$`, sc.jwtRetentionSteps.andContinueWithRemainingSecrets)
ctx.Step(`^and not crash the cleanup process$`, sc.jwtRetentionSteps.andNotCrashTheCleanupProcess)
ctx.Step(`^the server is running with default retention settings$`, sc.jwtRetentionSteps.theServerIsRunningWithDefaultRetentionSettings)
ctx.Step(`^I update the retention factor via configuration$`, sc.jwtRetentionSteps.iUpdateTheRetentionFactorViaConfiguration)
ctx.Step(`^the new settings should take effect immediately$`, sc.jwtRetentionSteps.theNewSettingsShouldTakeEffectImmediately)
ctx.Step(`^and existing secrets should be reevaluated$`, sc.jwtRetentionSteps.andExistingSecretsShouldBeReevaluated)
ctx.Step(`^and cleanup should use new retention periods$`, sc.jwtRetentionSteps.andCleanupShouldUseNewRetentionPeriods)
ctx.Step(`^I enable audit logging$`, sc.jwtRetentionSteps.iEnableAuditLogging)
ctx.Step(`^I should see audit log entry with event type "([^"]*)"$`, sc.jwtRetentionSteps.iShouldSeeAuditLogEntryWithEventType)
ctx.Step(`^I authenticate and receive token A$`, sc.jwtRetentionSteps.iAuthenticateAndReceiveTokenA)
ctx.Step(`^I refresh my token during retention period$`, sc.jwtRetentionSteps.iRefreshMyTokenDuringRetentionPeriod)
ctx.Step(`^I should receive new token B$`, sc.jwtRetentionSteps.iShouldReceiveNewTokenB)
ctx.Step(`^and token A should still be valid until retention expires$`, sc.jwtRetentionSteps.andTokenAShouldStillBeValidUntilRetentionExpires)
ctx.Step(`^and both tokens should work concurrently$`, sc.jwtRetentionSteps.andBothTokensShouldWorkConcurrently)
ctx.Step(`^given a security incident requires immediate rotation$`, sc.jwtRetentionSteps.givenASecurityIncidentRequiresImmediateRotation)
ctx.Step(`^I rotate to a new primary secret$`, sc.jwtRetentionSteps.iRotateToANewPrimarySecret)
ctx.Step(`^old tokens should be invalidated immediately$`, sc.jwtRetentionSteps.oldTokensShouldBeInvalidatedImmediately)
ctx.Step(`^and new tokens should use the emergency secret$`, sc.jwtRetentionSteps.andNewTokensShouldUseTheEmergencySecret)
ctx.Step(`^and cleanup should remove compromised secrets$`, sc.jwtRetentionSteps.andCleanupShouldRemoveCompromisedSecrets)
ctx.Step(`^I have monitoring configured$`, sc.jwtRetentionSteps.iHaveMonitoringConfigured)
ctx.Step(`^the cleanup job fails repeatedly$`, sc.jwtRetentionSteps.theCleanupJobFailsRepeatedly)
ctx.Step(`^I should receive alert notification$`, sc.jwtRetentionSteps.iShouldReceiveAlertNotification)
ctx.Step(`^the alert should include error details$`, sc.jwtRetentionSteps.theAlertShouldIncludeErrorDetails)
ctx.Step(`^and suggest remediation steps$`, sc.jwtRetentionSteps.andSuggestRemediationSteps)
// Additional missing steps for JWT retention
ctx.Step(`^a security incident requires immediate rotation$`, sc.jwtRetentionSteps.givenASecurityIncidentRequiresImmediateRotation)
ctx.Step(`^both tokens should work concurrently$`, sc.jwtRetentionSteps.bothTokensShouldWorkConcurrently)
ctx.Step(`^both tokens should work until retention period expires$`, sc.jwtRetentionSteps.bothTokensShouldWorkUntilRetentionPeriodExpires)
ctx.Step(`^cleanup should remove compromised secrets$`, sc.jwtRetentionSteps.andCleanupShouldRemoveCompromisedSecrets)
ctx.Step(`^cleanup should use new retention periods$`, sc.jwtRetentionSteps.andCleanupShouldUseNewRetentionPeriods)
ctx.Step(`^continue with remaining secrets$`, sc.jwtRetentionSteps.andContinueWithRemainingSecrets)
ctx.Step(`^existing secrets should be reevaluated$`, sc.jwtRetentionSteps.andExistingSecretsShouldBeReevaluated)
ctx.Step(`^I add a new JWT secret$`, sc.jwtRetentionSteps.iAddANewJWTSecretNoArgs)
ctx.Step(`^I add a new secondary secret and rotate to it$`, sc.authSteps.iAddANewSecondaryJWTSecretAndRotateToIt)
ctx.Step(`^I add an expired JWT secret$`, sc.jwtRetentionSteps.iAddAnExpiredJWTSecret)
ctx.Step(`^I add expired JWT secrets$`, sc.jwtRetentionSteps.iAddExpiredJWTSecrets)
ctx.Step(`^I authenticate again with username "([^"]*)" and password "([^"]*)"$`, sc.jwtRetentionSteps.iAuthenticateAgainWithUsernameAndPassword)
ctx.Step(`^I have (\d+) JWT secrets of different ages$`, sc.jwtRetentionSteps.iHaveJWTSecretsOfDifferentAges)
ctx.Step(`^I receive a valid JWT token signed with primary secret$`, sc.jwtRetentionSteps.iReceiveAValidJWTTokenSignedWithPrimarySecret)
ctx.Step(`^I should receive a new token signed with secondary secret$`, sc.jwtRetentionSteps.iShouldReceiveANewTokenSignedWithSecondarySecret)
ctx.Step(`^it tries to remove a secret$`, sc.jwtRetentionSteps.itTriesToRemoveASecret)
ctx.Step(`^manual cleanup should still be possible$`, sc.jwtRetentionSteps.manualCleanupShouldStillBePossible)
ctx.Step(`^new tokens should use the emergency secret$`, sc.jwtRetentionSteps.newTokensShouldUseTheEmergencySecret)
ctx.Step(`^not crash the cleanup process$`, sc.jwtRetentionSteps.andNotCrashTheCleanupProcess)
ctx.Step(`^not exceed the maximum retention limit$`, sc.jwtRetentionSteps.notExceedTheMaximumRetentionLimit)
ctx.Step(`^not expose the full secret in logs$`, sc.jwtRetentionSteps.notExposeTheFullSecretInLogs)
ctx.Step(`^not impact server performance$`, sc.jwtRetentionSteps.andNotImpactServerPerformance)
ctx.Step(`^remove all (\d+) expired secrets$`, sc.jwtRetentionSteps.removeAllExpiredSecrets)
ctx.Step(`^secret A is (\d+) hour old \(within retention\)$`, sc.jwtRetentionSteps.secretAIsHourOldWithinRetention)
ctx.Step(`^secret A should be retained$`, sc.jwtRetentionSteps.secretAShouldBeRetained)
ctx.Step(`^secret B is (\d+) hours old \(expired\)$`, sc.jwtRetentionSteps.secretBIsHoursOldExpired)
ctx.Step(`^secret B should be removed$`, sc.jwtRetentionSteps.secretBShouldBeRemoved)
ctx.Step(`^secret C is the primary secret$`, sc.jwtRetentionSteps.secretCIsThePrimarySecret)
ctx.Step(`^secret C should be retained as primary$`, sc.jwtRetentionSteps.secretCShouldBeRetainedAsPrimary)
ctx.Step(`^suggest remediation steps$`, sc.jwtRetentionSteps.andSuggestRemediationSteps)
ctx.Step(`^the cleanup job removes expired secrets$`, sc.jwtRetentionSteps.theCleanupJobRemovesExpiredSecrets)
ctx.Step(`^the cleanup job runs$`, sc.jwtRetentionSteps.theCleanupJobRuns)
ctx.Step(`^the JWT TTL is (\d+) hour$`, sc.jwtRetentionSteps.theJWTTTLIsHour)
ctx.Step(`^the old token should still be valid during retention period$`, sc.jwtRetentionSteps.theOldTokenShouldStillBeValidDuringRetentionPeriod)
ctx.Step(`^the primary secret is older than retention period$`, sc.jwtRetentionSteps.thePrimarySecretIsOlderThanRetentionPeriod)
ctx.Step(`^the primary secret should not be removed$`, sc.jwtRetentionSteps.thePrimarySecretShouldNotBeRemoved)
ctx.Step(`^the secret is less than (\d+) characters$`, sc.jwtRetentionSteps.theSecretIsLessThanCharacters)
ctx.Step(`^the secret should expire after (\d+) hours$`, sc.jwtRetentionSteps.theSecretShouldExpireAfterHours)
ctx.Step(`^token A should still be valid until retention expires$`, sc.jwtRetentionSteps.tokenAShouldStillBeValidUntilRetentionExpires)
ctx.Step(`^when the secret is removed by cleanup$`, sc.jwtRetentionSteps.whenTheSecretIsRemovedByCleanup)
// Config steps
ctx.Step(`^the server is running with config file monitoring enabled$`, sc.configSteps.theServerIsRunningWithConfigFileMonitoringEnabled)
ctx.Step(`^I update the logging level to "([^"]*)" in the config file$`, sc.configSteps.iUpdateTheLoggingLevelToInTheConfigFile)
ctx.Step(`^the logging level should be updated without restart$`, sc.configSteps.theLoggingLevelShouldBeUpdatedWithoutRestart)
ctx.Step(`^debug logs should appear in the output$`, sc.configSteps.debugLogsShouldAppearInTheOutput)
ctx.Step(`^the v2 API is disabled$`, sc.configSteps.theV2APIIsDisabled)
ctx.Step(`^I enable the v2 API in the config file$`, sc.configSteps.iEnableTheV2APIInTheConfigFile)
ctx.Step(`^the v2 API should become available without restart$`, sc.configSteps.theV2APIShouldBecomeAvailableWithoutRestart)
ctx.Step(`^v2 API requests should succeed$`, sc.configSteps.v2APIRequestsShouldSucceed)
ctx.Step(`^telemetry is enabled$`, sc.configSteps.telemetryIsEnabled)
ctx.Step(`^I update the sampler type to "([^"]*)" in the config file$`, sc.configSteps.iUpdateTheSamplerTypeToInTheConfigFile)
ctx.Step(`^I set the sampler ratio to "([^"]*)" in the config file$`, sc.configSteps.iSetTheSamplerRatioToInTheConfigFile)
ctx.Step(`^the telemetry sampling should be updated without restart$`, sc.configSteps.theTelemetrySamplingShouldBeUpdatedWithoutRestart)
ctx.Step(`^the new sampling settings should be applied$`, sc.configSteps.theNewSamplingSettingsShouldBeApplied)
ctx.Step(`^JWT TTL is set to (\d+) hour$`, sc.configSteps.jwtTTLIsSetToHour)
ctx.Step(`^I update the JWT TTL to (\d+) hours in the config file$`, sc.configSteps.iUpdateTheJWTTTLToHoursInTheConfigFile)
ctx.Step(`^the JWT TTL should be updated without restart$`, sc.configSteps.theJWTTTLShouldBeUpdatedWithoutRestart)
ctx.Step(`^new JWT tokens should have the updated expiration$`, sc.configSteps.newJWTTokensShouldHaveTheUpdatedExpiration)
ctx.Step(`^I update the server port to (\d+) in the config file$`, sc.configSteps.iUpdateTheServerPortToInTheConfigFile)
ctx.Step(`^the server port should remain unchanged$`, sc.configSteps.theServerPortShouldRemainUnchanged)
ctx.Step(`^the server should continue running on the original port$`, sc.configSteps.theServerShouldContinueRunningOnTheOriginalPort)
ctx.Step(`^a warning should be logged about ignored configuration change$`, sc.configSteps.aWarningShouldBeLoggedAboutIgnoredConfigurationChange)
// Removed duplicate logging level update step - using the main version that handles both valid and invalid levels
ctx.Step(`^the logging level should remain unchanged$`, sc.configSteps.theLoggingLevelShouldRemainUnchanged)
ctx.Step(`^an error should be logged about invalid configuration$`, sc.configSteps.anErrorShouldBeLoggedAboutInvalidConfiguration)
ctx.Step(`^the server should continue running normally$`, sc.configSteps.theServerShouldContinueRunningNormally)
ctx.Step(`^I delete the config file$`, sc.configSteps.iDeleteTheConfigFile)
ctx.Step(`^the server should continue running with last known good configuration$`, sc.configSteps.theServerShouldContinueRunningWithLastKnownGoodConfiguration)
ctx.Step(`^a warning should be logged about missing config file$`, sc.configSteps.aWarningShouldBeLoggedAboutMissingConfigFile)
ctx.Step(`^I have deleted the config file$`, sc.configSteps.iHaveDeletedTheConfigFile)
ctx.Step(`^I recreate the config file with valid configuration$`, sc.configSteps.iRecreateTheConfigFileWithValidConfiguration)
ctx.Step(`^the server should reload the configuration$`, sc.configSteps.theServerShouldReloadTheConfiguration)
ctx.Step(`^the new configuration should be applied$`, sc.configSteps.theNewConfigurationShouldBeApplied)
ctx.Step(`^I rapidly update the logging level multiple times$`, sc.configSteps.iRapidlyUpdateTheLoggingLevelMultipleTimes)
ctx.Step(`^all changes should be processed in order$`, sc.configSteps.allChangesShouldBeProcessedInOrder)
ctx.Step(`^the final configuration should be applied$`, sc.configSteps.theFinalConfigurationShouldBeApplied)
ctx.Step(`^no configuration changes should be lost$`, sc.configSteps.noConfigurationChangesShouldBeLost)
ctx.Step(`^audit logging is enabled$`, sc.configSteps.auditLoggingIsEnabled)
ctx.Step(`^an audit log entry should be created$`, sc.configSteps.anAuditLogEntryShouldBeCreated)
ctx.Step(`^the audit entry should contain the previous and new values$`, sc.configSteps.theAuditEntryShouldContainThePreviousAndNewValues)
ctx.Step(`^the audit entry should contain the timestamp of the change$`, sc.configSteps.theAuditEntryShouldContainTheTimestampOfTheChange)
// Common steps
ctx.Step(`^the response should be "{\\"([^"]*)":\\"([^"]*)"}"$`, sc.commonSteps.theResponseShouldBe)
ctx.Step(`^the response should contain error "([^"]*)"$`, sc.commonSteps.theResponseShouldContainError)