""" Deep behavioral tests for FolderManager. Tests CRUD operations, subscription management, and serialization with mocked sessions. """ from datetime import datetime, timezone from unittest.mock import Mock, MagicMock, PropertyMock from local_deep_research.news.folder_manager import FolderManager def _make_mock_session(): """Create a mock SQLAlchemy session.""" session = MagicMock() return session def _make_mock_sub( sub_id="sub-1", sub_type="search", query="AI news", status="active", folder=None, created_at=None, last_refresh=None, next_refresh=None, refresh_interval=240, ): """Create a mock subscription model object.""" sub = Mock() sub.id = sub_id sub.subscription_type = sub_type sub.query_or_topic = query sub.status = status sub.folder = folder sub.created_at = created_at or datetime(2025, 1, 1, tzinfo=timezone.utc) sub.last_refresh = last_refresh sub.next_refresh = next_refresh sub.refresh_interval_minutes = refresh_interval sub.updated_at = None return sub def _make_mock_folder( folder_id="f-1", name="Tech News", description="Technology news", ): """Create a mock folder model object.""" folder = Mock() folder.id = folder_id folder.name = name folder.description = description folder.created_at = datetime(2025, 1, 1, tzinfo=timezone.utc) folder.updated_at = None folder.to_dict.return_value = { "id": folder_id, "name": name, "description": description, } return folder # --- _sub_to_dict --- class TestSubToDict: """Tests for the _sub_to_dict helper method.""" def test_includes_id(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub(sub_id="test-123") result = fm._sub_to_dict(sub) assert result["id"] == "test-123" def test_includes_type(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub(sub_type="topic") result = fm._sub_to_dict(sub) assert result["type"] == "topic" def test_includes_query_or_topic(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub(query="climate change") result = fm._sub_to_dict(sub) assert result["query_or_topic"] == "climate change" def test_created_at_isoformat(self): fm = FolderManager(_make_mock_session()) dt = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc) sub = _make_mock_sub(created_at=dt) result = fm._sub_to_dict(sub) assert result["created_at"] == dt.isoformat() def test_created_at_none(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub() sub.created_at = None result = fm._sub_to_dict(sub) assert result["created_at"] is None def test_last_refresh_none(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub(last_refresh=None) result = fm._sub_to_dict(sub) assert result["last_refresh"] is None def test_last_refresh_isoformat(self): fm = FolderManager(_make_mock_session()) dt = datetime(2025, 6, 15, 14, 30, 0, tzinfo=timezone.utc) sub = _make_mock_sub(last_refresh=dt) result = fm._sub_to_dict(sub) assert result["last_refresh"] == dt.isoformat() def test_next_refresh_none(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub(next_refresh=None) result = fm._sub_to_dict(sub) assert result["next_refresh"] is None def test_next_refresh_isoformat(self): fm = FolderManager(_make_mock_session()) dt = datetime(2025, 6, 16, 0, 0, 0, tzinfo=timezone.utc) sub = _make_mock_sub(next_refresh=dt) result = fm._sub_to_dict(sub) assert result["next_refresh"] == dt.isoformat() def test_includes_refresh_interval(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub(refresh_interval=120) result = fm._sub_to_dict(sub) assert result["refresh_interval_minutes"] == 120 def test_includes_status(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub(status="paused") result = fm._sub_to_dict(sub) assert result["status"] == "paused" def test_all_keys_present(self): fm = FolderManager(_make_mock_session()) sub = _make_mock_sub() result = fm._sub_to_dict(sub) expected_keys = { "id", "type", "query_or_topic", "created_at", "last_refresh", "next_refresh", "refresh_interval_minutes", "status", } assert set(result.keys()) == expected_keys # --- create_folder --- class TestCreateFolder: """Tests for folder creation.""" def test_creates_folder_with_name(self): session = _make_mock_session() fm = FolderManager(session) fm.create_folder("My Folder") session.add.assert_called_once() session.commit.assert_called_once() def test_creates_folder_with_description(self): session = _make_mock_session() fm = FolderManager(session) fm.create_folder("My Folder", description="A test folder") added_folder = session.add.call_args[0][0] assert added_folder.description == "A test folder" def test_folder_has_uuid_id(self): import uuid session = _make_mock_session() fm = FolderManager(session) fm.create_folder("Test") added_folder = session.add.call_args[0][0] uuid.UUID(added_folder.id) # Should not raise # --- update_folder --- class TestUpdateFolder: """Tests for folder updates.""" def test_returns_none_for_nonexistent(self): session = _make_mock_session() session.query.return_value.get.return_value = None fm = FolderManager(session) result = fm.update_folder("nonexistent", name="New Name") assert result is None def test_updates_name(self): session = _make_mock_session() folder = _make_mock_folder() folder.name = "Old Name" type(folder).id = PropertyMock(return_value="f-1") type(folder).created_at = PropertyMock( return_value=datetime(2025, 1, 1, tzinfo=timezone.utc) ) session.query.return_value.get.return_value = folder fm = FolderManager(session) result = fm.update_folder("f-1", name="New Name") assert result is not None session.commit.assert_called_once() def test_does_not_update_id(self): session = _make_mock_session() folder = _make_mock_folder() session.query.return_value.get.return_value = folder fm = FolderManager(session) fm.update_folder("f-1", id="should-not-change") # id is in the protected list, so setattr should not be called for it def test_does_not_update_created_at(self): session = _make_mock_session() folder = _make_mock_folder() session.query.return_value.get.return_value = folder fm = FolderManager(session) fm.update_folder("f-1", created_at="should-not-change") # created_at is in the protected list def test_sets_updated_at(self): session = _make_mock_session() folder = _make_mock_folder() session.query.return_value.get.return_value = folder fm = FolderManager(session) fm.update_folder("f-1", name="Updated") assert folder.updated_at is not None # --- delete_folder --- class TestDeleteFolder: """Tests for folder deletion.""" def test_returns_false_for_nonexistent(self): session = _make_mock_session() session.query.return_value.get.return_value = None fm = FolderManager(session) assert fm.delete_folder("nonexistent") is False def test_deletes_existing_folder(self): session = _make_mock_session() folder = _make_mock_folder() session.query.return_value.get.return_value = folder fm = FolderManager(session) result = fm.delete_folder("f-1") assert result is True session.delete.assert_called_once_with(folder) session.commit.assert_called_once() def test_moves_subscriptions_when_move_to(self): session = _make_mock_session() folder = _make_mock_folder(name="Old Folder") session.query.return_value.get.return_value = folder fm = FolderManager(session) fm.delete_folder("f-1", move_to="New Folder") # Should update subscriptions' folder session.query.return_value.filter_by.return_value.update.assert_called() def test_sets_folder_none_when_no_move_to(self): session = _make_mock_session() folder = _make_mock_folder() session.query.return_value.get.return_value = folder fm = FolderManager(session) fm.delete_folder("f-1") # Should set folder to None for orphaned subscriptions session.query.return_value.filter_by.return_value.update.assert_called() # --- delete_subscription --- class TestDeleteSubscription: """Tests for subscription deletion.""" def test_returns_false_for_nonexistent(self): session = _make_mock_session() session.query.return_value.filter_by.return_value.first.return_value = ( None ) fm = FolderManager(session) assert fm.delete_subscription("nonexistent") is False def test_deletes_existing_subscription(self): session = _make_mock_session() sub = _make_mock_sub() session.query.return_value.filter_by.return_value.first.return_value = ( sub ) fm = FolderManager(session) result = fm.delete_subscription("sub-1") assert result is True session.delete.assert_called_once_with(sub) session.commit.assert_called_once() # --- update_subscription --- class TestUpdateSubscription: """Tests for subscription updates.""" def test_returns_none_for_nonexistent(self): session = _make_mock_session() session.query.return_value.filter_by.return_value.first.return_value = ( None ) fm = FolderManager(session) result = fm.update_subscription("nonexistent", status="paused") assert result is None def test_updates_status(self): session = _make_mock_session() sub = _make_mock_sub() session.query.return_value.filter_by.return_value.first.return_value = ( sub ) fm = FolderManager(session) result = fm.update_subscription("sub-1", status="paused") assert result is not None session.commit.assert_called_once() def test_does_not_update_id(self): session = _make_mock_session() sub = _make_mock_sub() session.query.return_value.filter_by.return_value.first.return_value = ( sub ) fm = FolderManager(session) fm.update_subscription("sub-1", id="should-not-change") # id is protected def test_recalculates_next_refresh_on_interval_change(self): session = _make_mock_session() sub = _make_mock_sub() sub.last_refresh = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc) sub.next_refresh = datetime(2025, 6, 15, 16, 0, 0, tzinfo=timezone.utc) session.query.return_value.filter_by.return_value.first.return_value = ( sub ) fm = FolderManager(session) fm.update_subscription("sub-1", refresh_interval_minutes=120) # next_refresh should be last_refresh + 120 minutes expected = datetime(2025, 6, 15, 14, 0, 0, tzinfo=timezone.utc) assert sub.next_refresh == expected def test_uses_now_when_no_last_refresh(self): session = _make_mock_session() sub = _make_mock_sub() sub.last_refresh = None sub.next_refresh = None session.query.return_value.filter_by.return_value.first.return_value = ( sub ) fm = FolderManager(session) fm.update_subscription("sub-1", refresh_interval_minutes=60) # Should set next_refresh to now + 60 minutes assert sub.next_refresh is not None def test_sets_updated_at(self): session = _make_mock_session() sub = _make_mock_sub() session.query.return_value.filter_by.return_value.first.return_value = ( sub ) fm = FolderManager(session) fm.update_subscription("sub-1", status="active") assert sub.updated_at is not None # --- get_subscription_stats --- class TestGetSubscriptionStats: """Tests for subscription statistics.""" def test_returns_dict_with_expected_keys(self): session = _make_mock_session() session.query.return_value.count.return_value = 0 session.query.return_value.filter_by.return_value.count.return_value = 0 session.query.return_value.order_by.return_value.all.return_value = [] fm = FolderManager(session) result = fm.get_subscription_stats("user1") assert "total" in result assert "active" in result assert "by_type" in result assert "folders" in result def test_by_type_includes_search_and_topic(self): session = _make_mock_session() session.query.return_value.count.return_value = 5 session.query.return_value.filter_by.return_value.count.return_value = 2 session.query.return_value.order_by.return_value.all.return_value = [] fm = FolderManager(session) result = fm.get_subscription_stats("user1") assert "search" in result["by_type"] assert "topic" in result["by_type"]