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>
113 lines
2.2 KiB
Go
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",
|
|
},
|
|
}
|
|
}
|