test: add comprehensive test coverage for API endpoints and services

This commit is contained in:
2026-02-01 13:21:34 -05:00
parent 9b1282cd08
commit 351c68b0b8
11 changed files with 4275 additions and 0 deletions
+470
View File
@@ -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)
})
}
+691
View File
@@ -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")
})
}
+439
View File
@@ -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)
})
}
+457
View File
@@ -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))
})
}
+352
View File
@@ -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)
})
}
+396
View File
@@ -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)
})
}
+413
View File
@@ -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")
})
}