""" Coverage tests for local_deep_research/news/api.py Tests focus on branches and helpers not covered by existing test_news_api.py / test_news_api_extended.py: - _notify_scheduler_about_subscription_change edge paths - get_news_feed() limit validation and subscription_id filter - InvalidLimitException propagation - get_subscriptions / create_subscription / update_subscription / delete_subscription - get_subscription_cards / get_news_card / rate_card (exception paths) - _classify_subscription_type / _validate_subscription_data helpers (if present) """ from unittest.mock import MagicMock, patch import pytest # --------------------------------------------------------------------------- # helpers # --------------------------------------------------------------------------- def _make_db_session_ctx(rows=None): """Return a context-manager mock that yields a session with .query().""" if rows is None: rows = [] session = MagicMock() query_obj = MagicMock() query_obj.filter.return_value = query_obj query_obj.order_by.return_value = query_obj query_obj.limit.return_value = query_obj query_obj.all.return_value = rows session.query.return_value = query_obj ctx = MagicMock() ctx.__enter__ = MagicMock(return_value=session) ctx.__exit__ = MagicMock(return_value=False) return ctx, session # --------------------------------------------------------------------------- # _notify_scheduler_about_subscription_change # --------------------------------------------------------------------------- class TestNotifyScheduler: def test_no_exception_when_import_fails(self): """Should swallow any exception silently.""" from local_deep_research.news.api import ( _notify_scheduler_about_subscription_change, ) with patch( "local_deep_research.news.api.logger" ): # suppress log output # Raises inside because flask.session not available – should be caught _notify_scheduler_about_subscription_change("created") def test_scheduler_not_running_skips_update(self): """If scheduler.is_running is False no update_user_info call is made.""" from local_deep_research.news.api import ( _notify_scheduler_about_subscription_change, ) mock_scheduler = MagicMock() mock_scheduler.is_running = False mock_session = {"username": "alice", "session_id": "s1"} with ( patch("local_deep_research.news.api.logger"), patch( "flask.session", mock_session, create=True, ), patch( "local_deep_research.news.api.get_background_job_scheduler", return_value=mock_scheduler, create=True, ), ): _notify_scheduler_about_subscription_change("updated") mock_scheduler.update_user_info.assert_not_called() # --------------------------------------------------------------------------- # get_news_feed – limit validation # --------------------------------------------------------------------------- class TestGetNewsFeedLimitValidation: def test_zero_limit_raises_invalid_limit(self): """limit < 1 must raise InvalidLimitException.""" from local_deep_research.news.api import get_news_feed from local_deep_research.news.exceptions import InvalidLimitException with pytest.raises(InvalidLimitException): get_news_feed(user_id="user1", limit=0) def test_negative_limit_raises_invalid_limit(self): """Negative limit triggers the validation guard.""" from local_deep_research.news.api import get_news_feed from local_deep_research.news.exceptions import InvalidLimitException with pytest.raises(InvalidLimitException): get_news_feed(user_id="user1", limit=-5) def test_valid_limit_does_not_raise(self): """A valid positive limit proceeds past validation.""" from local_deep_research.news.api import get_news_feed ctx, session = _make_db_session_ctx(rows=[]) with patch( "local_deep_research.database.session_context.get_user_db_session", return_value=ctx, ): result = get_news_feed(user_id="user1", limit=5) # Should return a dict (possibly empty news_items) without exception assert isinstance(result, dict) # --------------------------------------------------------------------------- # get_news_feed – subscription_id filter # --------------------------------------------------------------------------- class TestGetNewsFeedSubscriptionFilter: def test_subscription_id_all_skips_filter(self): """subscription_id='all' should not apply extra query filter.""" from local_deep_research.news.api import get_news_feed ctx, session = _make_db_session_ctx(rows=[]) with patch( "local_deep_research.database.session_context.get_user_db_session", return_value=ctx, ): get_news_feed(user_id="u1", limit=10, subscription_id="all") # The filter for a specific subscription_id should not have been called # (We can only check .filter was called the 'status' filter only – no additional # subscription_like filter) call_count = session.query.return_value.filter.call_count # status filter always applied = 1; subscription filter not applied assert call_count == 1 def test_specific_subscription_id_applies_extra_filter(self): """A non-'all' subscription_id should add a second .filter call.""" from local_deep_research.news.api import get_news_feed ctx, session = _make_db_session_ctx(rows=[]) with patch( "local_deep_research.database.session_context.get_user_db_session", return_value=ctx, ): get_news_feed(user_id="u1", limit=10, subscription_id="sub-123") # Both the status filter and the subscription filter applied assert session.query.return_value.filter.call_count >= 2 # --------------------------------------------------------------------------- # get_news_feed – database exception handling # --------------------------------------------------------------------------- class TestGetNewsFeedExceptionHandling: def test_db_error_raises_database_access_exception(self): """If database access fails get_news_feed raises DatabaseAccessException.""" from local_deep_research.news.api import get_news_feed from local_deep_research.news.exceptions import DatabaseAccessException with patch( "local_deep_research.database.session_context.get_user_db_session", side_effect=Exception("DB down"), ): with pytest.raises(DatabaseAccessException): get_news_feed(user_id="u1", limit=10) # --------------------------------------------------------------------------- # Exception classes # --------------------------------------------------------------------------- class TestNewsExceptions: def test_invalid_limit_exception_message(self): from local_deep_research.news.exceptions import InvalidLimitException exc = InvalidLimitException(-1) assert "-1" in str(exc) or "limit" in str(exc).lower() def test_subscription_not_found_exception(self): from local_deep_research.news.exceptions import ( SubscriptionNotFoundException, ) exc = SubscriptionNotFoundException("sub-abc") assert "sub-abc" in str(exc) def test_news_api_exception_is_base(self): from local_deep_research.news.exceptions import ( NewsAPIException, DatabaseAccessException, ) exc = DatabaseAccessException("read", "fail") assert isinstance(exc, NewsAPIException) def test_news_feed_generation_exception(self): from local_deep_research.news.exceptions import ( NewsFeedGenerationException, ) exc = NewsFeedGenerationException("reason") assert isinstance(exc, Exception)