🧪 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

@@ -115,6 +115,59 @@ func (c *Client) CustomRequest(method, path string, body interface{}) (*http.Res
return resp, nil
}
// RequestWithHeader allows setting custom headers for the request
func (c *Client) RequestWithHeader(method, path string, body interface{}, headers map[string]string) error {
url := c.server.GetBaseURL() + path
var reqBody io.Reader
if body != nil {
// Handle different body types
switch b := body.(type) {
case []byte:
reqBody = bytes.NewReader(b)
case string:
reqBody = strings.NewReader(b)
case map[string]string:
jsonBody, err := json.Marshal(b)
if err != nil {
return fmt.Errorf("failed to marshal JSON body: %w", err)
}
reqBody = bytes.NewReader(jsonBody)
default:
return fmt.Errorf("unsupported body type: %T", body)
}
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Set content type for JSON bodies
if body != nil && reqBody != nil {
req.Header.Set("Content-Type", "application/json")
}
// Set custom headers
for key, value := range headers {
req.Header.Set(key, value)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
c.lastResp = resp
c.lastBody, err = io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
return nil
}
func (c *Client) ExpectResponseBody(expected string) error {
if c.lastResp == nil {
return fmt.Errorf("no response received")

View File

@@ -2,13 +2,16 @@ package testserver
import (
"context"
"database/sql"
"fmt"
"net/http"
"strings"
"time"
"dance-lessons-coach/pkg/config"
"dance-lessons-coach/pkg/server"
_ "github.com/lib/pq"
"github.com/rs/zerolog/log"
)
@@ -16,6 +19,7 @@ type Server struct {
httpServer *http.Server
port int
baseURL string
db *sql.DB
}
func NewServer() *Server {
@@ -31,6 +35,11 @@ func (s *Server) Start() error {
cfg := createTestConfig(s.port)
realServer := server.NewServer(cfg, context.Background())
// Initialize database connection for cleanup
if err := s.initDBConnection(); err != nil {
return fmt.Errorf("failed to initialize database connection: %w", err)
}
// Start HTTP server in same process
s.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
@@ -49,6 +58,148 @@ func (s *Server) Start() error {
return s.waitForServerReady()
}
// initDBConnection initializes a direct database connection for cleanup operations
func (s *Server) initDBConnection() error {
cfg := createTestConfig(s.port)
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Database.Host,
cfg.Database.Port,
cfg.Database.User,
cfg.Database.Password,
cfg.Database.Name,
cfg.Database.SSLMode,
)
var err error
s.db, err = sql.Open("postgres", dsn)
if err != nil {
return fmt.Errorf("failed to open database connection: %w", err)
}
// Test the connection
if err := s.db.Ping(); err != nil {
return fmt.Errorf("failed to ping database: %w", err)
}
return nil
}
// CleanupDatabase deletes all test data from all tables
// This uses raw SQL to avoid dependency on repositories and handles foreign keys properly
// Uses SET CONSTRAINTS ALL DEFERRED to temporarily disable foreign key checks
func (s *Server) CleanupDatabase() error {
if s.db == nil {
return nil // No database connection, skip cleanup
}
// Start a transaction for atomic cleanup
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("failed to start cleanup transaction: %w", err)
}
// Ensure transaction is rolled back if cleanup fails
defer func() {
if err != nil {
tx.Rollback()
}
}()
// Disable foreign key constraints temporarily
// This is valid PostgreSQL syntax: https://www.postgresql.org/docs/current/sql-set-constraints.html
if _, err := tx.Exec("SET CONSTRAINTS ALL DEFERRED"); err != nil {
log.Warn().Err(err).Msg("Failed to set constraints deferred, continuing cleanup")
// Continue anyway, some constraints might still work
}
// Get all tables in the database
rows, err := tx.Query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
`)
if err != nil {
return fmt.Errorf("failed to query tables: %w", err)
}
// Ensure rows are closed
defer func() {
if rows != nil {
rows.Close()
}
}()
// Collect all tables
var tables []string
for rows.Next() {
var tableName string
if err := rows.Scan(&tableName); err != nil {
log.Warn().Err(err).Str("table", tableName).Msg("Failed to scan table name")
continue
}
// Skip system tables and internal tables
if strings.HasPrefix(tableName, "pg_") ||
strings.HasPrefix(tableName, "sql_") ||
tableName == "spatial_ref_sys" ||
tableName == "goose_db_version" {
continue
}
tables = append(tables, tableName)
}
// Check for errors during table scanning
if err = rows.Err(); err != nil {
return fmt.Errorf("error during table scanning: %w", err)
}
// Delete from tables in reverse order to handle foreign keys
// This works better when constraints are deferred
for i := len(tables) - 1; i >= 0; i-- {
table := tables[i]
query := fmt.Sprintf("DELETE FROM %s", table)
if _, err := tx.Exec(query); err != nil {
log.Warn().Err(err).Str("table", table).Msg("Failed to cleanup table")
// Continue with other tables even if one fails
continue
}
log.Debug().Str("table", table).Msg("Cleaned up table")
}
// Reset sequence counters for all tables
for _, table := range tables {
// Try the common pattern first: table_id_seq
query := fmt.Sprintf("ALTER SEQUENCE IF EXISTS %s_id_seq RESTART WITH 1", table)
if _, err := tx.Exec(query); err != nil {
// Try alternative sequence naming patterns
altQueries := []string{
fmt.Sprintf("ALTER SEQUENCE IF EXISTS %s_seq RESTART WITH 1", table),
fmt.Sprintf("ALTER SEQUENCE IF EXISTS %s RESTART WITH 1", table),
}
for _, altQuery := range altQueries {
if _, err := tx.Exec(altQuery); err == nil {
break
}
}
}
}
// Commit the transaction
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit cleanup transaction: %w", err)
}
log.Debug().Msg("Database cleanup completed successfully")
return nil
}
// CloseDatabase closes the database connection
func (s *Server) CloseDatabase() error {
if s.db != nil {
return s.db.Close()
}
return nil
}
func (s *Server) waitForServerReady() error {
maxAttempts := 30
attempt := 0
@@ -108,5 +259,16 @@ func createTestConfig(port int) *config.Config {
JWTSecret: "default-secret-key-please-change-in-production",
AdminMasterPassword: "admin123",
},
Database: config.DatabaseConfig{
Host: "localhost",
Port: 5432,
User: "postgres",
Password: "postgres",
Name: "dance_lessons_coach_bdd_test", // Separate BDD test database
SSLMode: "disable",
MaxOpenConns: 10,
MaxIdleConns: 5,
ConnMaxLifetime: time.Hour,
},
}
}