- Replace manual config construction with config.LoadConfig() - Remove problematic password validation logic - Apply test-specific overrides after loading config - Clean up unused imports (os, strings) - Tests now use same configuration method as main application - Fixes database authentication issues in integration tests
264 lines
8.1 KiB
Go
264 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,
|
|
CollectionHandler: nil, // Not needed for tests
|
|
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
|
|
}
|