Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

408 lines
14 KiB
Python

"""
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"]