🧪 test: add JWT edge case scenarios with validation endpoint

- Add expired JWT token scenario

- Add wrong secret JWT token scenario

- Add malformed JWT token scenario

- Implement /api/v1/auth/validate endpoint

- Add JWT parsing and validation to BDD steps

Generated by Mistral Vibe.

Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-04-07 18:21:56 +02:00
parent 81e0afe1c7
commit f39a0df338
15 changed files with 1012 additions and 405 deletions

View File

@@ -2,410 +2,82 @@ package steps
import (
"dance-lessons-coach/pkg/bdd/testserver"
"fmt"
"net/http"
"strings"
"github.com/cucumber/godog"
)
// StepContext holds the test client and implements all step definitions
type StepContext struct {
client *testserver.Client
client *testserver.Client
greetSteps *GreetSteps
healthSteps *HealthSteps
authSteps *AuthSteps
commonSteps *CommonSteps
}
// NewStepContext creates a new step context
func NewStepContext(client *testserver.Client) *StepContext {
return &StepContext{client: client}
return &StepContext{
client: client,
greetSteps: NewGreetSteps(client),
healthSteps: NewHealthSteps(client),
authSteps: NewAuthSteps(client),
commonSteps: NewCommonSteps(client),
}
}
// InitializeAllSteps registers all step definitions for the BDD tests
func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) {
sc := NewStepContext(client)
ctx.Step(`^I request a greeting for "([^"]*)"$`, sc.iRequestAGreetingFor)
ctx.Step(`^I request the default greeting$`, sc.iRequestTheDefaultGreeting)
ctx.Step(`^I request the health endpoint$`, sc.iRequestTheHealthEndpoint)
ctx.Step(`^the response should be "{\\"([^"]*)":\\"([^"]*)"}"$`, sc.theResponseShouldBe)
ctx.Step(`^the server is running$`, sc.theServerIsRunning)
ctx.Step(`^the server is running with v2 enabled$`, sc.theServerIsRunningWithV2Enabled)
ctx.Step(`^I send a POST request to v2 greet with name "([^"]*)"$`, sc.iSendPOSTRequestToV2GreetWithName)
ctx.Step(`^I send a POST request to v2 greet with invalid JSON "([^"]*)"$`, sc.iSendPOSTRequestToV2GreetWithInvalidJSON)
ctx.Step(`^the response should contain error "([^"]*)"$`, sc.theResponseShouldContainError)
// 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)
// User Authentication Steps
ctx.Step(`^a user "([^"]*)" exists with password "([^"]*)"$`, sc.aUserExistsWithPassword)
ctx.Step(`^I authenticate with username "([^"]*)" and password "([^"]*)"$`, sc.iAuthenticateWithUsernameAndPassword)
ctx.Step(`^the authentication should be successful$`, sc.theAuthenticationShouldBeSuccessful)
ctx.Step(`^I should receive a valid JWT token$`, sc.iShouldReceiveAValidJWTToken)
ctx.Step(`^the authentication should fail$`, sc.theAuthenticationShouldFail)
ctx.Step(`^I authenticate as admin with master password "([^"]*)"$`, sc.iAuthenticateAsAdminWithMasterPassword)
ctx.Step(`^the token should contain admin claims$`, sc.theTokenShouldContainAdminClaims)
ctx.Step(`^I register a new user "([^"]*)" with password "([^"]*)"$`, sc.iRegisterANewUserWithPassword)
ctx.Step(`^the registration should be successful$`, sc.theRegistrationShouldBeSuccessful)
ctx.Step(`^I should be able to authenticate with the new credentials$`, sc.iShouldBeAbleToAuthenticateWithTheNewCredentials)
ctx.Step(`^I am authenticated as admin$`, sc.iAmAuthenticatedAsAdmin)
ctx.Step(`^I request password reset for user "([^"]*)"$`, sc.iRequestPasswordResetForUser)
ctx.Step(`^the password reset should be allowed$`, sc.thePasswordResetShouldBeAllowed)
ctx.Step(`^the user should be flagged for password reset$`, sc.theUserShouldBeFlaggedForPasswordReset)
ctx.Step(`^I complete password reset for "([^"]*)" with new password "([^"]*)"$`, sc.iCompletePasswordResetForWithNewPassword)
ctx.Step(`^I should be able to authenticate with the new password$`, sc.iShouldBeAbleToAuthenticateWithTheNewPassword)
ctx.Step(`^a user "([^"]*)" exists and is flagged for password reset$`, sc.aUserExistsAndIsFlaggedForPasswordReset)
ctx.Step(`^the password reset should be successful$`, sc.thePasswordResetShouldBeSuccessful)
ctx.Step(`^the password reset should fail$`, sc.thePasswordResetShouldFail)
ctx.Step(`^the status code should be (\d+)$`, sc.theStatusCodeShouldBe)
ctx.Step(`^I validate the received JWT token$`, sc.iValidateTheReceivedJWTToken)
ctx.Step(`^the token should be valid$`, sc.theTokenShouldBeValid)
ctx.Step(`^it should contain the correct user ID$`, sc.itShouldContainTheCorrectUserID)
ctx.Step(`^I should receive a different JWT token$`, sc.iShouldReceiveADifferentJWTToken)
ctx.Step(`^I authenticate with username "([^"]*)" and password "([^"]*)" again$`, sc.iAuthenticateWithUsernameAndPasswordAgain)
ctx.Step(`^the registration should fail$`, sc.theRegistrationShouldFail)
ctx.Step(`^the authentication should fail with validation error$`, sc.theAuthenticationShouldFailWithValidationError)
}
func (sc *StepContext) iRequestAGreetingFor(name string) error {
return sc.client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
}
func (sc *StepContext) iRequestTheDefaultGreeting() error {
return sc.client.Request("GET", "/api/v1/greet/", nil)
}
func (sc *StepContext) iRequestTheHealthEndpoint() error {
return sc.client.Request("GET", "/api/health", nil)
}
func (sc *StepContext) theResponseShouldBe(arg1, arg2 string) error {
// The regex captures the full JSON from the feature file, including quotes
// We need to extract just the key and value without the surrounding quotes and backslashes
// Remove the surrounding quotes and backslashes
cleanArg1 := strings.Trim(arg1, `"\`)
cleanArg2 := strings.Trim(arg2, `"\`)
// Build the expected JSON string
expected := fmt.Sprintf(`{"%s":"%s"}`, cleanArg1, cleanArg2)
return sc.client.ExpectResponseBody(expected)
}
func (sc *StepContext) theServerIsRunning() error {
// Actually verify the server is running by checking the readiness endpoint
return sc.client.Request("GET", "/api/ready", nil)
}
func (sc *StepContext) theServerIsRunningWithV2Enabled() error {
// Verify the server is running and v2 is enabled by checking v2 endpoint exists
// First check server is running
if err := sc.client.Request("GET", "/api/ready", nil); err != nil {
return err
}
// Check if v2 endpoint is available (should return 405 Method Not Allowed for GET, which means endpoint exists)
// If v2 is disabled, this will return 404
resp, err := sc.client.CustomRequest("GET", "/api/v2/greet", nil)
if err != nil {
return err
}
defer resp.Body.Close()
// If we get 405, v2 is enabled (endpoint exists but doesn't allow GET)
// If we get 404, v2 is disabled
if resp.StatusCode == 404 {
return fmt.Errorf("v2 endpoint not available - v2 feature flag not enabled")
}
return nil
}
func (sc *StepContext) iSendPOSTRequestToV2GreetWithName(name string) error {
// Create JSON request body
requestBody := map[string]string{"name": name}
return sc.client.Request("POST", "/api/v2/greet", requestBody)
}
func (sc *StepContext) iSendPOSTRequestToV2GreetWithInvalidJSON(invalidJSON string) error {
// Send raw invalid JSON
return sc.client.Request("POST", "/api/v2/greet", invalidJSON)
}
func (sc *StepContext) theResponseShouldContainError(expectedError string) error {
// Check if the response contains the expected error
body := string(sc.client.GetLastBody())
if !strings.Contains(body, expectedError) {
return fmt.Errorf("expected response to contain error %q, got %q", expectedError, body)
}
return nil
}
// User Authentication Steps
func (sc *StepContext) aUserExistsWithPassword(username, password string) error {
// Register the user first
req := map[string]string{"username": username, "password": password}
if err := sc.client.Request("POST", "/api/v1/auth/register", req); err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
func (sc *StepContext) iAuthenticateWithUsernameAndPassword(username, password string) error {
req := map[string]string{"username": username, "password": password}
return sc.client.Request("POST", "/api/v1/auth/login", req)
}
func (sc *StepContext) theAuthenticationShouldBeSuccessful() error {
// Check if we got a 200 status code
if sc.client.GetLastStatusCode() != http.StatusOK {
return fmt.Errorf("expected status 200, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains a token
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "token") {
return fmt.Errorf("expected response to contain token, got %s", body)
}
return nil
}
func (sc *StepContext) iShouldReceiveAValidJWTToken() error {
// This is already verified in theAuthenticationShouldBeSuccessful
return nil
}
func (sc *StepContext) theAuthenticationShouldFail() error {
// Check if we got a 401 status code
if sc.client.GetLastStatusCode() != http.StatusUnauthorized {
return fmt.Errorf("expected status 401, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains invalid_credentials error
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "invalid_credentials") {
return fmt.Errorf("expected response to contain invalid_credentials error, got %s", body)
}
return nil
}
func (sc *StepContext) iAuthenticateAsAdminWithMasterPassword(password string) error {
req := map[string]string{"username": "admin", "password": password}
return sc.client.Request("POST", "/api/v1/auth/login", req)
}
func (sc *StepContext) theTokenShouldContainAdminClaims() error {
// Check if we got a 200 status code
if sc.client.GetLastStatusCode() != http.StatusOK {
return fmt.Errorf("expected status 200, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains a token
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "token") {
return fmt.Errorf("expected response to contain token, got %s", body)
}
// TODO: Actually decode and verify JWT claims contain admin=true
// For now, we'll just check that authentication succeeded
return nil
}
func (sc *StepContext) iRegisterANewUserWithPassword(username, password string) error {
req := map[string]string{"username": username, "password": password}
return sc.client.Request("POST", "/api/v1/auth/register", req)
}
func (sc *StepContext) theRegistrationShouldBeSuccessful() error {
// Check if we got a 201 status code
if sc.client.GetLastStatusCode() != http.StatusCreated {
return fmt.Errorf("expected status 201, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains success message
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "User registered successfully") {
return fmt.Errorf("expected response to contain success message, got %s", body)
}
return nil
}
func (sc *StepContext) iShouldBeAbleToAuthenticateWithTheNewCredentials() error {
// This is the same as regular authentication
return nil
}
func (sc *StepContext) iAmAuthenticatedAsAdmin() error {
// For now, we'll just authenticate as admin
return sc.iAuthenticateAsAdminWithMasterPassword("admin123")
}
func (sc *StepContext) iRequestPasswordResetForUser(username string) error {
req := map[string]string{"username": username}
return sc.client.Request("POST", "/api/v1/auth/password-reset/request", req)
}
func (sc *StepContext) thePasswordResetShouldBeAllowed() error {
// Check if we got a 200 status code
if sc.client.GetLastStatusCode() != http.StatusOK {
return fmt.Errorf("expected status 200, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains success message
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "Password reset allowed") {
return fmt.Errorf("expected response to contain success message, got %s", body)
}
return nil
}
func (sc *StepContext) theUserShouldBeFlaggedForPasswordReset() error {
// This is verified by the password reset request being successful
return nil
}
func (sc *StepContext) iCompletePasswordResetForWithNewPassword(username, password string) error {
req := map[string]string{"username": username, "new_password": password}
return sc.client.Request("POST", "/api/v1/auth/password-reset/complete", req)
}
func (sc *StepContext) aUserExistsAndIsFlaggedForPasswordReset(username string) error {
// First, create the user
if err := sc.iRegisterANewUserWithPassword(username, "oldpassword123"); err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
// Then flag for password reset
if err := sc.iRequestPasswordResetForUser(username); err != nil {
return fmt.Errorf("failed to flag user for password reset: %w", err)
}
return nil
}
func (sc *StepContext) thePasswordResetShouldBeSuccessful() error {
// Check if we got a 200 status code
if sc.client.GetLastStatusCode() != http.StatusOK {
return fmt.Errorf("expected status 200, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains success message
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "Password reset completed successfully") {
return fmt.Errorf("expected response to contain success message, got %s", body)
}
return nil
}
func (sc *StepContext) iShouldBeAbleToAuthenticateWithTheNewPassword() error {
// This is the same as regular authentication
return nil
}
func (sc *StepContext) thePasswordResetShouldFail() error {
// Check if we got a 500 status code (server error for non-existent users)
if sc.client.GetLastStatusCode() != http.StatusInternalServerError {
return fmt.Errorf("expected status 500, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains server_error
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "server_error") {
return fmt.Errorf("expected response to contain server_error, got %s", body)
}
return nil
}
func (sc *StepContext) theStatusCodeShouldBe(expectedStatus int) error {
actualStatus := sc.client.GetLastStatusCode()
if actualStatus != expectedStatus {
return fmt.Errorf("expected status %d, got %d", expectedStatus, actualStatus)
}
return nil
}
func (sc *StepContext) iValidateTheReceivedJWTToken() error {
// Store the current token for comparison
// In a real implementation, we would decode and validate the JWT
// For now, we'll just store it
return nil
}
func (sc *StepContext) theTokenShouldBeValid() error {
// Check if we got a 200 status code
if sc.client.GetLastStatusCode() != http.StatusOK {
return fmt.Errorf("expected status 200, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains a token
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "token") {
return fmt.Errorf("expected response to contain token, got %s", body)
}
// TODO: Actually decode and verify JWT
// For now, we'll just check that authentication succeeded
return nil
}
func (sc *StepContext) itShouldContainTheCorrectUserID() error {
// TODO: Actually decode JWT and verify user ID
// For now, we'll skip this verification
return nil
}
func (sc *StepContext) iShouldReceiveADifferentJWTToken() error {
// Check if we got a 200 status code
if sc.client.GetLastStatusCode() != http.StatusOK {
return fmt.Errorf("expected status 200, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains a token
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "token") {
return fmt.Errorf("expected response to contain token, got %s", body)
}
// TODO: Compare with previous token to ensure it's different
// For now, we'll just check that authentication succeeded
return nil
}
func (sc *StepContext) iAuthenticateWithUsernameAndPasswordAgain(username, password string) error {
// This is the same as regular authentication
return sc.iAuthenticateWithUsernameAndPassword(username, password)
}
func (sc *StepContext) theRegistrationShouldFail() error {
// Check if we got a 400 or 409 status code
statusCode := sc.client.GetLastStatusCode()
if statusCode != http.StatusBadRequest && statusCode != http.StatusConflict {
return fmt.Errorf("expected status 400 or 409, got %d", statusCode)
}
// Check if response contains error
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "error") {
return fmt.Errorf("expected response to contain error, got %s", body)
}
return nil
}
func (sc *StepContext) theAuthenticationShouldFailWithValidationError() error {
// Check if we got a 400 status code
if sc.client.GetLastStatusCode() != http.StatusBadRequest {
return fmt.Errorf("expected status 400, got %d", sc.client.GetLastStatusCode())
}
// Check if response contains validation error (new structured format)
body := string(sc.client.GetLastBody())
if !strings.Contains(body, "validation_failed") && !strings.Contains(body, "invalid_request") {
return fmt.Errorf("expected response to contain validation_failed or invalid_request error, got %s", body)
}
return nil
// 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)
// 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)
}