""" Unit tests for FastAPI endpoints """ import pytest from fastapi.testclient import TestClient from unittest.mock import patch, AsyncMock, Mock from datetime import datetime # Import the FastAPI app from main import app class TestAPIRoot: """Tests for root endpoint""" def test_root_endpoint(self): """Test root endpoint returns API info""" client = TestClient(app) response = client.get("/") assert response.status_code == 200 data = response.json() assert data["message"] == "Ohm Stream Downloader API" assert data["status"] == "running" assert "version" in data assert "endpoints" in data def test_root_endpoint_version(self): """Test that API version is present""" client = TestClient(app) response = client.get("/") data = response.json() assert data["version"] in ["2.1", "2.2"] class TestAPIHealth: """Tests for health check endpoint""" def test_health_check(self): """Test health check endpoint""" client = TestClient(app) response = client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" class TestAPIProviders: """Tests for providers endpoint""" def test_providers_list(self): """Test getting list of providers""" client = TestClient(app) response = client.get("/api/providers") assert response.status_code == 200 data = response.json() assert "anime_providers" in data assert "file_hosts" in data assert isinstance(data["anime_providers"], dict) assert isinstance(data["file_hosts"], dict) # Check for known providers assert "anime-sama" in data["anime_providers"] assert "neko-sama" in data["anime_providers"] class TestAPIDownloadCreate: """Tests for download creation endpoint""" def test_create_download_success(self): """Test creating a new download""" client = TestClient(app) response = client.post( "/api/download", json={"url": "https://example.com/file.mp4"} ) assert response.status_code == 200 data = response.json() assert "task_id" in data # Status is in the task object assert "task" in data assert data["task"]["status"] == "pending" def test_create_download_with_filename(self): """Test creating download with custom filename""" client = TestClient(app) response = client.post( "/api/download", json={ "url": "https://example.com/file.mp4", "filename": "custom_name.mp4" } ) assert response.status_code == 200 data = response.json() assert "task_id" in data def test_create_download_invalid_url(self): """Test creating download with invalid URL""" client = TestClient(app) response = client.post( "/api/download", json={"url": "not-a-valid-url"} ) # API accepts the URL even if invalid (will fail later) assert response.status_code == 200 data = response.json() assert "task_id" in data def test_create_download_missing_url(self): """Test creating download without URL""" client = TestClient(app) response = client.post("/api/download", json={}) assert response.status_code == 422 class TestAPIDownloadList: """Tests for download list endpoint""" def test_list_downloads_empty(self): """Test listing downloads when none exist""" client = TestClient(app) response = client.get("/api/downloads") assert response.status_code == 200 data = response.json() assert "downloads" in data assert isinstance(data["downloads"], list) # May or may not be empty depending on test order def test_list_downloads_after_creation(self): """Test listing downloads after creating one""" client = TestClient(app) # Create a download first create_response = client.post( "/api/download", json={"url": "https://example.com/test.mp4"} ) assert create_response.status_code == 200 # List downloads response = client.get("/api/downloads") assert response.status_code == 200 data = response.json() assert len(data["downloads"]) >= 1 class TestAPIDownloadDetails: """Tests for download details endpoint""" def test_get_download_details_success(self): """Test getting details of existing download""" client = TestClient(app) # Create a download first create_response = client.post( "/api/download", json={"url": "https://example.com/details.mp4"} ) task_id = create_response.json()["task_id"] # Get details response = client.get(f"/api/download/{task_id}") assert response.status_code == 200 data = response.json() assert data["id"] == task_id assert "url" in data assert "status" in data def test_get_download_details_not_found(self): """Test getting details of non-existent download""" client = TestClient(app) response = client.get("/api/download/nonexistent-id") assert response.status_code == 404 class TestAPIDownloadPause: """Tests for pause download endpoint""" def test_pause_download_success(self): """Test pausing a download""" client = TestClient(app) # Create a download first create_response = client.post( "/api/download", json={"url": "https://example.com/pause.mp4"} ) task_id = create_response.json()["task_id"] # Pause it response = client.post(f"/api/download/{task_id}/pause") assert response.status_code == 200 data = response.json() assert data["status"] == "paused" def test_pause_download_not_found(self): """Test pausing non-existent download""" client = TestClient(app) response = client.post("/api/download/nonexistent-id/pause") assert response.status_code == 404 class TestAPIDownloadResume: """Tests for resume download endpoint""" def test_resume_download_success(self): """Test resuming a paused download""" client = TestClient(app) # Create and pause a download create_response = client.post( "/api/download", json={"url": "https://example.com/resume.mp4"} ) task_id = create_response.json()["task_id"] # Pause first client.post(f"/api/download/{task_id}/pause") # Resume response = client.post(f"/api/download/{task_id}/resume") assert response.status_code == 200 data = response.json() assert "status" in data assert data["status"] in ["resumed", "already running or completed"] class TestAPIDownloadCancel: """Tests for cancel download endpoint""" def test_cancel_download_success(self): """Test canceling a download""" client = TestClient(app) # Create a download first create_response = client.post( "/api/download", json={"url": "https://example.com/cancel.mp4"} ) task_id = create_response.json()["task_id"] # Cancel it (DELETE marks as deleted) response = client.delete(f"/api/download/{task_id}") assert response.status_code == 200 data = response.json() assert data["status"] == "deleted" def test_cancel_download_not_found(self): """Test canceling non-existent download""" client = TestClient(app) response = client.delete("/api/download/nonexistent-id") assert response.status_code == 404 class TestAPIAnimeSearch: """Tests for anime search endpoint""" def test_anime_search_missing_query(self): """Test anime search without query parameter""" client = TestClient(app) response = client.get("/api/anime/search") # Now returns 422 for validation error assert response.status_code == 422 # Bad request def test_anime_search_with_query(self): """Test anime search with query parameter""" client = TestClient(app) response = client.get("/api/anime/search?q=naruto") # This will likely fail without actual network/anime sites # but we're testing the endpoint exists assert response.status_code in [200, 500, 503] # Various possibilities def test_anime_search_with_language(self): """Test anime search with language parameter""" client = TestClient(app) response = client.get("/api/anime/search?q=one+piece&lang=vostfr") # Similar to above, testing endpoint exists assert response.status_code in [200, 500, 503] def test_anime_search_with_metadata(self): """Test anime search with metadata flag""" client = TestClient(app) response = client.get("/api/anime/search?q=bleach&include_metadata=true") # Testing endpoint accepts the parameter assert response.status_code in [200, 500, 503] class TestAPIAnimeMetadata: """Tests for anime metadata endpoint""" def test_anime_metadata_missing_url(self): """Test metadata endpoint without URL""" client = TestClient(app) response = client.get("/api/anime/metadata") # Returns 422 for validation error assert response.status_code == 422 def test_anime_metadata_with_url(self): """Test metadata endpoint with URL""" client = TestClient(app) response = client.get("/api/anime/metadata?url=https://anime-sama.si/test/") # Will likely fail without real anime site assert response.status_code in [200, 404, 500, 503] class TestAPIAnimeEpisodes: """Tests for anime episodes endpoint""" def test_anime_episodes_missing_url(self): """Test episodes endpoint without URL""" client = TestClient(app) response = client.get("/api/anime/episodes") # Returns 422 for validation error assert response.status_code == 422 def test_anime_episodes_with_url(self): """Test episodes endpoint with URL""" client = TestClient(app) response = client.get( "/api/anime/episodes?url=https://anime-sama.si/test/&lang=vostfr" ) # Will likely fail without real anime site assert response.status_code in [200, 404, 500, 503] class TestAPIFavorites: """Tests for favorites endpoints""" def test_list_favorites_empty(self): """Test listing favorites when empty""" client = TestClient(app) response = client.get("/api/favorites") assert response.status_code == 200 data = response.json() assert "favorites" in data assert "total" in data def test_add_favorite_success(self): """Test adding a favorite""" client = TestClient(app) response = client.post( "/api/favorites", json={ "anime_id": "test-anime-123", "title": "Test Anime", "url": "https://example.com/anime", "provider": "anime-sama", "metadata": {"genres": ["Action"]}, "poster_url": "https://example.com/poster.jpg" } ) assert response.status_code == 200 data = response.json() assert data["status"] == "added" assert "favorite" in data def test_add_favorite_missing_fields(self): """Test adding favorite with missing required fields""" client = TestClient(app) response = client.post( "/api/favorites", json={"title": "Test Anime"} # Missing anime_id, url, provider ) assert response.status_code == 400 def test_remove_favorite_success(self): """Test removing a favorite""" client = TestClient(app) # Add first client.post( "/api/favorites", json={ "anime_id": "test-remove-123", "title": "Remove Me", "url": "https://example.com", "provider": "anime-sama" } ) # Remove response = client.delete("/api/favorites/test-remove-123") assert response.status_code == 200 data = response.json() assert data["status"] == "removed" def test_remove_favorite_not_found(self): """Test removing non-existent favorite""" client = TestClient(app) response = client.delete("/api/favorites/nonexistent-id") assert response.status_code == 404 def test_get_favorite_success(self): """Test getting a specific favorite""" client = TestClient(app) # Add first client.post( "/api/favorites", json={ "anime_id": "test-get-123", "title": "Get Me", "url": "https://example.com", "provider": "anime-sama" } ) # Get response = client.get("/api/favorites/test-get-123") assert response.status_code == 200 data = response.json() assert "favorite" in data assert data["favorite"]["id"] == "test-get-123" def test_get_favorite_not_found(self): """Test getting non-existent favorite""" client = TestClient(app) response = client.get("/api/favorites/nonexistent-id") assert response.status_code == 404 def test_get_favorites_stats(self): """Test getting favorites statistics""" client = TestClient(app) response = client.get("/api/favorites/stats") assert response.status_code == 200 data = response.json() assert "total" in data assert "by_provider" in data assert "by_genre" in data def test_toggle_favorite_add(self): """Test toggling favorite to add""" client = TestClient(app) # Make sure it doesn't exist first try: client.delete("/api/favorites/test-toggle-add") except: pass response = client.post( "/api/favorites/toggle", json={ "anime_id": "test-toggle-add", "title": "Toggle Add", "url": "https://example.com", "provider": "anime-sama" } ) assert response.status_code == 200 data = response.json() assert data["action"] == "added" def test_toggle_favorite_remove(self): """Test toggling favorite to remove""" client = TestClient(app) # Make sure it doesn't exist first try: client.delete("/api/favorites/test-toggle-remove") except: pass # Add first client.post( "/api/favorites/toggle", json={ "anime_id": "test-toggle-remove", "title": "Toggle Remove", "url": "https://example.com", "provider": "anime-sama" } ) # Toggle to remove response = client.post( "/api/favorites/toggle", json={ "anime_id": "test-toggle-remove", "title": "Toggle Remove", "url": "https://example.com", "provider": "anime-sama" } ) assert response.status_code == 200 data = response.json() assert data["action"] == "removed" class TestAPIFavoritesSorting: """Tests for favorites sorting and filtering""" def test_favorites_sort_by_title(self): """Test sorting favorites by title""" client = TestClient(app) # Add multiple favorites for title in ["Z Anime", "A Anime", "M Anime"]: client.post( "/api/favorites", json={ "anime_id": f"sort-{title}", "title": title, "url": f"https://example.com/{title}", "provider": "anime-sama" } ) response = client.get("/api/favorites?sort_by=title&order=asc") assert response.status_code == 200 data = response.json() if len(data["favorites"]) > 0: # Check if sorted titles = [f["title"] for f in data["favorites"][:3]] assert titles == sorted(titles) def test_favorites_filter_by_provider(self): """Test filtering favorites by provider""" client = TestClient(app) # Add favorites with different providers client.post( "/api/favorites", json={ "anime_id": "filter-anime-sama", "title": "Anime Sama", "url": "https://example.com", "provider": "anime-sama" } ) client.post( "/api/favorites", json={ "anime_id": "filter-neko-sama", "title": "Neko Sama", "url": "https://example.com", "provider": "neko-sama" } ) response = client.get("/api/favorites?filter_provider=anime-sama") assert response.status_code == 200 data = response.json() # Should only have anime-sama for fav in data["favorites"]: assert fav["provider"] == "anime-sama" class TestAPIWebInterface: """Tests for web interface endpoint""" def test_web_interface(self): """Test web interface endpoint""" client = TestClient(app) response = client.get("/web") assert response.status_code == 200 # Should return HTML assert "text/html" in response.headers["content-type"] class TestAPIVideoStreaming: """Tests for video streaming endpoints""" def test_video_player_by_id_not_found(self): """Test video player with non-existent task ID""" client = TestClient(app) response = client.get("/player/nonexistent-id") # Should return 404 or similar assert response.status_code in [404, 403] def test_video_stream_by_filename_not_found(self): """Test video stream with non-existent filename""" client = TestClient(app) response = client.get("/stream/nonexistent_file.mp4") # Should return 404 assert response.status_code == 404 class TestAPIErrorHandling: """Tests for API error handling""" def test_invalid_endpoint(self): """Test accessing invalid endpoint""" client = TestClient(app) response = client.get("/api/invalid_endpoint") assert response.status_code == 404 def test_invalid_method(self): """Test using invalid HTTP method""" client = TestClient(app) response = client.post("/api/downloads") # Should be GET assert response.status_code == 405 # Method Not Allowed def test_malformed_json(self): """Test sending malformed JSON""" client = TestClient(app) response = client.post( "/api/download", data="invalid json", headers={"Content-Type": "application/json"} ) assert response.status_code == 422 class TestAPICorsHeaders: """Tests for CORS headers""" def test_cors_headers_present(self): """Test that CORS headers are present""" client = TestClient(app) response = client.get("/") # Check for CORS headers # Note: Actual CORS configuration may vary # This is just to ensure the endpoint responds assert response.status_code == 200 # Mock-based tests for endpoints requiring external resources class TestAPIWithMocks: """Tests with mocked external dependencies""" def test_download_with_mocked_downloader(self): """Test download creation with mocked downloader""" client = TestClient(app) with patch('app.download_manager.get_downloader') as mock_get_downloader: mock_downloader = AsyncMock() mock_downloader.get_download_link.return_value = ( "https://example.com/direct", "video.mp4" ) mock_downloader.can_handle.return_value = True mock_get_downloader.return_value = mock_downloader response = client.post( "/api/download", json={"url": "https://doodstream.com/test"} ) # Should succeed assert response.status_code == 200