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 { return &Server{ port: 9191, } } func (s *Server) Start() error { s.baseURL = fmt.Sprintf("http://localhost:%d", s.port) // Create real server instance from pkg/server 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), Handler: realServer.Router(), } go func() { if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err != http.ErrServerClosed { log.Error().Err(err).Msg("Test server failed") } } }() // Wait for server to be ready 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 for attempt < maxAttempts { resp, err := http.Get(fmt.Sprintf("%s/api/ready", s.baseURL)) if err == nil && resp.StatusCode == http.StatusOK { resp.Body.Close() return nil } if resp != nil { resp.Body.Close() } attempt++ time.Sleep(100 * time.Millisecond) } return fmt.Errorf("server did not become ready after %d attempts", maxAttempts) } func (s *Server) Stop() error { if s.httpServer == nil { return nil } // Shutdown HTTP server gracefully ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return s.httpServer.Shutdown(ctx) } func (s *Server) GetBaseURL() string { return s.baseURL } func createTestConfig(port int) *config.Config { // 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 }