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.
This commit is contained in:
@@ -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")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user