"""Integration tests for sources API endpoints. Tests all source endpoints with mocked services. """ from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 import pytest from fastapi.testclient import TestClient from notebooklm_agent.api.main import app from notebooklm_agent.api.models.responses import Source @pytest.mark.unit class TestCreateSourceEndpoint: """Test suite for POST /api/v1/notebooks/{id}/sources endpoint.""" def test_create_url_source_returns_201(self): """Should return 201 for valid URL source.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) source_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() mock_source = AsyncMock() mock_source.id = source_id mock_source.notebook_id = notebook_id mock_source.type = "url" mock_source.title = "Example Article" mock_source.url = "https://example.com/article" mock_source.status = "processing" mock_source.created_at = "2026-04-06T10:00:00Z" mock_service.create.return_value = mock_source mock_service_class.return_value = mock_service # Act response = client.post( f"/api/v1/notebooks/{notebook_id}/sources", json={"type": "url", "url": "https://example.com/article", "title": "Example"}, ) # Assert assert response.status_code == 201 data = response.json() assert data["success"] is True assert data["data"]["type"] == "url" def test_create_source_invalid_notebook_id_returns_400(self): """Should return 400 for invalid notebook ID.""" # Arrange client = TestClient(app) # Act response = client.post( "/api/v1/notebooks/invalid-id/sources", json={"type": "url", "url": "https://example.com"}, ) # Assert assert response.status_code in [400, 422] def test_create_source_missing_url_returns_400(self): """Should return 400 when URL missing for url type.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() from notebooklm_agent.core.exceptions import ValidationError mock_service.create.side_effect = ValidationError("URL is required") mock_service_class.return_value = mock_service # Act response = client.post( f"/api/v1/notebooks/{notebook_id}/sources", json={"type": "url", "url": None}, ) # Assert assert response.status_code in [400, 422] @pytest.mark.unit class TestListSourcesEndpoint: """Test suite for GET /api/v1/notebooks/{id}/sources endpoint.""" def test_list_sources_returns_200(self): """Should return 200 with list of sources.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) source_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() mock_source = Source( id=source_id, notebook_id=notebook_id, type="url", title="Example", url="https://example.com", status="ready", created_at=datetime.utcnow(), ) mock_service.list.return_value = [mock_source] mock_service_class.return_value = mock_service # Act response = client.get(f"/api/v1/notebooks/{notebook_id}/sources") # Assert assert response.status_code == 200 data = response.json() assert data["success"] is True assert len(data["data"]["items"]) == 1 assert data["data"]["items"][0]["type"] == "url" def test_list_sources_with_type_filter(self): """Should filter sources by type.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() mock_service.list.return_value = [] mock_service_class.return_value = mock_service # Act response = client.get(f"/api/v1/notebooks/{notebook_id}/sources?source_type=youtube") # Assert assert response.status_code == 200 from uuid import UUID mock_service.list.assert_called_once_with(UUID(notebook_id), "youtube", None) def test_list_sources_invalid_notebook_id_returns_400(self): """Should return 400 for invalid notebook ID.""" # Arrange client = TestClient(app) # Act response = client.get("/api/v1/notebooks/invalid-id/sources") # Assert assert response.status_code in [400, 422] @pytest.mark.unit class TestDeleteSourceEndpoint: """Test suite for DELETE /api/v1/notebooks/{id}/sources/{source_id} endpoint.""" def test_delete_source_returns_204(self): """Should return 204 No Content for successful delete.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) source_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() mock_source = Source( id=source_id, notebook_id=notebook_id, type="url", title="Example", url="https://example.com", status="ready", created_at=datetime.utcnow(), ) mock_service.list.return_value = [mock_source] mock_service_class.return_value = mock_service # Act response = client.delete(f"/api/v1/notebooks/{notebook_id}/sources/{source_id}") # Assert assert response.status_code == 204 assert response.content == b"" def test_delete_source_not_found_returns_404(self): """Should return 404 when source not found.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) source_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() from notebooklm_agent.core.exceptions import NotFoundError mock_service.delete.side_effect = NotFoundError("Source", source_id) mock_service_class.return_value = mock_service # Act response = client.delete(f"/api/v1/notebooks/{notebook_id}/sources/{source_id}") # Assert assert response.status_code == 404 def test_delete_source_invalid_notebook_id_returns_400(self): """Should return 400 for invalid notebook ID.""" # Arrange client = TestClient(app) source_id = str(uuid4()) # Act response = client.delete(f"/api/v1/notebooks/invalid-id/sources/{source_id}") # Assert assert response.status_code in [400, 422] @pytest.mark.unit class TestResearchEndpoint: """Test suite for POST /api/v1/notebooks/{id}/sources/research endpoint.""" def test_research_returns_202(self): """Should return 202 Accepted for research start.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() mock_service.research.return_value = { "research_id": str(uuid4()), "status": "pending", "query": "AI trends", "mode": "fast", "sources_found": 0, } mock_service_class.return_value = mock_service # Act response = client.post( f"/api/v1/notebooks/{notebook_id}/sources/research", json={"query": "AI trends", "mode": "fast", "auto_import": True}, ) # Assert assert response.status_code == 202 data = response.json() assert data["success"] is True assert data["data"]["status"] == "pending" def test_research_invalid_mode_returns_400(self): """Should return 400 for invalid research mode.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() from notebooklm_agent.core.exceptions import ValidationError mock_service.research.side_effect = ValidationError("Mode must be 'fast' or 'deep'") mock_service_class.return_value = mock_service # Act response = client.post( f"/api/v1/notebooks/{notebook_id}/sources/research", json={"query": "AI trends", "mode": "invalid"}, ) # Assert assert response.status_code in [400, 422] def test_research_empty_query_returns_400(self): """Should return 400 for empty query.""" # Arrange client = TestClient(app) notebook_id = str(uuid4()) with patch("notebooklm_agent.api.routes.sources.SourceService") as mock_service_class: mock_service = AsyncMock() from notebooklm_agent.core.exceptions import ValidationError mock_service.research.side_effect = ValidationError("Query cannot be empty") mock_service_class.return_value = mock_service # Act response = client.post( f"/api/v1/notebooks/{notebook_id}/sources/research", json={"query": "", "mode": "fast"}, ) # Assert assert response.status_code in [400, 422] def test_research_invalid_notebook_id_returns_400(self): """Should return 400 for invalid notebook ID.""" # Arrange client = TestClient(app) # Act response = client.post( "/api/v1/notebooks/invalid-id/sources/research", json={"query": "AI trends", "mode": "fast"}, ) # Assert assert response.status_code in [400, 422]