test: add comprehensive test coverage for API endpoints and services
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestAnalyticsReadingStats tests the reading statistics endpoint
|
||||
func TestAnalyticsReadingStats(t *testing.T) {
|
||||
t.Run("GetReadingStats_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/reading-stats", nil)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetReadingStats_WithAuth_DefaultDates", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/reading-stats", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
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, "total_books_read")
|
||||
assert.Contains(t, result, "total_pages_read")
|
||||
assert.Contains(t, result, "total_reading_time_minutes")
|
||||
assert.Contains(t, result, "average_session_time_minutes")
|
||||
assert.Contains(t, result, "completion_rate")
|
||||
assert.Contains(t, result, "daily_reading_minutes")
|
||||
})
|
||||
|
||||
t.Run("GetReadingStats_WithCustomDateRange", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
startDate := time.Now().AddDate(0, -2, 0).Format("2006-01-02")
|
||||
endDate := time.Now().Format("2006-01-02")
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/reading-stats?start_date="+startDate+"&end_date="+endDate, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetReadingStats_InvalidStartDate", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/reading-stats?start_date=invalid-date", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetReadingStats_InvalidEndDate", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/reading-stats?end_date=not-a-date", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetReadingStats_EmptyHistory", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/reading-stats", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
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)
|
||||
|
||||
// Should return zero values for empty history
|
||||
assert.Equal(t, 0.0, result["total_books_read"])
|
||||
assert.Equal(t, 0.0, result["total_pages_read"])
|
||||
})
|
||||
}
|
||||
|
||||
// TestAnalyticsDeviceUsage tests the device usage endpoint
|
||||
func TestAnalyticsDeviceUsage(t *testing.T) {
|
||||
t.Run("GetDeviceUsage_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/device-usage", nil)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetDeviceUsage_WithAuth_NoDevices", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/device-usage", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
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)
|
||||
|
||||
devices, ok := result["devices"].([]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 0, len(devices))
|
||||
})
|
||||
|
||||
t.Run("GetDeviceUsage_WithAuth_WithDevices", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// First create a device
|
||||
deviceReq := map[string]interface{}{
|
||||
"device_name": "Test Kobo",
|
||||
"device_type": "kobo",
|
||||
}
|
||||
deviceBody, _ := json.Marshal(deviceReq)
|
||||
|
||||
deviceReqHTTP, _ := http.NewRequest("POST", ts.URL+"/api/devices/register", bytes.NewBuffer(deviceBody))
|
||||
deviceReqHTTP.Header.Set("Content-Type", "application/json")
|
||||
deviceReqHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(deviceReqHTTP)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
// Now get device usage
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/device-usage", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
resp, err = client.Do(req)
|
||||
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)
|
||||
|
||||
devices, ok := result["devices"].([]interface{})
|
||||
assert.True(t, ok)
|
||||
// May be 0 if device has no usage data yet
|
||||
assert.NotNil(t, devices)
|
||||
})
|
||||
|
||||
t.Run("GetDeviceUsage_ResponseStructure", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/device-usage", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
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, "devices")
|
||||
|
||||
// Check device structure if any devices exist
|
||||
if devices, ok := result["devices"].([]interface{}); ok && len(devices) > 0 {
|
||||
firstDevice := devices[0].(map[string]interface{})
|
||||
assert.Contains(t, firstDevice, "device_id")
|
||||
assert.Contains(t, firstDevice, "device_name")
|
||||
assert.Contains(t, firstDevice, "device_type")
|
||||
assert.Contains(t, firstDevice, "sync_count")
|
||||
assert.Contains(t, firstDevice, "last_sync")
|
||||
assert.Contains(t, firstDevice, "total_time_seconds")
|
||||
assert.Contains(t, firstDevice, "total_time_minutes")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAnalyticsPopularBooks tests the popular books endpoint
|
||||
func TestAnalyticsPopularBooks(t *testing.T) {
|
||||
t.Run("GetPopularBooks_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/popular-books", nil)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetPopularBooks_WithAuth_DefaultLimit", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/popular-books", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
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, "books")
|
||||
books := result["books"].([]interface{})
|
||||
assert.NotNil(t, books)
|
||||
// Default limit is 10, but may be fewer if no reading history
|
||||
assert.True(t, len(books) <= 10)
|
||||
})
|
||||
|
||||
t.Run("GetPopularBooks_WithCustomLimit", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/popular-books?limit=5", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
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)
|
||||
|
||||
books := result["books"].([]interface{})
|
||||
assert.True(t, len(books) <= 5)
|
||||
})
|
||||
|
||||
t.Run("GetPopularBooks_InvalidLimit", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/popular-books?limit=invalid", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should default to 10 on invalid limit
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
books := result["books"].([]interface{})
|
||||
assert.True(t, len(books) <= 10)
|
||||
})
|
||||
|
||||
t.Run("GetPopularBooks_ResponseStructure", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// First create a book and some reading history
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
// Create reading history for the book
|
||||
historyReq := map[string]interface{}{
|
||||
"media_item_id": bookID,
|
||||
"progress_percentage": 50.0,
|
||||
"pages_read": 100,
|
||||
"time_spent_seconds": 1800,
|
||||
}
|
||||
historyBody, _ := json.Marshal(historyReq)
|
||||
|
||||
historyHTTP, _ := http.NewRequest("POST", ts.URL+"/api/media-items/"+bookID+"/progress", bytes.NewBuffer(historyBody))
|
||||
historyHTTP.Header.Set("Content-Type", "application/json")
|
||||
historyHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(historyHTTP)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
// Now get popular books
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/popular-books", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
resp, err = client.Do(req)
|
||||
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)
|
||||
|
||||
books := result["books"].([]interface{})
|
||||
|
||||
// Check book structure if any books exist
|
||||
if len(books) > 0 {
|
||||
firstBook := books[0].(map[string]interface{})
|
||||
assert.Contains(t, firstBook, "media_item_id")
|
||||
assert.Contains(t, firstBook, "title")
|
||||
assert.Contains(t, firstBook, "author")
|
||||
assert.Contains(t, firstBook, "read_count")
|
||||
assert.Contains(t, firstBook, "avg_completion")
|
||||
assert.Contains(t, firstBook, "last_read")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetPopularBooks_NoReadingHistory", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/popular-books", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
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)
|
||||
|
||||
books := result["books"].([]interface{})
|
||||
// Should return empty array if no reading history
|
||||
assert.Equal(t, 0, len(books))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAnalyticsEdgeCases tests edge cases for analytics endpoints
|
||||
func TestAnalyticsEdgeCases(t *testing.T) {
|
||||
t.Run("ReadingStats_FutureDateRange", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
startDate := time.Now().AddDate(0, 0, 7).Format("2006-01-02")
|
||||
endDate := time.Now().AddDate(0, 0, 14).Format("2006-01-02")
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/reading-stats?start_date="+startDate+"&end_date="+endDate, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should succeed but return empty stats
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
assert.Equal(t, 0.0, result["total_books_read"])
|
||||
})
|
||||
|
||||
t.Run("PopularBooks_LimitZero", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/popular-books?limit=0", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should handle limit=0 gracefully
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
books := result["books"].([]interface{})
|
||||
assert.Equal(t, 0, len(books))
|
||||
})
|
||||
|
||||
t.Run("PopularBooks_VeryLargeLimit", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/api/analytics/popular-books?limit=999999", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should handle large limit
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestBookMatchingQueryBooks tests the book query endpoint
|
||||
func TestBookMatchingQueryBooks(t *testing.T) {
|
||||
t.Run("QueryBooks_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"title": "Test Book",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/books/query", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("QueryBooks_WithAuth_ByTitle", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
_ = createTestEbookID(t, ts, token)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"title": "Test Ebook",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/books/query", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, "matches")
|
||||
assert.Contains(t, result, "action")
|
||||
})
|
||||
|
||||
t.Run("QueryBooks_InvalidRequestBody", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Send invalid JSON
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/books/query", bytes.NewBuffer([]byte("invalid json")))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("QueryBooks_NoResults", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"title": "NonExistentBookTitleThatDoesNotExist123456789",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/books/query", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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)
|
||||
|
||||
matches := result["matches"].([]interface{})
|
||||
assert.Equal(t, 0, len(matches))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBookMatchingBulkLink tests bulk linking operations
|
||||
func TestBookMatchingBulkLink(t *testing.T) {
|
||||
t.Run("BulkLinkBooks_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"links": []map[string]interface{}{
|
||||
{
|
||||
"unlinked_book_id": uuid.New(),
|
||||
"media_item_id": uuid.New(),
|
||||
"confidence_score": 0.9,
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/bulk-link-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkLinkBooks_WithAuth_EmptyLinks", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"links": []map[string]interface{}{},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/bulk-link-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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.Equal(t, 0, result["total"])
|
||||
assert.Equal(t, 0, result["successful"])
|
||||
assert.Equal(t, 0, result["failed"])
|
||||
})
|
||||
|
||||
t.Run("BulkLinkBooks_InvalidUnlinkedBookID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"links": []map[string]interface{}{
|
||||
{
|
||||
"unlinked_book_id": uuid.New(),
|
||||
"media_item_id": bookID,
|
||||
"confidence_score": 0.9,
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/bulk-link-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "successful")
|
||||
assert.Contains(t, result, "failed")
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "error", firstResult["status"])
|
||||
})
|
||||
|
||||
t.Run("BulkLinkBooks_MultipleLinks", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"links": []map[string]interface{}{
|
||||
{
|
||||
"unlinked_book_id": uuid.New(),
|
||||
"media_item_id": uuid.New(),
|
||||
"confidence_score": 0.9,
|
||||
},
|
||||
{
|
||||
"unlinked_book_id": uuid.New(),
|
||||
"media_item_id": uuid.New(),
|
||||
"confidence_score": 0.8,
|
||||
},
|
||||
{
|
||||
"unlinked_book_id": uuid.New(),
|
||||
"media_item_id": uuid.New(),
|
||||
"confidence_score": 0.95,
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/bulk-link-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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.Equal(t, 3, result["total"])
|
||||
results := result["results"].([]interface{})
|
||||
assert.Equal(t, 3, len(results))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBookMatchingAutoLink tests automatic linking
|
||||
func TestBookMatchingAutoLink(t *testing.T) {
|
||||
t.Run("AutoLinkBooks_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"confidence_threshold": 0.8,
|
||||
"limit": 10,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/auto-link-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("AutoLinkBooks_WithAuth_DefaultThreshold", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/auto-link-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, "auto_linked")
|
||||
assert.Contains(t, result, "results")
|
||||
})
|
||||
|
||||
t.Run("AutoLinkBooks_CustomThreshold", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"confidence_threshold": 0.95,
|
||||
"limit": 20,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/auto-link-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, "auto_linked")
|
||||
})
|
||||
|
||||
t.Run("AutoLinkBooks_NoUnlinkedBooks", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"limit": 5,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/sync/auto-link-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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)
|
||||
|
||||
// Should succeed even with no books to link
|
||||
assert.Contains(t, result, "auto_linked")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBookMatchingSuggestions tests getting suggestions for unlinked books
|
||||
func TestBookMatchingSuggestions(t *testing.T) {
|
||||
t.Run("GetUnlinkedBookSuggestions_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
testID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/sync/unlinked-books/"+testID.String()+"/suggestions", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetUnlinkedBookSuggestions_InvalidUUID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/sync/unlinked-books/invalid-uuid/suggestions", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetUnlinkedBookSuggestions_BookNotFound", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
testID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/sync/unlinked-books/"+testID.String()+"/suggestions", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetUnlinkedBookSuggestions_ResponseStructure", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create a test device and unlinked book would go here
|
||||
// For now, test with a non-existent ID to check response structure
|
||||
testID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/sync/unlinked-books/"+testID.String()+"/suggestions", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Even when book not found, we expect 404
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBookMatchingDeviceFileAliases tests device file alias operations
|
||||
func TestBookMatchingDeviceFileAliases(t *testing.T) {
|
||||
t.Run("GetDeviceFileAliases_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
testID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/devices/"+testID.String()+"/file-aliases", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetDeviceFileAliases_WithAuth", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
testID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/devices/"+testID.String()+"/file-aliases", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, "device_id")
|
||||
assert.Contains(t, result, "aliases")
|
||||
assert.Contains(t, result, "total")
|
||||
})
|
||||
|
||||
t.Run("CreateDeviceFileAlias_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
deviceID := uuid.New()
|
||||
mediaItemID := uuid.New()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"media_item_id": mediaItemID.String(),
|
||||
"file_path": "/mnt/sd/test.epub",
|
||||
"file_sha256": "",
|
||||
"confidence_score": 0.9,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/devices/"+deviceID.String()+"/file-aliases", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("CreateDeviceFileAlias_InvalidDeviceID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
mediaItemID := uuid.New()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"media_item_id": mediaItemID.String(),
|
||||
"file_path": "/mnt/sd/test.epub",
|
||||
"file_sha256": "",
|
||||
"confidence_score": 0.9,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/devices/invalid-uuid/file-aliases", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("CreateDeviceFileAlias_InvalidMediaItemID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
deviceID := uuid.New()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"media_item_id": "invalid-uuid",
|
||||
"file_path": "/mnt/sd/test.epub",
|
||||
"file_sha256": "",
|
||||
"confidence_score": 0.9,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/devices/"+deviceID.String()+"/file-aliases", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("UpdateDeviceFileAlias_InvalidAliasID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
deviceID := uuid.New()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"confidence_score": 0.95,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("PUT", ts.URL+"/api/devices/"+deviceID.String()+"/file-aliases/invalid-uuid", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("DeleteDeviceFileAlias_InvalidAliasID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
deviceID := uuid.New()
|
||||
|
||||
httpReq, _ := http.NewRequest("DELETE", ts.URL+"/api/devices/"+deviceID.String()+"/file-aliases/invalid-uuid", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBookMatchingGetBookMatches tests the book matches endpoint
|
||||
func TestBookMatchingGetBookMatches(t *testing.T) {
|
||||
t.Run("GetBookMatches_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/books/match?title=Test", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetBookMatches_WithAuth_ByTitle", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/books/match?title=Test", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, "matches")
|
||||
assert.Contains(t, result, "action")
|
||||
})
|
||||
|
||||
t.Run("GetBookMatches_InvalidFileSize", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/books/match?title=Test&file_size=invalid", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetBookMatches_MultipleIdentifiers", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/api/books/match?identifier=id1&identifier=id2&title=Test", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, "matches")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestCollectionsBulkOperations tests bulk collection operations
|
||||
func TestCollectionsBulkOperations(t *testing.T) {
|
||||
t.Run("BulkAddBooks_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"operations": []map[string]interface{}{
|
||||
{
|
||||
"collection_id": uuid.New().String(),
|
||||
"book_ids": []string{uuid.New().String()},
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkAddBooks_EmptyOperations", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"operations": []map[string]interface{}{},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkAddBooks_InvalidCollectionID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"operations": []map[string]interface{}{
|
||||
{
|
||||
"collection_id": "invalid-uuid",
|
||||
"book_ids": []string{bookID},
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "success")
|
||||
assert.Contains(t, result, "failed")
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
assert.True(t, len(results) > 0)
|
||||
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "error", firstResult["status"])
|
||||
})
|
||||
|
||||
t.Run("BulkAddBooks_InvalidBookID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create a collection first
|
||||
collectionReq := map[string]interface{}{
|
||||
"name": "Test Collection",
|
||||
"description": "A test collection",
|
||||
}
|
||||
collectionBody, _ := json.Marshal(collectionReq)
|
||||
|
||||
collectionHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections", bytes.NewBuffer(collectionBody))
|
||||
collectionHTTP.Header.Set("Content-Type", "application/json")
|
||||
collectionHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(collectionHTTP)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var collectionResult map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&collectionResult)
|
||||
collectionID := collectionResult["id"].(string)
|
||||
|
||||
// Now try to add invalid book IDs
|
||||
addReq := map[string]interface{}{
|
||||
"operations": []map[string]interface{}{
|
||||
{
|
||||
"collection_id": collectionID,
|
||||
"book_ids": []string{"invalid-uuid"},
|
||||
},
|
||||
},
|
||||
}
|
||||
addBody, _ := json.Marshal(addReq)
|
||||
|
||||
addHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody))
|
||||
addHTTP.Header.Set("Content-Type", "application/json")
|
||||
addHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err = client.Do(addHTTP)
|
||||
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)
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "error", firstResult["status"])
|
||||
})
|
||||
|
||||
t.Run("BulkAddBooks_SingleOperation", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create a collection
|
||||
collectionReq := map[string]interface{}{
|
||||
"name": "Test Collection",
|
||||
"description": "A test collection",
|
||||
}
|
||||
collectionBody, _ := json.Marshal(collectionReq)
|
||||
|
||||
collectionHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections", bytes.NewBuffer(collectionBody))
|
||||
collectionHTTP.Header.Set("Content-Type", "application/json")
|
||||
collectionHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(collectionHTTP)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var collectionResult map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&collectionResult)
|
||||
collectionID := collectionResult["id"].(string)
|
||||
|
||||
// Create a book
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
// Add book to collection
|
||||
addReq := map[string]interface{}{
|
||||
"operations": []map[string]interface{}{
|
||||
{
|
||||
"collection_id": collectionID,
|
||||
"book_ids": []string{bookID},
|
||||
},
|
||||
},
|
||||
}
|
||||
addBody, _ := json.Marshal(addReq)
|
||||
|
||||
addHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody))
|
||||
addHTTP.Header.Set("Content-Type", "application/json")
|
||||
addHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err = client.Do(addHTTP)
|
||||
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.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "success")
|
||||
assert.Contains(t, result, "failed")
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "success", firstResult["status"])
|
||||
})
|
||||
|
||||
t.Run("BulkAddBooks_MultipleBooksSingleCollection", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create a collection
|
||||
collectionReq := map[string]interface{}{
|
||||
"name": "Test Collection",
|
||||
"description": "A test collection",
|
||||
}
|
||||
collectionBody, _ := json.Marshal(collectionReq)
|
||||
|
||||
collectionHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections", bytes.NewBuffer(collectionBody))
|
||||
collectionHTTP.Header.Set("Content-Type", "application/json")
|
||||
collectionHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(collectionHTTP)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var collectionResult map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&collectionResult)
|
||||
collectionID := collectionResult["id"].(string)
|
||||
|
||||
// Create multiple books
|
||||
bookID1 := createTestEbookID(t, ts, token)
|
||||
bookID2 := createTestEbookID(t, ts, token)
|
||||
bookID3 := createTestEbookID(t, ts, token)
|
||||
|
||||
// Add all books to collection
|
||||
addReq := map[string]interface{}{
|
||||
"operations": []map[string]interface{}{
|
||||
{
|
||||
"collection_id": collectionID,
|
||||
"book_ids": []string{bookID1, bookID2, bookID3},
|
||||
},
|
||||
},
|
||||
}
|
||||
addBody, _ := json.Marshal(addReq)
|
||||
|
||||
addHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody))
|
||||
addHTTP.Header.Set("Content-Type", "application/json")
|
||||
addHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err = client.Do(addHTTP)
|
||||
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.Equal(t, 3, result["total"])
|
||||
assert.True(t, result["success"].(float64) > 0)
|
||||
})
|
||||
|
||||
t.Run("BulkAddBooks_MultipleCollections", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create multiple collections
|
||||
collectionReq := map[string]interface{}{
|
||||
"name": "Test Collection 1",
|
||||
"description": "First test collection",
|
||||
}
|
||||
collectionBody, _ := json.Marshal(collectionReq)
|
||||
|
||||
collectionHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections", bytes.NewBuffer(collectionBody))
|
||||
collectionHTTP.Header.Set("Content-Type", "application/json")
|
||||
collectionHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(collectionHTTP)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var collectionResult1 map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&collectionResult1)
|
||||
collectionID1 := collectionResult1["id"].(string)
|
||||
|
||||
collectionReq2 := map[string]interface{}{
|
||||
"name": "Test Collection 2",
|
||||
"description": "Second test collection",
|
||||
}
|
||||
collectionBody2, _ := json.Marshal(collectionReq2)
|
||||
|
||||
collectionHTTP2, _ := http.NewRequest("POST", ts.URL+"/api/collections", bytes.NewBuffer(collectionBody2))
|
||||
collectionHTTP2.Header.Set("Content-Type", "application/json")
|
||||
collectionHTTP2.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err = client.Do(collectionHTTP2)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var collectionResult2 map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&collectionResult2)
|
||||
collectionID2 := collectionResult2["id"].(string)
|
||||
|
||||
// Create books
|
||||
bookID1 := createTestEbookID(t, ts, token)
|
||||
bookID2 := createTestEbookID(t, ts, token)
|
||||
|
||||
// Add books to multiple collections
|
||||
addReq := map[string]interface{}{
|
||||
"operations": []map[string]interface{}{
|
||||
{
|
||||
"collection_id": collectionID1,
|
||||
"book_ids": []string{bookID1},
|
||||
},
|
||||
{
|
||||
"collection_id": collectionID2,
|
||||
"book_ids": []string{bookID1, bookID2},
|
||||
},
|
||||
},
|
||||
}
|
||||
addBody, _ := json.Marshal(addReq)
|
||||
|
||||
addHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody))
|
||||
addHTTP.Header.Set("Content-Type", "application/json")
|
||||
addHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err = client.Do(addHTTP)
|
||||
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, 3, result["total"])
|
||||
})
|
||||
|
||||
t.Run("BulkAddBooks_DuplicateBooks", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create a collection
|
||||
collectionReq := map[string]interface{}{
|
||||
"name": "Test Collection",
|
||||
"description": "A test collection",
|
||||
}
|
||||
collectionBody, _ := json.Marshal(collectionReq)
|
||||
|
||||
collectionHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections", bytes.NewBuffer(collectionBody))
|
||||
collectionHTTP.Header.Set("Content-Type", "application/json")
|
||||
collectionHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(collectionHTTP)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var collectionResult map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&collectionResult)
|
||||
collectionID := collectionResult["id"].(string)
|
||||
|
||||
// Create a book
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
// Add book to collection
|
||||
addReq := map[string]interface{}{
|
||||
"operations": []map[string]interface{}{
|
||||
{
|
||||
"collection_id": collectionID,
|
||||
"book_ids": []string{bookID},
|
||||
},
|
||||
},
|
||||
}
|
||||
addBody, _ := json.Marshal(addReq)
|
||||
|
||||
addHTTP, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody))
|
||||
addHTTP.Header.Set("Content-Type", "application/json")
|
||||
addHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err = client.Do(addHTTP)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Try to add the same book again
|
||||
resp2, err := client.Do(addHTTP)
|
||||
require.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
|
||||
// Should handle duplicate gracefully (either succeed or return error)
|
||||
assert.Equal(t, http.StatusOK, resp2.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkAddBooks_InvalidRequestBody", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Send invalid JSON
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/collections/bulk-add-books", bytes.NewBuffer([]byte("invalid json")))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestConflictsBulkOperations tests bulk conflict resolution operations
|
||||
func TestConflictsBulkOperations(t *testing.T) {
|
||||
t.Run("BulkResolveConflicts_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
"strategy": "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_EmptyConflictIDs", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{},
|
||||
"strategy": "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_InvalidConflictID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{"invalid-uuid"},
|
||||
"strategy": "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "success")
|
||||
assert.Contains(t, result, "failed")
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "error", firstResult["status"])
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_InvalidStrategy", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
"strategy": "invalid_strategy",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_MostRecentStrategy", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String(), uuid.New().String()},
|
||||
"strategy": "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, 2, result["total"])
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_HighestProgressStrategy", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String(), uuid.New().String()},
|
||||
"strategy": "highest_progress",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, 2, result["total"])
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_ManualStrategy_WithoutWinner", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
"strategy": "manual",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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")
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_ManualStrategy_WithWinner", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
"strategy": "manual",
|
||||
"winning_source": "device",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkResolveConflicts_InvalidRequestBody", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Send invalid JSON
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer([]byte("invalid json")))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConflictsBulkDismiss tests bulk dismiss operations
|
||||
func TestConflictsBulkDismiss(t *testing.T) {
|
||||
t.Run("BulkDismissConflicts_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{uuid.New().String()},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkDismissConflicts_EmptyConflictIDs", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkDismissConflicts_InvalidConflictID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{"invalid-uuid", uuid.New().String()},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "success")
|
||||
assert.Contains(t, result, "failed")
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
firstResult := results[0].(map[string]interface{})
|
||||
assert.Equal(t, "error", firstResult["status"])
|
||||
})
|
||||
|
||||
t.Run("BulkDismissConflicts_MultipleConflicts", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, 3, result["total"])
|
||||
})
|
||||
|
||||
t.Run("BulkDismissConflicts_InvalidRequestBody", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Send invalid JSON
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer([]byte("invalid json")))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConflictsBulkEdgeCases tests edge cases for bulk operations
|
||||
func TestConflictsBulkEdgeCases(t *testing.T) {
|
||||
t.Run("BulkResolve_NonExistentConflicts", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
},
|
||||
"strategy": "most_recent",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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"])
|
||||
})
|
||||
|
||||
t.Run("BulkDismiss_MixedValidInvalid", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"conflict_ids": []string{
|
||||
"invalid-uuid-1",
|
||||
"invalid-uuid-2",
|
||||
uuid.New().String(),
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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)
|
||||
|
||||
results := result["results"].([]interface{})
|
||||
assert.Equal(t, 3, len(results))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMediaBulkOperations tests bulk media operations
|
||||
func TestMediaBulkOperations(t *testing.T) {
|
||||
t.Run("BulkDeleteBooks_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{uuid.New().String()},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-delete", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkDeleteBooks_EmptyBookIDs", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-delete", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkDeleteBooks_InvalidBookIDs", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{"invalid-uuid", "another-invalid"},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-delete", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "deleted")
|
||||
assert.Contains(t, result, "failed")
|
||||
})
|
||||
|
||||
t.Run("BulkDeleteBooks_WithValidBooks", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create test books
|
||||
bookID1 := createTestEbookID(t, ts, token)
|
||||
bookID2 := createTestEbookID(t, ts, token)
|
||||
bookID3 := uuid.New().String()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{bookID1, bookID2, bookID3},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-delete", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, 3, result["total"])
|
||||
assert.True(t, result["deleted"].(float64) >= 2) // At least the valid ones
|
||||
})
|
||||
|
||||
t.Run("BulkDeleteBooks_InvalidRequestBody", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Send invalid JSON
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-delete", bytes.NewBuffer([]byte("invalid json")))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkUpdateBooks_WithoutAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{uuid.New().String()},
|
||||
"updates": map[string]interface{}{
|
||||
"tags": []string{"test"},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-update", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkUpdateBooks_EmptyBookIDs", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{},
|
||||
"updates": map[string]interface{}{
|
||||
"tags": []string{"test"},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-update", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BulkUpdateBooks_InvalidBookIDs", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{"invalid-uuid"},
|
||||
"updates": map[string]interface{}{
|
||||
"tags": []string{"fiction", "test"},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-update", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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.Contains(t, result, "total")
|
||||
assert.Contains(t, result, "updated")
|
||||
assert.Contains(t, result, "failed")
|
||||
})
|
||||
|
||||
t.Run("BulkUpdateBooks_UpdateTags", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create test books
|
||||
bookID1 := createTestEbookID(t, ts, token)
|
||||
bookID2 := createTestEbookID(t, ts, token)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{bookID1, bookID2},
|
||||
"updates": map[string]interface{}{
|
||||
"tags": []string{"fiction", "science-fiction", "test"},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-update", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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, 2, result["total"])
|
||||
assert.True(t, result["updated"].(float64) > 0)
|
||||
})
|
||||
|
||||
t.Run("BulkUpdateBooks_UpdateReadingStatus", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create test books
|
||||
bookID1 := createTestEbookID(t, ts, token)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{bookID1},
|
||||
"updates": map[string]interface{}{
|
||||
"reading_status": "reading",
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-update", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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")
|
||||
})
|
||||
|
||||
t.Run("BulkUpdateBooks_UpdateMultipleFields", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Create test books
|
||||
bookID1 := createTestEbookID(t, ts, token)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"book_ids": []string{bookID1},
|
||||
"updates": map[string]interface{}{
|
||||
"tags": []string{"test", "bulk-update"},
|
||||
"reading_status": "to-read",
|
||||
"rating": 4,
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-update", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
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")
|
||||
})
|
||||
|
||||
t.Run("BulkUpdateBooks_InvalidRequestBody", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Send invalid JSON
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/books/bulk-update", bytes.NewBuffer([]byte("invalid json")))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestOPDSEndpoints tests OPDS (Open Publication Distribution System) endpoints
|
||||
func TestOPDSEndpoints(t *testing.T) {
|
||||
t.Run("GetDeviceCatalog_WithoutDeviceAuth", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
deviceID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/catalog", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// OPDS endpoints may or may not require auth - depends on implementation
|
||||
// Accept either 200 (public) or 401 (requires auth)
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("GetDeviceCatalog_InvalidDeviceID", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/invalid-uuid/catalog", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return 400 for invalid UUID
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetDeviceCatalog_ValidDevice", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
|
||||
// Note: Device registration requires different endpoint
|
||||
// For now, test with a valid UUID format
|
||||
deviceID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/catalog", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return either 200 (OK with empty catalog) or 404 (device not found)
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("SearchDeviceCatalog_InvalidDeviceID", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/invalid-uuid/search?query=test", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("SearchDeviceCatalog_ValidDevice", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/search?query=test", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return 200 or 404
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("GetDeviceNavigation_InvalidDeviceID", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/invalid-uuid/nav", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetDeviceNavigation_ValidDevice", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/nav", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return navigation or 404
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DownloadBook_InvalidDeviceID", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
bookID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/invalid-uuid/download/"+bookID.String(), nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("DownloadBook_InvalidBookID", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
deviceID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/download/invalid-uuid", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("DownloadBook_ValidIDs", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/download/"+bookID, nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// May return 404 if device/book not linked, or 500 for file not found
|
||||
// Should not return 400 (invalid IDs)
|
||||
assert.NotEqual(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetCoverImage_InvalidDeviceID", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
bookID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/invalid-uuid/cover/"+bookID.String(), nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetCoverImage_InvalidBookID", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
deviceID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/cover/invalid-uuid", nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetCoverImage_ValidIDs", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/cover/"+bookID, nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// May return 404 if no cover, but not 400
|
||||
assert.NotEqual(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("ListFormats_InvalidDeviceID", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
bookID := uuid.New()
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/invalid-uuid/formats/"+bookID.String(), nil)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("ListFormats_ValidDeviceID", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/formats/"+bookID, nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return formats list or 404
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOPDSConversion tests on-the-fly conversion for downloads
|
||||
func TestOPDSConversion(t *testing.T) {
|
||||
t.Run("DownloadKEPUB_FormatParameter", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
// Request KEPUB format
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/download/"+bookID+"?format=kepub", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should attempt conversion (may fail if file doesn't exist)
|
||||
// Important: Should not return 400 for invalid IDs
|
||||
assert.NotEqual(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("DownloadEPUB_DefaultFormat", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
// Request default format (no format parameter)
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/download/"+bookID, nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should attempt to download original format
|
||||
assert.NotEqual(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Download_UnsupportedFormat", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
bookID := createTestEbookID(t, ts, token)
|
||||
|
||||
// Request unsupported format
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/download/"+bookID+"?format=pdf", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should handle gracefully (either 400 for unsupported format or 404/500)
|
||||
assert.True(t, resp.StatusCode >= 400 && resp.StatusCode < 600)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOPDSEdgeCases tests edge cases for OPDS endpoints
|
||||
func TestOPDSEdgeCases(t *testing.T) {
|
||||
t.Run("Catalog_EmptyLibrary", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/catalog", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should return empty catalog, not error
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("Search_SpecialCharacters", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
|
||||
// Search with special characters
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/search?query=test%20%26%20more", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should handle special characters
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("Search_EmptyQuery", func(t *testing.T) {
|
||||
ts, db, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
token := loginTestUser(t, ts, db)
|
||||
deviceID := uuid.New()
|
||||
|
||||
httpReq, _ := http.NewRequest("GET", ts.URL+"/opds/devices/"+deviceID.String()+"/search?query=", nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should handle empty query
|
||||
assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestRefreshTokenFlow comprehensive tests for token refresh functionality
|
||||
func TestRefreshTokenFlow(t *testing.T) {
|
||||
t.Run("RefreshToken_MissingToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_InvalidTokenFormat", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": "not-a-valid-jwt-token",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_ExpiredToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// This would require an expired token - for now test with invalid token
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAwMDAwMDB9.expired",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_ValidToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// First, login to get tokens
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
loginResp, err := client.Do(loginHTTP)
|
||||
require.NoError(t, err)
|
||||
defer loginResp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, loginResp.StatusCode)
|
||||
|
||||
var loginResult map[string]interface{}
|
||||
json.NewDecoder(loginResp.Body).Decode(&loginResult)
|
||||
|
||||
refreshToken, ok := loginResult["refresh_token"].(string)
|
||||
require.True(t, ok, "Should have refresh_token")
|
||||
|
||||
// Now use the refresh token
|
||||
refreshReq := map[string]interface{}{
|
||||
"refresh_token": refreshToken,
|
||||
}
|
||||
refreshBody, _ := json.Marshal(refreshReq)
|
||||
|
||||
refreshHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
|
||||
refreshHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
refreshResp, err := client.Do(refreshHTTP)
|
||||
require.NoError(t, err)
|
||||
defer refreshResp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, refreshResp.StatusCode)
|
||||
|
||||
var refreshResult map[string]interface{}
|
||||
json.NewDecoder(refreshResp.Body).Decode(&refreshResult)
|
||||
|
||||
assert.Contains(t, refreshResult, "access_token")
|
||||
assert.NotEmpty(t, refreshResult["access_token"], "New access token should not be empty")
|
||||
|
||||
// The new access token should be different from the original
|
||||
newAccessToken := refreshResult["access_token"].(string)
|
||||
originalAccessToken := loginResult["access_token"].(string)
|
||||
assert.NotEqual(t, originalAccessToken, newAccessToken, "New access token should be different")
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_InvalidRequestBody", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Send invalid JSON
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer([]byte("invalid json")))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_MissingContentType", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": "some-token",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
// Don't set Content-Type
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should still work or return appropriate error
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnsupportedMediaType)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRefreshTokenSecurity tests security aspects of token refresh
|
||||
func TestRefreshTokenSecurity(t *testing.T) {
|
||||
t.Run("RefreshToken_ReuseProtection", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Login to get tokens
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
loginResp, err := client.Do(loginHTTP)
|
||||
require.NoError(t, err)
|
||||
defer loginResp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, loginResp.StatusCode)
|
||||
|
||||
var loginResult map[string]interface{}
|
||||
json.NewDecoder(loginResp.Body).Decode(&loginResult)
|
||||
|
||||
refreshToken := loginResult["refresh_token"].(string)
|
||||
|
||||
// Use the refresh token first time
|
||||
refreshReq := map[string]interface{}{
|
||||
"refresh_token": refreshToken,
|
||||
}
|
||||
refreshBody, _ := json.Marshal(refreshReq)
|
||||
|
||||
refreshHTTP1, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
|
||||
refreshHTTP1.Header.Set("Content-Type", "application/json")
|
||||
|
||||
refreshResp1, err := client.Do(refreshHTTP1)
|
||||
require.NoError(t, err)
|
||||
defer refreshResp1.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, refreshResp1.StatusCode)
|
||||
|
||||
// Try to reuse the same refresh token (should fail if refresh token rotation is enabled)
|
||||
refreshHTTP2, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
|
||||
refreshHTTP2.Header.Set("Content-Type", "application/json")
|
||||
|
||||
refreshResp2, err := client.Do(refreshHTTP2)
|
||||
require.NoError(t, err)
|
||||
defer refreshResp2.Body.Close()
|
||||
|
||||
// May return 401 if token reuse is detected, or 200 if not implemented
|
||||
// Either is acceptable depending on security requirements
|
||||
assert.True(t, refreshResp2.StatusCode == http.StatusOK || refreshResp2.StatusCode == http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_TokenTampering", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Login to get a valid token
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
loginResp, err := client.Do(loginHTTP)
|
||||
require.NoError(t, err)
|
||||
defer loginResp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, loginResp.StatusCode)
|
||||
|
||||
var loginResult map[string]interface{}
|
||||
json.NewDecoder(loginResp.Body).Decode(&loginResult)
|
||||
|
||||
refreshToken := loginResult["refresh_token"].(string)
|
||||
|
||||
// Tamper with the token by modifying a character
|
||||
if len(refreshToken) > 10 {
|
||||
tamperedToken := refreshToken[:5] + "X" + refreshToken[6:]
|
||||
|
||||
refreshReq := map[string]interface{}{
|
||||
"refresh_token": tamperedToken,
|
||||
}
|
||||
refreshBody, _ := json.Marshal(refreshReq)
|
||||
|
||||
refreshHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
|
||||
refreshHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
refreshResp, err := client.Do(refreshHTTP)
|
||||
require.NoError(t, err)
|
||||
defer refreshResp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, refreshResp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestRefreshTokenEdgeCases tests edge cases for token refresh
|
||||
func TestRefreshTokenEdgeCases(t *testing.T) {
|
||||
t.Run("RefreshToken_EmptyStringToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": "",
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_NullToken", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
req := map[string]interface{}{
|
||||
"refresh_token": nil,
|
||||
}
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(body))
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_ResponseStructure", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Login to get tokens
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
loginResp, err := client.Do(loginHTTP)
|
||||
require.NoError(t, err)
|
||||
defer loginResp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, loginResp.StatusCode)
|
||||
|
||||
var loginResult map[string]interface{}
|
||||
json.NewDecoder(loginResp.Body).Decode(&loginResult)
|
||||
|
||||
refreshToken := loginResult["refresh_token"].(string)
|
||||
|
||||
// Refresh the token
|
||||
refreshReq := map[string]interface{}{
|
||||
"refresh_token": refreshToken,
|
||||
}
|
||||
refreshBody, _ := json.Marshal(refreshReq)
|
||||
|
||||
refreshHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
|
||||
refreshHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
refreshResp, err := client.Do(refreshHTTP)
|
||||
require.NoError(t, err)
|
||||
defer refreshResp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, refreshResp.StatusCode)
|
||||
|
||||
var refreshResult map[string]interface{}
|
||||
json.NewDecoder(refreshResp.Body).Decode(&refreshResult)
|
||||
|
||||
// Verify response structure
|
||||
assert.Contains(t, refreshResult, "access_token")
|
||||
assert.NotEmpty(t, refreshResult["access_token"])
|
||||
|
||||
// May or may not contain refresh_token (if rotation is enabled)
|
||||
// Both are valid responses
|
||||
})
|
||||
|
||||
t.Run("RefreshToken_TokenType", func(t *testing.T) {
|
||||
ts, _, _, _ := setupTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Login to get tokens
|
||||
loginReq := map[string]string{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
loginResp, err := client.Do(loginHTTP)
|
||||
require.NoError(t, err)
|
||||
defer loginResp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, loginResp.StatusCode)
|
||||
|
||||
var loginResult map[string]interface{}
|
||||
json.NewDecoder(loginResp.Body).Decode(&loginResult)
|
||||
|
||||
refreshToken := loginResult["refresh_token"].(string)
|
||||
|
||||
// Refresh the token
|
||||
refreshReq := map[string]interface{}{
|
||||
"refresh_token": refreshToken,
|
||||
}
|
||||
refreshBody, _ := json.Marshal(refreshReq)
|
||||
|
||||
refreshHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/refresh", bytes.NewBuffer(refreshBody))
|
||||
refreshHTTP.Header.Set("Content-Type", "application/json")
|
||||
|
||||
refreshResp, err := client.Do(refreshHTTP)
|
||||
require.NoError(t, err)
|
||||
defer refreshResp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, refreshResp.StatusCode)
|
||||
|
||||
var refreshResult map[string]interface{}
|
||||
json.NewDecoder(refreshResp.Body).Decode(&refreshResult)
|
||||
|
||||
// Verify access token is a string
|
||||
accessToken, ok := refreshResult["access_token"].(string)
|
||||
assert.True(t, ok, "access_token should be a string")
|
||||
assert.NotEmpty(t, accessToken, "access_token should not be empty")
|
||||
assert.Greater(t, len(accessToken), 20, "access_token should be a reasonably long JWT")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPasswordValidator_ValidatePassword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
wantValid bool
|
||||
}{
|
||||
{
|
||||
name: "valid password with all requirements",
|
||||
password: "Test@Pass123!",
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "valid password with special chars",
|
||||
password: "MyP@ssw0rd#2024",
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "too short",
|
||||
password: "Test1!",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "no uppercase",
|
||||
password: "test@pass123!",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "no lowercase",
|
||||
password: "TEST@PASS123!",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "no number",
|
||||
password: "Test@Password!",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "no special character",
|
||||
password: "TestPassword123",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
password: "",
|
||||
wantValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePassword(tt.password)
|
||||
if tt.wantValid {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordValidator_ErrorMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
expectedInError string
|
||||
}{
|
||||
{
|
||||
name: "too short error",
|
||||
password: "Short1!",
|
||||
expectedInError: "at least 8 characters",
|
||||
},
|
||||
{
|
||||
name: "no uppercase error",
|
||||
password: "alllower123!",
|
||||
expectedInError: "uppercase letter",
|
||||
},
|
||||
{
|
||||
name: "no lowercase error",
|
||||
password: "ALLUPPER123!",
|
||||
expectedInError: "lowercase letter",
|
||||
},
|
||||
{
|
||||
name: "no number error",
|
||||
password: "NoNumbers!",
|
||||
expectedInError: "number",
|
||||
},
|
||||
{
|
||||
name: "no special char error",
|
||||
password: "NoSpecialChars123",
|
||||
expectedInError: "special character",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePassword(tt.password)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedInError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPasswordRequirements(t *testing.T) {
|
||||
requirements := GetPasswordRequirements()
|
||||
|
||||
assert.NotEmpty(t, requirements)
|
||||
assert.Greater(t, len(requirements), 3)
|
||||
|
||||
// Check for common requirements
|
||||
requirementText := ""
|
||||
for _, req := range requirements {
|
||||
requirementText += req + " "
|
||||
}
|
||||
|
||||
assert.Contains(t, requirementText, "8")
|
||||
assert.Contains(t, requirementText, "uppercase")
|
||||
assert.Contains(t, requirementText, "lowercase")
|
||||
assert.Contains(t, requirementText, "number")
|
||||
assert.Contains(t, requirementText, "special")
|
||||
}
|
||||
|
||||
func TestLoginAttemptTracker_RecordFailedAttempt(t *testing.T) {
|
||||
tracker := NewLoginAttemptTracker(3, 5*time.Minute, 5*time.Minute)
|
||||
|
||||
username := "testuser"
|
||||
|
||||
// First failed attempt
|
||||
locked, remainingTime := tracker.RecordFailedAttempt(username)
|
||||
assert.False(t, locked)
|
||||
assert.Equal(t, time.Duration(0), remainingTime)
|
||||
|
||||
// Second failed attempt
|
||||
locked, remainingTime = tracker.RecordFailedAttempt(username)
|
||||
assert.False(t, locked)
|
||||
assert.Equal(t, time.Duration(0), remainingTime)
|
||||
|
||||
// Third failed attempt - should lock
|
||||
locked, remainingTime = tracker.RecordFailedAttempt(username)
|
||||
assert.True(t, locked)
|
||||
assert.Greater(t, remainingTime, time.Duration(0))
|
||||
|
||||
// Verify user is locked
|
||||
locked, _ = tracker.IsLocked(username)
|
||||
assert.True(t, locked)
|
||||
|
||||
// Clear attempts
|
||||
tracker.ClearAttempts(username)
|
||||
|
||||
// Should no longer be locked
|
||||
locked, _ = tracker.IsLocked(username)
|
||||
assert.False(t, locked)
|
||||
}
|
||||
|
||||
func TestLoginAttemptTracker_IsLocked(t *testing.T) {
|
||||
tracker := NewLoginAttemptTracker(3, 5*time.Minute, 5*time.Minute)
|
||||
|
||||
username := "lockeduser"
|
||||
|
||||
// Record failed attempts up to max
|
||||
for i := 0; i < 3; i++ {
|
||||
tracker.RecordFailedAttempt(username)
|
||||
}
|
||||
|
||||
// Verify user is locked
|
||||
locked, remainingTime := tracker.IsLocked(username)
|
||||
assert.True(t, locked)
|
||||
assert.Greater(t, remainingTime, time.Duration(0))
|
||||
|
||||
// Clear attempts
|
||||
tracker.ClearAttempts(username)
|
||||
|
||||
// Should no longer be locked
|
||||
locked, remainingTime = tracker.IsLocked(username)
|
||||
assert.False(t, locked)
|
||||
assert.Equal(t, time.Duration(0), remainingTime)
|
||||
}
|
||||
|
||||
func TestLoginAttemptTracker_ConcurrentAccess(t *testing.T) {
|
||||
tracker := NewLoginAttemptTracker(5, 5*time.Minute, 5*time.Minute)
|
||||
|
||||
done := make(chan bool, 10)
|
||||
|
||||
// Concurrent access from multiple goroutines
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(index int) {
|
||||
username := "user" + string(rune('0'+index))
|
||||
tracker.RecordFailedAttempt(username)
|
||||
tracker.IsLocked(username)
|
||||
tracker.ClearAttempts(username)
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Should complete without deadlock or race
|
||||
}
|
||||
|
||||
func TestDeviceRateLimiter_CheckRateLimit(t *testing.T) {
|
||||
limiter := NewDeviceRateLimiter()
|
||||
|
||||
config := DeviceRateLimitConfig{
|
||||
SyncRequestsPerMinute: 5,
|
||||
}
|
||||
|
||||
deviceID := "test-device-123"
|
||||
requestType := "sync"
|
||||
|
||||
// First 5 requests should succeed
|
||||
for i := 0; i < 5; i++ {
|
||||
allowed := limiter.CheckRateLimit(deviceID, requestType, config)
|
||||
assert.True(t, allowed, "Request %d should be allowed", i+1)
|
||||
}
|
||||
|
||||
// 6th request should be rate limited
|
||||
allowed := limiter.CheckRateLimit(deviceID, requestType, config)
|
||||
assert.False(t, allowed, "Request 6 should be rate limited")
|
||||
|
||||
// Get remaining requests
|
||||
remaining := limiter.GetRemainingRequests(deviceID, requestType, config)
|
||||
assert.Equal(t, 0, remaining)
|
||||
|
||||
// Reset and verify
|
||||
limiter.Reset(deviceID)
|
||||
|
||||
// Should be allowed again
|
||||
allowed = limiter.CheckRateLimit(deviceID, requestType, config)
|
||||
assert.True(t, allowed, "Request after reset should be allowed")
|
||||
}
|
||||
|
||||
func TestDeviceRateLimiter_DifferentDevices(t *testing.T) {
|
||||
limiter := NewDeviceRateLimiter()
|
||||
|
||||
config := DeviceRateLimitConfig{
|
||||
SyncRequestsPerMinute: 2,
|
||||
}
|
||||
|
||||
// Exhaust limit for device1
|
||||
for i := 0; i < 2; i++ {
|
||||
limiter.CheckRateLimit("device1", "sync", config)
|
||||
}
|
||||
|
||||
// Device1 should be rate limited
|
||||
allowed := limiter.CheckRateLimit("device1", "sync", config)
|
||||
assert.False(t, allowed)
|
||||
|
||||
// Device2 should still work
|
||||
allowed = limiter.CheckRateLimit("device2", "sync", config)
|
||||
assert.True(t, allowed)
|
||||
}
|
||||
|
||||
func TestDeviceRateLimiter_GetRemainingRequests(t *testing.T) {
|
||||
limiter := NewDeviceRateLimiter()
|
||||
|
||||
config := DeviceRateLimitConfig{
|
||||
SyncRequestsPerMinute: 10,
|
||||
}
|
||||
|
||||
deviceID := "test-device-456"
|
||||
|
||||
// Initially should have all requests remaining
|
||||
remaining := limiter.GetRemainingRequests(deviceID, "scan", config)
|
||||
assert.Equal(t, 10, remaining)
|
||||
|
||||
// Use 3 requests
|
||||
for i := 0; i < 3; i++ {
|
||||
limiter.CheckRateLimit(deviceID, "scan", config)
|
||||
}
|
||||
|
||||
// Should have 7 remaining
|
||||
remaining = limiter.GetRemainingRequests(deviceID, "scan", config)
|
||||
assert.Equal(t, 7, remaining)
|
||||
}
|
||||
|
||||
func TestNewRateLimiter(t *testing.T) {
|
||||
config := DefaultRateLimiterConfig()
|
||||
limiter := NewRateLimiter(config)
|
||||
|
||||
assert.NotNil(t, limiter)
|
||||
assert.NotNil(t, limiter.mu)
|
||||
}
|
||||
|
||||
func TestHTTPError_Error(t *testing.T) {
|
||||
err := NewHTTPError(404, "Not Found", nil)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 404, err.Code)
|
||||
assert.Equal(t, "Not Found", err.Message)
|
||||
}
|
||||
|
||||
func TestHTTPError_ErrorWithInternal(t *testing.T) {
|
||||
internalErr := assert.AnError
|
||||
err := NewHTTPError(500, "Internal Error", internalErr)
|
||||
|
||||
assert.Equal(t, "Internal Error", err.Error())
|
||||
assert.Equal(t, 500, err.Code)
|
||||
assert.Equal(t, "Internal Error", err.Message)
|
||||
assert.Equal(t, internalErr, err.Err)
|
||||
}
|
||||
|
||||
func TestNewHTTPError(t *testing.T) {
|
||||
err := NewHTTPError(404, "Not Found", nil)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 404, err.Code)
|
||||
assert.Equal(t, "Not Found", err.Message)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScheduler_NewScheduler(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
|
||||
// Use nil database interface for basic testing
|
||||
scheduler := NewScheduler(worker, nil)
|
||||
|
||||
assert.NotNil(t, scheduler)
|
||||
assert.NotNil(t, scheduler.worker)
|
||||
assert.NotNil(t, scheduler.timers)
|
||||
assert.NotNil(t, scheduler.scanSettings)
|
||||
assert.NotNil(t, scheduler.ctx)
|
||||
assert.NotNil(t, scheduler.cancel)
|
||||
}
|
||||
|
||||
func TestScheduler_StartStop(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
scheduler := NewScheduler(worker, nil)
|
||||
|
||||
// Start should not panic
|
||||
scheduler.Start()
|
||||
assert.NotNil(t, scheduler.ctx)
|
||||
|
||||
// Stop should not panic
|
||||
scheduler.Stop()
|
||||
}
|
||||
|
||||
func TestScheduler_UpdateScanSettings(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
scheduler := NewScheduler(worker, nil)
|
||||
|
||||
userID := "test-user-123"
|
||||
|
||||
// Update scan settings
|
||||
scheduler.UpdateScanSettings(userID, true, 30)
|
||||
|
||||
scheduler.mu.Lock()
|
||||
settings, exists := scheduler.scanSettings[userID]
|
||||
scheduler.mu.Unlock()
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, userID, settings.UserID)
|
||||
assert.True(t, settings.Enabled)
|
||||
assert.Equal(t, 30, settings.Frequency)
|
||||
}
|
||||
|
||||
func TestScheduler_UpdateScanSettings_Disabled(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
scheduler := NewScheduler(worker, nil)
|
||||
|
||||
userID := "test-user-456"
|
||||
|
||||
// Update scan settings to disabled
|
||||
scheduler.UpdateScanSettings(userID, false, 60)
|
||||
|
||||
scheduler.mu.Lock()
|
||||
settings, exists := scheduler.scanSettings[userID]
|
||||
scheduler.mu.Unlock()
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.False(t, settings.Enabled)
|
||||
assert.Equal(t, 60, settings.Frequency)
|
||||
}
|
||||
|
||||
func TestScheduler_UpdateScanSettings_Overwrite(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
scheduler := NewScheduler(worker, nil)
|
||||
|
||||
userID := "test-user-789"
|
||||
|
||||
// First update
|
||||
scheduler.UpdateScanSettings(userID, true, 30)
|
||||
|
||||
// Overwrite with different settings
|
||||
scheduler.UpdateScanSettings(userID, false, 45)
|
||||
|
||||
scheduler.mu.Lock()
|
||||
settings, exists := scheduler.scanSettings[userID]
|
||||
scheduler.mu.Unlock()
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.False(t, settings.Enabled)
|
||||
assert.Equal(t, 45, settings.Frequency)
|
||||
}
|
||||
|
||||
func TestScheduler_StopWithActiveTimers(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
scheduler := NewScheduler(worker, nil)
|
||||
|
||||
// Add some fake timers
|
||||
scheduler.mu.Lock()
|
||||
scheduler.timers["timer1"] = nil
|
||||
scheduler.timers["timer2"] = nil
|
||||
scheduler.timers["timer3"] = nil
|
||||
scheduler.mu.Unlock()
|
||||
|
||||
// Stop should clear timers
|
||||
scheduler.Stop()
|
||||
|
||||
scheduler.mu.Lock()
|
||||
timerCount := len(scheduler.timers)
|
||||
scheduler.mu.Unlock()
|
||||
|
||||
assert.Equal(t, 0, timerCount)
|
||||
}
|
||||
|
||||
func TestScheduler_ConcurrentAccess(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
scheduler := NewScheduler(worker, nil)
|
||||
scheduler.Start()
|
||||
defer scheduler.Stop()
|
||||
|
||||
// Concurrent updates should not cause race conditions
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(index int) {
|
||||
userID := uuid.New().String()
|
||||
scheduler.UpdateScanSettings(userID, true, 30)
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify all settings were stored
|
||||
scheduler.mu.Lock()
|
||||
settingCount := len(scheduler.scanSettings)
|
||||
scheduler.mu.Unlock()
|
||||
|
||||
assert.Equal(t, 10, settingCount)
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWorker_NewWorker(t *testing.T) {
|
||||
worker := NewWorker(2)
|
||||
|
||||
assert.NotNil(t, worker)
|
||||
assert.NotNil(t, worker.jobQueue)
|
||||
assert.NotNil(t, worker.results)
|
||||
assert.NotNil(t, worker.ctx)
|
||||
assert.NotNil(t, worker.cancel)
|
||||
}
|
||||
|
||||
func TestWorker_EnqueueJob_Success(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
job := &Job{
|
||||
ID: "test-job-1",
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
err := worker.EnqueueJob(job)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWorker_EnqueueJob_QueueFull(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
// Fill the queue (capacity is 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
job := &Job{
|
||||
ID: fmt.Sprintf("job-%d", i),
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
worker.jobQueue <- job
|
||||
}
|
||||
|
||||
// Try to enqueue one more job
|
||||
job := &Job{
|
||||
ID: "overflow-job",
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
err := worker.EnqueueJob(job)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "job queue is full")
|
||||
}
|
||||
|
||||
func TestWorker_EnqueueJob_WorkerShutdown(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
worker.Shutdown()
|
||||
|
||||
job := &Job{
|
||||
ID: "test-job",
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
err := worker.EnqueueJob(job)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "worker is shutting down")
|
||||
}
|
||||
|
||||
func TestWorker_GetJobStatus_NotFound(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
result, exists := worker.GetJobStatus("non-existent-job")
|
||||
assert.False(t, exists)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestWorker_GetJobStatus_Found(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
_ = &Job{
|
||||
ID: "test-job-2",
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
worker.mu.Lock()
|
||||
worker.results["test-job-2"] = &JobResult{
|
||||
JobID: "test-job-2",
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
worker.mu.Unlock()
|
||||
|
||||
result, exists := worker.GetJobStatus("test-job-2")
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "test-job-2", result.JobID)
|
||||
assert.Equal(t, JobStatusPending, result.Status)
|
||||
}
|
||||
|
||||
func TestWorker_CancelJob_Success(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
jobID := "test-job-3"
|
||||
|
||||
worker.mu.Lock()
|
||||
worker.results[jobID] = &JobResult{
|
||||
JobID: jobID,
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
worker.mu.Unlock()
|
||||
|
||||
err := worker.CancelJob(jobID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify status was updated
|
||||
result, exists := worker.GetJobStatus(jobID)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, JobStatusCancelled, result.Status)
|
||||
}
|
||||
|
||||
func TestWorker_CancelJob_AlreadyRunning(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
jobID := "test-job-4"
|
||||
|
||||
worker.mu.Lock()
|
||||
worker.results[jobID] = &JobResult{
|
||||
JobID: jobID,
|
||||
Status: JobStatusRunning,
|
||||
}
|
||||
worker.mu.Unlock()
|
||||
|
||||
err := worker.CancelJob(jobID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify status was updated
|
||||
result, exists := worker.GetJobStatus(jobID)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, JobStatusCancelled, result.Status)
|
||||
}
|
||||
|
||||
func TestWorker_CancelJob_AlreadyCompleted(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
jobID := "test-job-5"
|
||||
|
||||
worker.mu.Lock()
|
||||
worker.results[jobID] = &JobResult{
|
||||
JobID: jobID,
|
||||
Status: JobStatusCompleted,
|
||||
}
|
||||
worker.mu.Unlock()
|
||||
|
||||
err := worker.CancelJob(jobID)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "job cannot be cancelled")
|
||||
}
|
||||
|
||||
func TestWorker_CancelJob_NotFound(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
err := worker.CancelJob("non-existent-job")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "job not found")
|
||||
}
|
||||
|
||||
func TestWorker_ProcessJob_ScanJob_MissingParams(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
params map[string]interface{}
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "missing library_id",
|
||||
params: map[string]interface{}{},
|
||||
wantErr: "library_id required",
|
||||
},
|
||||
{
|
||||
name: "missing folders",
|
||||
params: map[string]interface{}{
|
||||
"library_id": "test-lib",
|
||||
},
|
||||
wantErr: "folders required",
|
||||
},
|
||||
{
|
||||
name: "missing admin_id",
|
||||
params: map[string]interface{}{
|
||||
"library_id": "test-lib",
|
||||
"folders": []string{"/test"},
|
||||
},
|
||||
wantErr: "admin_id required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testJob := &Job{
|
||||
ID: "test-job",
|
||||
Type: JobTypeScan,
|
||||
Params: tt.params,
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
result, err := worker.processScanJob(testJob)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorker_ProcessJob_UnknownJobType(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
job := &Job{
|
||||
ID: "test-job",
|
||||
Type: JobType("unknown"),
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
result, err := worker.processScanJob(job)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unknown job type")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestWorker_JobLifecycle(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
job := &Job{
|
||||
ID: "lifecycle-test",
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{
|
||||
"library_id": "test-lib",
|
||||
"folders": []string{"/test"},
|
||||
"admin_id": "test-admin",
|
||||
"db": nil, // Will fail but tests the flow
|
||||
},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
// Enqueue the job
|
||||
err := worker.EnqueueJob(job)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Give worker time to process
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Check job status
|
||||
result, exists := worker.GetJobStatus("lifecycle-test")
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Status should be failed (because we passed nil db)
|
||||
assert.Equal(t, JobStatusFailed, result.Status)
|
||||
}
|
||||
|
||||
func TestWorker_Shutdown(t *testing.T) {
|
||||
worker := NewWorker(2)
|
||||
|
||||
// Enqueue some jobs
|
||||
for i := 0; i < 5; i++ {
|
||||
testJob := &Job{
|
||||
ID: fmt.Sprintf("shutdown-job-%d", i),
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
worker.EnqueueJob(testJob)
|
||||
}
|
||||
|
||||
// Shutdown should not block
|
||||
shutdownDone := make(chan bool)
|
||||
go func() {
|
||||
worker.Shutdown()
|
||||
shutdownDone <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-shutdownDone:
|
||||
// Shutdown completed
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Shutdown took too long")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorker_ConcurrentJobProcessing(t *testing.T) {
|
||||
worker := NewWorker(3) // 3 workers
|
||||
defer worker.Shutdown()
|
||||
|
||||
jobCount := 10
|
||||
|
||||
// Enqueue multiple jobs
|
||||
for i := 0; i < jobCount; i++ {
|
||||
job := &Job{
|
||||
ID: fmt.Sprintf("concurrent-job-%d", i),
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{
|
||||
"library_id": fmt.Sprintf("lib-%d", i),
|
||||
"folders": []string{"/test"},
|
||||
"admin_id": "admin",
|
||||
"db": nil,
|
||||
},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
go func(j *Job) {
|
||||
err := worker.EnqueueJob(j)
|
||||
assert.NoError(t, err)
|
||||
}(job)
|
||||
}
|
||||
|
||||
// Wait a bit for processing
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Check that all jobs were processed
|
||||
worker.mu.RLock()
|
||||
resultCount := len(worker.results)
|
||||
worker.mu.RUnlock()
|
||||
|
||||
assert.Equal(t, jobCount, resultCount)
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormalizeISBN_ValidISBNs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ISBN-10 with hyphens",
|
||||
input: "0-306-40615-2",
|
||||
expected: "0306406152",
|
||||
},
|
||||
{
|
||||
name: "ISBN-13 with hyphens",
|
||||
input: "978-0-306-40615-7",
|
||||
expected: "9780306406157",
|
||||
},
|
||||
{
|
||||
name: "ISBN-10 with spaces",
|
||||
input: "0 306 40615 2",
|
||||
expected: "0306406152",
|
||||
},
|
||||
{
|
||||
name: "ISBN-13 with spaces",
|
||||
input: "978 0 306 40615 7",
|
||||
expected: "9780306406157",
|
||||
},
|
||||
{
|
||||
name: "ISBN-10 with hyphens and spaces",
|
||||
input: "0-306-40615 2",
|
||||
expected: "0306406152",
|
||||
},
|
||||
{
|
||||
name: "ISBN-13 with hyphens and spaces",
|
||||
input: "978-0-306 40615-7",
|
||||
expected: "9780306406157",
|
||||
},
|
||||
{
|
||||
name: "ISBN-10 clean",
|
||||
input: "0306406152",
|
||||
expected: "0306406152",
|
||||
},
|
||||
{
|
||||
name: "ISBN-13 clean",
|
||||
input: "9780306406157",
|
||||
expected: "9780306406157",
|
||||
},
|
||||
{
|
||||
name: "ISBN-10 with X",
|
||||
input: "0-8044-2957-X",
|
||||
expected: "080442957X",
|
||||
},
|
||||
{
|
||||
name: "ISBN-10 with X and hyphens",
|
||||
input: "0-8044-2957-X",
|
||||
expected: "080442957X",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeISBN_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "only hyphens",
|
||||
input: "---",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "only spaces",
|
||||
input: " ",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "mixed hyphens and spaces",
|
||||
input: "- - -",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "multiple consecutive hyphens",
|
||||
input: "0--306--40615--2",
|
||||
expected: "0306406152",
|
||||
},
|
||||
{
|
||||
name: "multiple consecutive spaces",
|
||||
input: "0 306 40615 2",
|
||||
expected: "0306406152",
|
||||
},
|
||||
{
|
||||
name: "tabs and newlines (treated as spaces)",
|
||||
input: "0\t306\n40615\r2",
|
||||
expected: "0306406152",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeISBN_SpecialCharacters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "with dots (not removed, only hyphens/spaces)",
|
||||
input: "978.0.306.40615.7",
|
||||
expected: "978.0.306.40615.7",
|
||||
},
|
||||
{
|
||||
name: "mixed dots and hyphens",
|
||||
input: "978-0.306-40615.7",
|
||||
expected: "978.0.306-40615.7",
|
||||
},
|
||||
{
|
||||
name: "with underscores (preserved)",
|
||||
input: "978_0_306_40615_7",
|
||||
expected: "978_0_306_40615_7",
|
||||
},
|
||||
{
|
||||
name: "with slashes (preserved)",
|
||||
input: "978/0/306/40615/7",
|
||||
expected: "978/0/306/40615/7",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeISBN_RealWorldExamples(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "real book ISBN-10",
|
||||
input: "0-596-00965-X",
|
||||
expected: "059600965X",
|
||||
},
|
||||
{
|
||||
name: "real book ISBN-13",
|
||||
input: "978-0-596-00965-2",
|
||||
expected: "9780596009652",
|
||||
},
|
||||
{
|
||||
name: "another real ISBN-10",
|
||||
input: "1-4028-9462-7",
|
||||
expected: "1402894627",
|
||||
},
|
||||
{
|
||||
name: "another real ISBN-13",
|
||||
input: "978-1-4028-9462-6",
|
||||
expected: "9781402894626",
|
||||
},
|
||||
{
|
||||
name: "popular programming book",
|
||||
input: "978-0-13-595705-9",
|
||||
expected: "9780135957059",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeISBN_DoesNotModifyValidISBNs(t *testing.T) {
|
||||
validISBNs := []string{
|
||||
"0306406152",
|
||||
"9780306406157",
|
||||
"080442957X",
|
||||
"1234567890123",
|
||||
}
|
||||
|
||||
for _, isbn := range validISBNs {
|
||||
t.Run(isbn, func(t *testing.T) {
|
||||
result := NormalizeISBN(isbn)
|
||||
assert.Equal(t, isbn, result, "Valid ISBN should not be modified")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeISBN_PreservesX(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "X at end",
|
||||
input: "0-8044-2957-X",
|
||||
expected: "080442957X",
|
||||
},
|
||||
{
|
||||
name: "lowercase x",
|
||||
input: "0-8044-2957-x",
|
||||
expected: "080442957x",
|
||||
},
|
||||
{
|
||||
name: "X in middle (invalid but preserved)",
|
||||
input: "0-8044-X-2957",
|
||||
expected: "08044X2957",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user