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 EbookID 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("Ebooks", func(t *testing.T) { testEbooks(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) }) 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 testEbooks(t *testing.T, ctx *TestContext) { t.Run("ListEbooks", func(t *testing.T) { resp := makeRequest(t, "GET", "/api/ebooks", nil, ctx.UserToken) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("CreateEbook_Admin", func(t *testing.T) { req := map[string]interface{}{ "title": "Integration Test Ebook", "author": "Test Author", "isbn": "9999999999", "description": "Created during integration test", "file_path": "/tmp/test_integration.epub", "file_size": 1024, "mime_type": "application/epub+zip", "publisher": "Test Publisher", "date_published": "2024-01-01", } resp := makeRequest(t, "POST", "/api/ebooks", req, ctx.AdminToken) defer resp.Body.Close() if resp.StatusCode == http.StatusCreated { var ebook map[string]interface{} body, _ := io.ReadAll(resp.Body) err := json.Unmarshal(body, &ebook) require.NoError(t, err) ctx.EbookID = ebook["id"].(string) } }) t.Run("CreateEbook_UserForbidden", func(t *testing.T) { req := map[string]interface{}{ "title": "User Ebook", "file_path": "/tmp/user.epub", "file_size": 1024, "mime_type": "application/epub+zip", } resp := makeRequest(t, "POST", "/api/ebooks", req, ctx.UserToken) defer resp.Body.Close() assert.Equal(t, http.StatusForbidden, resp.StatusCode) }) if ctx.EbookID != "" { t.Run("GetEbook", func(t *testing.T) { resp := makeRequest(t, "GET", fmt.Sprintf("/api/ebooks/%s", ctx.EbookID), nil, ctx.UserToken) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("UpdateEbook_Admin", func(t *testing.T) { req := map[string]interface{}{ "title": "Updated Ebook Title", "description": "Updated during test", } resp := makeRequest(t, "PUT", fmt.Sprintf("/api/ebooks/%s", ctx.EbookID), req, ctx.AdminToken) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("CreateEbookRating", func(t *testing.T) { req := map[string]interface{}{ "rating": 5, } resp := makeRequest(t, "POST", fmt.Sprintf("/api/ebooks/%s/rating", ctx.EbookID), req, ctx.UserToken) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK) }) t.Run("GetEbookRating", func(t *testing.T) { resp := makeRequest(t, "GET", fmt.Sprintf("/api/ebooks/%s/rating", ctx.EbookID), nil, ctx.UserToken) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound) }) t.Run("UpdateReadingProgress", func(t *testing.T) { req := map[string]interface{}{ "progress_percentage": 50, "current_page": 125, "total_pages": 250, } resp := makeRequest(t, "PUT", fmt.Sprintf("/api/ebooks/%s/progress", ctx.EbookID), req, ctx.UserToken) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("GetReadingProgress", func(t *testing.T) { resp := makeRequest(t, "GET", fmt.Sprintf("/api/ebooks/%s/progress", ctx.EbookID), nil, ctx.UserToken) 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) } } } }) if ctx.MediaItemID != "" { 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) }) }