Compare commits
7 Commits
520da07bfe
...
4df20585b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 4df20585b8 | |||
| aa4823eb11 | |||
| 756fc5abfd | |||
| 1f92302eff | |||
| 4292f79c6a | |||
| e9fd453a88 | |||
| a75f87777b |
@@ -26,6 +26,66 @@ Used to categorize tests by system component:
|
|||||||
- `@database` - Database interaction tests
|
- `@database` - Database interaction tests
|
||||||
- `@security` - Security-related tests
|
- `@security` - Security-related tests
|
||||||
|
|
||||||
|
### Exclusion Tags
|
||||||
|
Used to exclude tests from execution:
|
||||||
|
- `@flaky` - Tests that are unstable or intermittently fail
|
||||||
|
- `@todo` - Tests with pending step implementations
|
||||||
|
- `@skip` - Tests that should be skipped entirely
|
||||||
|
|
||||||
|
### Work In Progress Tag
|
||||||
|
Used to override exclusions for active development:
|
||||||
|
- `@wip` - Work In Progress - overrides exclusion tags to allow focused development
|
||||||
|
|
||||||
|
**Usage:** Add `@wip` to scenarios you're actively working on, even if they have other exclusion tags like `@todo` or `@skip`. The `@wip` tag takes precedence and allows the scenario to run.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```gherkin
|
||||||
|
@todo @wip
|
||||||
|
Scenario: JWT authentication with multiple secrets
|
||||||
|
Given the server is running with multiple JWT secrets
|
||||||
|
When I authenticate with valid credentials
|
||||||
|
Then I should receive a valid JWT token
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-Line Tag Override
|
||||||
|
You can override the default tag filtering by setting the `GODOG_TAGS` environment variable when running tests.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Run only @wip scenarios
|
||||||
|
GODOG_TAGS="@wip" go test ./features/jwt/...
|
||||||
|
|
||||||
|
# Run smoke tests only
|
||||||
|
GODOG_TAGS="@smoke" go test ./features/...
|
||||||
|
|
||||||
|
# Run specific combination
|
||||||
|
GODOG_TAGS="@jwt && ~@todo" go test ./features/...
|
||||||
|
|
||||||
|
# Combine with other environment variables
|
||||||
|
DLC_DATABASE_HOST=localhost GODOG_TAGS="@wip" go test ./features/jwt/...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop On Failure Control
|
||||||
|
You can control whether tests stop on first failure using the `GODOG_STOP_ON_FAILURE` environment variable.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Stop on first failure (strict mode)
|
||||||
|
GODOG_STOP_ON_FAILURE="true" go test ./features/jwt/...
|
||||||
|
|
||||||
|
# Continue after failures (lenient mode)
|
||||||
|
GODOG_STOP_ON_FAILURE="false" go test ./features/jwt/...
|
||||||
|
|
||||||
|
# Combine with tag filtering
|
||||||
|
GODOG_TAGS="@wip" GODOG_STOP_ON_FAILURE="true" go test ./features/jwt/...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default Behavior:**
|
||||||
|
- If `GODOG_TAGS` is not set, the test uses the default tag filter: `~@flaky && ~@todo && ~@skip`
|
||||||
|
- If `GODOG_STOP_ON_FAILURE` is not set, each feature uses its default:
|
||||||
|
- `jwt`, `greet`, `auth`, `health`: `true` (stop on failure)
|
||||||
|
- `config`, `all features`: `false` (continue after failures)
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
### Running Smoke Tests
|
### Running Smoke Tests
|
||||||
@@ -150,6 +210,10 @@ Feature: Health Endpoint
|
|||||||
| `@config` | Configuration | `@config` on config scenarios |
|
| `@config` | Configuration | `@config` on config scenarios |
|
||||||
| `@api` | API endpoints | `@api` on endpoint tests |
|
| `@api` | API endpoints | `@api` on endpoint tests |
|
||||||
| `@v2` | V2 API | `@v2` on version 2 tests |
|
| `@v2` | V2 API | `@v2` on version 2 tests |
|
||||||
|
| `@flaky` | Exclude flaky tests | `@flaky` on unstable scenarios |
|
||||||
|
| `@todo` | Exclude pending tests | `@todo` on unimplemented scenarios |
|
||||||
|
| `@skip` | Exclude tests entirely | `@skip` on disabled scenarios |
|
||||||
|
| `@wip` | Work in progress | `@wip` on actively developed scenarios |
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"dance-lessons-coach/pkg/bdd"
|
"dance-lessons-coach/pkg/bdd/testsetup"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAuthBDD(t *testing.T) {
|
func TestAuthBDD(t *testing.T) {
|
||||||
// Set FEATURE environment variable for feature-specific configuration
|
config := testsetup.NewFeatureConfig("auth", "progress", false)
|
||||||
os.Setenv("FEATURE", "auth")
|
suite := testsetup.CreateTestSuite(t, config, "dance-lessons-coach BDD Tests - Auth Feature")
|
||||||
|
|
||||||
suite := godog.TestSuite{
|
|
||||||
Name: "dance-lessons-coach BDD Tests - Auth Feature",
|
|
||||||
TestSuiteInitializer: bdd.InitializeTestSuite,
|
|
||||||
ScenarioInitializer: bdd.InitializeScenario,
|
|
||||||
Options: &godog.Options{
|
|
||||||
Format: "progress",
|
|
||||||
Paths: []string{"."},
|
|
||||||
TestingT: t,
|
|
||||||
Strict: true,
|
|
||||||
Randomize: -1,
|
|
||||||
StopOnFailure: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if suite.Run() != 0 {
|
if suite.Run() != 0 {
|
||||||
t.Fatal("non-zero status returned, failed to run auth BDD tests")
|
t.Fatal("non-zero status returned, failed to run auth BDD tests")
|
||||||
|
|||||||
@@ -1,50 +1,30 @@
|
|||||||
package features
|
package features
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"dance-lessons-coach/pkg/bdd"
|
"dance-lessons-coach/pkg/bdd/testsetup"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBDD(t *testing.T) {
|
func TestBDD(t *testing.T) {
|
||||||
// Get feature name from environment variable or default to all features
|
// Get feature name from environment variable or default to all features
|
||||||
feature := os.Getenv("FEATURE")
|
feature := testsetup.GetFeatureFromEnv()
|
||||||
|
|
||||||
var paths []string
|
|
||||||
var suiteName string
|
var suiteName string
|
||||||
|
var paths []string
|
||||||
|
|
||||||
if feature == "" {
|
if feature == "" {
|
||||||
// Run all features
|
// Run all features
|
||||||
suiteName = "dance-lessons-coach BDD Tests - All Features"
|
suiteName = "dance-lessons-coach BDD Tests - All Features"
|
||||||
paths = []string{
|
paths = testsetup.GetAllFeaturePaths()
|
||||||
"auth",
|
|
||||||
"config",
|
|
||||||
"greet",
|
|
||||||
"health",
|
|
||||||
"jwt",
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Run specific feature
|
// Run specific feature
|
||||||
suiteName = "dance-lessons-coach BDD Tests - " + feature + " Feature"
|
suiteName = "dance-lessons-coach BDD Tests - " + feature + " Feature"
|
||||||
paths = []string{feature}
|
paths = []string{feature}
|
||||||
}
|
}
|
||||||
|
|
||||||
suite := godog.TestSuite{
|
config := testsetup.NewMultiFeatureConfig(paths, "progress", false)
|
||||||
Name: suiteName,
|
suite := testsetup.CreateMultiFeatureTestSuite(t, config, suiteName)
|
||||||
TestSuiteInitializer: bdd.InitializeTestSuite,
|
|
||||||
ScenarioInitializer: bdd.InitializeScenario,
|
|
||||||
Options: &godog.Options{
|
|
||||||
Format: "progress",
|
|
||||||
Paths: paths,
|
|
||||||
TestingT: t,
|
|
||||||
Strict: true,
|
|
||||||
Randomize: -1,
|
|
||||||
// StopOnFailure: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if suite.Run() != 0 {
|
if suite.Run() != 0 {
|
||||||
t.Fatal("non-zero status returned, failed to run BDD tests")
|
t.Fatal("non-zero status returned, failed to run BDD tests")
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
Feature: Config Hot Reloading
|
Feature: Config Hot Reloading
|
||||||
The system should support selective hot reloading of configuration changes
|
The system should support selective hot reloading of configuration changes
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Hot reloading logging level changes
|
Scenario: Hot reloading logging level changes
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
When I update the logging level to "debug" in the config file
|
When I update the logging level to "debug" in the config file
|
||||||
Then the logging level should be updated without restart
|
Then the logging level should be updated without restart
|
||||||
And debug logs should appear in the output
|
And debug logs should appear in the output
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Hot reloading feature flags
|
Scenario: Hot reloading feature flags
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
And the v2 API is disabled
|
And the v2 API is disabled
|
||||||
@@ -15,6 +17,7 @@ Feature: Config Hot Reloading
|
|||||||
Then the v2 API should become available without restart
|
Then the v2 API should become available without restart
|
||||||
And v2 API requests should succeed
|
And v2 API requests should succeed
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Hot reloading telemetry sampling settings
|
Scenario: Hot reloading telemetry sampling settings
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
And telemetry is enabled
|
And telemetry is enabled
|
||||||
@@ -23,6 +26,7 @@ Feature: Config Hot Reloading
|
|||||||
Then the telemetry sampling should be updated without restart
|
Then the telemetry sampling should be updated without restart
|
||||||
And the new sampling settings should be applied
|
And the new sampling settings should be applied
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Hot reloading JWT TTL
|
Scenario: Hot reloading JWT TTL
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
And JWT TTL is set to 1 hour
|
And JWT TTL is set to 1 hour
|
||||||
@@ -30,6 +34,7 @@ Feature: Config Hot Reloading
|
|||||||
Then the JWT TTL should be updated without restart
|
Then the JWT TTL should be updated without restart
|
||||||
And new JWT tokens should have the updated expiration
|
And new JWT tokens should have the updated expiration
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Attempting to hot reload non-reloadable settings should be ignored
|
Scenario: Attempting to hot reload non-reloadable settings should be ignored
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
When I update the server port to 9090 in the config file
|
When I update the server port to 9090 in the config file
|
||||||
@@ -37,6 +42,7 @@ Feature: Config Hot Reloading
|
|||||||
And the server should continue running on the original port
|
And the server should continue running on the original port
|
||||||
And a warning should be logged about ignored configuration change
|
And a warning should be logged about ignored configuration change
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Invalid configuration changes should be handled gracefully
|
Scenario: Invalid configuration changes should be handled gracefully
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
When I update the logging level to "invalid_level" in the config file
|
When I update the logging level to "invalid_level" in the config file
|
||||||
@@ -44,12 +50,14 @@ Feature: Config Hot Reloading
|
|||||||
And an error should be logged about invalid configuration
|
And an error should be logged about invalid configuration
|
||||||
And the server should continue running normally
|
And the server should continue running normally
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Config file monitoring should handle file deletion gracefully
|
Scenario: Config file monitoring should handle file deletion gracefully
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
When I delete the config file
|
When I delete the config file
|
||||||
Then the server should continue running with last known good configuration
|
Then the server should continue running with last known good configuration
|
||||||
And a warning should be logged about missing config file
|
And a warning should be logged about missing config file
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Config file monitoring should handle file recreation
|
Scenario: Config file monitoring should handle file recreation
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
And I have deleted the config file
|
And I have deleted the config file
|
||||||
@@ -57,6 +65,7 @@ Feature: Config Hot Reloading
|
|||||||
Then the server should reload the configuration
|
Then the server should reload the configuration
|
||||||
And the new configuration should be applied
|
And the new configuration should be applied
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Multiple rapid configuration changes should be handled
|
Scenario: Multiple rapid configuration changes should be handled
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
When I rapidly update the logging level multiple times
|
When I rapidly update the logging level multiple times
|
||||||
@@ -64,6 +73,7 @@ Feature: Config Hot Reloading
|
|||||||
And the final configuration should be applied
|
And the final configuration should be applied
|
||||||
And no configuration changes should be lost
|
And no configuration changes should be lost
|
||||||
|
|
||||||
|
@flaky
|
||||||
Scenario: Configuration changes should be audited
|
Scenario: Configuration changes should be audited
|
||||||
Given the server is running with config file monitoring enabled
|
Given the server is running with config file monitoring enabled
|
||||||
And audit logging is enabled
|
And audit logging is enabled
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"dance-lessons-coach/pkg/bdd"
|
"dance-lessons-coach/pkg/bdd/testsetup"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigBDD(t *testing.T) {
|
func TestConfigBDD(t *testing.T) {
|
||||||
// Set FEATURE environment variable for feature-specific configuration
|
config := testsetup.NewFeatureConfig("config", "progress", false)
|
||||||
os.Setenv("FEATURE", "config")
|
suite := testsetup.CreateTestSuite(t, config, "dance-lessons-coach BDD Tests - Config Feature")
|
||||||
|
|
||||||
suite := godog.TestSuite{
|
|
||||||
Name: "dance-lessons-coach BDD Tests - Config Feature",
|
|
||||||
TestSuiteInitializer: bdd.InitializeTestSuite,
|
|
||||||
ScenarioInitializer: bdd.InitializeScenario,
|
|
||||||
Options: &godog.Options{
|
|
||||||
Format: "progress",
|
|
||||||
Paths: []string{"."},
|
|
||||||
TestingT: t,
|
|
||||||
Strict: true,
|
|
||||||
Randomize: -1,
|
|
||||||
StopOnFailure: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if suite.Run() != 0 {
|
if suite.Run() != 0 {
|
||||||
t.Fatal("non-zero status returned, failed to run config BDD tests")
|
t.Fatal("non-zero status returned, failed to run config BDD tests")
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
package greet
|
package greet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"dance-lessons-coach/pkg/bdd"
|
"dance-lessons-coach/pkg/bdd/testsetup"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGreetBDD(t *testing.T) {
|
func TestGreetBDD(t *testing.T) {
|
||||||
// Set FEATURE environment variable for feature-specific configuration
|
config := testsetup.NewFeatureConfig("greet", "progress", false)
|
||||||
os.Setenv("FEATURE", "greet")
|
suite := testsetup.CreateTestSuite(t, config, "dance-lessons-coach BDD Tests - Greet Feature")
|
||||||
|
|
||||||
suite := godog.TestSuite{
|
|
||||||
Name: "dance-lessons-coach BDD Tests - Greet Feature",
|
|
||||||
TestSuiteInitializer: bdd.InitializeTestSuite,
|
|
||||||
ScenarioInitializer: bdd.InitializeScenario,
|
|
||||||
Options: &godog.Options{
|
|
||||||
Format: "progress",
|
|
||||||
Paths: []string{"."},
|
|
||||||
TestingT: t,
|
|
||||||
Strict: true,
|
|
||||||
Randomize: -1,
|
|
||||||
StopOnFailure: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if suite.Run() != 0 {
|
if suite.Run() != 0 {
|
||||||
t.Fatal("non-zero status returned, failed to run greet BDD tests")
|
t.Fatal("non-zero status returned, failed to run greet BDD tests")
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
package health
|
package health
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"dance-lessons-coach/pkg/bdd"
|
"dance-lessons-coach/pkg/bdd/testsetup"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHealthBDD(t *testing.T) {
|
func TestHealthBDD(t *testing.T) {
|
||||||
// Set FEATURE environment variable for feature-specific configuration
|
config := testsetup.NewFeatureConfig("health", "progress", false)
|
||||||
os.Setenv("FEATURE", "health")
|
suite := testsetup.CreateTestSuite(t, config, "dance-lessons-coach BDD Tests - Health Feature")
|
||||||
|
|
||||||
suite := godog.TestSuite{
|
|
||||||
Name: "dance-lessons-coach BDD Tests - Health Feature",
|
|
||||||
TestSuiteInitializer: bdd.InitializeTestSuite,
|
|
||||||
ScenarioInitializer: bdd.InitializeScenario,
|
|
||||||
Options: &godog.Options{
|
|
||||||
Format: "progress",
|
|
||||||
Paths: []string{"."},
|
|
||||||
TestingT: t,
|
|
||||||
Strict: true,
|
|
||||||
Randomize: -1,
|
|
||||||
StopOnFailure: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if suite.Run() != 0 {
|
if suite.Run() != 0 {
|
||||||
t.Fatal("non-zero status returned, failed to run health BDD tests")
|
t.Fatal("non-zero status returned, failed to run health BDD tests")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
And the retention factor is 2.0
|
And the retention factor is 2.0
|
||||||
And the maximum retention is 72 hours
|
And the maximum retention is 72 hours
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Automatic cleanup of expired secrets
|
Scenario: Automatic cleanup of expired secrets
|
||||||
Given a primary JWT secret exists
|
Given a primary JWT secret exists
|
||||||
And I add a secondary JWT secret with 1 hour expiration
|
And I add a secondary JWT secret with 1 hour expiration
|
||||||
@@ -18,6 +19,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
And the primary secret should remain active
|
And the primary secret should remain active
|
||||||
And I should see cleanup event in logs
|
And I should see cleanup event in logs
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Secret retention based on TTL factor
|
Scenario: Secret retention based on TTL factor
|
||||||
Given the JWT TTL is set to 2 hours
|
Given the JWT TTL is set to 2 hours
|
||||||
And the retention factor is 3.0
|
And the retention factor is 3.0
|
||||||
@@ -25,6 +27,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
Then the secret should expire after 6 hours
|
Then the secret should expire after 6 hours
|
||||||
And the retention period should be 6 hours
|
And the retention period should be 6 hours
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Maximum retention period enforcement
|
Scenario: Maximum retention period enforcement
|
||||||
Given the JWT TTL is set to 72 hours
|
Given the JWT TTL is set to 72 hours
|
||||||
And the retention factor is 3.0
|
And the retention factor is 3.0
|
||||||
@@ -33,6 +36,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
Then the retention period should be capped at 72 hours
|
Then the retention period should be capped at 72 hours
|
||||||
And not exceed the maximum retention limit
|
And not exceed the maximum retention limit
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Cleanup preserves primary secret
|
Scenario: Cleanup preserves primary secret
|
||||||
Given a primary JWT secret exists
|
Given a primary JWT secret exists
|
||||||
And the primary secret is older than retention period
|
And the primary secret is older than retention period
|
||||||
@@ -40,6 +44,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
Then the primary secret should not be removed
|
Then the primary secret should not be removed
|
||||||
And the primary secret should remain active
|
And the primary secret should remain active
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Multiple secrets with different ages
|
Scenario: Multiple secrets with different ages
|
||||||
Given I have 3 JWT secrets of different ages
|
Given I have 3 JWT secrets of different ages
|
||||||
And secret A is 1 hour old (within retention)
|
And secret A is 1 hour old (within retention)
|
||||||
@@ -50,12 +55,14 @@ Feature: JWT Secret Retention Policy
|
|||||||
And secret B should be removed
|
And secret B should be removed
|
||||||
And secret C should be retained as primary
|
And secret C should be retained as primary
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Cleanup frequency configuration
|
Scenario: Cleanup frequency configuration
|
||||||
Given the cleanup interval is set to 30 minutes
|
Given the cleanup interval is set to 30 minutes
|
||||||
When I add an expired JWT secret
|
When I add an expired JWT secret
|
||||||
Then it should be removed within 30 minutes
|
Then it should be removed within 30 minutes
|
||||||
And I should see cleanup events every 30 minutes
|
And I should see cleanup events every 30 minutes
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Token validation with expired secret
|
Scenario: Token validation with expired secret
|
||||||
Given a user "retentionuser" exists with password "testpass123"
|
Given a user "retentionuser" exists with password "testpass123"
|
||||||
And I authenticate with username "retentionuser" and password "testpass123"
|
And I authenticate with username "retentionuser" and password "testpass123"
|
||||||
@@ -65,6 +72,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
Then the token validation should fail
|
Then the token validation should fail
|
||||||
And I should receive "invalid_token" error
|
And I should receive "invalid_token" error
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Graceful rotation during retention period
|
Scenario: Graceful rotation during retention period
|
||||||
Given a user "gracefuluser" exists with password "testpass123"
|
Given a user "gracefuluser" exists with password "testpass123"
|
||||||
And I authenticate with username "gracefuluser" and password "testpass123"
|
And I authenticate with username "gracefuluser" and password "testpass123"
|
||||||
@@ -81,6 +89,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
Then I should receive configuration validation error
|
Then I should receive configuration validation error
|
||||||
And the error should mention "retention_factor must be ≥ 1.0"
|
And the error should mention "retention_factor must be ≥ 1.0"
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Metrics for secret retention
|
Scenario: Metrics for secret retention
|
||||||
Given I have enabled Prometheus metrics
|
Given I have enabled Prometheus metrics
|
||||||
When the cleanup job removes expired secrets
|
When the cleanup job removes expired secrets
|
||||||
@@ -88,12 +97,14 @@ Feature: JWT Secret Retention Policy
|
|||||||
And I should see "jwt_secrets_active_count" metric decrease
|
And I should see "jwt_secrets_active_count" metric decrease
|
||||||
And I should see "jwt_secret_retention_duration_seconds" histogram update
|
And I should see "jwt_secret_retention_duration_seconds" histogram update
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Log masking for security
|
Scenario: Log masking for security
|
||||||
Given I add a new JWT secret "super-secret-key-123456"
|
Given I add a new JWT secret "super-secret-key-123456"
|
||||||
When the cleanup job runs
|
When the cleanup job runs
|
||||||
Then the logs should show masked secret "supe****123456"
|
Then the logs should show masked secret "supe****123456"
|
||||||
And not expose the full secret in logs
|
And not expose the full secret in logs
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Cleanup with high volume of secrets
|
Scenario: Cleanup with high volume of secrets
|
||||||
Given I have 1000 JWT secrets
|
Given I have 1000 JWT secrets
|
||||||
And 300 of them are expired
|
And 300 of them are expired
|
||||||
@@ -102,12 +113,14 @@ Feature: JWT Secret Retention Policy
|
|||||||
And remove all 300 expired secrets
|
And remove all 300 expired secrets
|
||||||
And not impact server performance
|
And not impact server performance
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Disabled cleanup via configuration
|
Scenario: Disabled cleanup via configuration
|
||||||
Given I set cleanup interval to 8760 hours
|
Given I set cleanup interval to 8760 hours
|
||||||
When I add expired JWT secrets
|
When I add expired JWT secrets
|
||||||
Then they should not be automatically removed
|
Then they should not be automatically removed
|
||||||
And manual cleanup should still be possible
|
And manual cleanup should still be possible
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Retention period calculation edge cases
|
Scenario: Retention period calculation edge cases
|
||||||
Given the JWT TTL is 1 hour
|
Given the JWT TTL is 1 hour
|
||||||
And the retention factor is 1.0
|
And the retention factor is 1.0
|
||||||
@@ -115,12 +128,14 @@ Feature: JWT Secret Retention Policy
|
|||||||
Then the retention period should be 1 hour
|
Then the retention period should be 1 hour
|
||||||
And the secret should expire after 1 hour
|
And the secret should expire after 1 hour
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Secret validation with retention policy
|
Scenario: Secret validation with retention policy
|
||||||
Given I try to add an invalid JWT secret
|
Given I try to add an invalid JWT secret
|
||||||
When the secret is less than 16 characters
|
When the secret is less than 16 characters
|
||||||
Then I should receive validation error
|
Then I should receive validation error
|
||||||
And the error should mention "must be at least 16 characters"
|
And the error should mention "must be at least 16 characters"
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Cleanup job error handling
|
Scenario: Cleanup job error handling
|
||||||
Given the cleanup job encounters an error
|
Given the cleanup job encounters an error
|
||||||
When it tries to remove a secret
|
When it tries to remove a secret
|
||||||
@@ -128,6 +143,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
And continue with remaining secrets
|
And continue with remaining secrets
|
||||||
And not crash the cleanup process
|
And not crash the cleanup process
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Configuration reload without restart
|
Scenario: Configuration reload without restart
|
||||||
Given the server is running with default retention settings
|
Given the server is running with default retention settings
|
||||||
When I update the retention factor via configuration
|
When I update the retention factor via configuration
|
||||||
@@ -135,6 +151,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
And existing secrets should be reevaluated
|
And existing secrets should be reevaluated
|
||||||
And cleanup should use new retention periods
|
And cleanup should use new retention periods
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Audit trail for secret operations
|
Scenario: Audit trail for secret operations
|
||||||
Given I enable audit logging
|
Given I enable audit logging
|
||||||
When I add a new JWT secret
|
When I add a new JWT secret
|
||||||
@@ -142,6 +159,7 @@ Feature: JWT Secret Retention Policy
|
|||||||
And when the secret is removed by cleanup
|
And when the secret is removed by cleanup
|
||||||
Then I should see audit log entry with event type "secret_removed"
|
Then I should see audit log entry with event type "secret_removed"
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Retention policy with token refresh
|
Scenario: Retention policy with token refresh
|
||||||
Given a user "refreshuser" exists with password "testpass123"
|
Given a user "refreshuser" exists with password "testpass123"
|
||||||
And I authenticate and receive token A
|
And I authenticate and receive token A
|
||||||
@@ -150,13 +168,15 @@ Feature: JWT Secret Retention Policy
|
|||||||
And token A should still be valid until retention expires
|
And token A should still be valid until retention expires
|
||||||
And both tokens should work concurrently
|
And both tokens should work concurrently
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Emergency secret rotation
|
Scenario: Emergency secret rotation
|
||||||
Given a security incident requires immediate rotation
|
Given a security incident requires immediate rotation
|
||||||
When I rotate to a new primary secret
|
When I rotate to a new primary secret
|
||||||
Then old tokens should be invalidated immediately
|
Then old tokens should be invalidated immediately
|
||||||
And new tokens should use the emergency secret
|
And new tokens should use the emergency secret
|
||||||
And cleanup should remove compromised secrets
|
And cleanup should remove compromised secrets
|
||||||
|
|
||||||
|
@todo
|
||||||
Scenario: Monitoring and alerting
|
Scenario: Monitoring and alerting
|
||||||
Given I have monitoring configured
|
Given I have monitoring configured
|
||||||
When the cleanup job fails repeatedly
|
When the cleanup job fails repeatedly
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
package jwt
|
package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"dance-lessons-coach/pkg/bdd"
|
"dance-lessons-coach/pkg/bdd/testsetup"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJWTBDD(t *testing.T) {
|
func TestJWTBDD(t *testing.T) {
|
||||||
// Set FEATURE environment variable for feature-specific configuration
|
config := testsetup.NewFeatureConfig("jwt", "pretty", false)
|
||||||
os.Setenv("FEATURE", "jwt")
|
suite := testsetup.CreateTestSuite(t, config, "dance-lessons-coach BDD Tests - JWT Feature")
|
||||||
|
|
||||||
suite := godog.TestSuite{
|
|
||||||
Name: "dance-lessons-coach BDD Tests - JWT Feature",
|
|
||||||
TestSuiteInitializer: bdd.InitializeTestSuite,
|
|
||||||
ScenarioInitializer: bdd.InitializeScenario,
|
|
||||||
Options: &godog.Options{
|
|
||||||
Format: "pretty",
|
|
||||||
Paths: []string{"."},
|
|
||||||
TestingT: t,
|
|
||||||
Strict: true,
|
|
||||||
Randomize: -1,
|
|
||||||
StopOnFailure: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if suite.Run() != 0 {
|
if suite.Run() != 0 {
|
||||||
t.Fatal("non-zero status returned, failed to run jwt BDD tests")
|
t.Fatal("non-zero status returned, failed to run jwt BDD tests")
|
||||||
|
|||||||
212
pkg/bdd/testsetup/testsetup.go
Normal file
212
pkg/bdd/testsetup/testsetup.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package testsetup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dance-lessons-coach/pkg/bdd"
|
||||||
|
|
||||||
|
"github.com/cucumber/godog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getWorkingDir returns the current working directory
|
||||||
|
func getWorkingDir() string {
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeatureConfig holds configuration for a feature test
|
||||||
|
type FeatureConfig struct {
|
||||||
|
FeatureName string
|
||||||
|
Format string
|
||||||
|
StopOnFailure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiFeatureConfig holds configuration for multi-feature tests
|
||||||
|
type MultiFeatureConfig struct {
|
||||||
|
Paths []string
|
||||||
|
Format string
|
||||||
|
StopOnFailure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFeatureConfig creates a new feature configuration
|
||||||
|
func NewFeatureConfig(featureName, format string, stopOnFailure bool) *FeatureConfig {
|
||||||
|
return &FeatureConfig{
|
||||||
|
FeatureName: featureName,
|
||||||
|
Format: format,
|
||||||
|
StopOnFailure: stopOnFailure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultiFeatureConfig creates a new multi-feature configuration
|
||||||
|
func NewMultiFeatureConfig(paths []string, format string, stopOnFailure bool) *MultiFeatureConfig {
|
||||||
|
return &MultiFeatureConfig{
|
||||||
|
Paths: paths,
|
||||||
|
Format: format,
|
||||||
|
StopOnFailure: stopOnFailure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFeatureFromEnv gets the feature name from environment variable
|
||||||
|
func GetFeatureFromEnv() string {
|
||||||
|
return os.Getenv("FEATURE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllFeaturePaths returns paths for all features by scanning the filesystem
|
||||||
|
func GetAllFeaturePaths() []string {
|
||||||
|
// Get the project root directory
|
||||||
|
projectRoot, err := getProjectRoot()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to hardcoded list if we can't determine project root
|
||||||
|
return []string{
|
||||||
|
"auth",
|
||||||
|
"config",
|
||||||
|
"greet",
|
||||||
|
"health",
|
||||||
|
"jwt",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the features directory from project root
|
||||||
|
featuresPath := filepath.Join(projectRoot, "features")
|
||||||
|
entries, err := os.ReadDir(featuresPath)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to hardcoded list if filesystem access fails
|
||||||
|
return []string{
|
||||||
|
"auth",
|
||||||
|
"config",
|
||||||
|
"greet",
|
||||||
|
"health",
|
||||||
|
"jwt",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var paths []string
|
||||||
|
for _, entry := range entries {
|
||||||
|
// Only include directories (features) that are not hidden and not test files
|
||||||
|
if entry.IsDir() && !strings.HasPrefix(entry.Name(), ".") {
|
||||||
|
paths = append(paths, entry.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort paths for consistent ordering
|
||||||
|
sort.Strings(paths)
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProjectRoot finds the project root directory by looking for go.mod
|
||||||
|
func getProjectRoot() (string, error) {
|
||||||
|
// Start from current directory and walk up the tree
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk up the directory tree until we find go.mod or reach root
|
||||||
|
for {
|
||||||
|
// Check if go.mod exists in current directory
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move up one directory
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
// Reached root directory
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, we didn't find go.mod - return original working directory
|
||||||
|
return "", fmt.Errorf("could not find project root (go.mod not found)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestSuite creates a configured godog test suite
|
||||||
|
func CreateTestSuite(t *testing.T, config *FeatureConfig, suiteName string) godog.TestSuite {
|
||||||
|
// Set FEATURE environment variable for feature-specific configuration
|
||||||
|
os.Setenv("FEATURE", config.FeatureName)
|
||||||
|
|
||||||
|
// Allow tag override via environment variable
|
||||||
|
tags := os.Getenv("GODOG_TAGS")
|
||||||
|
if tags == "" {
|
||||||
|
// Default tags if not overridden
|
||||||
|
tags = "~@flaky && ~@todo && ~@skip"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow stop on failure override via environment variable
|
||||||
|
stopOnFailure := config.StopOnFailure
|
||||||
|
if envStop := os.Getenv("GODOG_STOP_ON_FAILURE"); envStop != "" {
|
||||||
|
// Support various boolean formats
|
||||||
|
stopOnFailure, _ = strconv.ParseBool(envStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the correct path for feature files
|
||||||
|
// When running from within a feature directory, use "." to find feature files in current dir
|
||||||
|
// When running from outside, use the feature name as a relative path
|
||||||
|
featurePath := "."
|
||||||
|
if workingDir := getWorkingDir(); !strings.HasSuffix(workingDir, "/"+config.FeatureName) && !strings.HasSuffix(workingDir, "\\"+config.FeatureName) {
|
||||||
|
// Not running from within the feature directory, use feature name
|
||||||
|
featurePath = config.FeatureName
|
||||||
|
}
|
||||||
|
|
||||||
|
return godog.TestSuite{
|
||||||
|
Name: suiteName,
|
||||||
|
TestSuiteInitializer: bdd.InitializeTestSuite,
|
||||||
|
ScenarioInitializer: bdd.InitializeScenario,
|
||||||
|
Options: &godog.Options{
|
||||||
|
Format: config.Format,
|
||||||
|
Paths: []string{featurePath},
|
||||||
|
TestingT: t,
|
||||||
|
Strict: true,
|
||||||
|
Randomize: -1,
|
||||||
|
StopOnFailure: stopOnFailure,
|
||||||
|
Tags: tags,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMultiFeatureTestSuite creates a configured godog test suite for multiple features
|
||||||
|
func CreateMultiFeatureTestSuite(t *testing.T, config *MultiFeatureConfig, suiteName string) godog.TestSuite {
|
||||||
|
// Set FEATURE environment variable for feature-specific configuration
|
||||||
|
// For multi-feature tests, we don't set a specific feature
|
||||||
|
os.Setenv("FEATURE", "")
|
||||||
|
|
||||||
|
// Allow tag override via environment variable
|
||||||
|
tags := os.Getenv("GODOG_TAGS")
|
||||||
|
if tags == "" {
|
||||||
|
// Default tags if not overridden
|
||||||
|
tags = "~@flaky && ~@todo && ~@skip"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow stop on failure override via environment variable
|
||||||
|
stopOnFailure := config.StopOnFailure
|
||||||
|
if envStop := os.Getenv("GODOG_STOP_ON_FAILURE"); envStop != "" {
|
||||||
|
// Support various boolean formats
|
||||||
|
stopOnFailure, _ = strconv.ParseBool(envStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
return godog.TestSuite{
|
||||||
|
Name: suiteName,
|
||||||
|
TestSuiteInitializer: bdd.InitializeTestSuite,
|
||||||
|
ScenarioInitializer: bdd.InitializeScenario,
|
||||||
|
Options: &godog.Options{
|
||||||
|
Format: config.Format,
|
||||||
|
Paths: config.Paths,
|
||||||
|
TestingT: t,
|
||||||
|
Strict: true,
|
||||||
|
Randomize: -1,
|
||||||
|
StopOnFailure: stopOnFailure,
|
||||||
|
Tags: tags,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,17 +122,17 @@ run_tests_with_tags() {
|
|||||||
echo "🏗️ CI environment detected, using service configuration"
|
echo "🏗️ CI environment detected, using service configuration"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run tests with proper coverage measurement
|
# Run tests with proper coverage measurement and tag exclusion
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
if [ -n "$tags" ]; then
|
if [ -n "$tags" ]; then
|
||||||
# Use godog directly for tag filtering
|
# Use godog directly for tag filtering with exclusion
|
||||||
echo "🚀 Running: godog $tags features/"
|
echo "🚀 Running: godog $tags --tags=~@flaky --tags=~@todo --tags=~@skip features/"
|
||||||
test_output=$(godog $tags features/ 2>&1)
|
test_output=$(godog $tags --tags=~@flaky --tags=~@todo --tags=~@skip features/ 2>&1)
|
||||||
else
|
else
|
||||||
# Use go test for full test suite
|
# Use go test for full test suite with tag exclusion
|
||||||
echo "🚀 Running: go test ./features/..."
|
echo "🚀 Running: go test ./features/... -tags=~@flaky,~@todo,~@skip"
|
||||||
test_output=$(go test ./features/... -v -cover -coverpkg=./... -coverprofile=coverage.out 2>&1)
|
test_output=$(go test ./features/... -tags=~@flaky,~@todo,~@skip -v -cover -coverpkg=./... -coverprofile=coverage.out 2>&1)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test_exit_code=$?
|
test_exit_code=$?
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ run_feature_test() {
|
|||||||
docker exec dance-lessons-coach-postgres createdb -U postgres "${DLC_DATABASE_NAME}"
|
docker exec dance-lessons-coach-postgres createdb -U postgres "${DLC_DATABASE_NAME}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run the feature tests
|
# Run the feature tests with tag exclusion
|
||||||
cd "features/${feature_name}"
|
cd "features/${feature_name}"
|
||||||
FEATURE=${feature_name} DLC_DATABASE_NAME="${DLC_DATABASE_NAME}" go test -v . 2>&1 | grep -E "(PASS|FAIL|RUN)" || true
|
FEATURE=${feature_name} DLC_DATABASE_NAME="${DLC_DATABASE_NAME}" go test -v . -tags="~@flaky && ~@todo && ~@skip" 2>&1 | grep -E "(PASS|FAIL|RUN)" || true
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
cd ../..
|
cd ../..
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ run_feature_tests() {
|
|||||||
export DLC_DATABASE_SSL_MODE="disable"
|
export DLC_DATABASE_SSL_MODE="disable"
|
||||||
export DLC_CONFIG_FILE="${CONFIG}"
|
export DLC_CONFIG_FILE="${CONFIG}"
|
||||||
|
|
||||||
# Run tests with proper coverage measurement
|
# Run tests with proper coverage measurement and tag exclusion
|
||||||
set +e
|
set +e
|
||||||
test_output=$(go test ./features/${FEATURE}/... -v -cover -coverpkg=./... -coverprofile=coverage-${FEATURE}.out 2>&1)
|
test_output=$(go test ./features/${FEATURE}/... -tags=~@flaky,~@todo,~@skip -v -cover -coverpkg=./... -coverprofile=coverage-${FEATURE}.out 2>&1)
|
||||||
test_exit_code=$?
|
test_exit_code=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user