diff --git a/pkg/bdd/steps/jwt_retention_steps.go b/pkg/bdd/steps/jwt_retention_steps.go index baceb3f..8232941 100644 --- a/pkg/bdd/steps/jwt_retention_steps.go +++ b/pkg/bdd/steps/jwt_retention_steps.go @@ -6,6 +6,8 @@ import ( "strings" "dance-lessons-coach/pkg/bdd/testserver" + + "github.com/cucumber/godog" ) // JWTRetentionSteps holds JWT secret retention-related step definitions @@ -231,6 +233,14 @@ func (s *JWTRetentionSteps) iAddANewJWTSecret(secret string) error { }) } +func (s *JWTRetentionSteps) iAddANewJWTSecretNoArgs() error { + // Add a new JWT secret without specifying the secret (for testing) + return s.client.Request("POST", "/api/v1/admin/jwt/secrets", map[string]string{ + "secret": "test-secret-key-123456", + "is_primary": "false", + }) +} + func (s *JWTRetentionSteps) theLogsShouldShowMaskedSecret(masked string) error { // Verify log masking if !strings.Contains(masked, "****") { @@ -418,11 +428,6 @@ func (s *JWTRetentionSteps) andBothTokensShouldWorkConcurrently() error { // Emergency Rotation Steps -func (s *JWTRetentionSteps) givenASecurityIncidentRequiresImmediateRotation() error { - // Simulate security incident - return nil -} - func (s *JWTRetentionSteps) iRotateToANewPrimarySecret() error { // Emergency rotation return s.client.Request("POST", "/api/v1/admin/jwt/secrets/rotate", map[string]string{ @@ -445,6 +450,191 @@ func (s *JWTRetentionSteps) andCleanupShouldRemoveCompromisedSecrets() error { return nil } +// Additional missing steps for JWT retention +func (s *JWTRetentionSteps) givenASecurityIncidentRequiresImmediateRotation() error { + // Simulate security incident + return godog.ErrPending +} + +func (s *JWTRetentionSteps) bothTokensShouldWorkConcurrently() error { + // Verify concurrent validity + return godog.ErrPending +} + +func (s *JWTRetentionSteps) bothTokensShouldWorkUntilRetentionPeriodExpires() error { + // Verify tokens work until retention expires + return godog.ErrPending +} + +func (s *JWTRetentionSteps) continueWithRemainingSecrets() error { + // Verify continuation + return godog.ErrPending +} + +func (s *JWTRetentionSteps) existingSecretsShouldBeReevaluated() error { + // Verify reevaluation + return godog.ErrPending +} + +func (s *JWTRetentionSteps) iAddAnExpiredJWTSecret() error { + // Add expired secret + return godog.ErrPending +} + +func (s *JWTRetentionSteps) iAddExpiredJWTSecrets() error { + // Add multiple expired secrets + return godog.ErrPending +} + +func (s *JWTRetentionSteps) iAuthenticateAgainWithUsernameAndPassword(username, password string) error { + return godog.ErrPending +} + +func (s *JWTRetentionSteps) iHaveJWTSecretsOfDifferentAges(count int) error { + // Simulate having secrets of different ages + return godog.ErrPending +} + +func (s *JWTRetentionSteps) iReceiveAValidJWTTokenSignedWithPrimarySecret() error { + // Extract and store the token + return godog.ErrPending +} + +func (s *JWTRetentionSteps) iShouldReceiveANewTokenSignedWithSecondarySecret() error { + // Verify new token received + return godog.ErrPending +} + +func (s *JWTRetentionSteps) itTriesToRemoveASecret() error { + // Simulate secret removal attempt + return godog.ErrPending +} + +func (s *JWTRetentionSteps) manualCleanupShouldStillBePossible() error { + // Verify manual cleanup works + return godog.ErrPending +} + +func (s *JWTRetentionSteps) newTokensShouldUseTheEmergencySecret() error { + // Verify new tokens use emergency secret + return godog.ErrPending +} + +func (s *JWTRetentionSteps) notCrashTheCleanupProcess() error { + // Verify process doesn't crash + return godog.ErrPending +} + +func (s *JWTRetentionSteps) notExceedTheMaximumRetentionLimit() error { + // Verify maximum retention enforcement + return godog.ErrPending +} + +func (s *JWTRetentionSteps) notExposeTheFullSecretInLogs() error { + // Verify no full secret exposure + return godog.ErrPending +} + +func (s *JWTRetentionSteps) notImpactServerPerformance() error { + // Verify no performance impact + return godog.ErrPending +} + +func (s *JWTRetentionSteps) removeAllExpiredSecrets(count int) error { + // Verify all expired secrets removed + return godog.ErrPending +} + +func (s *JWTRetentionSteps) secretAIsHourOldWithinRetention(hours int) error { + // Simulate secret A within retention + return godog.ErrPending +} + +func (s *JWTRetentionSteps) secretAShouldBeRetained() error { + // Verify secret A retained + return godog.ErrPending +} + +func (s *JWTRetentionSteps) secretBIsHoursOldExpired(hours int) error { + // Simulate secret B expired + return godog.ErrPending +} + +func (s *JWTRetentionSteps) secretBShouldBeRemoved() error { + // Verify secret B removed + return godog.ErrPending +} + +func (s *JWTRetentionSteps) secretCIsThePrimarySecret() error { + // Verify secret C is primary + return godog.ErrPending +} + +func (s *JWTRetentionSteps) secretCShouldBeRetainedAsPrimary() error { + // Verify secret C retained as primary + return godog.ErrPending +} + +func (s *JWTRetentionSteps) suggestRemediationSteps() error { + // Verify remediation suggestions + return godog.ErrPending +} + +func (s *JWTRetentionSteps) theCleanupJobRemovesExpiredSecrets() error { + // Verify expired secrets removed + return godog.ErrPending +} + +func (s *JWTRetentionSteps) theCleanupJobRuns() error { + // Simulate cleanup job running + return godog.ErrPending +} + +func (s *JWTRetentionSteps) theJWTTTLIsHour(hours int) error { + // Set JWT TTL to 1 hour + return godog.ErrPending +} + +func (s *JWTRetentionSteps) theOldTokenShouldStillBeValidDuringRetentionPeriod() error { + // Verify old token still valid + return godog.ErrPending +} + +func (s *JWTRetentionSteps) thePrimarySecretIsOlderThanRetentionPeriod() error { + // Simulate primary secret older than retention + return godog.ErrPending +} + +func (s *JWTRetentionSteps) thePrimarySecretShouldNotBeRemoved() error { + // Verify primary secret not removed + return godog.ErrPending +} + +func (s *JWTRetentionSteps) theResponseShouldBe(arg1, arg2 string) error { + // Verify response content + return godog.ErrPending +} + +func (s *JWTRetentionSteps) theSecretIsLessThanCharacters(chars int) error { + // Verify secret validation + return godog.ErrPending +} + +func (s *JWTRetentionSteps) theSecretShouldExpireAfterHours(hours int) error { + // Verify expiration timing + return godog.ErrPending +} + +func (s *JWTRetentionSteps) tokenAShouldStillBeValidUntilRetentionExpires() error { + // Verify token A validity + return godog.ErrPending +} + +func (s *JWTRetentionSteps) whenTheSecretIsRemovedByCleanup() error { + // Simulate secret removal by cleanup + return godog.ErrPending +} + // Monitoring and Alerting Steps func (s *JWTRetentionSteps) iHaveMonitoringConfigured() error { diff --git a/pkg/bdd/steps/steps.go b/pkg/bdd/steps/steps.go index 8a6ee91..8411913 100644 --- a/pkg/bdd/steps/steps.go +++ b/pkg/bdd/steps/steps.go @@ -8,21 +8,23 @@ import ( // 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 } // 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), } } @@ -92,6 +94,122 @@ func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) { 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 calculated as "([^"]*)"$`, sc.jwtRetentionSteps.theRetentionPeriodShouldBeCalculatedAs) + ctx.Step(`^the retention period should be capped at (\d+) hours$`, sc.jwtRetentionSteps.theRetentionPeriodShouldBeCappedAtHours) + 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) + ctx.Step(`^a user "([^"]*)" exists with password "([^"]*)"$`, sc.jwtRetentionSteps.aUserExistsWithPassword) + ctx.Step(`^I authenticate with username "([^"]*)" and password "([^"]*)"$`, sc.jwtRetentionSteps.iAuthenticateWithUsernameAndPassword) + 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) + // Common steps ctx.Step(`^the response should be "{\\"([^"]*)":\\"([^"]*)"}"$`, sc.commonSteps.theResponseShouldBe) ctx.Step(`^the response should contain error "([^"]*)"$`, sc.commonSteps.theResponseShouldContainError) diff --git a/pkg/bdd/steps/steps.go.backup b/pkg/bdd/steps/steps.go.backup new file mode 100644 index 0000000..0955c5a --- /dev/null +++ b/pkg/bdd/steps/steps.go.backup @@ -0,0 +1,101 @@ +package steps + +import ( + "dance-lessons-coach/pkg/bdd/testserver" + + "github.com/cucumber/godog" +) + +// StepContext holds the test client and implements all step definitions +type StepContext struct { + client *testserver.Client + greetSteps *GreetSteps + healthSteps *HealthSteps + authSteps *AuthSteps + commonSteps *CommonSteps + jwtRetentionSteps *JWTRetentionSteps +} + +// 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), + jwtRetentionSteps: NewJWTRetentionSteps(client), + } +} + +// InitializeAllSteps registers all step definitions for the BDD tests +func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) { + sc := NewStepContext(client) + + // Greet steps + ctx.Step(`^I request a greeting for "([^"]*)"$`, sc.greetSteps.iRequestAGreetingFor) + ctx.Step(`^I request the default greeting$`, sc.greetSteps.iRequestTheDefaultGreeting) + ctx.Step(`^I send a POST request to v2 greet with name "([^"]*)"$`, sc.greetSteps.iSendPOSTRequestToV2GreetWithName) + ctx.Step(`^I send a POST request to v2 greet with invalid JSON "([^"]*)"$`, sc.greetSteps.iSendPOSTRequestToV2GreetWithInvalidJSON) + ctx.Step(`^the server is running with v2 enabled$`, sc.greetSteps.theServerIsRunningWithV2Enabled) + + // Health steps + ctx.Step(`^I request the health endpoint$`, sc.healthSteps.iRequestTheHealthEndpoint) + ctx.Step(`^the server is running$`, sc.healthSteps.theServerIsRunning) + + // Auth steps + ctx.Step(`^a user "([^"]*)" exists with password "([^"]*)"$`, sc.authSteps.aUserExistsWithPassword) + ctx.Step(`^I authenticate with username "([^"]*)" and password "([^"]*)"$`, sc.authSteps.iAuthenticateWithUsernameAndPassword) + ctx.Step(`^the authentication should be successful$`, sc.authSteps.theAuthenticationShouldBeSuccessful) + ctx.Step(`^I should receive a valid JWT token$`, sc.authSteps.iShouldReceiveAValidJWTToken) + ctx.Step(`^the authentication should fail$`, sc.authSteps.theAuthenticationShouldFail) + ctx.Step(`^I authenticate as admin with master password "([^"]*)"$`, sc.authSteps.iAuthenticateAsAdminWithMasterPassword) + ctx.Step(`^the token should contain admin claims$`, sc.authSteps.theTokenShouldContainAdminClaims) + ctx.Step(`^I register a new user "([^"]*)" with password "([^"]*)"$`, sc.authSteps.iRegisterANewUserWithPassword) + ctx.Step(`^the registration should be successful$`, sc.authSteps.theRegistrationShouldBeSuccessful) + ctx.Step(`^I should be able to authenticate with the new credentials$`, sc.authSteps.iShouldBeAbleToAuthenticateWithTheNewCredentials) + ctx.Step(`^I am authenticated as admin$`, sc.authSteps.iAmAuthenticatedAsAdmin) + ctx.Step(`^I request password reset for user "([^"]*)"$`, sc.authSteps.iRequestPasswordResetForUser) + ctx.Step(`^the password reset should be allowed$`, sc.authSteps.thePasswordResetShouldBeAllowed) + ctx.Step(`^the user should be flagged for password reset$`, sc.authSteps.theUserShouldBeFlaggedForPasswordReset) + ctx.Step(`^I complete password reset for "([^"]*)" with new password "([^"]*)"$`, sc.authSteps.iCompletePasswordResetForWithNewPassword) + ctx.Step(`^I should be able to authenticate with the new password$`, sc.authSteps.iShouldBeAbleToAuthenticateWithTheNewPassword) + ctx.Step(`^a user "([^"]*)" exists and is flagged for password reset$`, sc.authSteps.aUserExistsAndIsFlaggedForPasswordReset) + ctx.Step(`^the password reset should be successful$`, sc.authSteps.thePasswordResetShouldBeSuccessful) + ctx.Step(`^the password reset should fail$`, sc.authSteps.thePasswordResetShouldFail) + ctx.Step(`^the registration should fail$`, sc.authSteps.theRegistrationShouldFail) + ctx.Step(`^the authentication should fail with validation error$`, sc.authSteps.theAuthenticationShouldFailWithValidationError) + + // JWT edge case steps + ctx.Step(`^I use an expired JWT token for authentication$`, sc.authSteps.iUseAnExpiredJWTTokenForAuthentication) + ctx.Step(`^I use a JWT token signed with wrong secret for authentication$`, sc.authSteps.iUseAJWTTokenSignedWithWrongSecretForAuthentication) + ctx.Step(`^I use a malformed JWT token for authentication$`, sc.authSteps.iUseAMalformedJWTTokenForAuthentication) + + // JWT validation steps + ctx.Step(`^I validate the received JWT token$`, sc.authSteps.iValidateTheReceivedJWTToken) + ctx.Step(`^the token should be valid$`, sc.authSteps.theTokenShouldBeValid) + ctx.Step(`^it should contain the correct user ID$`, sc.authSteps.itShouldContainTheCorrectUserID) + 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) + + // Common steps + ctx.Step(`^the response should be "{\\"([^"]*)":\\"([^"]*)"}"$`, sc.commonSteps.theResponseShouldBe) + ctx.Step(`^the response should contain error "([^"]*)"$`, sc.commonSteps.theResponseShouldContainError) + ctx.Step(`^the status code should be (\d+)$`, sc.commonSteps.theStatusCodeShouldBe) +} diff --git a/pkg/bdd/testserver/server.go b/pkg/bdd/testserver/server.go index e59856a..8967b69 100644 --- a/pkg/bdd/testserver/server.go +++ b/pkg/bdd/testserver/server.go @@ -92,6 +92,7 @@ func (s *Server) initDBConnection() error { // Uses SET CONSTRAINTS ALL DEFERRED to temporarily disable foreign key checks func (s *Server) CleanupDatabase() error { if s.db == nil { + log.Debug().Msg("No database connection, skipping cleanup") return nil // No database connection, skip cleanup } diff --git a/scripts/run-bdd-tests.sh b/scripts/run-bdd-tests.sh index 3289fea..30c72a5 100755 --- a/scripts/run-bdd-tests.sh +++ b/scripts/run-bdd-tests.sh @@ -103,27 +103,31 @@ echo "$test_output" # Check for undefined steps if echo "$test_output" | grep -q "undefined"; then echo "❌ FAILED: Found undefined steps" + echo 'DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres DLC_DATABASE_NAME=dance_lessons_coach_bdd_test DLC_DATABASE_SSL_MODE=disable go test ./features/... -v' exit 1 fi # Check for pending steps if echo "$test_output" | grep -q "pending"; then echo "❌ FAILED: Found pending steps" + echo 'DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres DLC_DATABASE_NAME=dance_lessons_coach_bdd_test DLC_DATABASE_SSL_MODE=disable go test ./features/... -v' exit 1 fi # Check for skipped steps if echo "$test_output" | grep -q "skipped"; then echo "❌ FAILED: Found skipped steps" + echo 'DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres DLC_DATABASE_NAME=dance_lessons_coach_bdd_test DLC_DATABASE_SSL_MODE=disable go test ./features/... -v' exit 1 fi # Check if tests passed if [ $test_exit_code -eq 0 ]; then echo "✅ All BDD tests passed successfully!" + echo 'DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres DLC_DATABASE_NAME=dance_lessons_coach_bdd_test DLC_DATABASE_SSL_MODE=disable go test ./features/... -v' exit 0 else echo "❌ BDD tests failed" - echo echo 'DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres DLC_DATABASE_NAME=dance_lessons_coach_bdd_test DLC_DATABASE_SSL_MODE=disable go test ./features/... -v' + echo 'DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres DLC_DATABASE_NAME=dance_lessons_coach_bdd_test DLC_DATABASE_SSL_MODE=disable go test ./features/... -v' exit 1 fi diff --git a/scripts/run-bdd-tests.sh.backup b/scripts/run-bdd-tests.sh.backup deleted file mode 100755 index abdf8c1..0000000 --- a/scripts/run-bdd-tests.sh.backup +++ /dev/null @@ -1,177 +0,0 @@ -#!/bin/bash - -# BDD Test Runner Script -# Runs all BDD tests and fails if there are undefined, pending, or skipped steps - -set -e - -echo "🧪 Running BDD Tests..." -cd /Users/gabrielradureau/Work/Vibe/DanceLessonsCoach - -# Check if we're in CI environment -if [ -n "$GITHUB_ACTIONS" ] || [ -n "$GITEA_ACTIONS" ]; then - # CI environment - PostgreSQL is already running as a service - echo "🏗️ CI environment detected" - echo "🐋 PostgreSQL service is already running" - - # Check if database is accessible - echo "📦 Checking PostgreSQL connectivity..." - if ! pg_isready -h postgres -p 5432 -U postgres -d dance_lessons_coach_bdd_test; then - echo "❌ PostgreSQL is not ready or accessible" - exit 1 - fi - echo "✅ PostgreSQL is ready!" -else - # Local environment - use docker compose - echo "💻 Local environment detected" - - # Check if PostgreSQL container is running, start it if not - echo "🐋 Checking PostgreSQL container..." - if ! docker ps --format '{{.Names}}' | grep -q "^dance-lessons-coach-postgres$"; then - echo "🐋 Starting PostgreSQL container..." - docker compose up -d postgres - - # Wait for PostgreSQL to be ready - echo "⏳ Waiting for PostgreSQL to be ready..." - max_attempts=30 - attempt=0 - while [ $attempt -lt $max_attempts ]; do - if docker exec dance-lessons-coach-postgres pg_isready -U postgres 2>/dev/null; then - echo "✅ PostgreSQL is ready!" - break - fi - attempt=$((attempt + 1)) - sleep 1 - done - - if [ $attempt -eq $max_attempts ]; then - echo "❌ PostgreSQL failed to start" - exit 1 - fi - - # Create BDD test database (separate from development database) - echo "📦 Creating BDD test database..." - # Drop database if it exists, then create fresh - docker exec dance-lessons-coach-postgres psql -U postgres -c "DROP DATABASE IF EXISTS dance_lessons_coach_bdd_test;" - if docker exec dance-lessons-coach-postgres createdb -U postgres dance_lessons_coach_bdd_test; then - echo "✅ BDD test database created successfully!" - else - echo "❌ Failed to create BDD test database" - exit 1 - fi - else - echo "✅ PostgreSQL container is already running" - - # Check if BDD test database exists, create if not - echo "📦 Checking BDD test database..." - if docker exec dance-lessons-coach-postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -qw "dance_lessons_coach_bdd_test"; then - echo "✅ BDD test database already exists" - else - echo "📦 Creating BDD test database..." - if docker exec dance-lessons-coach-postgres createdb -U postgres dance_lessons_coach_bdd_test; then - echo "✅ BDD test database created successfully!" - else - echo "❌ Failed to create BDD test database" - exit 1 - fi - fi - fi -else - # CI environment - PostgreSQL is already running as a service - echo "🏗️ CI environment detected" - echo "🐋 PostgreSQL service is already running" - - # Check if database is accessible - echo "📦 Checking PostgreSQL connectivity..." - if ! pg_isready -h postgres -p 5432 -U postgres -d dance_lessons_coach_bdd_test; then - echo "❌ PostgreSQL is not ready or accessible" - exit 1 - fi - echo "✅ PostgreSQL is ready!" -else - # Check if PostgreSQL container is running, start it if not - echo "🐋 Checking PostgreSQL container..." - if ! docker ps --format '{{.Names}}' | grep -q "^dance-lessons-coach-postgres$"; then - echo "🐋 Starting PostgreSQL container..." - docker compose up -d postgres - - # Wait for PostgreSQL to be ready - echo "⏳ Waiting for PostgreSQL to be ready..." - max_attempts=30 - attempt=0 - while [ $attempt -lt $max_attempts ]; do - if docker exec dance-lessons-coach-postgres pg_isready -U postgres 2>/dev/null; then - echo "✅ PostgreSQL is ready!" - break - fi - attempt=$((attempt + 1)) - sleep 1 - done - - if [ $attempt -eq $max_attempts ]; then - echo "❌ PostgreSQL failed to start" - exit 1 - fi - - # Create BDD test database (separate from development database) - echo "📦 Creating BDD test database..." - # Drop database if it exists, then create fresh - docker exec dance-lessons-coach-postgres psql -U postgres -c "DROP DATABASE IF EXISTS dance_lessons_coach_bdd_test;" - if docker exec dance-lessons-coach-postgres createdb -U postgres dance_lessons_coach_bdd_test; then - echo "✅ BDD test database created successfully!" - else - echo "❌ Failed to create BDD test database" - exit 1 - fi - else - echo "✅ PostgreSQL container is already running" - - # Check if BDD test database exists, create if not - echo "📦 Checking BDD test database..." - if docker exec dance-lessons-coach-postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -qw "dance_lessons_coach_bdd_test"; then - echo "✅ BDD test database already exists" - else - echo "📦 Creating BDD test database..." - if docker exec dance-lessons-coach-postgres createdb -U postgres dance_lessons_coach_bdd_test; then - echo "✅ BDD test database created successfully!" - else - echo "❌ Failed to create BDD test database" - exit 1 - fi - fi - fi -fi - -# Run the BDD tests -test_output=$(go test ./features/... -v 2>&1) -test_exit_code=$? - -echo "$test_output" - -# Check for undefined steps -if echo "$test_output" | grep -q "undefined"; then - echo "❌ FAILED: Found undefined steps" - exit 1 -fi - -# Check for pending steps -if echo "$test_output" | grep -q "pending"; then - echo "❌ FAILED: Found pending steps" - exit 1 -fi - -# Check for skipped steps -if echo "$test_output" | grep -q "skipped"; then - echo "❌ FAILED: Found skipped steps" - exit 1 -fi - -# Check if tests passed -if [ $test_exit_code -eq 0 ]; then - echo "✅ All BDD tests passed successfully!" - exit 0 -else - echo "❌ BDD tests failed" - echo 'DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres DLC_DATABASE_NAME=dance_lessons_coach_bdd_test DLC_DATABASE_SSL_MODE=disable go test ./features/... -v' - exit 1 -fi