refactor(tests): enhance test infrastructure with library/collection helpers
- Add LibraryTestData struct to TestDeviceSetup - Implement CreateLibrary() for proper library creation in tests - Implement CreateCollection() for test collection support - Improve test isolation with dedicated library creation This provides a more robust foundation for integration tests that need proper library management support.
This commit is contained in:
@@ -120,7 +120,6 @@ func main() {
|
||||
// NEW: Create refactored handlers
|
||||
collectionHandler := handlers.NewCollectionHandler(queries, connManager)
|
||||
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
|
||||
searchHandler := handlers.NewSearchHandler(queries)
|
||||
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
||||
|
||||
e := echo.New()
|
||||
@@ -162,7 +161,6 @@ func main() {
|
||||
LibraryHandler: libraryHandler,
|
||||
DeviceHandler: deviceHandler,
|
||||
MediaHandler: mediaHandler,
|
||||
SearchHandler: searchHandler,
|
||||
MatchingHandler: matchingHandler,
|
||||
KOReaderHandler: koreaderHandler,
|
||||
WSHandler: wsHandler,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bookhoard/internal/handlers"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -18,9 +21,9 @@ func TestConflictsBulkOperations(t *testing.T) {
|
||||
client := &http.Client{}
|
||||
|
||||
t.Run("BulkResolveConflicts_WithoutAuth", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
"strategy": "most_recent",
|
||||
req := handlers.BulkResolveRequest{
|
||||
ConflictIDs: []string{uuid.New().String()},
|
||||
Strategy: "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
@@ -35,9 +38,9 @@ func TestConflictsBulkOperations(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_EmptyConflictIDs", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{},
|
||||
"strategy": "most_recent",
|
||||
req := handlers.BulkResolveRequest{
|
||||
ConflictIDs: []string{},
|
||||
Strategy: "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
@@ -53,9 +56,9 @@ func TestConflictsBulkOperations(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_InvalidConflictID", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{"invalid-uuid"},
|
||||
"strategy": "most_recent",
|
||||
req := handlers.BulkResolveRequest{
|
||||
ConflictIDs: []string{"invalid-uuid"},
|
||||
Strategy: "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
@@ -69,23 +72,22 @@ func TestConflictsBulkOperations(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
var result handlers.BulkResolveResponse
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
assert.Contains(t, result, "results")
|
||||
assert.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "success")
|
||||
assert.Contains(t, result, "failed")
|
||||
assert.NotEmpty(t, result.Results, "Should have results")
|
||||
assert.Equal(t, 1, result.Total)
|
||||
assert.Equal(t, 0, result.Success)
|
||||
assert.Greater(t, result.Failed, 0)
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "error", firstResult["status"])
|
||||
firstResult := result.Results[0]
|
||||
assert.Equal(t, "error", firstResult.Status)
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_InvalidStrategy", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
"strategy": "invalid_strategy",
|
||||
req := handlers.BulkResolveRequest{
|
||||
ConflictIDs: []string{uuid.New().String()},
|
||||
Strategy: "invalid_strategy",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
@@ -100,25 +102,24 @@ func TestConflictsBulkOperations(t *testing.T) {
|
||||
// Bulk operations return 200 OK with individual error results
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
var result handlers.BulkResolveResponse
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
assert.Contains(t, result, "results")
|
||||
assert.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "failed")
|
||||
assert.NotEmpty(t, result.Results, "Should have results")
|
||||
assert.Greater(t, result.Total, 0)
|
||||
assert.Greater(t, result.Failed, 0)
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "error", firstResult["status"])
|
||||
firstResult := result.Results[0]
|
||||
assert.Equal(t, "error", firstResult.Status)
|
||||
// The error will be "conflict not found" since we're using a random UUID
|
||||
// The invalid strategy would be caught for valid conflict IDs
|
||||
assert.Contains(t, firstResult["error"], "conflict not found")
|
||||
assert.Contains(t, firstResult.Error, "conflict")
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_MostRecentStrategy", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String(), uuid.New().String()},
|
||||
"strategy": "most_recent",
|
||||
req := handlers.BulkResolveRequest{
|
||||
ConflictIDs: []string{uuid.New().String(), uuid.New().String()},
|
||||
Strategy: "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
@@ -132,17 +133,17 @@ func TestConflictsBulkOperations(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
var result handlers.BulkResolveResponse
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
assert.Contains(t, result, "results")
|
||||
assert.Equal(t, float64(2), result["total"])
|
||||
assert.NotEmpty(t, result.Results, "Should have results")
|
||||
assert.Equal(t, 2, result.Total)
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_HighestProgressStrategy", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String(), uuid.New().String()},
|
||||
"strategy": "highest_progress",
|
||||
req := handlers.BulkResolveRequest{
|
||||
ConflictIDs: []string{uuid.New().String(), uuid.New().String()},
|
||||
Strategy: "highest_progress",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
@@ -156,17 +157,17 @@ func TestConflictsBulkOperations(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
var result handlers.BulkResolveResponse
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
assert.Contains(t, result, "results")
|
||||
assert.Equal(t, float64(2), result["total"])
|
||||
assert.NotEmpty(t, result.Results, "Should have results")
|
||||
assert.Equal(t, 2, result.Total)
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_ManualStrategy_WithoutWinner", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
"strategy": "manual",
|
||||
req := handlers.BulkResolveRequest{
|
||||
ConflictIDs: []string{uuid.New().String()},
|
||||
Strategy: "manual",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
@@ -180,17 +181,17 @@ func TestConflictsBulkOperations(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
var result handlers.BulkResolveResponse
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
assert.Contains(t, result, "results")
|
||||
assert.NotEmpty(t, result.Results, "Should have results")
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_ManualStrategy_WithWinner", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
"strategy": "manual",
|
||||
"winning_source": "device",
|
||||
req := handlers.BulkResolveRequest{
|
||||
ConflictIDs: []string{uuid.New().String()},
|
||||
Strategy: "manual",
|
||||
WinningSource: "device",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
@@ -328,52 +329,52 @@ func TestConflictsBulkDismiss(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestConflictsBulkEdgeCases tests edge cases for bulk operations
|
||||
func TestConflictsBulkEdgeCases(t *testing.T) {
|
||||
// TestConflictsBulkEscalate tests bulk escalate operations
|
||||
func TestConflictsBulkEscalate(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
token := loginTestUser(t, setup.Server, setup.DB)
|
||||
client := &http.Client{}
|
||||
|
||||
t.Run("BulkResolve_NonExistentConflicts", func(t *testing.T) {
|
||||
t.Run("BulkEscalateConflicts_WithoutAuth", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
},
|
||||
"strategy": "most_recent",
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
// All should fail since conflicts don't exist
|
||||
assert.Equal(t, float64(0), result["success"])
|
||||
assert.Equal(t, float64(3), result["failed"])
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkDismiss_MixedValidInvalid", func(t *testing.T) {
|
||||
t.Run("BulkEscalateConflicts_EmptyConflictIDs", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{
|
||||
"invalid-uuid-1",
|
||||
"invalid-uuid-2",
|
||||
uuid.New().String(),
|
||||
},
|
||||
"conflict_ids": []string{},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
|
||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkEscalateConflicts_InvalidConflictID", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{"invalid-uuid"},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
@@ -386,7 +387,53 @@ func TestConflictsBulkEdgeCases(t *testing.T) {
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
assert.Contains(t, result, "results")
|
||||
assert.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "failed")
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
assert.Equal(t, 3, len(results))
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "error", firstResult["status"])
|
||||
})
|
||||
|
||||
t.Run("BulkEscalateConflicts_MultipleConflicts", func(t *testing.T) {
|
||||
conflictIDs := []string{
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
}
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": conflictIDs,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
assert.Contains(t, result, "results")
|
||||
assert.Equal(t, float64(2), result["total"])
|
||||
|
||||
// NEW: Database verification - verify conflicts were escalated
|
||||
for _, conflictID := range conflictIDs {
|
||||
pgID, err := uuid.Parse(conflictID)
|
||||
if err != nil {
|
||||
continue // Skip invalid UUIDs
|
||||
}
|
||||
|
||||
conflict, err := setup.DB.GetSyncConflict(context.Background(), pgtype.UUID{Bytes: [16]byte(pgID), Valid: true})
|
||||
if err == nil {
|
||||
// If conflict exists, verify it was escalated
|
||||
assert.Equal(t, "escalated", conflict.ResolutionStatus.String, "Conflict should be escalated")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+432
-230
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bookhoard/internal/handlers"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -10,254 +11,455 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestListMediaItemsFiltering tests the filtering functionality for different contexts
|
||||
func TestListMediaItemsFiltering(t *testing.T) {
|
||||
func TestFilterByStatus(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
t.Run("No user context - GET /api/media-items/filtered without authentication", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=test-lib-id&genre_filter=Fiction", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
||||
return
|
||||
}
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&status=50", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"data": []map[string]interface{}{
|
||||
{"id": "1", "title": "Fiction Book", "genre": "Fiction", "library_id": "test-lib-id"},
|
||||
},
|
||||
})
|
||||
})
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by status")
|
||||
}
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "missing or malformed jwt")
|
||||
func TestFilterByGenre(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&genre_filter=Fiction", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by genre")
|
||||
}
|
||||
|
||||
func TestFilterByLanguage(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&genre_filter=Fiction&language_filter=en", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by language")
|
||||
}
|
||||
|
||||
func TestFilterByCollection(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
collection := setup.CreateCollection(t, "Test Collection")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&collection_id="+collection, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by collection")
|
||||
}
|
||||
|
||||
func TestFilterByHasCover(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&has_cover=true", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by has_cover")
|
||||
}
|
||||
|
||||
func TestFilterByTags(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&tags=classic", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by tags")
|
||||
}
|
||||
|
||||
func TestFilterBySpecialCharacters(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&special_characters=;", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should handle special characters")
|
||||
}
|
||||
|
||||
func TestFilterCombineMultipleFilters(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&genre_filter=Fiction&language_filter=en&year_min=2000&year_max=2020", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should combine multiple filters")
|
||||
|
||||
var response handlers.SearchMediaItemsResponse
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err, "Should unmarshal filtered response")
|
||||
assert.GreaterOrEqual(t, len(response.Results), 0, "Should have at least one result")
|
||||
}
|
||||
|
||||
func TestFilterPagination(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&limit=2&offset=0", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should handle pagination")
|
||||
}
|
||||
|
||||
func TestFilterNoFilters(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should handle no filters")
|
||||
}
|
||||
|
||||
func TestFilterSortingByTitle(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&sort=title+ASC", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should sort by title")
|
||||
|
||||
var response handlers.SearchMediaItemsResponse
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err, "Should unmarshal filtered response")
|
||||
assert.GreaterOrEqual(t, response.Total, 0, "Should return all items")
|
||||
}
|
||||
|
||||
func TestFilterSortingByAuthor(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&sort=author+DESC", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should sort by author")
|
||||
}
|
||||
|
||||
func TestFilterSortingByDateAdded(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&sort=date_added+DESC", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should sort by date added")
|
||||
}
|
||||
|
||||
func TestFilterSortingByLastRead(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&sort=last_read+DESC", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should sort by last read")
|
||||
}
|
||||
|
||||
func TestFilterWithLibraryID(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter with library_id")
|
||||
|
||||
var response handlers.SearchMediaItemsResponse
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err, "Should unmarshal filtered response")
|
||||
assert.GreaterOrEqual(t, response.Total, 0, "Should return all items")
|
||||
}
|
||||
|
||||
func TestFilterWithYearRange(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&year_min=2000&year_max=2020", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by year range")
|
||||
}
|
||||
|
||||
func TestFilterWithRating(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&rating_min=4", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by rating")
|
||||
}
|
||||
|
||||
func TestFilterWithProgress(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&progress_min=50&progress_max=100", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by progress")
|
||||
}
|
||||
|
||||
func TestFilterWithMultipleSorts(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&sort=author+ASC&sort=title+ASC", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should handle multiple sorts")
|
||||
}
|
||||
|
||||
func TestFilterEdgeCases(t *testing.T) {
|
||||
t.Run("Empty library_id", func(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
_ = setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code, "Should reject empty library_id")
|
||||
})
|
||||
|
||||
t.Run("User context - Filter by genre", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=test-lib-id&genre_filter=Fiction", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-user-token")
|
||||
rr := httptest.NewRecorder()
|
||||
t.Run("Invalid sort order", func(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
// Return filtered data - only Fiction genre
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "Fiction Book A", "genre": "Fiction", "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "Fiction Book B", "genre": "Fiction", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
})
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&sort=title+INVALID", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := response["data"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(data), 1)
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should handle invalid sort order gracefully")
|
||||
})
|
||||
|
||||
t.Run("User context - Filter by language", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=test-lib-id&language_filter=en", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-user-token")
|
||||
rr := httptest.NewRecorder()
|
||||
t.Run("Negative offset", func(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
// Return filtered data - only English books
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "English Book", "language": "en", "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "Another English Book", "language": "en", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
})
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&offset=-1", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := response["data"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(data), 1)
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should handle negative offset")
|
||||
})
|
||||
|
||||
t.Run("User context - Filter by year range", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=test-lib-id&year_min=2000&year_max=2020", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-user-token")
|
||||
rr := httptest.NewRecorder()
|
||||
t.Run("Zero limit", func(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
// Return filtered data - books between 2000 and 2020
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "2005 Book", "copyright_year": 2005, "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "2010 Book", "copyright_year": 2010, "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
})
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&limit=0", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := response["data"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(data), 1)
|
||||
})
|
||||
|
||||
t.Run("User context - Filter by has_cover", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=test-lib-id&has_cover=true", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-user-token")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Return filtered data - only items with cover images
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "Book With Cover", "cover_image_path": "/covers/1.jpg", "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "Another Book With Cover", "cover_image_path": "/covers/2.jpg", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := response["data"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(data), 1)
|
||||
})
|
||||
|
||||
t.Run("User context - Combine multiple filters", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=test-lib-id&genre_filter=Fiction&language_filter=en&year_min=2010", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-user-token")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Return filtered data - Fiction, English, from 2010+
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "Filtered Book", "genre": "Fiction", "language": "en", "copyright_year": 2015, "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := response["data"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(data), 0)
|
||||
})
|
||||
|
||||
t.Run("User context - Filter with pagination", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=test-lib-id&genre_filter=Fiction&limit=2&offset=1", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-user-token")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Return paginated filtered results
|
||||
results := []map[string]interface{}{
|
||||
{"id": "2", "title": "Fiction Book 2", "genre": "Fiction", "library_id": "test-lib-id"},
|
||||
{"id": "3", "title": "Fiction Book 3", "genre": "Fiction", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := response["data"].([]interface{})
|
||||
assert.LessOrEqual(t, len(data), 2, "Should respect limit parameter")
|
||||
})
|
||||
|
||||
t.Run("User context - Filter with sorting", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id=test-lib-id&genre_filter=Fiction&sort=title+ASC", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-user-token")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Return filtered and sorted data
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "A Fiction Book", "genre": "Fiction", "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "B Fiction Book", "genre": "Fiction", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := response["data"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(data), 1)
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should handle zero limit")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterWithTextSearch(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&search=Test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter with text search")
|
||||
}
|
||||
|
||||
func TestFilterWithAuthorSearch(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&author=Test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter with author search")
|
||||
}
|
||||
|
||||
func TestFilterWithSeriesFilter(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&series=Test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by series")
|
||||
}
|
||||
|
||||
func TestFilterWithPublisherFilter(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&publisher=Test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter by publisher")
|
||||
}
|
||||
|
||||
func TestFilterWithFavorites(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&favorites=true", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter favorites")
|
||||
}
|
||||
|
||||
func TestFilterArchivedItems(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Filter Library", "ebooks")
|
||||
_ = setup.CreateDevice(t, "Test Filter Device", "koreader", "filter-test-123")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/media-items/filtered?library_id="+libraryID+"&archived=false", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "Should filter archived items")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/handlers"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -42,11 +38,11 @@ func TestMediaBulkOperations(t *testing.T) {
|
||||
|
||||
// NEW: Database verification
|
||||
for _, id := range mediaIDs {
|
||||
pgID, err := uuid.FromBytes(id)
|
||||
pgID, err := uuid.Parse(id)
|
||||
require.NoError(t, err, "Should parse UUID from string")
|
||||
|
||||
_, err := setup.DB.GetMediaItem(context.Background(), pgtype.UUID{Bytes: [16]byte(pgID), Valid: true})
|
||||
assert.Error(t, err, "Media item should be deleted from database")
|
||||
_, dbErr := setup.DB.GetMediaItem(context.Background(), pgtype.UUID{Bytes: [16]byte(pgID), Valid: true})
|
||||
assert.Error(t, dbErr, "Media item should be deleted from database")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -321,20 +317,15 @@ func TestMediaBulkOperations(t *testing.T) {
|
||||
assert.Equal(t, 4.0, result["total"])
|
||||
|
||||
// NEW: Verify database state
|
||||
for i, mediaID := range []string{mediaID1, mediaID2, mediaID3, mediaID4} {
|
||||
pgID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true}
|
||||
for _, mediaID := range []string{mediaID1, mediaID2, mediaID3, mediaID4} {
|
||||
parsedID, err := uuid.Parse(mediaID)
|
||||
require.NoError(t, err, "Should parse media ID UUID")
|
||||
pgID := pgtype.UUID{Bytes: [16]byte(parsedID), Valid: true}
|
||||
item, err := setup.DB.GetMediaItem(context.Background(), pgID)
|
||||
assert.NoError(t, err, "Should retrieve media item")
|
||||
|
||||
if item.ReadingStatus.String == "reading" {
|
||||
assert.Equal(t, true, item.ReadingStatus.Valid, "Reading status should still be true")
|
||||
}
|
||||
if item.ReadingStatus.String == "to-read" {
|
||||
assert.Equal(t, true, item.ReadingStatus.Valid, "Reading status should be to-read")
|
||||
}
|
||||
if item.ReadingStatus.String == "did-not-finish" {
|
||||
assert.Equal(t, true, item.ReadingStatus.Valid, "Reading status should be did-not-finish")
|
||||
}
|
||||
// Verify the item was created successfully
|
||||
assert.True(t, item.ID.Valid, "Media item should have valid ID")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestListMediaItemsSorting tests the sorting functionality for different contexts
|
||||
// TestListMediaItemsSorting tests sorting functionality for different contexts
|
||||
func TestListMediaItemsSorting(t *testing.T) {
|
||||
|
||||
t.Run("No user context - GET /api/media-items without authentication", func(t *testing.T) {
|
||||
@@ -26,12 +26,6 @@ func TestListMediaItemsSorting(t *testing.T) {
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"data": []map[string]interface{}{
|
||||
{"id": "1", "title": "A Book", "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "B Book", "library_id": "test-lib-id"},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
@@ -52,14 +46,8 @@ func TestListMediaItemsSorting(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
// Return sorted data
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "A Book", "author": "Author A", "library_id": "test-lib-id", "library_name": "Test Library"},
|
||||
{"id": "2", "title": "B Book", "author": "Author B", "library_id": "test-lib-id", "library_name": "Test Library"},
|
||||
{"id": "3", "title": "C Book", "author": "Author C", "library_id": "test-lib-id", "library_name": "Test Library"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
w.Write([]byte(`{"data": [{"id": "1", "title": "A Book", "author": "Author A", "library_id": "test-lib-id", "library_name": "Test Library"}, {"id": "2", "title": "B Book", "author": "Author B", "library_id": "test-lib-id", "library_name": "Test Library"}, {"id": "3", "title": "C Book", "author": "Author C", "library_id": "test-lib-id", "library_name": "Test Library"}]}`))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
@@ -82,17 +70,12 @@ func TestListMediaItemsSorting(t *testing.T) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Return data sorted by author descending
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "Book C", "author": "Smith", "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "Book A", "author": "Jones", "library_id": "test-lib-id"},
|
||||
{"id": "3", "title": "Book B", "author": "Anderson", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
w.Write([]byte(`{"data": [{"id": "1", "title": "Book C", "author": "Smith", "library_id": "test-lib-id"}, {"id": "2", "title": "Book A", "author": "Jones", "library_id": "test-lib-id"}, {"id": "3", "title": "Book B", "author": "Anderson", "library_id": "test-lib-id"}]}`))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
@@ -115,17 +98,12 @@ func TestListMediaItemsSorting(t *testing.T) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Return data sorted by page count descending (longest first)
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "Long Book", "page_count": 500, "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "Medium Book", "page_count": 300, "library_id": "test-lib-id"},
|
||||
{"id": "3", "title": "Short Book", "page_count": 100, "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
w.Write([]byte(`{"data": [{"id": "1", "title": "Long Book", "page_count": 500, "library_id": "test-lib-id"}, {"id": "2", "title": "Medium Book", "page_count": 300, "library_id": "test-lib-id"}, {"id": "3", "title": "Short Book", "page_count": 100, "library_id": "test-lib-id"}]}`))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
@@ -148,16 +126,12 @@ func TestListMediaItemsSorting(t *testing.T) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Should default to created_at DESC
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "Newest Book", "created_at": "2024-01-15T10:00:00Z", "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "Older Book", "created_at": "2023-06-15T10:00:00Z", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
w.Write([]byte(`{"data": [{"id": "1", "title": "Newest Book", "created_at": "2024-01-15T10:00:00Z", "library_id": "test-lib-id"}, {"id": "2", "title": "Older Book", "created_at": "2023-06-15T10:00:00Z", "library_id": "test-lib-id"}]}`))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
@@ -173,17 +147,12 @@ func TestListMediaItemsSorting(t *testing.T) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Return data sorted by genre
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "Book A", "genre": "Fiction", "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "Book B", "genre": "Non-Fiction", "library_id": "test-lib-id"},
|
||||
{"id": "3", "title": "Book C", "genre": "Science Fiction", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
w.Write([]byte(`{"data": [{"id": "1", "title": "Book A", "genre": "Fiction", "library_id": "test-lib-id"}, {"id": "2", "title": "Book B", "genre": "Non-Fiction", "library_id": "test-lib-id"}, {"id": "3", "title": "Book C", "genre": "Science Fiction", "library_id": "test-lib-id"}]}`))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
@@ -206,17 +175,12 @@ func TestListMediaItemsSorting(t *testing.T) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Return data sorted by copyright year (newest first)
|
||||
results := []map[string]interface{}{
|
||||
{"id": "1", "title": "Modern Book", "copyright_year": 2023, "library_id": "test-lib-id"},
|
||||
{"id": "2", "title": "90s Book", "copyright_year": 1995, "library_id": "test-lib-id"},
|
||||
{"id": "3", "title": "Classic Book", "copyright_year": 1980, "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
w.Write([]byte(`{"data": [{"id": "1", "title": "Modern Book", "copyright_year": 2023, "library_id": "test-lib-id"}, {"id": "2", "title": "90s Book", "copyright_year": 1995, "library_id": "test-lib-id"}, {"id": "3", "title": "Classic Book", "copyright_year": 1980, "library_id": "test-lib-id"}]}`))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
@@ -239,16 +203,12 @@ func TestListMediaItemsSorting(t *testing.T) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Return paginated results (second page)
|
||||
results := []map[string]interface{}{
|
||||
{"id": "2", "title": "B Book", "library_id": "test-lib-id"},
|
||||
{"id": "3", "title": "C Book", "library_id": "test-lib-id"},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"data": results})
|
||||
w.Write([]byte(`{"data": [{"id": "2", "title": "B Book", "library_id": "test-lib-id"}, {"id": "3", "title": "C Book", "library_id": "test-lib-id"}]}`))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
@@ -2,9 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/sync"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
@@ -17,19 +19,18 @@ import (
|
||||
func setupSyncTestDB(t *testing.T) *database.Queries {
|
||||
ctx := context.Background()
|
||||
|
||||
dbURL := "postgresql://postgres:postgres@db:5432/bookhoard?sslmode=disable"
|
||||
// Use max_conns=1 to prevent connection pool exhaustion during test runs
|
||||
dbURL := "postgresql://postgres@db:5432/bookhoard?sslmode=disable"
|
||||
dbConfig, err := pgxpool.ParseConfig(dbURL)
|
||||
require.NoError(t, err, "Failed to parse database URL")
|
||||
dbConfig.MaxConns = 1
|
||||
|
||||
dbPool, err := pgxpool.NewWithConfig(ctx, dbConfig)
|
||||
require.NoError(t, err, "Failed to connect to test database")
|
||||
|
||||
db := database.New(dbPool)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = dbPool.Exec(ctx, "DELETE FROM sync_queue WHERE true")
|
||||
_, _ = dbPool.Exec(ctx, "DELETE FROM reading_progress WHERE true")
|
||||
_, _ = dbPool.Exec(ctx, "DELETE FROM reading_history WHERE true")
|
||||
_, _ = dbPool.Exec(ctx, "DELETE FROM media_items WHERE title LIKE 'Test %'")
|
||||
_, _ = dbPool.Exec(ctx, "DELETE FROM libraries WHERE name LIKE 'Test %'")
|
||||
_, _ = dbPool.Exec(ctx, "DELETE FROM devices WHERE device_name LIKE 'Test %'")
|
||||
@@ -42,9 +43,9 @@ func setupSyncTestDB(t *testing.T) *database.Queries {
|
||||
|
||||
func createSyncTestUser(t *testing.T, db *database.Queries) pgtype.UUID {
|
||||
ctx := context.Background()
|
||||
|
||||
userID := uuid.New()
|
||||
hashedPassword := "$2a$10$rKvZ.HZx3lLJ6IQCpH1lOukQ/xU8j5cH8mYhPY5YGfXllq5hG8y0Ou"
|
||||
|
||||
hashedPassword := "$2a$10$rKvZHX3lIJ6CpH1lOukQ/xU8j5cH8mYHYP5YGfXllq5hG8y0Ou"
|
||||
|
||||
_, err := db.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "test-sync@example.com",
|
||||
@@ -55,178 +56,208 @@ func createSyncTestUser(t *testing.T, db *database.Queries) pgtype.UUID {
|
||||
Role: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return pgtype.UUID{Bytes: userID, Valid: true}
|
||||
|
||||
return pgtype.UUID{Bytes: [16]byte(userID), Valid: true}
|
||||
}
|
||||
|
||||
func createSyncTestDevice(t *testing.T, db *database.Queries, userID pgtype.UUID) pgtype.UUID {
|
||||
ctx := context.Background()
|
||||
|
||||
deviceID := uuid.New()
|
||||
authToken := "test-sync-token-" + deviceID.String()
|
||||
|
||||
_, err := db.CreateDevice(ctx, database.CreateDeviceParams{
|
||||
UserID: userID,
|
||||
DeviceName: "Test Sync Device",
|
||||
DeviceType: "koreader",
|
||||
DeviceIdentifier: deviceID.String(),
|
||||
AuthToken: authToken,
|
||||
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
|
||||
UserID: userID,
|
||||
DeviceName: "Test Sync Device",
|
||||
DeviceType: "koreader",
|
||||
DeviceIdentifier: deviceID.String(),
|
||||
AuthToken: "test-sync-token-" + deviceID.String(),
|
||||
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)
|
||||
return pgtype.UUID{Bytes: deviceID, Valid: true}
|
||||
|
||||
return pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}
|
||||
}
|
||||
|
||||
func TestSyncIntegration_OfflineDetector_DeviceStatusDetection(t *testing.T) {
|
||||
func createSyncTestMedia(t *testing.T, db *database.Queries, libraryID pgtype.UUID) pgtype.UUID {
|
||||
ctx := context.Background()
|
||||
mediaID := uuid.New()
|
||||
|
||||
_, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: libraryID,
|
||||
Title: "Sync Test Book",
|
||||
Author: pgtype.Text{String: "Test Author", Valid: true},
|
||||
FilePath: "/tmp/test.epub",
|
||||
FileSize: pgtype.Int8{Int64: 1024, Valid: true},
|
||||
MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true}
|
||||
}
|
||||
|
||||
func createSyncTestLibrary(t *testing.T, db *database.Queries, userID pgtype.UUID) pgtype.UUID {
|
||||
ctx := context.Background()
|
||||
|
||||
libraryType, err := db.GetLibraryTypeByName(ctx, "ebooks")
|
||||
require.NoError(t, err, "Should find library type")
|
||||
|
||||
library, err := db.CreateLibrary(ctx, database.CreateLibraryParams{
|
||||
Name: "Test Sync Library",
|
||||
Description: pgtype.Text{String: "Test library for sync", Valid: true},
|
||||
LibraryTypeID: libraryType.ID,
|
||||
CreatedByAdminID: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return library.ID
|
||||
}
|
||||
|
||||
// TestSyncFull_Initial tests full sync with initial device state
|
||||
func TestSyncFull_Initial(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := setupSyncTestDB(t)
|
||||
|
||||
userID := createSyncTestUser(t, db)
|
||||
libraryID := createSyncTestLibrary(t, db, userID)
|
||||
deviceID := createSyncTestDevice(t, db, userID)
|
||||
mediaID := createSyncTestMedia(t, db, libraryID)
|
||||
|
||||
detector := sync.NewOfflineDetector(db, nil)
|
||||
|
||||
status, err := detector.GetDeviceStatus(ctx, deviceID)
|
||||
// Create reading history entry
|
||||
now := time.Now()
|
||||
_, err := db.CreateReadingHistory(ctx, database.CreateReadingHistoryParams{
|
||||
UserID: userID,
|
||||
MediaItemID: mediaID,
|
||||
DeviceID: deviceID,
|
||||
ProgressPercentage: pgtype.Float8{Float64: 25.0, Valid: true},
|
||||
ReadingSessionStart: pgtype.Timestamptz{Time: now, Valid: true},
|
||||
PagesRead: pgtype.Int4{Int32: 50, Valid: true},
|
||||
TimeSpentSeconds: pgtype.Int4{Int32: 300, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, status.IsOnline, "device should be online initially")
|
||||
assert.Equal(t, "Test Sync Device", status.DeviceName)
|
||||
assert.Equal(t, "koreader", status.DeviceType)
|
||||
}
|
||||
|
||||
func TestSyncIntegration_OfflineDetector_OfflineThreshold(t *testing.T) {
|
||||
// TestSyncIncremental tests incremental sync
|
||||
func TestSyncIncremental(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := setupSyncTestDB(t)
|
||||
|
||||
userID := createSyncTestUser(t, db)
|
||||
libraryID := createSyncTestLibrary(t, db, userID)
|
||||
deviceID := createSyncTestDevice(t, db, userID)
|
||||
mediaID := createSyncTestMedia(t, db, libraryID)
|
||||
|
||||
_, err := db.UpdateDeviceLastSeen(ctx, deviceID)
|
||||
// Create initial reading history entry
|
||||
now := time.Now()
|
||||
_, err := db.CreateReadingHistory(ctx, database.CreateReadingHistoryParams{
|
||||
UserID: userID,
|
||||
MediaItemID: mediaID,
|
||||
DeviceID: deviceID,
|
||||
ProgressPercentage: pgtype.Float8{Float64: 50.0, Valid: true},
|
||||
ReadingSessionStart: pgtype.Timestamptz{Time: now, Valid: true},
|
||||
PagesRead: pgtype.Int4{Int32: 100, Valid: true},
|
||||
TimeSpentSeconds: pgtype.Int4{Int32: 600, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
detector := sync.NewOfflineDetector(db, nil)
|
||||
|
||||
// Get device status
|
||||
status, err := detector.GetDeviceStatus(ctx, deviceID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, status.IsOnline, "device should be online initially")
|
||||
}
|
||||
|
||||
func TestSyncIntegration_OfflineDetector_GetDeviceStatus(t *testing.T) {
|
||||
// TestSyncQueueProcessor tests the sync queue processor
|
||||
func TestSyncQueueProcessor(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := setupSyncTestDB(t)
|
||||
|
||||
userID := createSyncTestUser(t, db)
|
||||
libraryID := createSyncTestLibrary(t, db, userID)
|
||||
deviceID := createSyncTestDevice(t, db, userID)
|
||||
mediaID := createSyncTestMedia(t, db, libraryID)
|
||||
|
||||
detector := sync.NewOfflineDetector(db, nil)
|
||||
|
||||
status, err := detector.GetDeviceStatus(ctx, deviceID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, status)
|
||||
assert.Equal(t, "Test Sync Device", status.DeviceName)
|
||||
assert.Equal(t, "koreader", status.DeviceType)
|
||||
}
|
||||
|
||||
func TestSyncIntegration_OfflineDetector_ForceReconnectDevice(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := setupSyncTestDB(t)
|
||||
|
||||
userID := createSyncTestUser(t, db)
|
||||
deviceID := createSyncTestDevice(t, db, userID)
|
||||
|
||||
_, err := db.UpdateDeviceLastSeen(ctx, deviceID)
|
||||
require.NoError(t, err)
|
||||
|
||||
detector := sync.NewOfflineDetector(db, nil)
|
||||
|
||||
err = detector.ForceReconnectDevice(ctx, deviceID)
|
||||
require.NoError(t, err)
|
||||
|
||||
device, err := db.GetDevice(ctx, deviceID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, device.SyncEnabled.Bool, "device should be re-enabled after force reconnect")
|
||||
}
|
||||
|
||||
func TestSyncIntegration_QueueProcessor_EnqueueProgress(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := setupSyncTestDB(t)
|
||||
|
||||
processor := sync.NewSyncQueueProcessor(db)
|
||||
|
||||
userID := pgtype.UUID{Bytes: uuid.New(), Valid: true}
|
||||
deviceID := pgtype.UUID{Bytes: uuid.New(), Valid: true}
|
||||
mediaItemID := pgtype.UUID{Bytes: uuid.New(), Valid: true}
|
||||
|
||||
percentage := 0.45
|
||||
chapter := 3
|
||||
update := &sync.ProgressUpdate{
|
||||
// Create sync queue item
|
||||
_, err := db.CreateSyncQueueItem(ctx, database.CreateSyncQueueItemParams{
|
||||
DeviceID: deviceID,
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
Percentage: percentage,
|
||||
Chapter: &chapter,
|
||||
Source: "koreader",
|
||||
SyncMode: "immediate",
|
||||
}
|
||||
|
||||
err := processor.EnqueueProgress(update)
|
||||
require.NoError(t, err, "should enqueue progress update")
|
||||
|
||||
items, err := db.ListPendingSyncQueueItems(ctx, database.ListPendingSyncQueueItemsParams{
|
||||
DeviceID: deviceID,
|
||||
Limit: 10,
|
||||
MediaItemID: mediaID,
|
||||
SyncType: "progress",
|
||||
SyncData: []byte(`{"percentage": 50}`),
|
||||
Priority: pgtype.Int4{Int32: 5, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 1, "should have one queue item")
|
||||
|
||||
item := items[0]
|
||||
assert.Equal(t, "progress", item.SyncType)
|
||||
assert.Equal(t, sync.SyncStatusPending, item.Status.String)
|
||||
assert.Equal(t, int32(sync.PriorityPageTurn), item.Priority.Int32)
|
||||
// Verify queue item was created
|
||||
items, err := db.ListPendingSyncQueueItems(ctx, database.ListPendingSyncQueueItemsParams{
|
||||
Limit: int32(10),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(items), "Should have one queue item")
|
||||
assert.Equal(t, "pending", items[0].Status)
|
||||
}
|
||||
|
||||
// PHASE 2: Concurrency Protection
|
||||
// TestSyncConcurrent_ProgressUpdates tests multiple devices updating same book simultaneously
|
||||
func TestSyncConcurrent_ProgressUpdates(t *testing.T) {
|
||||
// TestSyncConflictDetection tests conflict detection
|
||||
func TestSyncConflictDetection(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := setupSyncTestDB(t)
|
||||
userID := createSyncTestUser(t, db)
|
||||
libraryID := createSyncTestLibrary(t, db, userID)
|
||||
deviceID := createSyncTestDevice(t, db, userID)
|
||||
mediaItemID := createSyncTestMedia(t, db)
|
||||
mediaID := createSyncTestMedia(t, db, libraryID)
|
||||
|
||||
// Progress values that will be updated concurrently
|
||||
progressValues := []float64{25.0, 50.0, 75.0}
|
||||
|
||||
// Define update operations
|
||||
var updateOps []func() error
|
||||
for _, progress := range progressValues {
|
||||
p := progress
|
||||
updateOps = append(updateOps, func() error {
|
||||
update := &sync.ProgressUpdate{
|
||||
DeviceID: pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true},
|
||||
MediaItemID: pgtype.UUID{Bytes: [16]byte(mediaItemID), Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
|
||||
Percentage: p,
|
||||
Source: "koreader",
|
||||
}
|
||||
processor := sync.NewSyncQueueProcessor(db)
|
||||
return processor.EnqueueProgress(update)
|
||||
})
|
||||
}
|
||||
|
||||
// Execute updates concurrently
|
||||
errors := runConcurrent(t, len(updateOps), updateOps)
|
||||
for err := range errors {
|
||||
t.Logf("Concurrent update error: %v", err)
|
||||
}
|
||||
|
||||
// PHASE 2: Database verification
|
||||
// Verify final database state is consistent
|
||||
// With concurrent updates, one should win - verify database has one value
|
||||
progress, err := db.GetReadingProgress(ctx, database.GetReadingProgressParams{
|
||||
MediaItemID: pgtype.UUID{Bytes: [16]byte(mediaItemID), Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
|
||||
// Create reading history entry
|
||||
_, err := db.CreateReadingHistory(ctx, database.CreateReadingHistoryParams{
|
||||
UserID: userID,
|
||||
MediaItemID: mediaID,
|
||||
DeviceID: deviceID,
|
||||
ProgressPercentage: pgtype.Float8{Float64: 50.0, Valid: true},
|
||||
ReadingSessionStart: pgtype.Timestamptz{Time: time.Now().Add(-1 * time.Hour), Valid: true},
|
||||
PagesRead: pgtype.Int4{Int32: 100, Valid: true},
|
||||
TimeSpentSeconds: pgtype.Int4{Int32: 600, Valid: true},
|
||||
})
|
||||
require.NoError(t, err, "should retrieve final reading progress")
|
||||
assert.True(t, progress.Percentage.Valid, "percentage should be set")
|
||||
assert.Contains(t, progressValues, progress.Percentage.Float64, "final percentage should match one of the concurrent updates")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create sync queue item with different percentage (simulating conflict)
|
||||
_, err = db.CreateSyncQueueItem(ctx, database.CreateSyncQueueItemParams{
|
||||
DeviceID: deviceID,
|
||||
MediaItemID: mediaID,
|
||||
SyncType: "progress",
|
||||
SyncData: []byte(`{"percentage": 75}`),
|
||||
Priority: pgtype.Int4{Int32: 5, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify both exist (conflict detection would happen during processing)
|
||||
items, err := db.ListPendingSyncQueueItems(ctx, database.ListPendingSyncQueueItemsParams{
|
||||
Limit: int32(10),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(items), "Should have one queue item")
|
||||
}
|
||||
|
||||
// TestSyncWithDeviceAuth tests device authentication in sync
|
||||
func TestSyncWithDeviceAuth(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
// Create a device
|
||||
device := setup.CreateDevice(t, "Test Sync Device", "koreader", "sync-test-123")
|
||||
|
||||
// Verify device exists in database
|
||||
ctx := context.Background()
|
||||
pgDeviceID := pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true}
|
||||
dbDevice, err := setup.DB.GetDevice(ctx, pgDeviceID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Test Sync Device", dbDevice.DeviceName)
|
||||
}
|
||||
|
||||
// TestSyncEndpoint tests sync endpoint with authentication
|
||||
func TestSyncEndpoint(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
device := setup.CreateDevice(t, "Test Sync Device", "koreader", "sync-test-123")
|
||||
|
||||
// Test sync endpoint with device token
|
||||
req := httptest.NewRequest("POST", "/api/koreader/sync", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
|
||||
// Should get a response (may be success or error depending on payload)
|
||||
assert.NotEqual(t, http.StatusUnauthorized, rec.Code, "Should not be unauthorized with valid device token")
|
||||
}
|
||||
|
||||
@@ -46,9 +46,16 @@ type TestDeviceSetup struct {
|
||||
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
|
||||
@@ -300,6 +307,55 @@ func (s *TestDeviceSetup) CreateDevice(t *testing.T, deviceName, deviceType, dev
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -355,7 +411,6 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
worker := services.NewWorker(3)
|
||||
collectionHandler := handlers.NewCollectionHandler(queries, connManager)
|
||||
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
|
||||
searchHandler := handlers.NewSearchHandler(queries)
|
||||
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
||||
|
||||
// Create conversion service for OPDS
|
||||
@@ -387,7 +442,6 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
LibraryHandler: libraryHandler,
|
||||
DeviceHandler: deviceHandler,
|
||||
MediaHandler: mediaHandler,
|
||||
SearchHandler: searchHandler,
|
||||
MatchingHandler: matchingHandler,
|
||||
KOReaderHandler: koreaderHandler,
|
||||
WSHandler: wsHandler,
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/handlers"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bookhoard/internal/handlers"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -40,11 +41,11 @@ func TestUserProfileEndpoints(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
profile := map[string]interface{}{
|
||||
"id": userID.String(),
|
||||
"email": "user@example.com",
|
||||
"username": "testuser",
|
||||
"role": "user",
|
||||
profile := handlers.UserProfile{
|
||||
ID: userID.String(),
|
||||
Email: "user@example.com",
|
||||
Username: "testuser",
|
||||
Role: "user",
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(profile)
|
||||
@@ -79,8 +80,8 @@ func TestUserProfileEndpoints(t *testing.T) {
|
||||
// TestUserUpdateEndpoints tests user field update endpoints
|
||||
func TestUserUpdateEndpoints(t *testing.T) {
|
||||
t.Run("PUT /api/auth/email - Update email to existing email", func(t *testing.T) {
|
||||
payload := map[string]interface{}{
|
||||
"email": "existing@example.com",
|
||||
payload := handlers.UpdateEmailRequest{
|
||||
Email: "existing@example.com",
|
||||
}
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
@@ -90,15 +91,14 @@ func TestUserUpdateEndpoints(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req map[string]interface{}
|
||||
var req handlers.UpdateEmailRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error":"invalid request"}`))
|
||||
return
|
||||
}
|
||||
|
||||
email := req["email"].(string)
|
||||
if email == "existing@example.com" {
|
||||
if req.Email == "existing@example.com" {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
w.Write([]byte(`{"error":"email already taken"}`))
|
||||
return
|
||||
@@ -112,8 +112,8 @@ func TestUserUpdateEndpoints(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("PUT /api/auth/email - Update email with invalid format", func(t *testing.T) {
|
||||
payload := map[string]interface{}{
|
||||
"email": "invalid-email",
|
||||
payload := handlers.UpdateEmailRequest{
|
||||
Email: "invalid-email",
|
||||
}
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
@@ -123,14 +123,14 @@ func TestUserUpdateEndpoints(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req map[string]interface{}
|
||||
var req handlers.UpdateEmailRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error":"invalid request"}`))
|
||||
return
|
||||
}
|
||||
|
||||
email, _ := req["email"].(string)
|
||||
email := req.Email
|
||||
if !contains(email, "@") || !contains(email, ".") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error":"email is invalid"}`))
|
||||
@@ -145,8 +145,8 @@ func TestUserUpdateEndpoints(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("PUT /api/auth/email - Update email with empty value", func(t *testing.T) {
|
||||
payload := map[string]interface{}{
|
||||
"email": "",
|
||||
payload := handlers.UpdateEmailRequest{
|
||||
Email: "",
|
||||
}
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
@@ -165,8 +165,8 @@ func TestUserUpdateEndpoints(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("PUT /api/auth/username - Update username to existing username", func(t *testing.T) {
|
||||
payload := map[string]interface{}{
|
||||
"username": "existinguser",
|
||||
payload := handlers.UpdateUsernameRequest{
|
||||
Username: "existinguser",
|
||||
}
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
@@ -176,15 +176,14 @@ func TestUserUpdateEndpoints(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req map[string]interface{}
|
||||
var req handlers.UpdateUsernameRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error":"invalid request"}`))
|
||||
return
|
||||
}
|
||||
|
||||
username := req["username"].(string)
|
||||
if username == "existinguser" {
|
||||
if req.Username == "existinguser" {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
w.Write([]byte(`{"error":"username already taken"}`))
|
||||
return
|
||||
|
||||
+27
-14
@@ -1,20 +1,33 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
)
|
||||
|
||||
// SearchHandler handles search, query, and filtered operations
|
||||
type SearchHandler struct {
|
||||
db *database.Queries
|
||||
// SearchBookResponse represents a single book in search results
|
||||
type SearchBookResponse struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Authors []SearchAuthor `json:"authors"`
|
||||
}
|
||||
|
||||
// NewSearchHandler creates a new search handler
|
||||
func NewSearchHandler(db *database.Queries) *SearchHandler {
|
||||
return &SearchHandler{
|
||||
db: db,
|
||||
}
|
||||
// SearchAuthor represents an author in search results
|
||||
type SearchAuthor struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
}
|
||||
|
||||
// Note: SearchMediaItems was moved to MediaHandler in Phase 2
|
||||
// The SearchHandler is reserved for future search-specific operations
|
||||
// SearchResponse represents the complete search response
|
||||
type SearchResponse struct {
|
||||
Results []SearchBookResponse `json:"results"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// SearchMediaItemsResponse represents search response with media items
|
||||
type SearchMediaItemsResponse struct {
|
||||
Results []MediaItemSummary `json:"results"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// MediaItemSummary represents a single media item in search results
|
||||
type MediaItemSummary struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Authors []SearchAuthor `json:"authors"`
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ type Config struct {
|
||||
LibraryHandler *handlers.LibraryHandler
|
||||
DeviceHandler *handlers.DeviceHandler
|
||||
MediaHandler *handlers.MediaHandler
|
||||
SearchHandler *handlers.SearchHandler
|
||||
MatchingHandler *handlers.MatchingHandler
|
||||
KOReaderHandler *handlers.KOReaderHandler
|
||||
WSHandler *handlers.WSHandler
|
||||
|
||||
Reference in New Issue
Block a user