package main import ( "bookhoard/internal/handlers" "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) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) client := &http.Client{} t.Run("GetReadingStats_WithoutAuth", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/reading-stats", nil) 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) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/reading-stats", 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 handlers.ReadingStatsResponse json.NewDecoder(resp.Body).Decode(&result) assert.GreaterOrEqual(t, result.TotalBooksRead, 0) assert.GreaterOrEqual(t, result.TotalPagesRead, 0) assert.GreaterOrEqual(t, result.TotalReadingTime, 0) }) t.Run("GetReadingStats_WithCustomDateRange", func(t *testing.T) { startDate := time.Now().AddDate(0, -2, 0).Format("2006-01-02") endDate := time.Now().Format("2006-01-02") req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/reading-stats?start_date="+startDate+"&end_date="+endDate, 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) }) t.Run("GetReadingStats_InvalidStartDate", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/reading-stats?start_date=invalid-date", nil) req.Header.Set("Authorization", "Bearer "+token) 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) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/reading-stats?end_date=not-a-date", nil) req.Header.Set("Authorization", "Bearer "+token) 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) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/reading-stats", 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) // 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) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) client := &http.Client{} t.Run("GetDeviceUsage_WithoutAuth", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/device-usage", nil) 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) { req, _ := http.NewRequest("GET", setup.Server.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 handlers.DeviceUsageResponse json.NewDecoder(resp.Body).Decode(&result) assert.NotNil(t, result.Devices) assert.Equal(t, 0, len(result.Devices)) }) t.Run("GetDeviceUsage_WithAuth_WithDevices", func(t *testing.T) { // First create a device deviceReq := map[string]interface{}{ "device_name": "Test Kobo", "device_type": "kobo", } deviceBody, _ := json.Marshal(deviceReq) deviceReqHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/devices/register", bytes.NewBuffer(deviceBody)) deviceReqHTTP.Header.Set("Content-Type", "application/json") deviceReqHTTP.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(deviceReqHTTP) require.NoError(t, err) resp.Body.Close() // Now get device usage req, _ := http.NewRequest("GET", setup.Server.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) { req, _ := http.NewRequest("GET", setup.Server.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) 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) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) client := &http.Client{} t.Run("GetPopularBooks_WithoutAuth", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/popular-books", nil) 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) { req, _ := http.NewRequest("GET", setup.Server.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 handlers.PopularBooksResponse json.NewDecoder(resp.Body).Decode(&result) assert.NotNil(t, result.Books) // Default limit is 10, but may be fewer if no reading history assert.True(t, len(result.Books) <= 10) }) t.Run("GetPopularBooks_WithCustomLimit", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/popular-books?limit=5", 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{}) assert.True(t, len(books) <= 5) }) t.Run("GetPopularBooks_InvalidLimit", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/popular-books?limit=invalid", nil) req.Header.Set("Authorization", "Bearer "+token) 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) { // First create a book and some reading history bookID := createTestMediaItemID(t, setup.Server, 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", setup.Server.URL+"/api/media-items/"+bookID+"/progress", bytes.NewBuffer(historyBody)) historyHTTP.Header.Set("Content-Type", "application/json") historyHTTP.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(historyHTTP) require.NoError(t, err) resp.Body.Close() // Now get popular books req, _ := http.NewRequest("GET", setup.Server.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) { req, _ := http.NewRequest("GET", setup.Server.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{}) // 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) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) client := &http.Client{} t.Run("ReadingStats_FutureDateRange", func(t *testing.T) { 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", setup.Server.URL+"/api/analytics/reading-stats?start_date="+startDate+"&end_date="+endDate, nil) req.Header.Set("Authorization", "Bearer "+token) 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) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/popular-books?limit=0", nil) req.Header.Set("Authorization", "Bearer "+token) 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) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/popular-books?limit=999999", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() // Should handle large limit assert.Equal(t, http.StatusOK, resp.StatusCode) }) }