""" Test that API functions raise appropriate exceptions instead of returning error dicts. """ import pytest from unittest.mock import patch, MagicMock from local_deep_research.news import api as news_api from local_deep_research.news.exceptions import ( InvalidLimitException, SubscriptionNotFoundException, SubscriptionCreationException, DatabaseAccessException, NotImplementedException, ) class TestGetNewsFeedExceptions: """Test exception handling in get_news_feed function.""" def test_invalid_limit_raises_exception(self): """Test that invalid limit raises InvalidLimitException.""" with pytest.raises(InvalidLimitException) as exc_info: news_api.get_news_feed(limit=0) assert exc_info.value.status_code == 400 assert exc_info.value.details["provided_limit"] == 0 def test_negative_limit_raises_exception(self): """Test that negative limit raises InvalidLimitException.""" with pytest.raises(InvalidLimitException) as exc_info: news_api.get_news_feed(limit=-10) assert exc_info.value.details["provided_limit"] == -10 @patch("local_deep_research.database.session_context.get_user_db_session") def test_database_error_raises_exception(self, mock_db_session): """Test that database errors raise DatabaseAccessException.""" mock_db_session.side_effect = Exception("Connection failed") with pytest.raises(DatabaseAccessException) as exc_info: news_api.get_news_feed(user_id="test_user") assert exc_info.value.status_code == 500 assert "research_history_query" in exc_info.value.details["operation"] # Raw exception text must NOT reach the client-facing message/response # (CWE-209) — it is logged server-side instead. See _GENERIC_ERROR_DETAIL. assert "Connection failed" not in exc_info.value.message assert "Connection failed" not in exc_info.value.to_dict()["error"] assert "an internal error occurred" in exc_info.value.message class TestSubscriptionExceptions: """Test exception handling in subscription functions.""" @patch("local_deep_research.database.session_context.get_user_db_session") def test_get_subscription_not_found(self, mock_db_session): """Test that missing subscription raises SubscriptionNotFoundException.""" mock_session = MagicMock() mock_db_session.return_value.__enter__.return_value = mock_session mock_session.query().filter_by().first.return_value = None with pytest.raises(SubscriptionNotFoundException) as exc_info: news_api.get_subscription("non-existent-id") assert exc_info.value.status_code == 404 assert exc_info.value.details["subscription_id"] == "non-existent-id" @patch("local_deep_research.database.session_context.get_user_db_session") def test_get_subscription_database_error(self, mock_db_session): """Test that database errors in get_subscription raise DatabaseAccessException.""" mock_db_session.side_effect = Exception("Database locked") with pytest.raises(DatabaseAccessException) as exc_info: news_api.get_subscription("sub-123") assert exc_info.value.status_code == 500 assert "get_subscription" in exc_info.value.details["operation"] @patch("local_deep_research.database.session_context.get_user_db_session") def test_update_subscription_not_found(self, mock_db_session): """Test that updating non-existent subscription raises exception.""" mock_session = MagicMock() mock_db_session.return_value.__enter__.return_value = mock_session mock_session.query().filter_by().first.return_value = None with pytest.raises(SubscriptionNotFoundException) as exc_info: news_api.update_subscription("non-existent", {"name": "New Name"}) assert exc_info.value.details["subscription_id"] == "non-existent" @patch("local_deep_research.database.session_context.get_user_db_session") def test_delete_subscription_not_found(self, mock_db_session): """Test that deleting non-existent subscription raises exception.""" mock_session = MagicMock() mock_db_session.return_value.__enter__.return_value = mock_session mock_session.query().filter_by().first.return_value = None with pytest.raises(SubscriptionNotFoundException) as exc_info: news_api.delete_subscription("non-existent") assert exc_info.value.details["subscription_id"] == "non-existent" @patch("local_deep_research.database.session_context.get_user_db_session") def test_create_subscription_database_error(self, mock_db_session): """Test that database error during creation raises exception.""" mock_db_session.side_effect = Exception("Constraint violation") with pytest.raises(SubscriptionCreationException) as exc_info: news_api.create_subscription( user_id="test_user", query="test query", subscription_type="search", ) assert exc_info.value.status_code == 500 # Raw exception text must NOT reach the client-facing message (CWE-209). assert "Constraint violation" not in exc_info.value.message assert "Constraint violation" not in exc_info.value.to_dict()["error"] assert "an internal error occurred" in exc_info.value.message assert exc_info.value.details["query"] == "test query" class TestNotImplementedExceptions: """Test that unimplemented features raise NotImplementedException.""" def test_research_news_item_not_implemented(self): """Test that research_news_item raises NotImplementedException.""" with pytest.raises(NotImplementedException) as exc_info: news_api.research_news_item("card-123", "detailed") assert exc_info.value.details["feature"] == "research_news_item" def test_save_preferences_not_implemented(self): """Test that save_news_preferences raises NotImplementedException.""" with pytest.raises(NotImplementedException) as exc_info: news_api.save_news_preferences("user", {"theme": "dark"}) assert exc_info.value.details["feature"] == "save_news_preferences" def test_get_categories_not_implemented(self): """Test that get_news_categories raises NotImplementedException.""" with pytest.raises(NotImplementedException) as exc_info: news_api.get_news_categories() assert exc_info.value.details["feature"] == "get_news_categories" class TestSubscriptionHistoryExceptions: """Test exception handling in subscription history functions.""" @patch("local_deep_research.database.session_context.get_user_db_session") def test_subscription_history_not_found(self, mock_db_session): """Test that history for non-existent subscription raises exception.""" mock_session = MagicMock() mock_db_session.return_value.__enter__.return_value = mock_session # First call for subscription lookup returns None mock_session.query().filter_by().first.return_value = None with pytest.raises(SubscriptionNotFoundException) as exc_info: news_api.get_subscription_history("non-existent") assert exc_info.value.details["subscription_id"] == "non-existent" @patch("local_deep_research.database.session_context.get_user_db_session") def test_subscription_history_database_error(self, mock_db_session): """Test that database error in history raises exception.""" mock_db_session.side_effect = Exception("Query timeout") with pytest.raises(DatabaseAccessException) as exc_info: news_api.get_subscription_history("sub-123") assert "get_subscription_history" in exc_info.value.details["operation"] # Raw exception text must NOT reach the client-facing message (CWE-209). assert "Query timeout" not in exc_info.value.message assert "Query timeout" not in exc_info.value.to_dict()["error"] assert "an internal error occurred" in exc_info.value.message