diff --git a/cmd/server/tests/auth_test.go b/cmd/server/tests/auth_test.go index 39ebeab..617b487 100644 --- a/cmd/server/tests/auth_test.go +++ b/cmd/server/tests/auth_test.go @@ -1,14 +1,15 @@ package main import ( - "encoding/json" "net/http" "net/http/httptest" "strings" "testing" + + "github.com/stretchr/testify/assert" ) -func TestAuthMiddleware(t *testing.T) { +func TestAuthMiddlewareAlt(t *testing.T) { // Test missing JWT header t.Run("Missing JWT", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/visible", nil) diff --git a/cmd/server/tests/library_test.go b/cmd/server/tests/library_test.go index 2fa1ee6..eced1ca 100644 --- a/cmd/server/tests/library_test.go +++ b/cmd/server/tests/library_test.go @@ -7,6 +7,9 @@ import ( "net/http/httptest" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestAuthMiddleware verifies JWT middleware works correctly @@ -15,7 +18,7 @@ func TestAuthMiddleware(t *testing.T) { t.Run("Missing JWT", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/visible", nil) rr := httptest.NewRecorder() - + // Simulate auth middleware handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") @@ -24,7 +27,7 @@ func TestAuthMiddleware(t *testing.T) { w.Write([]byte(`{"message":"missing or malformed jwt"}`)) return } - + w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"valid token"}`)) }) @@ -40,7 +43,7 @@ func TestAuthMiddleware(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/visible", nil) req.Header.Set("Authorization", "invalid-token") rr := httptest.NewRecorder() - + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { @@ -48,7 +51,7 @@ func TestAuthMiddleware(t *testing.T) { w.Write([]byte(`{"message":"missing or malformed jwt"}`)) return } - + w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"valid token"}`)) }) @@ -64,7 +67,7 @@ func TestAuthMiddleware(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/visible", 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 == "" || !strings.HasPrefix(authHeader, "Bearer ") { @@ -72,7 +75,7 @@ func TestAuthMiddleware(t *testing.T) { w.Write([]byte(`{"message":"missing or malformed jwt"}`)) return } - + w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"valid token"}`)) }) @@ -104,7 +107,7 @@ func TestLibraryCreationUnauthorized(t *testing.T) { req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() - + // Simulate missing user context (like non-authenticated) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate missing user context (like non-authenticated) @@ -138,7 +141,7 @@ func TestLibraryCreationWithValidAdmin(t *testing.T) { req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() - + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate admin user context if r.Header.Get("X-User-Role") != "admin" { @@ -163,32 +166,32 @@ func TestLibraryTypesResponse(t *testing.T) { require.NoError(t, err) rr := httptest.NewRecorder() - + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - + response := []map[string]interface{}{ { - "id": "test-id-1", - "name": "ebooks", - "description": "Ebook files including EPUB, PDF, MOBI, etc.", + "id": "test-id-1", + "name": "ebooks", + "description": "Ebook files including EPUB, PDF, MOBI, etc.", "allowed_extensions": []string{".epub", ".pdf", ".mobi", ".azw", ".azw3", ".txt", ".rtf", ".doc", ".docx", ".lit", ".fb2", ".pdb"}, }, { - "id": "test-id-2", - "name": "comics", - "description": "Comic book archives and image formats", + "id": "test-id-2", + "name": "comics", + "description": "Comic book archives and image formats", "allowed_extensions": []string{".cbz", ".cbr", ".cb7", ".cbt", ".pdf"}, }, { - "id": "test-id-3", - "name": "manga", - "description": "Manga files including archives and image folders", + "id": "test-id-3", + "name": "manga", + "description": "Manga files including archives and image folders", "allowed_extensions": []string{".cbz", ".cbr", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"}, }, } - + jsonData, _ := json.Marshal(response) w.Write(jsonData) }) @@ -208,26 +211,26 @@ func TestUserVisibleLibraries(t *testing.T) { req.Header.Set("Authorization", "Bearer fake-token") rr := httptest.NewRecorder() - + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - + response := []map[string]interface{}{ { - "id": "lib-1", - "name": "User Library 1", - "type_name": "ebooks", + "id": "lib-1", + "name": "User Library 1", + "type_name": "ebooks", "is_visible": true, }, { - "id": "lib-2", - "name": "Hidden Admin Library", - "type_name": "comics", + "id": "lib-2", + "name": "Hidden Admin Library", + "type_name": "comics", "is_visible": false, }, } - + jsonData, _ := json.Marshal(response) w.Write(jsonData) }) @@ -237,9 +240,7 @@ func TestUserVisibleLibraries(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Contains(t, rr.Body.String(), "User Library 1") assert.Contains(t, rr.Body.String(), "ebooks") - assert.Contains(t, // Should not contain hidden library - !strings.Contains(rr.Body.String(), "Hidden Admin Library") - ) + assert.False(t, strings.Contains(rr.Body.String(), "Hidden Admin Library")) // Should not contain hidden library } // TestMediaItemsList verifies media items endpoint with library filtering @@ -249,20 +250,20 @@ func TestMediaItemsList(t *testing.T) { req.Header.Set("Authorization", "Bearer fake-token") rr := httptest.NewRecorder() - + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - + response := []map[string]interface{}{ { - "id": "media-1", - "title": "Test Book 1", - "library_name": "User Library 1", + "id": "media-1", + "title": "Test Book 1", + "library_name": "User Library 1", "library_type_name": "ebooks", }, } - + jsonData, _ := json.Marshal(response) w.Write(jsonData) }) @@ -315,7 +316,7 @@ func TestJSONValidation(t *testing.T) { req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() - + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate successful creation for valid cases if tc.expectedStatus == http.StatusCreated { @@ -341,22 +342,22 @@ func TestJSONValidation(t *testing.T) { func TestErrorHandling(t *testing.T) { testCases := []struct { name string - endpoint string + endpoint string expectedStatus int }{ { name: "Missing library ID", - endpoint: "/api/libraries/nonexistent-id", + endpoint: "/api/libraries/nonexistent-id", expectedStatus: http.StatusBadRequest, }, { name: "Invalid UUID", - endpoint: "/api/libraries/invalid-uuid", + endpoint: "/api/libraries/invalid-uuid", expectedStatus: http.StatusBadRequest, }, { name: "Nonexistent user library", - endpoint: "/api/libraries/visible?user_id=nonexistent-user", + endpoint: "/api/libraries/visible?user_id=nonexistent-user", expectedStatus: http.StatusUnauthorized, }, } @@ -367,7 +368,7 @@ func TestErrorHandling(t *testing.T) { require.NoError(t, err) rr := httptest.NewRecorder() - + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(tc.expectedStatus) @@ -379,4 +380,4 @@ func TestErrorHandling(t *testing.T) { assert.Equal(t, tc.expectedStatus, rr.Code) }) } -} \ No newline at end of file +} diff --git a/cmd/server/tests/notes_highlights_test.go b/cmd/server/tests/notes_highlights_test.go new file mode 100644 index 0000000..8cd4c2b --- /dev/null +++ b/cmd/server/tests/notes_highlights_test.go @@ -0,0 +1,339 @@ +package main + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" +) + +func TestMediaNotesEndpoints(t *testing.T) { + t.Log("🔧 Testing Media Notes Endpoints") + + // Test GET /api/media-items/:id/notes (without auth - should fail) + t.Run("GET notes without auth", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/media-items/"+uuid.New().String()+"/notes", nil) + rr := httptest.NewRecorder() + + // Simulate missing auth middleware + 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) + + if rr.Code != http.StatusUnauthorized { + t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, rr.Code) + } + }) + + // Test POST /api/media-items/:id/notes request validation + t.Run("POST notes validation", func(t *testing.T) { + mediaItemID := uuid.New() + invalidPayload := `{"content": ""}` // Empty content should fail + + req := httptest.NewRequest("POST", "/api/media-items/"+mediaItemID.String()+"/notes", bytes.NewBufferString(invalidPayload)) + 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 struct { + Content string `json:"content"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"invalid request"}`)) + return + } + if req.Content == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"content is required"}`)) + return + } + w.WriteHeader(http.StatusCreated) + }) + + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rr.Code) + } + }) + + // Test valid note creation request payload + t.Run("Valid note creation payload", func(t *testing.T) { + mediaItemID := uuid.New() + validPayload := map[string]interface{}{ + "content": "This is a test note.", + "position": "page:45", + } + + payloadBytes, _ := json.Marshal(validPayload) + req := httptest.NewRequest("POST", "/api/media-items/"+mediaItemID.String()+"/notes", bytes.NewBuffer(payloadBytes)) + 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 struct { + Content string `json:"content"` + Position string `json:"position"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"invalid request"}`)) + return + } + if req.Content == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"content is required"}`)) + return + } + // Simulate successful creation + response := map[string]interface{}{ + "id": uuid.New().String(), + "media_item_id": mediaItemID.String(), + "content": req.Content, + "position": req.Position, + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(response) + }) + + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusCreated { + t.Errorf("Expected status %d, got %d", http.StatusCreated, rr.Code) + } + + var response map[string]interface{} + if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { + t.Errorf("Failed to decode response: %v", err) + } + + if response["content"] != "This is a test note." { + t.Errorf("Expected content 'This is a test note.', got %v", response["content"]) + } + }) +} + +func TestMediaHighlightsEndpoints(t *testing.T) { + t.Log("🔧 Testing Media Highlights Endpoints") + + // Test GET /api/media-items/:id/highlights (without auth - should fail) + t.Run("GET highlights without auth", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/media-items/"+uuid.New().String()+"/highlights", 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) + + if rr.Code != http.StatusUnauthorized { + t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, rr.Code) + } + }) + + // Test POST /api/media-items/:id/highlights request validation + t.Run("POST highlights validation", func(t *testing.T) { + mediaItemID := uuid.New() + invalidPayload := `{"selection_text": ""}` // Empty selection should fail + + req := httptest.NewRequest("POST", "/api/media-items/"+mediaItemID.String()+"/highlights", bytes.NewBufferString(invalidPayload)) + 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 struct { + SelectionText string `json:"selection_text"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"invalid request"}`)) + return + } + if req.SelectionText == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"selection_text is required"}`)) + return + } + w.WriteHeader(http.StatusCreated) + }) + + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rr.Code) + } + }) + + // Test valid highlight creation request payload + t.Run("Valid highlight creation payload", func(t *testing.T) { + mediaItemID := uuid.New() + validPayload := map[string]interface{}{ + "selection_text": "This is highlighted text.", + "start_position": "page:45:offset:120", + "end_position": "page:45:offset:145", + "color": "#ffff00", + } + + payloadBytes, _ := json.Marshal(validPayload) + req := httptest.NewRequest("POST", "/api/media-items/"+mediaItemID.String()+"/highlights", bytes.NewBuffer(payloadBytes)) + 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 struct { + SelectionText string `json:"selection_text"` + StartPosition string `json:"start_position"` + EndPosition string `json:"end_position"` + Color string `json:"color"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"invalid request"}`)) + return + } + if req.SelectionText == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"selection_text is required"}`)) + return + } + // Simulate successful creation + response := map[string]interface{}{ + "id": uuid.New().String(), + "media_item_id": mediaItemID.String(), + "selection_text": req.SelectionText, + "start_position": req.StartPosition, + "end_position": req.EndPosition, + "color": req.Color, + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(response) + }) + + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusCreated { + t.Errorf("Expected status %d, got %d", http.StatusCreated, rr.Code) + } + + var response map[string]interface{} + if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { + t.Errorf("Failed to decode response: %v", err) + } + + if response["selection_text"] != "This is highlighted text." { + t.Errorf("Expected selection_text 'This is highlighted text.', got %v", response["selection_text"]) + } + }) + + // Test color validation + t.Run("Highlight color validation", func(t *testing.T) { + mediaItemID := uuid.New() + invalidColorPayload := map[string]interface{}{ + "selection_text": "This is highlighted text.", + "start_position": "page:45:offset:120", + "end_position": "page:45:offset:145", + "color": "invalid-color", // Should be hex format + } + + payloadBytes, _ := json.Marshal(invalidColorPayload) + req := httptest.NewRequest("POST", "/api/media-items/"+mediaItemID.String()+"/highlights", bytes.NewBuffer(payloadBytes)) + 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 struct { + Color string `json:"color"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"invalid request"}`)) + return + } + // Simple validation for hex color + if req.Color != "" && len(req.Color) != 7 { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"color must be in hex format"}`)) + return + } + w.WriteHeader(http.StatusCreated) + }) + + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rr.Code) + } + }) +} + +func TestEbookNotesAndHighlightsBackwardCompatibility(t *testing.T) { + t.Log("🔧 Testing Ebook Notes and Highlights Backward Compatibility") + + // Test GET /api/ebooks/:id/notes (backward compatibility) + t.Run("GET ebook notes without auth", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/ebooks/"+uuid.New().String()+"/notes", 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) + + if rr.Code != http.StatusUnauthorized { + t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, rr.Code) + } + }) + + // Test GET /api/ebooks/:id/highlights (backward compatibility) + t.Run("GET ebook highlights without auth", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/ebooks/"+uuid.New().String()+"/highlights", 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) + + if rr.Code != http.StatusUnauthorized { + t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, rr.Code) + } + }) +}