test: rename test files to follow Go conventions
Rename test helper files from .go to _test.go suffix to comply with Go testing conventions. This ensures proper test file recognition by the Go toolchain and improves build organization. - library_test_comprehensive.go → library_test_comprehensive_test.go - test_helpers.go → test_helpers_test.go - test_helpers_db.go → test_helpers_db_test.go
This commit is contained in:
@@ -0,0 +1,757 @@
|
||||
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"
|
||||
wsync "bookhoard/internal/sync"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"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/v5"
|
||||
echomiddleware "github.com/labstack/echo/v5/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)
|
||||
}
|
||||
|
||||
// TestDeviceSetup provides a complete, isolated test environment for device tests
|
||||
type TestDeviceSetup struct {
|
||||
Server *httptest.Server
|
||||
DB *database.Queries
|
||||
Config *config.Config
|
||||
User UserTestData
|
||||
Device DeviceTestData
|
||||
Library LibraryTestData
|
||||
UserToken string
|
||||
}
|
||||
|
||||
type LibraryTestData struct {
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
type UserTestData struct {
|
||||
ID uuid.UUID
|
||||
Email string
|
||||
Username string
|
||||
Password string
|
||||
Token string
|
||||
}
|
||||
|
||||
type DeviceTestData struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Type string
|
||||
Identifier string
|
||||
AuthToken string
|
||||
PGType database.Devices
|
||||
}
|
||||
|
||||
// TestServerSetup manages the lifecycle of a test server with proper resource cleanup
|
||||
type TestServerSetup struct {
|
||||
Server *httptest.Server
|
||||
DB *database.Queries
|
||||
DBPool *pgxpool.Pool
|
||||
Config *config.Config
|
||||
ConnManager *wsync.ConnectionManager
|
||||
QueueProcessor *wsync.SyncQueueProcessor
|
||||
CleanupCancel context.CancelFunc
|
||||
QueueCtx context.Context
|
||||
QueueCancel context.CancelFunc
|
||||
Token string
|
||||
RegularToken string
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
// Close cleans up all resources in the correct order
|
||||
func (s *TestServerSetup) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop queue processor first
|
||||
if s.QueueCancel != nil {
|
||||
s.QueueCancel()
|
||||
s.QueueCancel = nil
|
||||
}
|
||||
|
||||
// Reset global worker instance
|
||||
services.WorkerInstance = nil
|
||||
|
||||
// Stop connection manager cleanup task
|
||||
if s.CleanupCancel != nil {
|
||||
s.CleanupCancel()
|
||||
s.CleanupCancel = nil
|
||||
}
|
||||
|
||||
// Close HTTP server
|
||||
if s.Server != nil {
|
||||
s.Server.Close()
|
||||
s.Server = nil
|
||||
}
|
||||
|
||||
// Close database pool (this waits for all connections to be released)
|
||||
if s.DBPool != nil {
|
||||
s.DBPool.Close()
|
||||
s.DBPool = nil
|
||||
}
|
||||
|
||||
s.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// isRunningInContainer detects if tests are running inside a Docker container
|
||||
func isRunningInContainer() bool {
|
||||
// Check for container-specific marker file
|
||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if /app/uploads exists (container path)
|
||||
if _, err := os.Stat("/app/uploads"); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check environment variable (explicit override)
|
||||
if os.Getenv("TEST_IN_CONTAINER") == "true" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getUploadPath returns the appropriate upload path based on runtime environment
|
||||
func getUploadPath() string {
|
||||
// Check for explicit override first
|
||||
if path := os.Getenv("TEST_UPLOAD_PATH"); path != "" {
|
||||
return path
|
||||
}
|
||||
|
||||
if isRunningInContainer() {
|
||||
return "/app/uploads" // Container path (right side of volume mount)
|
||||
}
|
||||
|
||||
return "./uploads" // Host path (left side of volume mount)
|
||||
}
|
||||
|
||||
// getCachePath returns the appropriate cache path based on runtime environment
|
||||
func getCachePath() string {
|
||||
// Check for explicit override first
|
||||
if path := os.Getenv("TEST_CACHE_PATH"); path != "" {
|
||||
return path
|
||||
}
|
||||
|
||||
if isRunningInContainer() {
|
||||
return "/app/cache/kepub" // Container path (volume mount)
|
||||
}
|
||||
|
||||
// Note: This is a Docker volume on host, not a folder
|
||||
// Tests using this should handle the volume appropriately
|
||||
return "/app/cache/kepub"
|
||||
}
|
||||
|
||||
// setupDeviceTest creates a complete test environment for device tests
|
||||
func setupDeviceTest(t *testing.T) *TestDeviceSetup {
|
||||
serverSetup := setupTestServer(t)
|
||||
|
||||
// Create user ONCE with known credentials
|
||||
user := createTestUserOnce(t, serverSetup.DB)
|
||||
|
||||
// Login to get token
|
||||
token := loginUserWithCredentials(t, serverSetup.Server, user.Email, user.Password)
|
||||
|
||||
return &TestDeviceSetup{
|
||||
Server: serverSetup.Server,
|
||||
DB: serverSetup.DB,
|
||||
Config: serverSetup.Config,
|
||||
User: user,
|
||||
UserToken: token,
|
||||
}
|
||||
}
|
||||
|
||||
// createTestUserOnce returns the pre-created test user info
|
||||
func createTestUserOnce(t *testing.T, db *database.Queries) UserTestData {
|
||||
ctx := context.Background()
|
||||
user, err := db.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal")
|
||||
require.NoError(t, err, "Test user should exist (created by setupTestServer)")
|
||||
userUUID, err := uuid.FromBytes(user.ID.Bytes[0:16])
|
||||
require.NoError(t, err, "Should parse user UUID")
|
||||
return UserTestData{
|
||||
ID: userUUID,
|
||||
Email: "testuser@tests.bookhoard.internal",
|
||||
Username: "testuser",
|
||||
Password: "Test@Pass123!",
|
||||
}
|
||||
}
|
||||
|
||||
// createRegularUserOnce creates a regular (non-admin) test user with unique credentials
|
||||
func createRegularUserOnce(t *testing.T, db *database.Queries) UserTestData {
|
||||
ctx := context.Background()
|
||||
uniqueID := uuid.New().String()[:8]
|
||||
email := fmt.Sprintf("regularuser-%s@tests.bookhoard.internal", uniqueID)
|
||||
username := fmt.Sprintf("regularuser-%s", uniqueID)
|
||||
|
||||
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
||||
newUser, err := db.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: email,
|
||||
Username: username,
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Regular", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "user",
|
||||
})
|
||||
require.NoError(t, err, "Should create regular test user")
|
||||
|
||||
userUUID, err := uuid.FromBytes(newUser.ID.Bytes[0:16])
|
||||
require.NoError(t, err, "Should parse user UUID")
|
||||
|
||||
createDefaultCollectionsForUser(t, db, pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
|
||||
t.Cleanup(func() {
|
||||
ctx := context.Background()
|
||||
db.DeleteUser(ctx, pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true})
|
||||
})
|
||||
|
||||
return UserTestData{
|
||||
ID: userUUID,
|
||||
Email: email,
|
||||
Username: username,
|
||||
Password: "Test@Pass123!",
|
||||
}
|
||||
}
|
||||
|
||||
// uuidToPGType converts uuid.UUID to pgtype.UUID
|
||||
func uuidToPGType(u uuid.UUID) pgtype.UUID {
|
||||
return pgtype.UUID{Bytes: [16]byte(u), Valid: true}
|
||||
}
|
||||
|
||||
// createDefaultCollectionsForUser creates the 4 default system collections for a user
|
||||
func createDefaultCollectionsForUser(t *testing.T, db *database.Queries, userID pgtype.UUID) {
|
||||
ctx := context.Background()
|
||||
defaultCollections := []struct {
|
||||
Name string
|
||||
Description string
|
||||
Icon string
|
||||
Color string
|
||||
QueryType string
|
||||
Priority int32
|
||||
}{
|
||||
{"continue-reading", "Books you're currently reading (0 < progress < 1)", "📖", "#7aa2f7", "continue-reading", 1},
|
||||
{"recently-added", "Newly added items to this library", "🆕", "#9ece6a", "recently-added", 2},
|
||||
{"recently-read", "Books you've finished (progress >= 1)", "✅", "#e0af68", "recently-read", 3},
|
||||
{"not-started", "Books you haven't read yet (progress = 0 or no record)", "📕", "#f7768e", "not-started", 4},
|
||||
}
|
||||
|
||||
for _, col := range defaultCollections {
|
||||
_, err := db.CreateSystemCollection(ctx, database.CreateSystemCollectionParams{
|
||||
UserID: userID,
|
||||
Name: col.Name,
|
||||
Description: pgtype.Text{String: col.Description, Valid: true},
|
||||
Icon: pgtype.Text{String: col.Icon, Valid: true},
|
||||
Color: pgtype.Text{String: col.Color, Valid: true},
|
||||
ShowOnDashboard: pgtype.Bool{Bool: true, Valid: true},
|
||||
QueryType: pgtype.Text{String: col.QueryType, Valid: true},
|
||||
Priority: pgtype.Int4{Int32: col.Priority, Valid: true},
|
||||
})
|
||||
require.NoError(t, err, "Should create default collection: "+col.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// loginUserWithCredentials performs explicit login with provided credentials
|
||||
func loginUserWithCredentials(t *testing.T, ts *httptest.Server, email, password string) string {
|
||||
loginRequest := map[string]interface{}{
|
||||
"login": email,
|
||||
"password": password,
|
||||
}
|
||||
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")
|
||||
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
|
||||
}
|
||||
|
||||
// CreateDevice creates a test device for the TestDeviceSetup
|
||||
func (s *TestDeviceSetup) CreateDevice(t *testing.T, deviceName, deviceType, deviceIdentifier string) *DeviceTestData {
|
||||
deviceToken := fmt.Sprintf("dev_%s", uuid.New().String())
|
||||
|
||||
pgUserID := pgtype.UUID{Bytes: [16]byte(s.User.ID), Valid: true}
|
||||
device, err := s.DB.CreateDevice(context.Background(), database.CreateDeviceParams{
|
||||
UserID: pgUserID,
|
||||
DeviceName: deviceName,
|
||||
DeviceType: deviceType,
|
||||
DeviceIdentifier: deviceIdentifier,
|
||||
AuthToken: deviceToken,
|
||||
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
|
||||
AutoSync: pgtype.Bool{Bool: true, Valid: true},
|
||||
SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true},
|
||||
DeviceMetadata: []byte("{}"),
|
||||
})
|
||||
require.NoError(t, err, "Should create device")
|
||||
|
||||
deviceUUID, err := uuid.FromBytes(device.ID.Bytes[0:16])
|
||||
require.NoError(t, err, "Should parse device ID")
|
||||
|
||||
return &DeviceTestData{
|
||||
ID: deviceUUID,
|
||||
Name: deviceName,
|
||||
Type: deviceType,
|
||||
Identifier: deviceIdentifier,
|
||||
AuthToken: deviceToken,
|
||||
PGType: device,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateLibrary creates a test library for the TestDeviceSetup
|
||||
func (s *TestDeviceSetup) CreateLibrary(t *testing.T, name, libraryType string) string {
|
||||
ctx := context.Background()
|
||||
|
||||
// Get the library_type_id for the specified type
|
||||
libraryTypeRow, err := s.DB.GetLibraryTypeByName(ctx, libraryType)
|
||||
require.NoError(t, err, "Should find library type")
|
||||
|
||||
pgUserID := pgtype.UUID{Bytes: [16]byte(s.User.ID), Valid: true}
|
||||
library, err := s.DB.CreateLibrary(ctx, database.CreateLibraryParams{
|
||||
Name: name,
|
||||
Description: pgtype.Text{String: "Test library description", Valid: true},
|
||||
LibraryTypeID: libraryTypeRow.ID,
|
||||
CreatedByAdminID: pgUserID,
|
||||
})
|
||||
require.NoError(t, err, "Should create library")
|
||||
|
||||
libraryUUID, err := uuid.FromBytes(library.ID.Bytes[0:16])
|
||||
require.NoError(t, err, "Should parse library ID")
|
||||
|
||||
s.Library = LibraryTestData{
|
||||
ID: libraryUUID.String(),
|
||||
Name: name,
|
||||
Type: libraryType,
|
||||
}
|
||||
|
||||
return libraryUUID.String()
|
||||
}
|
||||
|
||||
// CreateCollection creates a test collection for the TestDeviceSetup
|
||||
func (s *TestDeviceSetup) CreateCollection(t *testing.T, name string) string {
|
||||
ctx := context.Background()
|
||||
|
||||
pgUserID := pgtype.UUID{Bytes: [16]byte(s.User.ID), Valid: true}
|
||||
collection, err := s.DB.CreateCollection(ctx, database.CreateCollectionParams{
|
||||
UserID: pgUserID,
|
||||
Name: name,
|
||||
Description: pgtype.Text{String: "Test collection description", Valid: true},
|
||||
Color: pgtype.Text{String: "#FF5733", Valid: true},
|
||||
Icon: pgtype.Text{String: "folder", Valid: true},
|
||||
})
|
||||
require.NoError(t, err, "Should create collection")
|
||||
|
||||
collectionUUID, err := uuid.FromBytes(collection.ID.Bytes[0:16])
|
||||
require.NoError(t, err, "Should parse collection ID")
|
||||
|
||||
return collectionUUID.String()
|
||||
}
|
||||
|
||||
// setupTestServer creates a test server with a test database
|
||||
// Returns: *TestServerSetup with automatic cleanup via t.Cleanup
|
||||
func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
// 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 = getUploadPath()
|
||||
cfg.TestMode = true
|
||||
cfg.RateLimitEnabled = false
|
||||
cfg.RequestsPerMinute = 1000
|
||||
|
||||
// Connect to test database using the same method as main application
|
||||
// Use max_conns=1 to prevent connection pool exhaustion during test runs
|
||||
// (78 tests × 1 connection = 78 connections, well under PostgreSQL's 100 default max_connections)
|
||||
dbConfig, err := pgxpool.ParseConfig(cfg.DatabaseURL())
|
||||
require.NoError(t, err, "Failed to parse database URL")
|
||||
dbConfig.MaxConns = 1
|
||||
dbPool, err := pgxpool.NewWithConfig(context.Background(), dbConfig)
|
||||
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 := wsync.NewConnectionManager()
|
||||
cleanupCancel := connManager.StartCleanupTask()
|
||||
|
||||
// Create sync queue processor with cancellable context
|
||||
queueProcessor := wsync.NewSyncQueueProcessor(queries)
|
||||
queueCtx, queueCancel := context.WithCancel(context.Background())
|
||||
go queueProcessor.Start(queueCtx)
|
||||
|
||||
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)
|
||||
systemSettingsHandler := handlers.NewSystemSettingsHandler(queries)
|
||||
|
||||
// Create refactored handlers (matching main.go)
|
||||
libraryService := services.NewLibraryService(queries)
|
||||
worker := services.NewWorker(3, connManager)
|
||||
services.WorkerInstance = worker
|
||||
jobsHandler := handlers.NewJobsHandler(queries, worker)
|
||||
collectionHandler := handlers.NewCollectionHandler(queries, libraryService, connManager)
|
||||
dashboardService := services.NewDashboardService(queries)
|
||||
dashboardHandler := handlers.NewDashboardHandler(queries)
|
||||
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
|
||||
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
||||
|
||||
// Create conversion service for OPDS
|
||||
conversionService := services.NewConversionService(queries, getCachePath())
|
||||
opdsHandler := handlers.NewOPDSHandler(queries, libraryService, 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.RequestLogger())
|
||||
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,
|
||||
MediaHandler: mediaHandler,
|
||||
MatchingHandler: matchingHandler,
|
||||
KOReaderHandler: koreaderHandler,
|
||||
WSHandler: wsHandler,
|
||||
ConflictHandler: conflictHandler,
|
||||
AnalyticsHandler: analyticsHandler,
|
||||
QueueHandler: queueHandler,
|
||||
SystemSettingsHandler: systemSettingsHandler,
|
||||
CollectionHandler: collectionHandler,
|
||||
DashboardHandler: dashboardHandler,
|
||||
DashboardService: dashboardService,
|
||||
OPDSHandler: opdsHandler,
|
||||
JobsHandler: jobsHandler,
|
||||
ConnManager: connManager,
|
||||
QueueProcessor: queueProcessor,
|
||||
DeviceAuthMiddleware: deviceAuthMiddleware,
|
||||
LoginTracker: loginAttemptTracker,
|
||||
}
|
||||
|
||||
router.RegisterRoutes(routerConfig)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err, "Failed to create listener")
|
||||
// Configure Echo's HTTP server with the listener
|
||||
serverConfig := &http.Server{
|
||||
Handler: e,
|
||||
Addr: ln.Addr().String(),
|
||||
}
|
||||
ts := &httptest.Server{
|
||||
Listener: ln,
|
||||
Config: serverConfig,
|
||||
}
|
||||
ts.Start()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Delete ALL test users (any user with test email domains) to ensure clean state
|
||||
// This handles users created during tests that may have been promoted to admin, etc.
|
||||
allUsers, err := queries.ListUsers(ctx)
|
||||
if err == nil {
|
||||
for _, user := range allUsers {
|
||||
if strings.HasSuffix(user.Email, "@example.com") || strings.HasSuffix(user.Email, "@tests.bookhoard.internal") {
|
||||
queries.DeleteUser(ctx, user.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete test libraries (names containing "test" - case insensitive)
|
||||
// This cleans up libraries created by tests while preserving user-created libraries
|
||||
// NOTE: Do not use "test" in library names if you want to keep them!
|
||||
allLibs, _ := queries.ListLibraries(ctx)
|
||||
for _, lib := range allLibs {
|
||||
if strings.Contains(strings.ToLower(lib.Name), "test") {
|
||||
queries.DeleteLibrary(ctx, lib.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Create fresh admin test user
|
||||
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
||||
adminUser, err := queries.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testuser@tests.bookhoard.internal",
|
||||
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 admin test user")
|
||||
|
||||
adminUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse admin user UUID")
|
||||
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
|
||||
|
||||
// Create fresh regular test user
|
||||
regularUser, err := queries.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testregularuser@tests.bookhoard.internal",
|
||||
Username: "testregularuser",
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Regular", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "user",
|
||||
})
|
||||
require.NoError(t, err, "Failed to create regular test user")
|
||||
|
||||
regularUUID, err := uuid.FromBytes(regularUser.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse regular user UUID")
|
||||
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: regularUUID, Valid: true})
|
||||
|
||||
// Login to get tokens
|
||||
adminToken := loginWithCredentials(t, ts, "testuser@tests.bookhoard.internal", "Test@Pass123!")
|
||||
regularToken := loginWithCredentials(t, ts, "testregularuser@tests.bookhoard.internal", "Test@Pass123!")
|
||||
|
||||
// Create TestServerSetup struct with all resources
|
||||
setup := &TestServerSetup{
|
||||
Server: ts,
|
||||
DB: queries,
|
||||
DBPool: dbPool,
|
||||
Config: cfg,
|
||||
ConnManager: connManager,
|
||||
QueueProcessor: queueProcessor,
|
||||
CleanupCancel: cleanupCancel,
|
||||
QueueCtx: queueCtx,
|
||||
QueueCancel: queueCancel,
|
||||
Token: adminToken,
|
||||
RegularToken: regularToken,
|
||||
}
|
||||
|
||||
// Register cleanup function to run automatically when test completes
|
||||
t.Cleanup(func() {
|
||||
if err := setup.Close(); err != nil {
|
||||
t.Errorf("Failed to cleanup test server: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return setup
|
||||
}
|
||||
|
||||
func loginWithCredentials(t *testing.T, ts *httptest.Server, email, password string) string {
|
||||
loginRequest := map[string]interface{}{
|
||||
"login": email,
|
||||
"password": password,
|
||||
}
|
||||
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")
|
||||
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()
|
||||
user, err := db.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal")
|
||||
require.NoError(t, err, "Test user should exist")
|
||||
userUUID, err := uuid.FromBytes(user.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse user UUID")
|
||||
return userUUID
|
||||
}
|
||||
|
||||
func getRegularUserID(t *testing.T, db *database.Queries) uuid.UUID {
|
||||
ctx := context.Background()
|
||||
user, err := db.GetUserByEmail(ctx, "testregularuser@tests.bookhoard.internal")
|
||||
require.NoError(t, err, "Regular user should exist")
|
||||
userUUID, err := uuid.FromBytes(user.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse user UUID")
|
||||
return userUUID
|
||||
}
|
||||
|
||||
// createTestMediaItemID creates a test media item and returns its ID
|
||||
func createTestMediaItemID(t *testing.T, setup *TestServerSetup) string {
|
||||
uniqueName := fmt.Sprintf("Test Library %d", time.Now().UnixNano())
|
||||
httpClient := &http.Client{}
|
||||
|
||||
libReq := map[string]interface{}{
|
||||
"name": uniqueName,
|
||||
"description": "A test library for media items",
|
||||
"type": "ebooks",
|
||||
}
|
||||
libBody, _ := json.Marshal(libReq)
|
||||
|
||||
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
resp, err := httpClient.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)
|
||||
|
||||
folderReq := map[string]interface{}{
|
||||
"folder_path": "/app/uploads",
|
||||
}
|
||||
folderBody, _ := json.Marshal(folderReq)
|
||||
|
||||
folderReqHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries/"+libData+"/folders", bytes.NewBuffer(folderBody))
|
||||
folderReqHTTP.Header.Set("Content-Type", "application/json")
|
||||
folderReqHTTP.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
folderResp, err := httpClient.Do(folderReqHTTP)
|
||||
require.NoError(t, err)
|
||||
defer folderResp.Body.Close()
|
||||
require.Equal(t, http.StatusCreated, folderResp.StatusCode, "Library folder creation is required before adding media items")
|
||||
|
||||
mediaItemReq := map[string]interface{}{
|
||||
"library_id": libData,
|
||||
"title": "Test Media Item",
|
||||
"author": "Test Author",
|
||||
"file_path": "/tmp/test.epub",
|
||||
"file_size": 1024,
|
||||
"mime_type": "application/epub+zip",
|
||||
}
|
||||
mediaItemBody, _ := json.Marshal(mediaItemReq)
|
||||
|
||||
req2, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(mediaItemBody))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
resp2, err := httpClient.Do(req2)
|
||||
require.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusCreated, resp2.StatusCode)
|
||||
|
||||
var mediaItemResult map[string]interface{}
|
||||
json.NewDecoder(resp2.Body).Decode(&mediaItemResult)
|
||||
|
||||
mediaItemID := mediaItemResult["id"].(string)
|
||||
|
||||
t.Cleanup(func() {
|
||||
deleteReq, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/libraries/"+libData, nil)
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
httpClient.Do(deleteReq)
|
||||
})
|
||||
|
||||
return mediaItemID
|
||||
}
|
||||
|
||||
// addFolderToLibrary adds a folder to a test library via HTTP API
|
||||
func addFolderToLibrary(t *testing.T, setup *TestServerSetup, libraryID string, folderPath string) {
|
||||
t.Helper()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"folder_path": folderPath,
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries/"+libraryID+"/folders", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
}
|
||||
Reference in New Issue
Block a user