# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client """Tests for api_routes module - API endpoints.""" from unittest.mock import patch, MagicMock # API routes are registered under /research/api prefix API_PREFIX = "/research/api" class TestGetCurrentConfig: """Tests for /settings/current-config endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.get(f"{API_PREFIX}/settings/current-config") # Should redirect to login or return 401 assert response.status_code == 401, response.status_code def test_returns_config_when_authenticated(self, authenticated_client): """Should return config when authenticated.""" with patch( "local_deep_research.web.routes.api_routes.get_user_db_session" ) as mock_session_ctx: mock_session = MagicMock() mock_session_ctx.return_value.__enter__ = MagicMock( return_value=mock_session ) mock_session_ctx.return_value.__exit__ = MagicMock( return_value=None ) with patch( "local_deep_research.web.routes.api_routes.SettingsManager" ) as mock_sm: mock_instance = MagicMock() mock_instance.get_setting.side_effect = lambda key, default: { "llm.provider": "ollama", "llm.model": "llama3", "search.tool": "searxng", "search.iterations": 8, "search.questions_per_iteration": 5, "search.search_strategy": "focused_iteration", }.get(key, default) mock_sm.return_value = mock_instance response = authenticated_client.get( f"{API_PREFIX}/settings/current-config" ) assert response.status_code == 200 data = response.get_json() assert data["success"] is True assert "config" in data assert data["config"]["provider"] == "ollama" class TestApiStartResearch: """Tests for /start endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.post( f"{API_PREFIX}/start", json={"query": "test query", "mode": "quick"} ) assert response.status_code == 401, response.status_code def test_requires_query(self, authenticated_client): """Should require query parameter.""" response = authenticated_client.post( f"{API_PREFIX}/start", json={"mode": "quick"} ) assert response.status_code == 400 data = response.get_json() assert "Query is required" in data.get("message", "") def test_empty_query_rejected(self, authenticated_client): """Should reject empty query.""" response = authenticated_client.post( f"{API_PREFIX}/start", json={"query": "", "mode": "quick"} ) assert response.status_code == 400 def test_starts_research_successfully(self, authenticated_client): """Should delegate to start_research and return success.""" with patch( "local_deep_research.web.routes.research_routes.start_research" ) as mock_start: mock_start.return_value = ( authenticated_client.application.make_response( ( '{"status": "success", "research_id": "test-uuid"}', 200, {"Content-Type": "application/json"}, ) ) ) response = authenticated_client.post( f"{API_PREFIX}/start", json={"query": "What is AI?", "mode": "quick"}, ) assert response.status_code == 200 data = response.get_json() assert data["status"] == "success" assert data["research_id"] == "test-uuid" mock_start.assert_called_once() def test_starts_research_queued(self, authenticated_client): """Should delegate to start_research and return queued status.""" with patch( "local_deep_research.web.routes.research_routes.start_research" ) as mock_start: mock_start.return_value = authenticated_client.application.make_response( ( '{"status": "queued", "research_id": "test-uuid", "queue_position": 1}', 200, {"Content-Type": "application/json"}, ) ) response = authenticated_client.post( f"{API_PREFIX}/start", json={"query": "What is AI?", "mode": "quick"}, ) assert response.status_code == 200 data = response.get_json() assert data["status"] == "queued" assert data["research_id"] == "test-uuid" mock_start.assert_called_once() class TestApiResearchStatus: """Tests for /status/ endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.get(f"{API_PREFIX}/status/test-id") assert response.status_code == 401, response.status_code def test_returns_404_for_nonexistent(self, authenticated_client): """Should return 404 for non-existent research.""" with patch( "local_deep_research.web.routes.api_routes.get_user_db_session" ) as mock_session_ctx: mock_session = MagicMock() mock_session_ctx.return_value.__enter__ = MagicMock( return_value=mock_session ) mock_session_ctx.return_value.__exit__ = MagicMock( return_value=None ) mock_query = MagicMock() mock_query.filter_by.return_value.first.return_value = None mock_session.query.return_value = mock_query response = authenticated_client.get( f"{API_PREFIX}/status/nonexistent-id" ) assert response.status_code == 404 def test_returns_status_for_existing(self, authenticated_client): """Should return status for existing research.""" with patch( "local_deep_research.web.routes.api_routes.get_user_db_session" ) as mock_session_ctx: mock_session = MagicMock() mock_session_ctx.return_value.__enter__ = MagicMock( return_value=mock_session ) mock_session_ctx.return_value.__exit__ = MagicMock( return_value=None ) mock_research = MagicMock() mock_research.status = "completed" mock_research.progress = 100 mock_research.completed_at = "2024-01-01T00:00:00" mock_research.report_path = "/path/to/report.md" mock_research.research_meta = {"key": "value"} mock_query = MagicMock() mock_query.filter_by.return_value.first.return_value = mock_research mock_session.query.return_value = mock_query response = authenticated_client.get( f"{API_PREFIX}/status/existing-id" ) assert response.status_code == 200 data = response.get_json() assert data["status"] == "completed" assert data["progress"] == 100 class TestApiTerminateResearch: """Tests for /terminate/ endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.post(f"{API_PREFIX}/terminate/test-id") assert response.status_code == 401, response.status_code def test_terminates_research(self, authenticated_client): """Should terminate research.""" with patch( "local_deep_research.web.routes.api_routes.cancel_research" ) as mock_cancel: mock_cancel.return_value = True response = authenticated_client.post( f"{API_PREFIX}/terminate/test-id" ) assert response.status_code == 200 data = response.get_json() assert data["status"] == "success" def test_handles_not_found(self, authenticated_client): """Should handle research not found.""" with patch( "local_deep_research.web.routes.api_routes.cancel_research" ) as mock_cancel: mock_cancel.return_value = False response = authenticated_client.post( f"{API_PREFIX}/terminate/nonexistent" ) assert response.status_code == 200 data = response.get_json() assert "not found or already completed" in data["message"] class TestApiGetResources: """Tests for GET /resources/ endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.get(f"{API_PREFIX}/resources/test-id") assert response.status_code == 401, response.status_code def test_returns_resources(self, authenticated_client): """Should return resources for research.""" with patch( "local_deep_research.web.routes.api_routes.get_resources_for_research" ) as mock_get: mock_get.return_value = [ {"id": 1, "title": "Resource 1", "url": "https://example.com"} ] response = authenticated_client.get( f"{API_PREFIX}/resources/test-id" ) assert response.status_code == 200 data = response.get_json() assert data["status"] == "success" assert len(data["resources"]) == 1 class TestApiAddResource: """Tests for POST /resources/ endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.post( f"{API_PREFIX}/resources/test-id", json={"title": "Test", "url": "https://example.com"}, ) assert response.status_code == 401, response.status_code def test_requires_title_and_url(self, authenticated_client): """Should require both title and URL.""" response = authenticated_client.post( f"{API_PREFIX}/resources/test-id", json={"title": "Test only"} ) assert response.status_code == 400 response = authenticated_client.post( f"{API_PREFIX}/resources/test-id", json={"url": "https://example.com"}, ) assert response.status_code == 400 def test_returns_404_for_nonexistent_research(self, authenticated_client): """Should return 404 if research doesn't exist.""" with ( patch( "local_deep_research.web.routes.api_routes.get_user_db_session" ) as mock_session_ctx, patch( "local_deep_research.security.ssrf_validator.validate_url", return_value=(True, None), ), ): mock_session = MagicMock() mock_session_ctx.return_value.__enter__ = MagicMock( return_value=mock_session ) mock_session_ctx.return_value.__exit__ = MagicMock( return_value=None ) mock_query = MagicMock() mock_query.filter_by.return_value.first.return_value = None mock_session.query.return_value = mock_query response = authenticated_client.post( f"{API_PREFIX}/resources/nonexistent", json={"title": "Test", "url": "https://example.com"}, ) assert response.status_code == 404 class TestApiDeleteResource: """Tests for DELETE /resources//delete/ endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.delete(f"{API_PREFIX}/resources/test-id/delete/1") assert response.status_code == 401, response.status_code def test_deletes_resource(self, authenticated_client): """Should delete resource successfully.""" with patch( "local_deep_research.web.routes.api_routes.delete_resource" ) as mock_delete: mock_delete.return_value = True response = authenticated_client.delete( f"{API_PREFIX}/resources/test-id/delete/1" ) assert response.status_code == 200 data = response.get_json() assert data["status"] == "success" def test_returns_404_for_nonexistent(self, authenticated_client): """Should return 404 for non-existent resource.""" with patch( "local_deep_research.web.routes.api_routes.delete_resource" ) as mock_delete: mock_delete.return_value = False response = authenticated_client.delete( f"{API_PREFIX}/resources/test-id/delete/999" ) assert response.status_code == 404 class TestCheckOllamaStatus: """Tests for /check/ollama_status endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.get(f"{API_PREFIX}/check/ollama_status") assert response.status_code == 401, response.status_code def test_non_ollama_provider(self, authenticated_client, app): """Should return running=True for non-Ollama providers.""" app.config["LLM_CONFIG"] = {"provider": "openai"} response = authenticated_client.get(f"{API_PREFIX}/check/ollama_status") assert response.status_code == 200 data = response.get_json() assert data["running"] is True assert "openai" in data["message"].lower() def test_ollama_connection_error(self, authenticated_client, app): """Should return running=False on connection error.""" import requests app.config["LLM_CONFIG"] = { "provider": "ollama", "ollama_base_url": "http://localhost:11434", } with patch( "local_deep_research.web.routes.api_routes.safe_get", side_effect=requests.exceptions.ConnectionError( "Connection refused" ), ): response = authenticated_client.get( f"{API_PREFIX}/check/ollama_status" ) assert response.status_code == 200 data = response.get_json() assert data["running"] is False assert "error_type" in data def test_ollama_running(self, authenticated_client, app): """Should return running=True when Ollama is running.""" app.config["LLM_CONFIG"] = { "provider": "ollama", "ollama_base_url": "http://localhost:11434", } mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"models": [{"name": "llama3"}]} with patch( "local_deep_research.web.routes.api_routes.safe_get", return_value=mock_response, ): response = authenticated_client.get( f"{API_PREFIX}/check/ollama_status" ) assert response.status_code == 200 data = response.get_json() assert data["running"] is True assert data["model_count"] == 1 class TestCheckOllamaModel: """Tests for /check/ollama_model endpoint.""" def test_requires_authentication(self, client): """Should require authentication.""" response = client.get(f"{API_PREFIX}/check/ollama_model") assert response.status_code == 401, response.status_code def test_non_ollama_provider(self, authenticated_client, app): """Should return available=True for non-Ollama providers.""" app.config["LLM_CONFIG"] = {"provider": "openai"} response = authenticated_client.get(f"{API_PREFIX}/check/ollama_model") assert response.status_code == 200 data = response.get_json() assert data["available"] is True assert data["provider"] == "openai" def test_model_available(self, authenticated_client, app): """Should return available=True when model exists.""" app.config["LLM_CONFIG"] = { "provider": "ollama", "model": "llama3", "ollama_base_url": "http://localhost:11434", } mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"models": [{"name": "llama3"}]} with patch( "local_deep_research.web.routes.api_routes.safe_get", return_value=mock_response, ): response = authenticated_client.get( f"{API_PREFIX}/check/ollama_model" ) assert response.status_code == 200 data = response.get_json() assert data["available"] is True assert data["model"] == "llama3" def test_model_not_available(self, authenticated_client, app): """Should return available=False when model doesn't exist.""" app.config["LLM_CONFIG"] = { "provider": "ollama", "model": "nonexistent-model", "ollama_base_url": "http://localhost:11434", } mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"models": [{"name": "llama3"}]} with patch( "local_deep_research.web.routes.api_routes.safe_get", return_value=mock_response, ): response = authenticated_client.get( f"{API_PREFIX}/check/ollama_model" ) assert response.status_code == 200 data = response.get_json() assert data["available"] is False