🧪 test: add JWT secret rotation BDD scenarios and step implementations #12
@@ -6,6 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dance-lessons-coach/pkg/bdd/testserver"
|
"dance-lessons-coach/pkg/bdd/testserver"
|
||||||
|
|
||||||
|
"github.com/cucumber/godog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JWTRetentionSteps holds JWT secret retention-related step definitions
|
// 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 {
|
func (s *JWTRetentionSteps) theLogsShouldShowMaskedSecret(masked string) error {
|
||||||
// Verify log masking
|
// Verify log masking
|
||||||
if !strings.Contains(masked, "****") {
|
if !strings.Contains(masked, "****") {
|
||||||
@@ -418,11 +428,6 @@ func (s *JWTRetentionSteps) andBothTokensShouldWorkConcurrently() error {
|
|||||||
|
|
||||||
// Emergency Rotation Steps
|
// Emergency Rotation Steps
|
||||||
|
|
||||||
func (s *JWTRetentionSteps) givenASecurityIncidentRequiresImmediateRotation() error {
|
|
||||||
// Simulate security incident
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *JWTRetentionSteps) iRotateToANewPrimarySecret() error {
|
func (s *JWTRetentionSteps) iRotateToANewPrimarySecret() error {
|
||||||
// Emergency rotation
|
// Emergency rotation
|
||||||
return s.client.Request("POST", "/api/v1/admin/jwt/secrets/rotate", map[string]string{
|
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
|
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
|
// Monitoring and Alerting Steps
|
||||||
|
|
||||||
func (s *JWTRetentionSteps) iHaveMonitoringConfigured() error {
|
func (s *JWTRetentionSteps) iHaveMonitoringConfigured() error {
|
||||||
|
|||||||
@@ -8,21 +8,23 @@ import (
|
|||||||
|
|
||||||
// StepContext holds the test client and implements all step definitions
|
// StepContext holds the test client and implements all step definitions
|
||||||
type StepContext struct {
|
type StepContext struct {
|
||||||
client *testserver.Client
|
client *testserver.Client
|
||||||
greetSteps *GreetSteps
|
greetSteps *GreetSteps
|
||||||
healthSteps *HealthSteps
|
healthSteps *HealthSteps
|
||||||
authSteps *AuthSteps
|
authSteps *AuthSteps
|
||||||
commonSteps *CommonSteps
|
commonSteps *CommonSteps
|
||||||
|
jwtRetentionSteps *JWTRetentionSteps
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStepContext creates a new step context
|
// NewStepContext creates a new step context
|
||||||
func NewStepContext(client *testserver.Client) *StepContext {
|
func NewStepContext(client *testserver.Client) *StepContext {
|
||||||
return &StepContext{
|
return &StepContext{
|
||||||
client: client,
|
client: client,
|
||||||
greetSteps: NewGreetSteps(client),
|
greetSteps: NewGreetSteps(client),
|
||||||
healthSteps: NewHealthSteps(client),
|
healthSteps: NewHealthSteps(client),
|
||||||
authSteps: NewAuthSteps(client),
|
authSteps: NewAuthSteps(client),
|
||||||
commonSteps: NewCommonSteps(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 server is running with primary and expired secondary JWT secrets$`, sc.authSteps.theServerIsRunningWithPrimaryAndExpiredSecondaryJWTSecrets)
|
||||||
ctx.Step(`^the token should still be valid$`, sc.authSteps.theTokenShouldStillBeValid)
|
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
|
// Common steps
|
||||||
ctx.Step(`^the response should be "{\\"([^"]*)":\\"([^"]*)"}"$`, sc.commonSteps.theResponseShouldBe)
|
ctx.Step(`^the response should be "{\\"([^"]*)":\\"([^"]*)"}"$`, sc.commonSteps.theResponseShouldBe)
|
||||||
ctx.Step(`^the response should contain error "([^"]*)"$`, sc.commonSteps.theResponseShouldContainError)
|
ctx.Step(`^the response should contain error "([^"]*)"$`, sc.commonSteps.theResponseShouldContainError)
|
||||||
|
|||||||
101
pkg/bdd/steps/steps.go.backup
Normal file
101
pkg/bdd/steps/steps.go.backup
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ func (s *Server) initDBConnection() error {
|
|||||||
// Uses SET CONSTRAINTS ALL DEFERRED to temporarily disable foreign key checks
|
// Uses SET CONSTRAINTS ALL DEFERRED to temporarily disable foreign key checks
|
||||||
func (s *Server) CleanupDatabase() error {
|
func (s *Server) CleanupDatabase() error {
|
||||||
if s.db == nil {
|
if s.db == nil {
|
||||||
|
log.Debug().Msg("No database connection, skipping cleanup")
|
||||||
return nil // No database connection, skip cleanup
|
return nil // No database connection, skip cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,27 +103,31 @@ echo "$test_output"
|
|||||||
# Check for undefined steps
|
# Check for undefined steps
|
||||||
if echo "$test_output" | grep -q "undefined"; then
|
if echo "$test_output" | grep -q "undefined"; then
|
||||||
echo "❌ FAILED: Found undefined steps"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for pending steps
|
# Check for pending steps
|
||||||
if echo "$test_output" | grep -q "pending"; then
|
if echo "$test_output" | grep -q "pending"; then
|
||||||
echo "❌ FAILED: Found pending steps"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for skipped steps
|
# Check for skipped steps
|
||||||
if echo "$test_output" | grep -q "skipped"; then
|
if echo "$test_output" | grep -q "skipped"; then
|
||||||
echo "❌ FAILED: Found skipped steps"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if tests passed
|
# Check if tests passed
|
||||||
if [ $test_exit_code -eq 0 ]; then
|
if [ $test_exit_code -eq 0 ]; then
|
||||||
echo "✅ All BDD tests passed successfully!"
|
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
|
exit 0
|
||||||
else
|
else
|
||||||
echo "❌ BDD tests failed"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user