Files
dance-lessons-coach/pkg/bdd/testserver/server.go
Gabriel Radureau 72b9d35299 feat: implement user authentication system with in-memory SQLite
Implemented complete user authentication system following ADR-0018:

**Core Features:**
- User model with SQLite persistence (in-memory)
- JWT-based authentication with bcrypt hashing
- Admin master password authentication (non-persisted)
- Password reset workflow
- RESTful API endpoints

**API Endpoints:**
- POST /api/v1/auth/register - User registration
- POST /api/v1/auth/login - User login
- POST /api/v1/auth/admin/login - Admin login
- POST /api/v1/auth/password-reset/request - Request password reset
- POST /api/v1/auth/password-reset/complete - Complete password reset

**Technical Implementation:**
- SQLite in-memory database (file::memory:?cache=shared)
- GORM ORM for data access
- JWT with HS256 signing
- Bcrypt password hashing
- Context-aware services
- Interface-based design

**Testing:**
- All BDD tests passing (14 scenarios, 55 steps)
- Unit tests for repository, auth service, password reset
- No regression in existing functionality

**Configuration:**
- JWT secret via config/auth.jwt_secret
- Admin master password via config/auth.admin_master_password
- Environment variables: DLC_AUTH_JWT_SECRET, DLC_AUTH_ADMIN_MASTER_PASSWORD

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-06 23:13:13 +02:00

113 lines
2.2 KiB
Go

package testserver
import (
"context"
"fmt"
"net/http"
"time"
"dance-lessons-coach/pkg/config"
"dance-lessons-coach/pkg/server"
"github.com/rs/zerolog/log"
)
type Server struct {
httpServer *http.Server
port int
baseURL string
}
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())
// 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()
}
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 {
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",
},
}
}