Remove the CollectionHandler field from router.Config struct literal in cmd/server/tests/test_helpers.go. This field was removed from the Config struct in a previous commit. The collection routes are registered directly in handlers.SetupRoutes() and don't need to be passed through the router config.
263 lines
8.1 KiB
Go
263 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bookhoard/internal/config"
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/handlers"
|
|
"bookhoard/internal/middleware"
|
|
ratelimit "bookhoard/internal/middleware"
|
|
"bookhoard/internal/router"
|
|
"bookhoard/internal/services"
|
|
"bookhoard/internal/sync"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
"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"
|
|
)
|
|
|
|
// CustomValidator wraps the go-playground validator
|
|
type CustomValidator struct {
|
|
validator *validator.Validate
|
|
}
|
|
|
|
func (cv *CustomValidator) Validate(i interface{}) error {
|
|
return cv.validator.Struct(i)
|
|
}
|
|
|
|
// 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)
|
|
func setupTestServer(t *testing.T) (*httptest.Server, *database.Queries, *config.Config) {
|
|
// Load configuration using the same method as main application
|
|
cfg := config.LoadConfig()
|
|
|
|
// Apply test-specific overrides
|
|
cfg.ServerPort = "0" // Use random port for tests
|
|
cfg.BaseURL = "http://localhost"
|
|
cfg.JWTSecret = "test-secret-key"
|
|
cfg.UploadPath = "./test-uploads"
|
|
cfg.TestMode = true
|
|
cfg.RateLimitEnabled = false
|
|
cfg.RequestsPerMinute = 1000
|
|
|
|
// Connect to test database using the same method as main application
|
|
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)
|
|
libraryHandler := handlers.NewLibraryHandler(queries)
|
|
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
|
|
deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries)
|
|
|
|
// Create WebSocket connection manager
|
|
connManager := sync.NewConnectionManager()
|
|
connManager.StartCleanupTask()
|
|
|
|
// Create sync queue processor
|
|
queueProcessor := sync.NewSyncQueueProcessor(queries)
|
|
go queueProcessor.Start(context.Background())
|
|
|
|
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
|
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
|
conflictHandler := handlers.NewConflictHandler(queries, connManager)
|
|
analyticsHandler := handlers.NewAnalyticsHandler(queries)
|
|
queueHandler := handlers.NewQueueHandler(queries, queueProcessor)
|
|
|
|
// Create conversion service for OPDS
|
|
conversionService := services.NewConversionService(queries, "/var/bookhoard/cache/kepub")
|
|
opdsHandler := handlers.NewOPDSHandler(queries, conversionService)
|
|
|
|
// Create Echo instance
|
|
e := echo.New()
|
|
|
|
// Set up validator
|
|
v := validator.New()
|
|
if err := ratelimit.RegisterPasswordValidation(v); err != nil {
|
|
t.Fatal("Failed to register password validator:", err)
|
|
}
|
|
e.Validator = &CustomValidator{validator: v}
|
|
|
|
// Middleware
|
|
e.Use(echomiddleware.Logger())
|
|
e.Use(echomiddleware.Recover())
|
|
e.Use(echomiddleware.CORS())
|
|
|
|
// Setup routes using router package
|
|
routerConfig := &router.Config{
|
|
Echo: e,
|
|
Queries: queries,
|
|
Cfg: cfg,
|
|
DBPool: dbPool,
|
|
AuthHandler: authHandler,
|
|
LibraryHandler: libraryHandler,
|
|
DeviceHandler: deviceHandler,
|
|
KOReaderHandler: koreaderHandler,
|
|
WSHandler: wsHandler,
|
|
ConflictHandler: conflictHandler,
|
|
AnalyticsHandler: analyticsHandler,
|
|
QueueHandler: queueHandler,
|
|
OPDSHandler: opdsHandler,
|
|
ConnManager: connManager,
|
|
QueueProcessor: queueProcessor,
|
|
DeviceAuthMiddleware: deviceAuthMiddleware,
|
|
LoginTracker: loginAttemptTracker,
|
|
}
|
|
|
|
router.RegisterRoutes(routerConfig)
|
|
|
|
// Create test server
|
|
ts := httptest.NewServer(e)
|
|
|
|
// Return server, queries, and config
|
|
return ts, queries, cfg
|
|
}
|
|
|
|
// 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": "Test@Pass123!",
|
|
}
|
|
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, "Should have access_token")
|
|
require.NotEmpty(t, token, "Access token should not be empty")
|
|
|
|
return token
|
|
}
|
|
|
|
func getTestUserID(t *testing.T, db *database.Queries) uuid.UUID {
|
|
ctx := context.Background()
|
|
|
|
// Check if test user exists and delete them first to ensure fresh state
|
|
user, err := db.GetUserByEmail(ctx, "testuser@example.com")
|
|
if err == nil {
|
|
// User exists, delete them to ensure fresh password
|
|
err = db.DeleteUser(ctx, user.ID)
|
|
if err != nil {
|
|
// If delete fails (user might be referenced elsewhere), log and continue
|
|
t.Logf("Warning: Could not delete existing test user: %v", err)
|
|
}
|
|
}
|
|
|
|
// Create a fresh test user with a valid password
|
|
// Password: "Test@Pass123!" meets complexity requirements
|
|
// This is the bcrypt hash for "Test@Pass123!"
|
|
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
|
|
|
newUser, err := db.CreateUser(ctx, 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: "admin",
|
|
})
|
|
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
|
|
}
|
|
|
|
// createTestEbookID creates a test ebook and returns its ID
|
|
func createTestEbookID(t *testing.T, ts *httptest.Server, token string) string {
|
|
// First create a library
|
|
libReq := map[string]interface{}{
|
|
"name": "Test Library",
|
|
"description": "A test library for ebooks",
|
|
"type": "ebooks",
|
|
}
|
|
libBody, _ := json.Marshal(libReq)
|
|
|
|
req, _ := http.NewRequest("POST", ts.URL+"/api/libraries", bytes.NewBuffer(libBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
|
|
var libResult map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&libResult)
|
|
|
|
libData := libResult["id"].(string)
|
|
|
|
// Create a test ebook
|
|
ebookReq := map[string]interface{}{
|
|
"library_id": libData,
|
|
"title": "Test Ebook",
|
|
"author": "Test Author",
|
|
"file_path": "/tmp/test.epub",
|
|
"file_size": 1024,
|
|
"mime_type": "application/epub+zip",
|
|
}
|
|
ebookBody, _ := json.Marshal(ebookReq)
|
|
|
|
req2, _ := http.NewRequest("POST", ts.URL+"/api/media-items", bytes.NewBuffer(ebookBody))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
req2.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
resp2, err := client.Do(req2)
|
|
require.NoError(t, err)
|
|
defer resp2.Body.Close()
|
|
|
|
require.Equal(t, http.StatusCreated, resp2.StatusCode)
|
|
|
|
var ebookResult map[string]interface{}
|
|
json.NewDecoder(resp2.Body).Decode(&ebookResult)
|
|
|
|
ebookID := ebookResult["id"].(string)
|
|
return ebookID
|
|
}
|