Changes to test_helpers.go: - Import router package and use router.RegisterRoutes() - Create all necessary handlers (auth, device, koreader, ws, conflict, analytics, queue, opds) - Add proper validator setup - Add CustomValidator type - Remove unused pgtype import This makes integration tests use the same router configuration as production, ensuring tests cover the actual API behavior and route structure.
313 lines
9.7 KiB
Go
313 lines
9.7 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"
|
|
"os"
|
|
"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, *handlers.Handler)
|
|
func setupTestServer(t *testing.T) (*httptest.Server, *database.Queries, *config.Config, *handlers.Handler) {
|
|
// Check if DATABASE_URL is set (for containerized testing)
|
|
dbURL := os.Getenv("DATABASE_URL")
|
|
|
|
var cfg *config.Config
|
|
var dbPool *pgxpool.Pool
|
|
var err error
|
|
|
|
if dbURL != "" {
|
|
// Use provided DATABASE_URL (for testing against containerized database)
|
|
t.Logf("Using DATABASE_URL from environment for testing")
|
|
|
|
// Parse the DATABASE_URL to extract connection details for config
|
|
cfg = &config.Config{
|
|
ServerPort: "0",
|
|
BaseURL: "http://localhost",
|
|
DatabaseHost: "localhost",
|
|
DatabasePort: "5432",
|
|
DatabaseUser: "postgres",
|
|
DatabasePassword: "", // Not used when DATABASE_URL is set
|
|
DatabaseName: "bookhoard",
|
|
JWTSecret: "test-secret-key",
|
|
UploadPath: "./test-uploads",
|
|
TestMode: true,
|
|
RateLimitEnabled: false,
|
|
RequestsPerMinute: 1000,
|
|
}
|
|
|
|
// Connect using DATABASE_URL directly
|
|
dbPool, err = pgxpool.New(context.Background(), dbURL)
|
|
require.NoError(t, err, "Failed to connect to test database using DATABASE_URL")
|
|
} else {
|
|
// Legacy behavior: construct database URL from parts
|
|
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: "bookhoard",
|
|
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)
|
|
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)
|
|
|
|
// Setup ebook handler routes (for testing)
|
|
protected := e.Group("/api")
|
|
h := handlers.SetupRoutes(protected, queries, connManager)
|
|
|
|
// 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, "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 {
|
|
// Try to get existing test user
|
|
user, err := db.GetUserByEmail(context.Background(), "testuser@example.com")
|
|
if err == nil {
|
|
// User exists, return their ID
|
|
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 with a valid password
|
|
// Password: "Test@Pass123!" meets complexity requirements
|
|
// This is the bcrypt hash for "Test@Pass123!"
|
|
passwordHash := "$2a$10$vYI7j2zvH3vBmGHXqKbqMe.8hKqJVYOvQKHh8fPJWGjVPKpXzGvMqG"
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|