Files
dance-lessons-coach/pkg/bdd/steps/jwt_retention_steps.go
Gabriel Radureau 418aaeaf95 🧪 test(bdd): admin metadata endpoint security property — no secret leak
Activates a new @critical @admin-introspection scenario in
features/jwt/jwt_secret_retention.feature that exercises the GET
/api/v1/admin/jwt/secrets endpoint added in PR #51.

The scenario asserts the SECURITY-CRITICAL property: the metadata
endpoint exposes structure (count + per-secret is_primary, age,
fingerprint) WITHOUT leaking secret values. If a future change
accidentally adds the secret value to the response, this test fails
loud:
  SECURITY: response leaked the secret value "test-secret-do-not-leak..."

Specifically, the BDD asserts:
- After adding a secondary secret with a known value, GET returns 200
- The response contains 2 secrets in count
- The response does NOT contain the secret value anywhere
- Every entry has a non-empty SHA-256 fingerprint

4 new step definitions added to pkg/bdd/steps/jwt_retention_steps.go:
- iAddASecondaryJWTSecretNamed (parameterised by secret value)
- iRequestTheJWTSecretsMetadataEndpoint
- theMetadataShouldContainNSecrets
- theMetadataShouldNotContainTheSecretValue (the security check)
- everySecretInTheMetadataShouldHaveASHA256Fingerprint

Tests:
- Scenario passes via @admin-introspection tag filter.
- Full BDD suite (auth/config/greet/health/info/jwt) green.

The pre-existing @todo scenarios (Multiple secrets with different ages,
Cleanup frequency configuration, etc.) remain @todo — they require
arbitrary timestamp setup or manual cleanup triggers that aren't
exposed via API, by design. Documented as future test-infrastructure
work.
2026-05-05 09:55:52 +02:00

883 lines
26 KiB
Go

package steps
import (
"fmt"
"strconv"
"strings"
"dance-lessons-coach/pkg/bdd/testserver"
"github.com/cucumber/godog"
)
// JWTRetentionSteps holds JWT secret retention-related step definitions
type JWTRetentionSteps struct {
client *testserver.Client
scenarioKey string // Track current scenario for state isolation
cleanupLogs []string
expectedTTL int
retentionFactor float64
maxRetention int
elapsedHours int
metricsEnabled bool
lastMetric string
metricIncremented bool
metricDecremented bool
lastHistogramMetric string
histogramUpdated bool
}
func NewJWTRetentionSteps(client *testserver.Client) *JWTRetentionSteps {
return &JWTRetentionSteps{
client: client,
}
}
// SetScenarioKey sets the current scenario key for state isolation
func (s *JWTRetentionSteps) SetScenarioKey(key string) {
s.scenarioKey = key
}
// getState returns the per-scenario state
func (s *JWTRetentionSteps) getState() *ScenarioState {
if s.scenarioKey == "" {
s.scenarioKey = "default"
}
return GetScenarioState(s.scenarioKey)
}
// LastSecret returns the last secret from per-scenario state
func (s *JWTRetentionSteps) LastSecret() string {
return s.getState().LastSecret
}
// SetLastSecret sets the last secret in per-scenario state
func (s *JWTRetentionSteps) SetLastSecret(secret string) {
state := s.getState()
state.LastSecret = secret
}
// LastError returns the last error from per-scenario state
func (s *JWTRetentionSteps) LastError() string {
return s.getState().LastError
}
// SetLastError sets the last error in per-scenario state
func (s *JWTRetentionSteps) SetLastError(err string) {
state := s.getState()
state.LastError = err
}
// Configuration Steps
func (s *JWTRetentionSteps) theServerIsRunningWithJWTSecretRetentionConfigured() error {
// Verify server is running and has retention configuration
return s.client.Request("GET", "/api/ready", nil)
}
func (s *JWTRetentionSteps) theDefaultJWTTTLIsHours(hours int) error {
// Verify the default TTL configuration
// For now, we'll just verify server is running and store the expected value
s.expectedTTL = hours
return s.client.Request("GET", "/api/ready", nil)
}
func (s *JWTRetentionSteps) theRetentionFactorIs(factor float64) error {
// Set the retention factor for verification
s.retentionFactor = factor
return nil
}
func (s *JWTRetentionSteps) theMaximumRetentionIsHours(hours int) error {
// Set the maximum retention for verification
s.maxRetention = hours
return nil
}
func (s *JWTRetentionSteps) theRetentionPeriodShouldBeHours(hours int) error {
// Verify the retention period calculation
// Calculate expected retention: TTL * retentionFactor
expectedRetention := float64(s.expectedTTL) * s.retentionFactor
// Cap at maximum retention if specified
if s.maxRetention > 0 && expectedRetention > float64(s.maxRetention) {
expectedRetention = float64(s.maxRetention)
}
// Verify the calculated retention matches expected
if int(expectedRetention) != hours {
return fmt.Errorf("expected retention period %d hours, calculated %d hours", hours, int(expectedRetention))
}
return s.client.Request("GET", "/api/ready", nil)
}
// Secret Management Steps
func (s *JWTRetentionSteps) aPrimaryJWTSecretExists() error {
// Primary secret should exist by default
// Verify we can authenticate
req := map[string]string{"username": "testuser", "password": "testpass123"}
return s.client.Request("POST", "/api/v1/auth/register", req)
}
func (s *JWTRetentionSteps) iAddASecondaryJWTSecretWithHourExpiration(hours int) error {
// Add a secondary secret with specific expiration
secret := "secondary-secret-for-testing-" + strconv.Itoa(hours)
s.SetLastSecret(secret)
return s.client.Request("POST", "/api/v1/admin/jwt/secrets", map[string]string{
"secret": secret,
"is_primary": "false",
})
}
func (s *JWTRetentionSteps) iWaitForTheRetentionPeriodToElapse() error {
// Simulate waiting for retention period
// Calculate expected retention period
retentionHours := float64(s.expectedTTL) * s.retentionFactor
if s.maxRetention > 0 && retentionHours > float64(s.maxRetention) {
retentionHours = float64(s.maxRetention)
}
// Store the elapsed time for verification
s.elapsedHours = int(retentionHours)
return nil
}
func (s *JWTRetentionSteps) theExpiredSecondarySecretShouldBeAutomaticallyRemoved() error {
// Verify the secondary secret is no longer valid
// In our test implementation, we'll simulate cleanup by checking the secret list
// Get the current list of JWT secrets
err := s.client.Request("GET", "/api/v1/admin/jwt/secrets", nil)
if err != nil {
return err
}
// Parse the response to check if our secondary secret is still there
lastSecret := s.LastSecret()
body := string(s.client.GetLastBody())
if strings.Contains(body, lastSecret) {
return fmt.Errorf("expected secondary secret %s to be removed, but it's still present", lastSecret)
}
// Also verify that authentication still works with primary secret
req := map[string]string{"username": "testuser", "password": "testpass123"}
err = s.client.Request("POST", "/api/v1/auth/login", req)
if err != nil {
return fmt.Errorf("primary secret should still work after secondary secret removal: %v", err)
}
return nil
}
func (s *JWTRetentionSteps) thePrimarySecretShouldRemainActive() error {
// Verify primary secret still works
req := map[string]string{"username": "testuser", "password": "testpass123"}
return s.client.Request("POST", "/api/v1/auth/login", req)
}
func (s *JWTRetentionSteps) iShouldSeeCleanupEventInLogs() error {
// Check for cleanup events
// In our test implementation, we'll verify that the cleanup occurred by checking the secret count
// Get server status or logs to verify cleanup happened
err := s.client.Request("GET", "/api/v1/admin/jwt/secrets", nil)
if err != nil {
return err
}
// Parse the response to check if cleanup occurred (secret count should be reduced)
body := string(s.client.GetLastBody())
// For our test, we'll consider it successful if we can verify the secret was removed
// In a real implementation, this would check actual log files or monitoring endpoints
lastSecret := s.LastSecret()
if strings.Contains(body, lastSecret) {
return fmt.Errorf("cleanup should have removed secret %s, but it's still present", lastSecret)
}
// Simulate log verification - in real implementation would check actual logs
// For test purposes, we'll just verify the secret is gone
return nil
}
// Retention Calculation Steps
func (s *JWTRetentionSteps) theJWTTTLIsSetToHours(hours int) error {
// Set JWT TTL for testing
s.expectedTTL = hours
return nil
}
func (s *JWTRetentionSteps) theRetentionPeriodShouldBeCappedAtHours(hours int) error {
// Verify maximum retention enforcement
// Calculate expected retention: TTL * retentionFactor
expectedRetention := float64(s.expectedTTL) * s.retentionFactor
// Cap at maximum retention
if expectedRetention > float64(hours) {
expectedRetention = float64(hours)
}
// Verify the calculated retention matches expected maximum
if int(expectedRetention) != hours {
return fmt.Errorf("expected retention period to be capped at %d hours, calculated %d hours", hours, int(expectedRetention))
}
return s.client.Request("GET", "/api/ready", nil)
}
// Cleanup Frequency Steps
func (s *JWTRetentionSteps) theCleanupIntervalIsSetToMinutes(minutes int) error {
// Set cleanup interval
return godog.ErrPending
}
func (s *JWTRetentionSteps) itShouldBeRemovedWithinMinutes(minutes int) error {
// Verify timely removal
return godog.ErrPending
}
func (s *JWTRetentionSteps) iShouldSeeCleanupEventsEveryMinutes(minutes int) error {
// Verify regular cleanup events
return godog.ErrPending
}
// Token Validation Steps
func (s *JWTRetentionSteps) aUserExistsWithPassword(username, password string) error {
return s.client.Request("POST", "/api/v1/auth/register", map[string]string{
"username": username,
"password": password,
})
}
func (s *JWTRetentionSteps) iAuthenticateWithUsernameAndPassword(username, password string) error {
return s.client.Request("POST", "/api/v1/auth/login", map[string]string{
"username": username,
"password": password,
})
}
func (s *JWTRetentionSteps) iReceiveAValidJWTTokenSignedWithCurrentSecret() error {
// Extract and store the token
body := string(s.client.GetLastBody())
if strings.Contains(body, "token") {
// Parse and store token
}
return nil
}
func (s *JWTRetentionSteps) iWaitForTheSecretToExpire() error {
// Simulate waiting for secret expiration
return godog.ErrPending
}
func (s *JWTRetentionSteps) iTryToValidateTheExpiredToken() error {
// Try to validate an expired token
return s.client.Request("POST", "/api/v1/auth/validate", map[string]string{
"token": "expired-token-for-testing",
})
}
func (s *JWTRetentionSteps) theTokenValidationShouldFail() error {
// Verify validation fails
if s.client.GetLastStatusCode() != 401 {
return fmt.Errorf("expected token validation to fail with 401, got %d", s.client.GetLastStatusCode())
}
return nil
}
func (s *JWTRetentionSteps) iShouldReceiveInvalidTokenError() error {
// Verify error response
body := string(s.client.GetLastBody())
if !strings.Contains(body, "invalid_token") {
return fmt.Errorf("expected invalid_token error, got %s", body)
}
return nil
}
// Configuration Validation Steps
func (s *JWTRetentionSteps) iSetRetentionFactorTo(factor float64) error {
// Set the retention factor (validation happens when starting server)
s.retentionFactor = factor
return nil
}
func (s *JWTRetentionSteps) iTryToStartTheServer() error {
// Server should fail to start with invalid config
// Check if there was a previous validation error
if s.retentionFactor < 1.0 {
s.SetLastError("retention_factor must be ≥ 1.0")
return nil // Store error for later verification
}
s.SetLastError("configuration validation error")
return nil // Store error for later verification
}
func (s *JWTRetentionSteps) iShouldReceiveConfigurationValidationError() error {
// Verify validation error occurred
// The error should have been stored from the previous step
if s.LastError() == "" {
return fmt.Errorf("expected validation error but none occurred")
}
return nil
}
func (s *JWTRetentionSteps) theErrorShouldMention(message string) error {
// Verify error message content
if !strings.Contains(s.LastError(), message) {
return fmt.Errorf("expected error to mention '%s', got: '%s'", message, s.LastError())
}
return nil
}
// Metrics Steps
func (s *JWTRetentionSteps) iHaveEnabledPrometheusMetrics() error {
// Enable metrics in configuration
s.metricsEnabled = true
return nil
}
func (s *JWTRetentionSteps) iShouldSeeMetricIncrement(metric string) error {
// Verify metric was incremented
// In real implementation, this would check actual metrics
return godog.ErrPending
}
func (s *JWTRetentionSteps) iShouldSeeMetricDecrease(metric string) error {
// Verify metric was decremented
// In real implementation, this would check actual metrics
return godog.ErrPending
}
func (s *JWTRetentionSteps) iShouldSeeHistogramUpdate(metric string) error {
// Verify histogram was updated
// In real implementation, this would check actual histogram metrics
return godog.ErrPending
}
// Logging Steps
func (s *JWTRetentionSteps) iAddANewJWTSecret(secret string) error {
s.SetLastSecret(secret)
return s.client.Request("POST", "/api/v1/admin/jwt/secrets", map[string]string{
"secret": secret,
"is_primary": "false",
})
}
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, "****") {
return fmt.Errorf("expected masked secret, got %s", masked)
}
return nil
}
func (s *JWTRetentionSteps) theLogsShouldNotExposeTheFullSecret() error {
// Verify no full secret exposure
// In real implementation, this would check log output
return godog.ErrPending
}
// Performance Steps
func (s *JWTRetentionSteps) iHaveJWTSecrets(count int) error {
// Simulate having many secrets
return godog.ErrPending
}
func (s *JWTRetentionSteps) ofThemAreExpired(expiredCount int) error {
// Simulate expired secrets
return godog.ErrPending
}
func (s *JWTRetentionSteps) itShouldCompleteWithinMilliseconds(ms int) error {
// Verify performance
return godog.ErrPending
}
func (s *JWTRetentionSteps) andNotImpactServerPerformance() error {
// Verify no performance impact
return godog.ErrPending
}
// Configuration Management Steps
func (s *JWTRetentionSteps) iSetCleanupIntervalToHours(hours int) error {
// Set very high cleanup interval (effectively disabled)
return godog.ErrPending
}
func (s *JWTRetentionSteps) theyShouldNotBeAutomaticallyRemoved() error {
// Verify no automatic cleanup
return godog.ErrPending
}
func (s *JWTRetentionSteps) andManualCleanupShouldStillBePossible() error {
// Verify manual cleanup still works
return godog.ErrPending
}
// Edge Case Steps
func (s *JWTRetentionSteps) theRetentionPeriodShouldBeHour() error {
// Verify 1-hour retention
return godog.ErrPending
}
func (s *JWTRetentionSteps) theSecretShouldExpireAfterHour() error {
// Verify expiration timing
return godog.ErrPending
}
// Validation Steps
func (s *JWTRetentionSteps) iTryToAddAnInvalidJWTSecret() error {
// Try to add invalid secret
return s.client.Request("POST", "/api/v1/admin/jwt/secrets", map[string]string{
"secret": "short",
"is_primary": "false",
})
}
func (s *JWTRetentionSteps) iShouldReceiveValidationError() error {
// Verify validation error
if s.client.GetLastStatusCode() != 400 {
return fmt.Errorf("expected validation error")
}
return nil
}
func (s *JWTRetentionSteps) theErrorShouldMentionMinimumCharacters() error {
// Verify error message
body := string(s.client.GetLastBody())
if !strings.Contains(body, "16 characters") {
return fmt.Errorf("expected minimum characters error")
}
return nil
}
// Error Handling Steps
func (s *JWTRetentionSteps) theCleanupJobEncountersAnError() error {
// Simulate cleanup error
return godog.ErrPending
}
func (s *JWTRetentionSteps) itShouldLogTheError() error {
// Verify error logging
return godog.ErrPending
}
func (s *JWTRetentionSteps) andContinueWithRemainingSecrets() error {
// Verify continuation
return godog.ErrPending
}
func (s *JWTRetentionSteps) andNotCrashTheCleanupProcess() error {
// Verify process doesn't crash
return godog.ErrPending
}
// Configuration Reload Steps
func (s *JWTRetentionSteps) theServerIsRunningWithDefaultRetentionSettings() error {
// Verify default settings
return godog.ErrPending
}
func (s *JWTRetentionSteps) iUpdateTheRetentionFactorViaConfiguration() error {
// Update configuration
return godog.ErrPending
}
func (s *JWTRetentionSteps) theNewSettingsShouldTakeEffectImmediately() error {
// Verify immediate effect
return godog.ErrPending
}
func (s *JWTRetentionSteps) andExistingSecretsShouldBeReevaluated() error {
// Verify reevaluation
return godog.ErrPending
}
func (s *JWTRetentionSteps) andCleanupShouldUseNewRetentionPeriods() error {
// Verify new periods used
return godog.ErrPending
}
// Audit Trail Steps
func (s *JWTRetentionSteps) iEnableAuditLogging() error {
// Enable audit logging
return godog.ErrPending
}
func (s *JWTRetentionSteps) iShouldSeeAuditLogEntryWithEventType(eventType string) error {
// Verify audit log entry
return godog.ErrPending
}
// Token Refresh Steps
func (s *JWTRetentionSteps) iAuthenticateAndReceiveTokenA() error {
// First authentication
return s.client.Request("POST", "/api/v1/auth/login", map[string]string{
"username": "refreshuser",
"password": "testpass123",
})
}
func (s *JWTRetentionSteps) iRefreshMyTokenDuringRetentionPeriod() error {
// Token refresh
return s.client.Request("POST", "/api/v1/auth/login", map[string]string{
"username": "refreshuser",
"password": "testpass123",
})
}
func (s *JWTRetentionSteps) iShouldReceiveNewTokenB() error {
// Verify new token received
return godog.ErrPending
}
func (s *JWTRetentionSteps) andTokenAShouldStillBeValidUntilRetentionExpires() error {
// Verify old token still works
return godog.ErrPending
}
func (s *JWTRetentionSteps) andBothTokensShouldWorkConcurrently() error {
// Verify concurrent validity
return godog.ErrPending
}
// Emergency Rotation Steps
func (s *JWTRetentionSteps) iRotateToANewPrimarySecret() error {
// Emergency rotation
return s.client.Request("POST", "/api/v1/admin/jwt/secrets/rotate", map[string]string{
"new_secret": "emergency-secret-key-987654",
})
}
func (s *JWTRetentionSteps) oldTokensShouldBeInvalidatedImmediately() error {
// Verify immediate invalidation
return godog.ErrPending
}
func (s *JWTRetentionSteps) andNewTokensShouldUseTheEmergencySecret() error {
// Verify new tokens use emergency secret
return godog.ErrPending
}
func (s *JWTRetentionSteps) andCleanupShouldRemoveCompromisedSecrets() error {
// Verify compromised secrets removed
return godog.ErrPending
}
// 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 {
// Re-authenticate with the same credentials
req := map[string]string{"username": username, "password": password}
return s.client.Request("POST", "/api/v1/auth/login", req)
}
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
// Calculate expected retention: TTL * retentionFactor
expectedRetention := float64(s.expectedTTL) * s.retentionFactor
// Cap at maximum retention
if expectedRetention > float64(s.maxRetention) {
expectedRetention = float64(s.maxRetention)
}
// Verify the calculated retention doesn't exceed maximum
if int(expectedRetention) > s.maxRetention {
return fmt.Errorf("retention period %d hours exceeds maximum retention limit %d hours", int(expectedRetention), s.maxRetention)
}
return nil
}
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 {
// Trigger the cleanup job via admin API
return s.client.Request("POST", "/api/v1/admin/jwt/secrets/cleanup", nil)
}
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 {
// Set the primary secret creation time to be older than retention period
// This is a simulation for testing - in production this would be automatic
// For now, we skip this as the implementation is pending
return nil
}
func (s *JWTRetentionSteps) thePrimarySecretShouldNotBeRemoved() error {
// Verify primary secret not removed by ensuring we can still authenticate
req := map[string]string{"username": "testuser", "password": "testpass123"}
return s.client.Request("POST", "/api/v1/auth/login", req)
}
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 based on TTL and retention factor
expectedExpiration := float64(s.expectedTTL) * s.retentionFactor
if int(expectedExpiration) != hours {
return fmt.Errorf("expected secret to expire after %d hours, calculated %d hours", hours, int(expectedExpiration))
}
return nil
}
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 {
// Configure monitoring
return godog.ErrPending
}
func (s *JWTRetentionSteps) theCleanupJobFailsRepeatedly() error {
// Simulate repeated failures
return godog.ErrPending
}
func (s *JWTRetentionSteps) iShouldReceiveAlertNotification() error {
// Verify alert received
return godog.ErrPending
}
func (s *JWTRetentionSteps) theAlertShouldIncludeErrorDetails() error {
// Verify error details included
return godog.ErrPending
}
func (s *JWTRetentionSteps) andSuggestRemediationSteps() error {
// Verify remediation suggestions
return godog.ErrPending
}
// =====================================================================
// Admin metadata introspection steps (PR #51 + this scenario)
// =====================================================================
// iAddASecondaryJWTSecretNamed adds a secret with a specific value via the
// admin API. Used by the admin-introspection scenario to verify that the
// metadata endpoint returns metadata only, not the secret value.
func (s *JWTRetentionSteps) iAddASecondaryJWTSecretNamed(secretValue string) error {
s.SetLastSecret(secretValue)
return s.client.Request("POST", "/api/v1/admin/jwt/secrets", map[string]string{
"secret": secretValue,
"is_primary": "false",
})
}
// iRequestTheJWTSecretsMetadataEndpoint hits GET /api/v1/admin/jwt/secrets.
func (s *JWTRetentionSteps) iRequestTheJWTSecretsMetadataEndpoint() error {
return s.client.Request("GET", "/api/v1/admin/jwt/secrets", nil)
}
// theMetadataShouldContainNSecrets verifies the response count field.
func (s *JWTRetentionSteps) theMetadataShouldContainNSecrets(expected int) error {
body := string(s.client.GetLastBody())
expectedFragment := `"count":` + strconv.Itoa(expected)
if !strings.Contains(body, expectedFragment) {
return fmt.Errorf("expected response to contain %q, got: %s", expectedFragment, body)
}
return nil
}
// theMetadataShouldNotContainTheSecretValue is the SECURITY-CRITICAL
// assertion. If the response contains the raw secret string anywhere,
// the endpoint has leaked. This is the property the metadata-only design
// is supposed to guarantee.
func (s *JWTRetentionSteps) theMetadataShouldNotContainTheSecretValue(secretValue string) error {
body := string(s.client.GetLastBody())
if strings.Contains(body, secretValue) {
return fmt.Errorf("SECURITY: response leaked the secret value %q (response body: %s)", secretValue, body)
}
return nil
}
// everySecretInTheMetadataShouldHaveASHA256Fingerprint asserts the
// secret_sha256 field is present and non-empty for each entry. Cheap
// regex-style check on the JSON body.
func (s *JWTRetentionSteps) everySecretInTheMetadataShouldHaveASHA256Fingerprint() error {
body := string(s.client.GetLastBody())
// Expect at least one occurrence of "secret_sha256":"<non-empty>"
if !strings.Contains(body, `"secret_sha256":"`) {
return fmt.Errorf("response does not include any secret_sha256 fingerprint: %s", body)
}
// Reject obviously-empty values
if strings.Contains(body, `"secret_sha256":""`) {
return fmt.Errorf("at least one secret_sha256 fingerprint is empty in response: %s", body)
}
return nil
}