From b8a2dc4b5a679c01db4ca0b3e1a581d029f606c7 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 6 Mar 2026 14:17:50 -0500 Subject: [PATCH] test: remove obsolete test helper and comprehensive test files Remove legacy test files that are no longer used: - cmd/server/tests/library_test_comprehensive.go: Comprehensive library endpoint tests - cmd/server/tests/test_helpers.go: Test server setup and device test helpers - cmd/server/tests/test_helpers_db.go: Database verification utilities These files appear to be superseded by newer test infrastructure or were part of a test reorganization. Removing them reduces codebase maintenance burden and eliminates confusion about which test files are currently active. --- .../tests/library_test_comprehensive.go | 571 ------------- cmd/server/tests/test_helpers.go | 757 ------------------ cmd/server/tests/test_helpers_db.go | 158 ---- 3 files changed, 1486 deletions(-) delete mode 100644 cmd/server/tests/library_test_comprehensive.go delete mode 100644 cmd/server/tests/test_helpers.go delete mode 100644 cmd/server/tests/test_helpers_db.go diff --git a/cmd/server/tests/library_test_comprehensive.go b/cmd/server/tests/library_test_comprehensive.go deleted file mode 100644 index d1beec2..0000000 --- a/cmd/server/tests/library_test_comprehensive.go +++ /dev/null @@ -1,571 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" -) - -// TestLibraryManagementEndpoints tests library management operations -func TestLibraryManagementEndpoints(t *testing.T) { - libraryID := uuid.New() - - t.Run("POST /api/libraries - Create library without admin role", func(t *testing.T) { - payload := map[string]interface{}{ - "name": "New Library", - "description": "Test description", - "type": "ebooks", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer user-token") - req.Header.Set("X-User-Role", "user") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusCreated) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) - }) - - t.Run("POST /api/libraries - Create library with invalid type", func(t *testing.T) { - payload := map[string]interface{}{ - "name": "New Library", - "description": "Test description", - "type": "invalid-type", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer admin-token") - req.Header.Set("X-User-Role", "admin") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var req map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid request"}`)) - return - } - - libType := req["type"].(string) - if libType != "ebooks" && libType != "comics" && libType != "manga" { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid library type"}`)) - return - } - - w.WriteHeader(http.StatusCreated) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) - - t.Run("POST /api/libraries - Create library with missing required fields", func(t *testing.T) { - payload := map[string]interface{}{ - "description": "Test description", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer admin-token") - req.Header.Set("X-User-Role", "admin") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var req map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid request"}`)) - return - } - - if _, ok := req["name"]; !ok { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"name is required"}`)) - return - } - - if _, ok := req["type"]; !ok { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"type is required"}`)) - return - } - - w.WriteHeader(http.StatusCreated) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) - - t.Run("GET /api/libraries/:id - Get library with invalid UUID", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/invalid-uuid", nil) - req.Header.Set("Authorization", "Bearer admin-token") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid library id"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) - - t.Run("GET /api/libraries/:id - Get non-existent library", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/"+uuid.New().String(), nil) - req.Header.Set("Authorization", "Bearer admin-token") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte(`{"error":"library not found"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusNotFound, rr.Code) - }) - - t.Run("PUT /api/libraries/:id - Update library without admin role", func(t *testing.T) { - payload := map[string]interface{}{ - "name": "Updated Library", - "description": "Updated description", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", "/api/libraries/"+libraryID.String(), bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer user-token") - req.Header.Set("X-User-Role", "user") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) - }) - - t.Run("DELETE /api/libraries/:id - Delete library without admin role", func(t *testing.T) { - req := httptest.NewRequest("DELETE", "/api/libraries/"+libraryID.String(), nil) - req.Header.Set("Authorization", "Bearer user-token") - req.Header.Set("X-User-Role", "user") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusNoContent) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) - }) - - t.Run("DELETE /api/libraries/:id - Delete library with invalid UUID", func(t *testing.T) { - req := httptest.NewRequest("DELETE", "/api/libraries/invalid-uuid", nil) - req.Header.Set("Authorization", "Bearer admin-token") - req.Header.Set("X-User-Role", "admin") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid library id"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) -} - -// TestLibraryFolders tests library folder management -func TestLibraryFolders(t *testing.T) { - libraryID := uuid.New() - - t.Run("POST /api/libraries/:id/folders - Add folder without admin role", func(t *testing.T) { - payload := map[string]interface{}{ - "folder_path": "/path/to/folder", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries/"+libraryID.String()+"/folders", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer user-token") - req.Header.Set("X-User-Role", "user") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusCreated) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) - }) - - t.Run("POST /api/libraries/:id/folders - Add folder with invalid library ID", func(t *testing.T) { - payload := map[string]interface{}{ - "folder_path": "/path/to/folder", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries/invalid-uuid/folders", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer admin-token") - req.Header.Set("X-User-Role", "admin") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid library id"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) - - t.Run("POST /api/libraries/:id/folders - Add folder with missing path", func(t *testing.T) { - payload := map[string]interface{}{} - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries/"+libraryID.String()+"/folders", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer admin-token") - req.Header.Set("X-User-Role", "admin") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var req map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid request"}`)) - return - } - - if _, ok := req["folder_path"]; !ok { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"folder_path is required"}`)) - return - } - - w.WriteHeader(http.StatusCreated) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) - - t.Run("GET /api/libraries/:id/folders - Get folders without admin role", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/"+libraryID.String()+"/folders", nil) - req.Header.Set("Authorization", "Bearer user-token") - req.Header.Set("X-User-Role", "user") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) - }) - - t.Run("DELETE /api/libraries/:id/folders - Delete folder without admin role", func(t *testing.T) { - payload := map[string]interface{}{ - "folder_path": "/path/to/folder", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("DELETE", "/api/libraries/"+libraryID.String()+"/folders", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer user-token") - req.Header.Set("X-User-Role", "user") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusNoContent) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) - }) -} - -// TestLibraryVisibility tests library visibility controls -func TestLibraryVisibility(t *testing.T) { - libraryID := uuid.New() - userID := uuid.New() - - t.Run("POST /api/libraries/visibility - Set visibility without auth", func(t *testing.T) { - payload := map[string]interface{}{ - "library_id": libraryID.String(), - "is_visible": true, - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries/visibility", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(`{"message":"missing or malformed jwt"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusUnauthorized, rr.Code) - }) - - t.Run("POST /api/libraries/visibility - Set visibility with invalid library ID", func(t *testing.T) { - payload := map[string]interface{}{ - "library_id": "invalid-uuid", - "is_visible": true, - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries/visibility", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-token") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var req map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid request"}`)) - return - } - - libID := req["library_id"].(string) - if _, err := uuid.Parse(libID); err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid library id"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) - - t.Run("POST /api/libraries/visibility - Set visibility successfully", func(t *testing.T) { - payload := map[string]interface{}{ - "library_id": libraryID.String(), - "is_visible": true, - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", "/api/libraries/visibility", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-token") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - visibility := map[string]interface{}{ - "id": uuid.New().String(), - "user_id": userID.String(), - "library_id": libraryID.String(), - "is_visible": true, - } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(visibility) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - }) - - t.Run("GET /api/libraries/visible - Get visible libraries without auth", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/visible", nil) - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(`{"message":"missing or malformed jwt"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusUnauthorized, rr.Code) - }) - - t.Run("GET /api/libraries/visible - Get visible libraries with auth", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/visible", nil) - req.Header.Set("Authorization", "Bearer valid-token") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - libraries := []map[string]interface{}{ - { - "id": libraryID.String(), - "name": "Visible Library", - "is_visible": true, - }, - } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(libraries) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - }) -} - -// TestLibraryStats tests library statistics -func TestLibraryStats(t *testing.T) { - libraryID := uuid.New() - - t.Run("GET /api/libraries/:id/stats - Get stats without admin role", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/"+libraryID.String()+"/stats", nil) - req.Header.Set("Authorization", "Bearer user-token") - req.Header.Set("X-User-Role", "user") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) - }) - - t.Run("GET /api/libraries/:id/stats - Get stats with invalid library ID", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/invalid-uuid/stats", nil) - req.Header.Set("Authorization", "Bearer admin-token") - req.Header.Set("X-User-Role", "admin") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid library id"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) - - t.Run("GET /api/libraries/:id/stats - Get stats successfully", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/"+libraryID.String()+"/stats", nil) - req.Header.Set("Authorization", "Bearer admin-token") - req.Header.Set("X-User-Role", "admin") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - stats := map[string]interface{}{ - "media_count": 42, - "total_size": 1024000, - } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(stats) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - }) -} - -// TestLibraryTypes tests library type retrieval -func TestLibraryTypes(t *testing.T) { - t.Run("GET /api/libraries/types - Get all library types", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/libraries/types", nil) - req.Header.Set("Authorization", "Bearer valid-token") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - types := []map[string]interface{}{ - { - "id": uuid.New().String(), - "name": "ebooks", - "description": "Ebook files including EPUB, PDF, MOBI, etc.", - "allowed_extensions": []string{".epub", ".pdf", ".mobi"}, - }, - { - "id": uuid.New().String(), - "name": "comics", - "description": "Comic book archives and image formats", - "allowed_extensions": []string{".cbz", ".cbr", ".pdf"}, - }, - } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "ebooks") - assert.Contains(t, rr.Body.String(), "comics") - }) -} diff --git a/cmd/server/tests/test_helpers.go b/cmd/server/tests/test_helpers.go deleted file mode 100644 index 89234b2..0000000 --- a/cmd/server/tests/test_helpers.go +++ /dev/null @@ -1,757 +0,0 @@ -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) -} diff --git a/cmd/server/tests/test_helpers_db.go b/cmd/server/tests/test_helpers_db.go deleted file mode 100644 index e5be512..0000000 --- a/cmd/server/tests/test_helpers_db.go +++ /dev/null @@ -1,158 +0,0 @@ -package main - -import ( - "bookhoard/internal/database" - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "sync" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// Helper functions for database verification and test utilities -// These functions reduce code duplication and ensure consistent database state verification - -// verifyDeviceCreated verifies a device exists in database with expected values -func verifyDeviceCreated(t *testing.T, db *database.Queries, deviceID uuid.UUID, expectedName, expectedType, expectedIdentifier string) { - pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true} - device, err := db.GetDevice(context.Background(), pgDeviceID) - require.NoError(t, err, "Device should exist in database") - - assert.Equal(t, expectedName, device.DeviceName, "Device name should match") - assert.Equal(t, expectedType, device.DeviceType, "Device type should match") - assert.Equal(t, expectedIdentifier, device.DeviceIdentifier, "Device identifier should match") - assert.NotEmpty(t, device.AuthToken, "Device should have auth token") -} - -// verifyDeviceDeleted verifies a device does not exist in database -func verifyDeviceDeleted(t *testing.T, db *database.Queries, deviceID uuid.UUID) { - pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true} - _, err := db.GetDevice(context.Background(), pgDeviceID) - assert.Error(t, err, "Device should be deleted from database") -} - -// verifyUserField verifies a user has expected field value in database -func verifyUserField(t *testing.T, db *database.Queries, userID uuid.UUID, field string, expected interface{}) { - pgUserID := pgtype.UUID{Bytes: [16]byte(userID), Valid: true} - user, err := db.GetUser(context.Background(), pgUserID) - require.NoError(t, err, "User should exist in database") - - switch field { - case "email": - if em, ok := expected.(string); ok { - assert.Equal(t, em, user.Email, "Email should match") - } - case "first_name": - if fn, ok := expected.(string); ok { - assert.Equal(t, fn, user.FirstName.String, "First name should match") - } - case "last_name": - if ln, ok := expected.(string); ok { - assert.Equal(t, ln, user.LastName.String, "Last name should match") - } - case "username": - if un, ok := expected.(string); ok { - assert.Equal(t, un, user.Username, "Username should match") - } - case "theme": - if th, ok := expected.(string); ok { - assert.Equal(t, th, user.Theme.String, "Theme should match") - } - } -} - -// verifyMediaItemInDB verifies a media item exists in database -func verifyMediaItemInDB(t *testing.T, db *database.Queries, mediaID uuid.UUID) { - pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - _, err := db.GetMediaItem(context.Background(), pgMediaID) - require.NoError(t, err, "Media item should exist in database") -} - -// verifyMediaItemDeleted verifies a media item does not exist in database -func verifyMediaItemDeleted(t *testing.T, db *database.Queries, mediaID uuid.UUID) { - pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - _, err := db.GetMediaItem(context.Background(), pgMediaID) - assert.Error(t, err, "Media item should be deleted from database") -} - -// createTestLibraryWithFolder creates a test library with optional folder -func createTestLibraryWithFolder(t *testing.T, ts *httptest.Server, token, name string, withFolder bool) string { - libReq := map[string]interface{}{ - "name": name, - "type": "ebooks", - } - libBody, _ := json.Marshal(libReq) - - libHTTP, _ := http.NewRequest("POST", ts.URL+"/api/libraries", bytes.NewBuffer(libBody)) - libHTTP.Header.Set("Content-Type", "application/json") - libHTTP.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{} - resp, err := client.Do(libHTTP) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusCreated, resp.StatusCode, "Library creation should succeed") - - var libResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&libResponse) - libraryID := libResponse["id"].(string) - - if withFolder { - folderReq := map[string]interface{}{ - "folder_path": "/app/uploads", - } - folderBody, _ := json.Marshal(folderReq) - - folderHTTP, _ := http.NewRequest("POST", fmt.Sprintf("%s/api/libraries/%s/folders", ts.URL, libraryID), bytes.NewBuffer(folderBody)) - folderHTTP.Header.Set("Content-Type", "application/json") - folderHTTP.Header.Set("Authorization", "Bearer "+token) - - folderResp, err := client.Do(folderHTTP) - require.NoError(t, err) - defer folderResp.Body.Close() - require.Equal(t, http.StatusCreated, folderResp.StatusCode, "Folder creation should succeed") - } - - return libraryID -} - -// runConcurrent executes functions concurrently and waits for all to complete -func runConcurrent(t *testing.T, maxConcurrent int, fns []func() error) []error { - if len(fns) == 0 { - return nil - } - if len(fns) < maxConcurrent { - maxConcurrent = len(fns) - } - - errors := make(chan error, len(fns)) - var wg sync.WaitGroup - - for i := 0; i < maxConcurrent; i++ { - wg.Add(1) - go func(idx int) { - defer wg.Done() - if err := fns[idx](); err != nil { - errors <- err - } - }(i) - } - - wg.Wait() - close(errors) - - var allErrors []error - for err := range errors { - allErrors = append(allErrors, err) - } - - return allErrors -}