package main import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestProcessingIssuesListAuthentication tests authentication and authorization for list endpoint func TestProcessingIssuesListAuthentication(t *testing.T) { setup := setupTestServer(t) // Create a test library deviceSetup := setupDeviceTest(t) libraryID := deviceSetup.CreateLibrary(t, "Test Auth Library", "manga") t.Run("GET /api/libraries/:id/issues/list - No authentication", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("GET /api/libraries/:id/issues/list - Non-admin user forbidden", func(t *testing.T) { token := setup.RegularToken req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code) }) t.Run("GET /api/libraries/:id/issues/list - Admin user authorized", func(t *testing.T) { token := setup.Token req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) }) } // TestProcessingIssuesListResponseStructure tests the response structure of list endpoint func TestProcessingIssuesListResponseStructure(t *testing.T) { setup := setupTestServer(t) // Create a test library deviceSetup := setupDeviceTest(t) libraryID := deviceSetup.CreateLibrary(t, "Test Structure Library", "comics") t.Run("Returns JSON array for empty library", func(t *testing.T) { token := setup.Token req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var response []map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.IsType(t, []map[string]interface{}{}, response) assert.Equal(t, 0, len(response), "Empty library should return empty array") }) t.Run("Has correct content type", func(t *testing.T) { token := setup.Token req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) }) } // TestProcessingIssuesListInputValidation tests input validation for list endpoint func TestProcessingIssuesListInputValidation(t *testing.T) { setup := setupTestServer(t) token := setup.Token testCases := []struct { name string libraryID string expectedStatus int description string }{ { name: "Invalid UUID format", libraryID: "not-a-uuid", expectedStatus: http.StatusBadRequest, description: "Should reject malformed UUID", }, { name: "Empty UUID", libraryID: "", expectedStatus: http.StatusBadRequest, description: "Should return 400 for empty ID", }, { name: "UUID with extra path traversal", libraryID: "00000000-0000-0000-0000-000000000000/../../etc", expectedStatus: http.StatusBadRequest, description: "Should reject path traversal attempts", }, { name: "Nonexistent library UUID", libraryID: "00000000-0000-0000-0000-000000000000", expectedStatus: http.StatusOK, description: "Should return empty array for valid but nonexistent UUID", }, { name: "Special characters in UUID", libraryID: "'; DROP TABLE processing_issues; --", expectedStatus: http.StatusBadRequest, description: "Should reject SQL injection attempts", }, { name: "Very long UUID", libraryID: "00000000-0000-0000-0000-000000000000-extra-suffix", expectedStatus: http.StatusBadRequest, description: "Should reject overly long IDs", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+url.PathEscape(tc.libraryID)+"/issues/list", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, tc.expectedStatus, rec.Code, tc.description) // For successful requests, verify response structure if tc.expectedStatus == http.StatusOK { var response []map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.IsType(t, []map[string]interface{}{}, response) } }) } } // TestProcessingIssueStatsAuthentication tests authentication and authorization for stats endpoint func TestProcessingIssueStatsAuthentication(t *testing.T) { setup := setupTestServer(t) // Create a test library deviceSetup := setupDeviceTest(t) libraryID := deviceSetup.CreateLibrary(t, "Test Stats Auth Library", "manga") t.Run("GET /api/libraries/:id/issues/stats - No authentication", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("GET /api/libraries/:id/issues/stats - Non-admin user forbidden", func(t *testing.T) { token := setup.RegularToken req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code) }) t.Run("GET /api/libraries/:id/issues/stats - Admin user authorized", func(t *testing.T) { token := setup.Token req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) }) } // TestProcessingIssueStatsResponseStructure tests the response structure of stats endpoint func TestProcessingIssueStatsResponseStructure(t *testing.T) { setup := setupTestServer(t) // Create a test library deviceSetup := setupDeviceTest(t) libraryID := deviceSetup.CreateLibrary(t, "Test Stats Structure Library", "comics") t.Run("Returns correct structure for empty library", func(t *testing.T) { token := setup.Token req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var response map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) // Verify required fields assert.Contains(t, response, "error_count", "Should have error_count field") assert.Contains(t, response, "warning_count", "Should have warning_count field") assert.Contains(t, response, "info_count", "Should have info_count field") // Verify all counts are zero for empty library assert.Equal(t, float64(0), response["error_count"], "error_count should be 0") assert.Equal(t, float64(0), response["warning_count"], "warning_count should be 0") assert.Equal(t, float64(0), response["info_count"], "info_count should be 0") // Verify field types assert.IsType(t, float64(0), response["error_count"], "error_count should be number") assert.IsType(t, float64(0), response["warning_count"], "warning_count should be number") assert.IsType(t, float64(0), response["info_count"], "info_count should be number") }) t.Run("Has correct content type", func(t *testing.T) { token := setup.Token req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) }) t.Run("Has exactly 3 fields", func(t *testing.T) { token := setup.Token req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var response map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.Len(t, response, 3, "Should have exactly 3 fields") }) } // TestProcessingIssueStatsInputValidation tests input validation for stats endpoint func TestProcessingIssueStatsInputValidation(t *testing.T) { setup := setupTestServer(t) token := setup.Token testCases := []struct { name string libraryID string expectedStatus int description string }{ { name: "Invalid UUID format", libraryID: "not-a-uuid", expectedStatus: http.StatusBadRequest, description: "Should reject malformed UUID", }, { name: "Empty UUID", libraryID: "", expectedStatus: http.StatusBadRequest, description: "Should return 400 for empty ID", }, { name: "UUID with extra path traversal", libraryID: "00000000-0000-0000-0000-000000000000/../../etc", expectedStatus: http.StatusBadRequest, description: "Should reject path traversal attempts", }, { name: "Nonexistent library UUID", libraryID: "00000000-0000-0000-0000-000000000000", expectedStatus: http.StatusOK, description: "Should return zero counts for valid but nonexistent UUID", }, { name: "Special characters in UUID", libraryID: "'; DROP TABLE processing_issues; --", expectedStatus: http.StatusBadRequest, description: "Should reject SQL injection attempts", }, { name: "Very long UUID", libraryID: "00000000-0000-0000-0000-000000000000-extra-suffix", expectedStatus: http.StatusBadRequest, description: "Should reject overly long IDs", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+url.PathEscape(tc.libraryID)+"/issues/stats", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, tc.expectedStatus, rec.Code, tc.description) // For successful requests, verify response structure if tc.expectedStatus == http.StatusOK { var response map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) // Verify all counts are zero for nonexistent library assert.Equal(t, float64(0), response["error_count"]) assert.Equal(t, float64(0), response["warning_count"]) assert.Equal(t, float64(0), response["info_count"]) } }) } } // TestProcessingIssuesCrossLibraryIsolation tests that different libraries don't interfere func TestProcessingIssuesCrossLibraryIsolation(t *testing.T) { setup := setupTestServer(t) // Create two different libraries with different types deviceSetup1 := setupDeviceTest(t) library1ID := deviceSetup1.CreateLibrary(t, "Isolation Test Library 1", "manga") deviceSetup2 := setupDeviceTest(t) library2ID := deviceSetup2.CreateLibrary(t, "Isolation Test Library 2", "comics") token := setup.Token // Both libraries should return empty arrays (no issues created) t.Run("Library 1 returns its own empty list", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+library1ID+"/issues/list", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var response []map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.Equal(t, 0, len(response), "Library 1 should have 0 issues") }) t.Run("Library 2 returns its own empty list", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+library2ID+"/issues/list", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var response []map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.Equal(t, 0, len(response), "Library 2 should have 0 issues") }) t.Run("Both libraries have independent stats", func(t *testing.T) { // Get stats for library 1 req1 := httptest.NewRequest("GET", "/api/libraries/"+library1ID+"/issues/stats", nil) req1.Header.Set("Authorization", "Bearer "+token) rec1 := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec1, req1) var stats1 map[string]interface{} err := json.NewDecoder(rec1.Body).Decode(&stats1) require.NoError(t, err) // Get stats for library 2 req2 := httptest.NewRequest("GET", "/api/libraries/"+library2ID+"/issues/stats", nil) req2.Header.Set("Authorization", "Bearer "+token) rec2 := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec2, req2) var stats2 map[string]interface{} err = json.NewDecoder(rec2.Body).Decode(&stats2) require.NoError(t, err) // Both should have zero counts assert.Equal(t, float64(0), stats1["error_count"]) assert.Equal(t, float64(0), stats1["warning_count"]) assert.Equal(t, float64(0), stats1["info_count"]) assert.Equal(t, float64(0), stats2["error_count"]) assert.Equal(t, float64(0), stats2["warning_count"]) assert.Equal(t, float64(0), stats2["info_count"]) }) } // TestProcessingIssuesDifferentLibraryTypes tests endpoints with different library types func TestProcessingIssuesDifferentLibraryTypes(t *testing.T) { setup := setupTestServer(t) token := setup.Token libraryTypes := []struct { name string libraryType string }{ {"Ebooks library", "ebooks"}, {"Comics library", "comics"}, {"Manga library", "manga"}, } for _, lt := range libraryTypes { t.Run(lt.name+" - list endpoint works", func(t *testing.T) { deviceSetup := setupDeviceTest(t) libraryID := deviceSetup.CreateLibrary(t, "Test "+lt.name, lt.libraryType) req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var response []map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.IsType(t, []map[string]interface{}{}, response) }) t.Run(lt.name+" - stats endpoint works", func(t *testing.T) { deviceSetup := setupDeviceTest(t) libraryID := deviceSetup.CreateLibrary(t, "Test "+lt.name, lt.libraryType) req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var response map[string]interface{} err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.Contains(t, response, "error_count") assert.Contains(t, response, "warning_count") assert.Contains(t, response, "info_count") }) } } // TestProcessingIssuesInvalidTokens tests with invalid authentication tokens func TestProcessingIssuesInvalidTokens(t *testing.T) { setup := setupTestServer(t) // Create a test library deviceSetup := setupDeviceTest(t) libraryID := deviceSetup.CreateLibrary(t, "Test Token Library", "ebooks") t.Run("List endpoint rejects invalid token", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) req.Header.Set("Authorization", "Bearer invalid-token-12345") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("Stats endpoint rejects invalid token", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) req.Header.Set("Authorization", "Bearer invalid-token-12345") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("List endpoint rejects missing bearer prefix", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) req.Header.Set("Authorization", setup.Token) // Missing "Bearer " prefix rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("Stats endpoint rejects missing bearer prefix", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil) req.Header.Set("Authorization", setup.Token) // Missing "Bearer " prefix rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("List endpoint rejects empty authorization header", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil) req.Header.Set("Authorization", "") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) }