Files
bookhoard/cmd/server/tests/test_helpers.go
T
john-okeefe 77d683277a test: add WebSocket integration tests
Add comprehensive WebSocket test coverage:
- TestWebSocketConnection: Basic connection and JWT auth
- TestWebSocketDeviceAuth: Device token authentication
- TestWebSocketProgressBroadcast: Real-time update delivery
- TestWebSocketPingPong: Heartbeat mechanism
- TestWebSocketConnectionLimit: Multiple concurrent connections
- TestWebSocketInvalidToken: Rejection of invalid tokens
- Helper function for test media item creation

Update test helpers to create ConnectionManager for tests.
Tests verify WebSocket connection, authentication, and
real-time progress broadcast functionality.
2026-01-30 21:48:30 -05:00

183 lines
5.8 KiB
Go

package main
import (
"bookmann/internal/config"
"bookmann/internal/database"
"bookmann/internal/handlers"
ratelimit "bookmann/internal/middleware"
wsync "bookmann/internal/sync"
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4"
echomiddleware "github.com/labstack/echo/v4/middleware"
"github.com/stretchr/testify/require"
)
// Helper functions for testing
func containsPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
func contains(s, substr string) bool {
return strings.Contains(s, substr)
}
func trimSpace(s string) string {
return strings.TrimSpace(s)
}
// setupTestServer creates a test server with a test database
// Returns: (*httptest.Server, *database.Queries, *config.Config, *handlers.Handler)
func setupTestServer(t *testing.T) (*httptest.Server, *database.Queries, *config.Config, *handlers.Handler) {
// Get database password - use default for testing since .env password has special chars
// Tests will run against the local test database, not the Docker one
dbPass := os.Getenv("DATABASE_PASSWORD")
if dbPass == "" {
dbPass = os.Getenv("DBPASS")
}
// If password looks like it has special chars (=, +, /), use local postgres default
if strings.Contains(dbPass, "=") || strings.Contains(dbPass, "+") || len(dbPass) > 20 {
t.Logf("Warning: Database password has special characters, using local default 'postgres'")
dbPass = "postgres"
}
// Load test configuration
cfg := &config.Config{
ServerPort: "0", // Use random port for tests
BaseURL: "http://localhost",
DatabaseHost: "localhost",
DatabasePort: "5432",
DatabaseUser: "postgres",
DatabasePassword: dbPass,
DatabaseName: "bookmann",
JWTSecret: "test-secret-key",
UploadPath: "./test-uploads",
TestMode: true,
RateLimitEnabled: false,
RequestsPerMinute: 1000,
}
// Connect to test database
dbPool, err := pgxpool.New(context.Background(), cfg.DatabaseURL())
require.NoError(t, err, "Failed to connect to test database")
queries := database.New(dbPool)
// Create login attempt tracker
loginAttemptTracker := ratelimit.NewLoginAttemptTracker(5, 15*time.Minute, 5*time.Minute)
// Create handlers
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker)
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
// Create WebSocket connection manager for testing
connManager := wsync.NewConnectionManager()
// Create Echo instance
e := echo.New()
// Middleware
e.Use(echomiddleware.Logger())
e.Use(echomiddleware.Recover())
e.Use(echomiddleware.CORS())
// Setup routes
protected := e.Group("/api")
h := handlers.SetupRoutes(protected, queries, connManager)
// Device management routes (public - for registration)
e.POST("/api/devices/register", deviceHandler.InitiateRegistration)
e.POST("/api/devices/register/status", deviceHandler.CheckRegistrationStatus)
e.GET("/devices/approve/:registration_id", deviceHandler.ApproveDevice)
e.POST("/devices/reject/:registration_id", deviceHandler.RejectDevice)
// Device management routes (protected - require user auth)
devices := protected.Group("/devices")
devices.GET("", deviceHandler.ListDevices)
devices.GET("/:id", deviceHandler.GetDevice)
devices.PUT("/:id", deviceHandler.UpdateDevice)
devices.DELETE("/:id", deviceHandler.DeleteDevice)
devices.GET("/pending", deviceHandler.ListPendingRegistrations)
// Auth routes (public - for testing)
e.POST("/api/auth/register", authHandler.Register)
e.POST("/api/auth/login", authHandler.Login)
// Create test server
ts := httptest.NewServer(e)
// Return server, queries, config, and handler
return ts, queries, cfg, h
}
// loginTestUser logs in a test user and returns the JWT token
func loginTestUser(t *testing.T, ts *httptest.Server, db *database.Queries) string {
// Ensure test user exists first
_ = getTestUserID(t, db)
loginRequest := map[string]interface{}{
"login": "testuser@example.com",
"password": "testpass123",
}
body, _ := json.Marshal(loginRequest)
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err, "Failed to login test user")
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, "Login should succeed")
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
token, ok := result["access_token"].(string)
require.True(t, ok, "Response should contain access_token")
require.NotEmpty(t, token, "Token should not be empty")
return token
}
// getTestUserID retrieves the test user ID from the database
func getTestUserID(t *testing.T, db *database.Queries) uuid.UUID {
// Try to get the test user by email
user, err := db.GetUserByEmail(context.Background(), "testuser@example.com")
if err == nil {
userUUID, err := uuid.FromBytes(user.ID.Bytes[:])
require.NoError(t, err, "Failed to parse user UUID")
return userUUID
}
// If user doesn't exist, create one
passwordHash := "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy" // "testpass123" hashed
newUser, err := db.CreateUser(context.Background(), database.CreateUserParams{
Email: "testuser@example.com",
Username: "testuser",
PasswordHash: passwordHash,
FirstName: pgtype.Text{String: "Test", Valid: true},
LastName: pgtype.Text{String: "User", Valid: true},
Role: "user",
})
require.NoError(t, err, "Failed to create test user")
userUUID, err := uuid.FromBytes(newUser.ID.Bytes[:])
require.NoError(t, err, "Failed to parse user UUID")
return userUUID
}