🧪 test: add comprehensive BDD test suite for user authentication
Added BDD test scenarios covering: - User registration with validation - Successful and failed authentication - Admin authentication with master password - JWT token generation and validation - Password reset workflow - Edge cases and error handling BDD Features: - 20+ authentication scenarios - JWT validation edge cases - Password reset security scenarios - Input validation tests - Error response verification BDD Infrastructure: - Step definitions for authentication workflows - Test server with user management endpoints - JWT parsing and validation utilities - Common step patterns for reuse Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -2,20 +2,26 @@ 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"
|
||||
)
|
||||
|
||||
// getPostgresHost returns the appropriate PostgreSQL host based on environment
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
port int
|
||||
baseURL string
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
@@ -31,6 +37,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 +60,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
|
||||
@@ -86,23 +239,58 @@ func (s *Server) GetBaseURL() string {
|
||||
}
|
||||
|
||||
func createTestConfig(port int) *config.Config {
|
||||
return &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
},
|
||||
Shutdown: config.ShutdownConfig{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
Logging: config.LoggingConfig{
|
||||
JSON: false,
|
||||
Level: "trace",
|
||||
},
|
||||
Telemetry: config.TelemetryConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
API: config.APIConfig{
|
||||
V2Enabled: true, // Enable v2 for testing
|
||||
},
|
||||
// Load actual config to respect environment variables
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to load config, using defaults")
|
||||
// Fallback to defaults if config loading fails
|
||||
return &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
},
|
||||
Shutdown: config.ShutdownConfig{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
Logging: config.LoggingConfig{
|
||||
JSON: false,
|
||||
Level: "trace",
|
||||
},
|
||||
Telemetry: config.TelemetryConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
API: config.APIConfig{
|
||||
V2Enabled: true, // Enable v2 for testing
|
||||
},
|
||||
Auth: config.AuthConfig{
|
||||
JWTSecret: "default-secret-key-please-change-in-production",
|
||||
AdminMasterPassword: "admin123",
|
||||
},
|
||||
Database: config.DatabaseConfig{
|
||||
Host: "localhost", // Fallback if env vars not set
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Override server port for testing
|
||||
cfg.Server.Port = port
|
||||
cfg.API.V2Enabled = true // Ensure v2 is enabled for testing
|
||||
|
||||
// Set default auth values if not configured
|
||||
if cfg.Auth.JWTSecret == "" {
|
||||
cfg.Auth.JWTSecret = "default-secret-key-please-change-in-production"
|
||||
}
|
||||
if cfg.Auth.AdminMasterPassword == "" {
|
||||
cfg.Auth.AdminMasterPassword = "admin123"
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user