package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" ) // TestEbookEndpoints tests all ebook-related endpoints func TestEbookEndpoints(t *testing.T) { testEbookID := uuid.New() t.Run("GET /api/ebooks - List ebooks without authentication", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/ebooks?limit=10&offset=0", nil) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" || !containsPrefix(authHeader, "Bearer ") { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message":"missing or malformed jwt"}`)) return } ebooks := []map[string]interface{}{ { "id": testEbookID.String(), "title": "Test Ebook", "author": "Test Author", "mimeType": "application/epub+zip", }, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ebooks) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) t.Run("GET /api/ebooks - List ebooks with valid authentication", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/ebooks?limit=10&offset=0", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" || !containsPrefix(authHeader, "Bearer ") { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message":"missing or malformed jwt"}`)) return } ebooks := []map[string]interface{}{ { "id": testEbookID.String(), "title": "Test Ebook", "author": "Test Author", "mimeType": "application/epub+zip", }, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ebooks) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) assert.Contains(t, rr.Body.String(), "Test Ebook") }) t.Run("GET /api/ebooks/:id - Get specific ebook with invalid UUID", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/ebooks/invalid-uuid", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid id"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("GET /api/ebooks/:id - Get non-existent ebook", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/ebooks/"+uuid.New().String(), nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) w.Write([]byte(`{"error":"ebook not found"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusNotFound, rr.Code) }) t.Run("POST /api/ebooks - Create ebook without admin role", func(t *testing.T) { payload := map[string]interface{}{ "title": "New Ebook", "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer user-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userRole := r.Header.Get("X-User-Role") if userRole != "admin" { w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":"admin access required"}`)) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{"id": uuid.New().String()}) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) }) t.Run("POST /api/ebooks - Create ebook with invalid payload", func(t *testing.T) { payload := map[string]interface{}{ "title": "", // Invalid: empty title } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } if title, ok := req["title"].(string); !ok || title == "" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"title is required"}`)) return } w.WriteHeader(http.StatusCreated) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("PUT /api/ebooks/:id - Update ebook without admin role", func(t *testing.T) { payload := map[string]interface{}{ "title": "Updated Title", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/ebooks/"+testEbookID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer user-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userRole := r.Header.Get("X-User-Role") if userRole != "admin" { w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":"admin access required"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) }) t.Run("DELETE /api/ebooks/:id - Delete ebook without admin role", func(t *testing.T) { req := httptest.NewRequest("DELETE", "/api/ebooks/"+testEbookID.String(), nil) req.Header.Set("Authorization", "Bearer user-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userRole := r.Header.Get("X-User-Role") if userRole != "admin" { w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":"admin access required"}`)) return } w.WriteHeader(http.StatusNoContent) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) }) } // TestMediaItemsEndpoints tests media items endpoints func TestMediaItemsEndpoints(t *testing.T) { testMediaID := uuid.New() t.Run("GET /api/media-items - List without library_id filter", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/media-items?limit=10&offset=0", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { items := []map[string]interface{}{ { "id": testMediaID.String(), "title": "Test Media Item", "author": "Test Author", "mimeType": "application/pdf", }, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(items) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) t.Run("GET /api/media-items - List with library_id filter", func(t *testing.T) { libraryID := uuid.New() req := httptest.NewRequest("GET", "/api/media-items?library_id="+libraryID.String()+"&limit=10&offset=0", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { libID := r.URL.Query().Get("library_id") if libID != libraryID.String() { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid library id"}`)) return } items := []map[string]interface{}{ { "id": testMediaID.String(), "title": "Test Media Item", "library_id": libraryID.String(), }, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(items) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) t.Run("GET /api/media-items - List with invalid library_id", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/media-items?library_id=invalid-uuid", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid library id"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("GET /api/media-items/:id - Get specific media item", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/media-items/"+testMediaID.String(), nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { item := map[string]interface{}{ "id": testMediaID.String(), "title": "Test Media Item", "author": "Test Author", "mimeType": "application/pdf", } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(item) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) t.Run("GET /api/media-items/:id - Get non-existent media item", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/media-items/"+uuid.New().String(), nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) w.Write([]byte(`{"error":"media item not found"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusNotFound, rr.Code) }) } // TestEbookProgress tests reading progress endpoints func TestEbookProgress(t *testing.T) { ebookID := uuid.New() t.Run("GET /api/ebooks/:id/progress - Get progress without auth", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/ebooks/"+ebookID.String()+"/progress", nil) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message":"missing or malformed jwt"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) t.Run("PUT /api/ebooks/:id/progress - Update progress with invalid page number", func(t *testing.T) { payload := map[string]interface{}{ "current_page": -1, // Invalid: negative page } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/ebooks/"+ebookID.String()+"/progress", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } if currentPage, ok := req["current_page"].(float64); ok && currentPage < 0 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"current_page must be >= 0"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("PUT /api/ebooks/:id/progress - Update progress with invalid total pages", func(t *testing.T) { payload := map[string]interface{}{ "current_page": 10, "total_pages": 0, // Invalid: must be >= 1 } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/ebooks/"+ebookID.String()+"/progress", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } if totalPages, ok := req["total_pages"].(float64); ok && totalPages < 1 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"total_pages must be >= 1"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("DELETE /api/ebooks/:id/progress - Delete progress", func(t *testing.T) { req := httptest.NewRequest("DELETE", "/api/ebooks/"+ebookID.String()+"/progress", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"reading progress deleted"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) } // TestEbookRatings tests rating endpoints func TestEbookRatings(t *testing.T) { ebookID := uuid.New() t.Run("POST /api/ebooks/:id/rating - Create rating with invalid score", func(t *testing.T) { testCases := []struct { name string rating int expected bool }{ {"Rating too low (0)", 0, false}, {"Rating too high (11)", 11, false}, {"Valid rating (5)", 5, true}, {"Valid rating (10)", 10, true}, {"Valid rating (1)", 1, true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { payload := map[string]interface{}{ "rating": tc.rating, } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks/"+ebookID.String()+"/rating", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } if rating, ok := req["rating"].(float64); ok { if rating < 1 || rating > 10 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"rating must be between 1 and 10"}`)) return } } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) if !tc.expected { assert.Equal(t, http.StatusBadRequest, rr.Code) } else { assert.Equal(t, http.StatusOK, rr.Code) } }) } }) }