diff --git a/cmd/server/tests/ebook_test.go b/cmd/server/tests/ebook_test.go deleted file mode 100644 index 6d81c1d..0000000 --- a/cmd/server/tests/ebook_test.go +++ /dev/null @@ -1,479 +0,0 @@ -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) - } - }) - } - }) -} diff --git a/cmd/server/tests/integration_test.go b/cmd/server/tests/integration_test.go deleted file mode 100644 index 09b2e1a..0000000 --- a/cmd/server/tests/integration_test.go +++ /dev/null @@ -1,985 +0,0 @@ -package main - -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - baseURL = "http://localhost:8765" -) - -// Test requirements: -// 1. Server must be running with TEST_MODE=true and RATE_LIMIT_ENABLED=false -// or with significantly increased REQUESTS_PER_MINUTE -// 2. Database must be clean or test should handle existing data -// 3. Run with: TEST_MODE=true RATE_LIMIT_ENABLED=false go test -v ./cmd/server/tests -run TestIntegrationAPI - -type TestContext struct { - AdminToken string - UserToken string - AdminID string - UserID string - LibraryID string - MediaItemID string - NoteID string - HighlightID string -} - -type AuthResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - Token string `json:"token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - User struct { - ID string `json:"id"` - Email string `json:"email"` - Username string `json:"username"` - Role string `json:"role"` - } `json:"user"` -} - -type PaginatedResponse struct { - Data interface{} `json:"data"` - Total int `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -func makeRequest(t *testing.T, method, endpoint string, body interface{}, token string) *http.Response { - var reqBody io.Reader - if body != nil { - jsonBody, err := json.Marshal(body) - require.NoError(t, err) - reqBody = bytes.NewBuffer(jsonBody) - } - - req, err := http.NewRequest(method, baseURL+endpoint, reqBody) - require.NoError(t, err) - - req.Header.Set("Content-Type", "application/json") - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - require.NoError(t, err) - - return resp -} - -func extractToken(authResp AuthResponse) string { - if authResp.AccessToken != "" { - return authResp.AccessToken - } - if authResp.Token != "" { - return authResp.Token - } - return "" -} - -// cleanupTestData removes test users and libraries created during testing -// This helps maintain test isolation between runs -func cleanupTestData(adminToken string, createdUsers, createdLibraries []string) { - // Delete test libraries - for _, libID := range createdLibraries { - req, _ := http.NewRequest("DELETE", baseURL+"/api/libraries/"+libID, nil) - req.Header.Set("Authorization", "Bearer "+adminToken) - req.Header.Set("Content-Type", "application/json") - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if resp != nil { - resp.Body.Close() - } - if err != nil { - // Log but don't fail - cleanup is best-effort - fmt.Printf("Warning: failed to delete library %s: %v\n", libID, err) - } - } - - // Delete test users - for _, userID := range createdUsers { - req, _ := http.NewRequest("DELETE", baseURL+"/api/auth/account?user_id="+userID, nil) - req.Header.Set("Authorization", "Bearer "+adminToken) - req.Header.Set("Content-Type", "application/json") - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if resp != nil { - resp.Body.Close() - } - if err != nil { - // Log but don't fail - cleanup is best-effort - fmt.Printf("Warning: failed to delete user %s: %v\n", userID, err) - } - } -} - -func setupTestSuite(t *testing.T) *TestContext { - ctx := &TestContext{} - - t.Run("Setup_GetAdminCredentials", func(t *testing.T) { - // Try to login as existing admin first - loginReq := map[string]interface{}{ - "login": "test@example.com", - "password": "Password123!", - } - - resp := makeRequest(t, "POST", "/api/auth/login", loginReq, "") - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK { - var authResp AuthResponse - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &authResp) - require.NoError(t, err) - - ctx.AdminToken = extractToken(authResp) - ctx.AdminID = authResp.User.ID - t.Logf("Logged in as existing admin: %s (%s)", authResp.User.Email, authResp.User.Role) - return - } - - // If no admin exists, create one - adminReq := map[string]interface{}{ - "email": "integrationadmin@test.com", - "username": "integrationadmin", - "password": "AdminPass123!", - } - - resp = makeRequest(t, "POST", "/api/auth/register", adminReq, "") - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { - var authResp AuthResponse - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &authResp) - require.NoError(t, err) - - ctx.AdminToken = extractToken(authResp) - ctx.AdminID = authResp.User.ID - t.Logf("Created new admin: %s (%s)", authResp.User.Email, authResp.User.Role) - } else { - body, _ := io.ReadAll(resp.Body) - t.Fatalf("Failed to create admin: %s", string(body)) - } - - require.NotEmpty(t, ctx.AdminToken, "Admin token is empty") - }) - - t.Run("Setup_GetUserCredentials", func(t *testing.T) { - // Try to login as existing regular user - loginReq := map[string]interface{}{ - "login": "admin@test.com", - "password": "Admin123!", - } - - resp := makeRequest(t, "POST", "/api/auth/login", loginReq, "") - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK { - var authResp AuthResponse - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &authResp) - require.NoError(t, err) - - ctx.UserToken = extractToken(authResp) - ctx.UserID = authResp.User.ID - t.Logf("Logged in as existing user: %s (%s)", authResp.User.Email, authResp.User.Role) - return - } - - // Create a regular user - userReq := map[string]interface{}{ - "email": "integrationuser@test.com", - "username": "integrationuser", - "password": "UserPass123!", - } - - resp = makeRequest(t, "POST", "/api/auth/register", userReq, "") - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { - var authResp AuthResponse - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &authResp) - require.NoError(t, err) - - ctx.UserToken = extractToken(authResp) - ctx.UserID = authResp.User.ID - t.Logf("Created new user: %s (%s)", authResp.User.Email, authResp.User.Role) - } else if resp.StatusCode == http.StatusConflict { - // User already exists, log in instead - t.Logf("User already exists, attempting login...") - loginReq := map[string]interface{}{ - "login": "integrationuser@test.com", - "password": "UserPass123!", - } - loginResp := makeRequest(t, "POST", "/api/auth/login", loginReq, "") - defer loginResp.Body.Close() - - require.Equal(t, http.StatusOK, loginResp.StatusCode, "Login should succeed for existing user") - - var authResp AuthResponse - body, _ := io.ReadAll(loginResp.Body) - err := json.Unmarshal(body, &authResp) - require.NoError(t, err) - - ctx.UserToken = extractToken(authResp) - ctx.UserID = authResp.User.ID - t.Logf("Logged in as existing user: %s (%s)", authResp.User.Email, authResp.User.Role) - } - - require.NotEmpty(t, ctx.UserToken, "User token is empty") - }) - - t.Run("Setup_GetLibrary", func(t *testing.T) { - // First try to list existing libraries - resp := makeRequest(t, "GET", "/api/libraries", nil, ctx.AdminToken) - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK { - body, _ := io.ReadAll(resp.Body) - - // Try to unmarshal as array first - var librariesArray []map[string]interface{} - errArray := json.Unmarshal(body, &librariesArray) - - if errArray == nil && len(librariesArray) > 0 { - ctx.LibraryID = librariesArray[0]["id"].(string) - t.Logf("Using existing library: %v", librariesArray[0]["name"]) - return - } - - // Try to unmarshal as object with data field - var result map[string]interface{} - errObj := json.Unmarshal(body, &result) - if errObj == nil { - if libraries, ok := result["data"].([]interface{}); ok && len(libraries) > 0 { - if firstLib, ok := libraries[0].(map[string]interface{}); ok { - ctx.LibraryID = firstLib["id"].(string) - t.Logf("Using existing library: %v", firstLib["name"]) - return - } - } - } - } - - // Create a library - libReq := map[string]interface{}{ - "name": "Integration Test Library", - "description": "Library for integration tests", - "type": "ebooks", - } - - resp = makeRequest(t, "POST", "/api/libraries", libReq, ctx.AdminToken) - defer resp.Body.Close() - - require.Equal(t, http.StatusCreated, resp.StatusCode, "Failed to create library") - - var lib map[string]interface{} - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &lib) - require.NoError(t, err) - - ctx.LibraryID = lib["id"].(string) - require.NotEmpty(t, ctx.LibraryID, "Library ID is empty") - }) - - t.Run("Setup_WaitForRateLimit", func(t *testing.T) { - // Wait a bit to avoid rate limiting - time.Sleep(2 * time.Second) - }) - - t.Run("Setup_CreateDuplicateTestUsers", func(t *testing.T) { - // Create users that will be used for duplicate tests - users := []map[string]string{ - {"email": "test@example.com", "username": "testuser", "password": "Password123!"}, - {"email": "newemail@example.com", "username": "newuser123", "password": "Password123!"}, - } - - for _, user := range users { - req := map[string]interface{}{ - "email": user["email"], - "username": user["username"], - "password": user["password"], - } - - resp := makeRequest(t, "POST", "/api/auth/register", req, "") - defer resp.Body.Close() - - // If user already exists (409), that's fine - it was created in a previous test run - // If rate limited (429), skip creating this user - we'll test with existing data - if resp.StatusCode == http.StatusConflict { - t.Logf("User %s already exists from previous test run", user["email"]) - } else if resp.StatusCode == http.StatusTooManyRequests { - t.Logf("Rate limited while creating %s, will use existing data if available", user["email"]) - } else if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { - body, _ := io.ReadAll(resp.Body) - t.Logf("Warning: failed to create test user %s: %s", user["email"], string(body)) - } else { - t.Logf("Created test user: %s", user["email"]) - } - } - - // Small delay to avoid rate limiting in subsequent tests - time.Sleep(500 * time.Millisecond) - }) - - return ctx -} - -func TestIntegrationAPI(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration tests in short mode") - } - - ctx := setupTestSuite(t) - - t.Run("Authentication", func(t *testing.T) { - testAuthentication(t, ctx) - }) - - t.Run("UserProfile", func(t *testing.T) { - testUserProfile(t, ctx) - }) - - t.Run("Libraries", func(t *testing.T) { - testLibraries(t, ctx) - }) - - t.Run("MediaItems", func(t *testing.T) { - testMediaItems(t, ctx) - }) - - t.Run("Admin", func(t *testing.T) { - testAdmin(t, ctx) - }) - } - } - - func testAuthentication(t *testing.T, ctx *TestContext) { - t.Run("Register_DuplicateEmail", func(t *testing.T) { - req := map[string]interface{}{ - "email": "test@example.com", - "username": "newuser123", - "password": "Password123!", - } - - resp := makeRequest(t, "POST", "/api/auth/register", req, "") - defer resp.Body.Close() - - assert.Equal(t, http.StatusConflict, resp.StatusCode) - }) - - t.Run("Register_DuplicateUsername", func(t *testing.T) { - req := map[string]interface{}{ - "email": "newemail@example.com", - "username": "testuser", - "password": "Password123!", - } - - resp := makeRequest(t, "POST", "/api/auth/register", req, "") - - defer resp.Body.Close() - - assert.Equal(t, http.StatusConflict, resp.StatusCode) - }) - } -} - -func testAuthentication(t *testing.T, ctx *TestContext) { - t.Run("Register_DuplicateEmail", func(t *testing.T) { - req := map[string]interface{}{ - "email": "test@example.com", - "username": "newuser123", - "password": "Password123!", - } - - resp := makeRequest(t, "POST", "/api/auth/register", req, "") - defer resp.Body.Close() - - assert.Equal(t, http.StatusConflict, resp.StatusCode) - }) - - t.Run("Register_DuplicateUsername", func(t *testing.T) { - req := map[string]interface{}{ - "email": "newemail@example.com", - "username": "testuser", - "password": "Password123!", - } - - resp := makeRequest(t, "POST", "/api/auth/register", req, "") - defer resp.Body.Close() - - assert.Equal(t, http.StatusConflict, resp.StatusCode) - }) - - t.Run("Register_WeakPassword", func(t *testing.T) { - req := map[string]interface{}{ - "email": "weak@example.com", - "username": "weakuser", - "password": "weak", - } - - resp := makeRequest(t, "POST", "/api/auth/register", req, "") - defer resp.Body.Close() - - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - }) - - t.Run("Login_InvalidCredentials", func(t *testing.T) { - req := map[string]interface{}{ - "login": "test@example.com", - "password": "wrongpassword", - } - - resp := makeRequest(t, "POST", "/api/auth/login", req, "") - defer resp.Body.Close() - - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - }) - - t.Run("ProtectedEndpoint_NoAuth", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/auth/profile", nil, "") - defer resp.Body.Close() - - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - }) - - t.Run("ProtectedEndpoint_ValidAuth", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/auth/profile", nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) -} - -func testUserProfile(t *testing.T, ctx *TestContext) { - t.Run("GetProfile", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/auth/profile", nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - var profile map[string]interface{} - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &profile) - require.NoError(t, err) - - assert.Equal(t, ctx.UserID, profile["id"]) - }) - - t.Run("UpdateProfile", func(t *testing.T) { - req := map[string]interface{}{ - "first_name": "Integration", - "last_name": "Test User", - } - - resp := makeRequest(t, "PUT", "/api/auth/profile", req, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("UpdateEmail", func(t *testing.T) { - req := map[string]interface{}{ - "email": "integrationnew@example.com", - } - - resp := makeRequest(t, "PUT", "/api/auth/email", req, ctx.UserToken) - defer resp.Body.Close() - - // Should succeed - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict) - - // Change back - req = map[string]interface{}{ - "email": "integrationuser@test.com", - } - resp = makeRequest(t, "PUT", "/api/auth/email", req, ctx.UserToken) - defer resp.Body.Close() - }) - - t.Run("UpdateUsername", func(t *testing.T) { - req := map[string]interface{}{ - "username": "integrationuser2", - } - - resp := makeRequest(t, "PUT", "/api/auth/username", req, ctx.UserToken) - defer resp.Body.Close() - - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict) - }) - - t.Run("UpdatePassword", func(t *testing.T) { - // Create a temporary user specifically for password testing to avoid flakiness - tempReq := map[string]interface{}{ - "email": "passwordtest@example.com", - "username": "passwordtest", - "password": "OldPass123!", - } - - resp := makeRequest(t, "POST", "/api/auth/register", tempReq, "") - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { - t.Skipf("Cannot test password update: failed to create test user (status %d)", resp.StatusCode) - return - } - - var authResp AuthResponse - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &authResp) - require.NoError(t, err) - tempToken := extractToken(authResp) - - // Update password - updateReq := map[string]interface{}{ - "current_password": "OldPass123!", - "new_password": "NewPass123!", - "confirm_password": "NewPass123!", - } - - resp = makeRequest(t, "PUT", "/api/auth/password", updateReq, tempToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - // Verify new password works by logging in - loginReq := map[string]interface{}{ - "login": "passwordtest@example.com", - "password": "NewPass123!", - } - - resp = makeRequest(t, "POST", "/api/auth/login", loginReq, "") - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - // Cleanup: delete the test user - resp = makeRequest(t, "DELETE", "/api/auth/account", nil, tempToken) - defer resp.Body.Close() - }) - - t.Run("UpdateTheme", func(t *testing.T) { - req := map[string]interface{}{ - "theme": "tokyo-night", - } - - resp := makeRequest(t, "PUT", "/api/auth/theme", req, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("DeleteOwnAccount", func(t *testing.T) { - // Create a temporary user - tempReq := map[string]interface{}{ - "email": "tempdelete@example.com", - "username": "tempdelete", - "password": "TempPass123!", - } - - resp := makeRequest(t, "POST", "/api/auth/register", tempReq, "") - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { - var authResp AuthResponse - body, _ := io.ReadAll(resp.Body) - json.Unmarshal(body, &authResp) - - tempToken := extractToken(authResp) - - resp = makeRequest(t, "DELETE", "/api/auth/account", nil, tempToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - } - }) -} - -func testLibraries(t *testing.T, ctx *TestContext) { - t.Run("GetLibraryTypes", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/libraries/types", nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("CreateLibrary_UserForbidden", func(t *testing.T) { - req := map[string]interface{}{ - "name": "User Library", - "description": "Should fail", - "type": "ebooks", - } - - resp := makeRequest(t, "POST", "/api/libraries", req, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - }) - - t.Run("ListLibraries_Admin", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/libraries", nil, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("ListLibraries_UserForbidden", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/libraries", nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - }) - - t.Run("GetLibrary_Admin", func(t *testing.T) { - resp := makeRequest(t, "GET", fmt.Sprintf("/api/libraries/%s", ctx.LibraryID), nil, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("UpdateLibrary_Admin", func(t *testing.T) { - req := map[string]interface{}{ - "name": "Updated Library Name", - "description": "Updated during integration test", - "type": "ebooks", - } - - resp := makeRequest(t, "PUT", fmt.Sprintf("/api/libraries/%s", ctx.LibraryID), req, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("SetLibraryVisibility", func(t *testing.T) { - req := map[string]interface{}{ - "library_id": ctx.LibraryID, - "is_visible": true, - } - - resp := makeRequest(t, "POST", "/api/libraries/visibility", req, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("GetUserVisibleLibraries", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/libraries/visible", nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("GetLibraryStats", func(t *testing.T) { - resp := makeRequest(t, "GET", fmt.Sprintf("/api/libraries/%s/stats", ctx.LibraryID), nil, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("AddLibraryFolder", func(t *testing.T) { - req := map[string]interface{}{ - "folder_path": "/tmp/test_integration_folder", - } - - resp := makeRequest(t, "POST", fmt.Sprintf("/api/libraries/%s/folders", ctx.LibraryID), req, ctx.AdminToken) - defer resp.Body.Close() - - // May fail if path doesn't exist, but endpoint should be accessible - assert.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusConflict) - }) - - t.Run("GetLibraryFolders", func(t *testing.T) { - resp := makeRequest(t, "GET", fmt.Sprintf("/api/libraries/%s/folders", ctx.LibraryID), nil, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) -} - -func testMediaItems(t *testing.T, ctx *TestContext) { - t.Run("ListMediaItems", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/media-items", nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - body, _ := io.ReadAll(resp.Body) - - // Try to unmarshal as array first - var itemsArray []map[string]interface{} - errArray := json.Unmarshal(body, &itemsArray) - - if errArray == nil && len(itemsArray) > 0 { - ctx.MediaItemID = itemsArray[0]["id"].(string) - return - } - - // Try to unmarshal as object with data field - var result map[string]interface{} - errObj := json.Unmarshal(body, &result) - if errObj == nil { - if items, ok := result["data"].([]interface{}); ok && len(items) > 0 { - if firstItem, ok := items[0].(map[string]interface{}); ok { - ctx.MediaItemID = firstItem["id"].(string) - } - } - } - }) - - // Admin operations - t.Run("CreateMediaItem_Admin", func(t *testing.T) { - if ctx.LibraryID == "" { - t.Skip("No library available for creating media item") - } - - req := map[string]interface{}{ - "library_id": ctx.LibraryID, - "title": "Integration Test Media Item", - "author": "Test Author", - "isbn": "9999999999", - "description": "Created during integration test", - "file_path": "/tmp/test_integration.epub", - "file_size": int64(1024), - "mime_type": "application/epub+zip", - "publisher": "Test Publisher", - "date_published": "2024-01-01", - } - - resp := makeRequest(t, "POST", "/api/media-items", req, ctx.AdminToken) - defer resp.Body.Close() - - if resp.StatusCode == http.StatusCreated { - var mediaItem map[string]interface{} - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &mediaItem) - require.NoError(t, err) - - ctx.MediaItemID = mediaItem["id"].(string) - } - }) - - t.Run("CreateMediaItem_UserForbidden", func(t *testing.T) { - if ctx.LibraryID == "" { - t.Skip("No library available") - } - - req := map[string]interface{}{ - "library_id": ctx.LibraryID, - "title": "User Media Item", - "file_path": "/tmp/user.epub", - "file_size": int64(1024), - "mime_type": "application/epub+zip", - } - - resp := makeRequest(t, "POST", "/api/media-items", req, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - }) - - if ctx.MediaItemID != "" { - t.Run("UpdateMediaItem_Admin", func(t *testing.T) { - req := map[string]interface{}{ - "title": "Updated Media Item Title", - "description": "Updated during test", - } - - resp := makeRequest(t, "PUT", fmt.Sprintf("/api/media-items/%s", ctx.MediaItemID), req, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("DeleteMediaItem_Admin", func(t *testing.T) { - resp := makeRequest(t, "DELETE", fmt.Sprintf("/api/media-items/%s", ctx.MediaItemID), nil, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusNoContent, resp.StatusCode) - }) - - t.Run("GetMediaItem", func(t *testing.T) { - resp := makeRequest(t, "GET", fmt.Sprintf("/api/media-items/%s", ctx.MediaItemID), nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("CreateMediaNote", func(t *testing.T) { - req := map[string]interface{}{ - "content": "Integration test note", - "position": "page:42", - } - - resp := makeRequest(t, "POST", fmt.Sprintf("/api/media-items/%s/notes", ctx.MediaItemID), req, ctx.UserToken) - defer resp.Body.Close() - - if resp.StatusCode == http.StatusCreated { - var note map[string]interface{} - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, ¬e) - require.NoError(t, err) - - ctx.NoteID = note["id"].(string) - } - }) - - t.Run("GetMediaNotes", func(t *testing.T) { - resp := makeRequest(t, "GET", fmt.Sprintf("/api/media-items/%s/notes", ctx.MediaItemID), nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - if ctx.NoteID != "" { - t.Run("UpdateMediaNote", func(t *testing.T) { - req := map[string]interface{}{ - "content": "Updated integration test note", - } - - resp := makeRequest(t, "PUT", fmt.Sprintf("/api/media-items/%s/notes/%s", ctx.MediaItemID, ctx.NoteID), req, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("DeleteMediaNote", func(t *testing.T) { - resp := makeRequest(t, "DELETE", fmt.Sprintf("/api/media-items/%s/notes/%s", ctx.MediaItemID, ctx.NoteID), nil, ctx.UserToken) - defer resp.Body.Close() - - // 204 No Content is the standard success response for DELETE - assert.Equal(t, http.StatusNoContent, resp.StatusCode) - }) - } - - t.Run("CreateMediaHighlight", func(t *testing.T) { - req := map[string]interface{}{ - "content": "Integration test highlight", - "position": "page:42", - "color": "yellow", - } - - resp := makeRequest(t, "POST", fmt.Sprintf("/api/media-items/%s/highlights", ctx.MediaItemID), req, ctx.UserToken) - defer resp.Body.Close() - - if resp.StatusCode == http.StatusCreated { - var highlight map[string]interface{} - body, _ := io.ReadAll(resp.Body) - err := json.Unmarshal(body, &highlight) - require.NoError(t, err) - - ctx.HighlightID = highlight["id"].(string) - } - }) - - t.Run("GetMediaHighlights", func(t *testing.T) { - resp := makeRequest(t, "GET", fmt.Sprintf("/api/media-items/%s/highlights", ctx.MediaItemID), nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - if ctx.HighlightID != "" { - t.Run("UpdateMediaHighlight", func(t *testing.T) { - req := map[string]interface{}{ - "content": "Updated integration test highlight", - "color": "blue", - } - - resp := makeRequest(t, "PUT", fmt.Sprintf("/api/media-items/%s/highlights/%s", ctx.MediaItemID, ctx.HighlightID), req, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("DeleteMediaHighlight", func(t *testing.T) { - resp := makeRequest(t, "DELETE", fmt.Sprintf("/api/media-items/%s/highlights/%s", ctx.MediaItemID, ctx.HighlightID), nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - } - } -} - -func testAdmin(t *testing.T, ctx *TestContext) { - t.Run("ListUsers_Admin", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/auth/users", nil, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - body, _ := io.ReadAll(resp.Body) - - // Try to unmarshal as array first - var usersArray []map[string]interface{} - errArray := json.Unmarshal(body, &usersArray) - - if errArray == nil { - assert.GreaterOrEqual(t, len(usersArray), 1) - return - } - - // Try to unmarshal as object with data field - var result map[string]interface{} - errObj := json.Unmarshal(body, &result) - if errObj == nil { - if users, ok := result["data"].([]interface{}); ok { - assert.GreaterOrEqual(t, len(users), 1) - } - } - }) - - t.Run("ListUsers_UserForbidden", func(t *testing.T) { - resp := makeRequest(t, "GET", "/api/auth/users", nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - }) - - t.Run("DeleteUserAccount_Admin", func(t *testing.T) { - // Create a user to delete - createReq := map[string]interface{}{ - "email": "deleteme@example.com", - "username": "deleteme", - "password": "DeleteMe123!", - } - - resp := makeRequest(t, "POST", "/api/auth/register", createReq, "") - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { - var authResp AuthResponse - body, _ := io.ReadAll(resp.Body) - json.Unmarshal(body, &authResp) - - deleteURL := fmt.Sprintf("/api/auth/account?user_id=%s", authResp.User.ID) - resp = makeRequest(t, "DELETE", deleteURL, nil, ctx.AdminToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - } - }) - - t.Run("DeleteUserAccount_UserForbidden", func(t *testing.T) { - deleteURL := fmt.Sprintf("/api/auth/account?user_id=%s", ctx.AdminID) - resp := makeRequest(t, "DELETE", deleteURL, nil, ctx.UserToken) - defer resp.Body.Close() - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - }) -} diff --git a/cmd/server/tests/notes_highlights_test.go b/cmd/server/tests/notes_highlights_test.go deleted file mode 100644 index 8cd4c2b..0000000 --- a/cmd/server/tests/notes_highlights_test.go +++ /dev/null @@ -1,339 +0,0 @@ -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) - } - }) -} diff --git a/cmd/server/tests/search_test.go b/cmd/server/tests/search_test.go index 210cbe3..895da43 100644 --- a/cmd/server/tests/search_test.go +++ b/cmd/server/tests/search_test.go @@ -169,7 +169,7 @@ func TestSearchMediaItemsTests(t *testing.T) { }) t.Run("Search with fuzzy match fallback", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/media-items/search?q=hary poter", nil) + req := httptest.NewRequest("GET", "/api/media-items/search?q=hary+poter", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() diff --git a/templates/bookshelf_templ.go b/templates/bookshelf_templ.go index 6cc9610..1e0120e 100644 --- a/templates/bookshelf_templ.go +++ b/templates/bookshelf_templ.go @@ -29,7 +29,15 @@ func BookShelf(user User) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Loading your library...
Select a library to view your collection
Loading your library...
Select a library to view your collection