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
433 lines
18 KiB
Python
433 lines
18 KiB
Python
"""
|
|
Tests for database/library_init.py.
|
|
|
|
Tests the library initialization module which handles:
|
|
- Seeding source_types table with predefined types
|
|
- Creating the default "Library" collection
|
|
- Full library initialization orchestration
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from tests.test_utils import add_src_to_path
|
|
|
|
add_src_to_path()
|
|
|
|
from local_deep_research.database.library_init import ( # noqa: E402
|
|
seed_source_types,
|
|
ensure_default_library_collection,
|
|
ensure_research_history_collection,
|
|
initialize_library_for_user,
|
|
get_default_library_id,
|
|
get_source_type_id,
|
|
)
|
|
|
|
|
|
class TestSeedSourceTypes:
|
|
"""Tests for seed_source_types function."""
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
@patch("local_deep_research.database.library_init.uuid.uuid4")
|
|
def test_creates_all_predefined_types_when_none_exist(
|
|
self, mock_uuid, mock_get_session
|
|
):
|
|
"""Should create all predefined source types."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
# Simulate no existing types
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
mock_uuid.return_value = "test-uuid-123"
|
|
|
|
seed_source_types("testuser", "testpass")
|
|
|
|
# Should have queried for each type and added 6 new ones
|
|
# (research_download, user_upload, manual_entry, research_report,
|
|
# research_source, zotero)
|
|
assert mock_session.add.call_count == 6
|
|
mock_session.commit.assert_called_once()
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_skips_existing_types(self, mock_get_session):
|
|
"""Should not duplicate existing source types."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
# Simulate all types already exist
|
|
mock_existing = MagicMock()
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing
|
|
|
|
seed_source_types("testuser", "testpass")
|
|
|
|
# Should not add any new types
|
|
mock_session.add.assert_not_called()
|
|
mock_session.commit.assert_called_once()
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_handles_integrity_error_gracefully(self, mock_get_session):
|
|
"""Should handle IntegrityError gracefully without raising."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
mock_session.commit.side_effect = IntegrityError(
|
|
"statement", "params", "orig"
|
|
)
|
|
|
|
# Should not raise
|
|
seed_source_types("testuser", "testpass")
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_raises_on_unexpected_error(self, mock_get_session):
|
|
"""Should re-raise unexpected exceptions."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.side_effect = RuntimeError(
|
|
"Database connection failed"
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="Database connection failed"):
|
|
seed_source_types("testuser", "testpass")
|
|
|
|
@patch("local_deep_research.database.library_init.logger")
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_logs_creation_messages(self, mock_get_session, mock_logger):
|
|
"""Should log when creating source types."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
|
|
seed_source_types("testuser", "testpass")
|
|
|
|
# Should log info for each created type and final success
|
|
assert mock_logger.info.call_count >= 3
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_creates_types_with_correct_attributes(self, mock_get_session):
|
|
"""Should create source types with correct name, display_name, description, icon."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
|
|
seed_source_types("testuser")
|
|
|
|
# Check the types created
|
|
added_types = [call.args[0] for call in mock_session.add.call_args_list]
|
|
type_names = [t.name for t in added_types]
|
|
|
|
assert "research_download" in type_names
|
|
assert "user_upload" in type_names
|
|
assert "manual_entry" in type_names
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_password_is_optional(self, mock_get_session):
|
|
"""Should work when password is not provided."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = MagicMock()
|
|
|
|
# Should not raise when password is None
|
|
seed_source_types("testuser")
|
|
|
|
mock_get_session.assert_called_once_with("testuser", None)
|
|
|
|
|
|
class TestEnsureDefaultLibraryCollection:
|
|
"""Tests for ensure_default_library_collection function."""
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
@patch("local_deep_research.database.library_init.uuid.uuid4")
|
|
def test_creates_collection_when_none_exists(
|
|
self, mock_uuid, mock_get_session
|
|
):
|
|
"""Should create default Library collection when none exists."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
mock_uuid.return_value = "new-library-uuid"
|
|
|
|
result = ensure_default_library_collection("testuser", "testpass")
|
|
|
|
assert result == "new-library-uuid"
|
|
mock_session.add.assert_called_once()
|
|
mock_session.commit.assert_called_once()
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_returns_existing_collection_id(self, mock_get_session):
|
|
"""Should return ID of existing default collection."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_existing = MagicMock()
|
|
mock_existing.id = "existing-library-uuid"
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing
|
|
|
|
result = ensure_default_library_collection("testuser", "testpass")
|
|
|
|
assert result == "existing-library-uuid"
|
|
mock_session.add.assert_not_called()
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_creates_with_is_default_true(self, mock_get_session):
|
|
"""Should set is_default=True on new collection."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
|
|
ensure_default_library_collection("testuser")
|
|
|
|
# Check that the collection was created with is_default=True
|
|
added_collection = mock_session.add.call_args.args[0]
|
|
assert added_collection.is_default is True
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_creates_with_correct_name_and_type(self, mock_get_session):
|
|
"""Should create collection with name='Library' and type='default_library'."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
|
|
ensure_default_library_collection("testuser")
|
|
|
|
added_collection = mock_session.add.call_args.args[0]
|
|
assert added_collection.name == "Library"
|
|
assert added_collection.collection_type == "default_library"
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_raises_on_database_error(self, mock_get_session):
|
|
"""Should re-raise exceptions from database operations."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.side_effect = RuntimeError("Database error")
|
|
|
|
with pytest.raises(RuntimeError, match="Database error"):
|
|
ensure_default_library_collection("testuser")
|
|
|
|
|
|
class TestEnsureResearchHistoryCollection:
|
|
"""Tests for ensure_research_history_collection function."""
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
@patch("local_deep_research.database.library_init.uuid.uuid4")
|
|
def test_creates_collection_when_none_exists(
|
|
self, mock_uuid, mock_get_session
|
|
):
|
|
"""Should create Research History collection when none exists."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
mock_uuid.return_value = "new-history-uuid"
|
|
|
|
result = ensure_research_history_collection("testuser", "testpass")
|
|
|
|
assert result == "new-history-uuid"
|
|
mock_session.add.assert_called_once()
|
|
mock_session.commit.assert_called_once()
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_returns_existing_collection_id(self, mock_get_session):
|
|
"""Should return ID of existing Research History collection."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_existing = MagicMock()
|
|
mock_existing.id = "existing-history-uuid"
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing
|
|
|
|
result = ensure_research_history_collection("testuser", "testpass")
|
|
|
|
assert result == "existing-history-uuid"
|
|
mock_session.add.assert_not_called()
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_reraises_exception(self, mock_get_session):
|
|
"""Should re-raise exceptions from database operations."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.side_effect = RuntimeError("Database error")
|
|
|
|
with pytest.raises(RuntimeError, match="Database error"):
|
|
ensure_research_history_collection("testuser")
|
|
|
|
|
|
class TestInitializeLibraryForUser:
|
|
"""Tests for initialize_library_for_user function."""
|
|
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_research_history_collection"
|
|
)
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_default_library_collection"
|
|
)
|
|
@patch("local_deep_research.database.library_init.seed_source_types")
|
|
def test_returns_success_result(self, mock_seed, mock_ensure, mock_history):
|
|
"""Should return dict with success=True on success."""
|
|
mock_ensure.return_value = "library-uuid-123"
|
|
mock_history.return_value = "history-uuid-456"
|
|
|
|
result = initialize_library_for_user("testuser", "testpass")
|
|
|
|
assert result["success"] is True
|
|
assert result["source_types_seeded"] is True
|
|
assert result["library_collection_id"] == "library-uuid-123"
|
|
assert result["research_history_collection_id"] == "history-uuid-456"
|
|
assert "error" not in result
|
|
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_research_history_collection"
|
|
)
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_default_library_collection"
|
|
)
|
|
@patch("local_deep_research.database.library_init.seed_source_types")
|
|
def test_returns_error_on_seed_failure(
|
|
self, mock_seed, mock_ensure, mock_history
|
|
):
|
|
"""Should include error message when seed_source_types fails."""
|
|
mock_seed.side_effect = RuntimeError("Seeding failed")
|
|
|
|
result = initialize_library_for_user("testuser")
|
|
|
|
assert result["success"] is False
|
|
assert result["source_types_seeded"] is False
|
|
assert "error" in result
|
|
assert "Seeding failed" in result["error"]
|
|
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_research_history_collection"
|
|
)
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_default_library_collection"
|
|
)
|
|
@patch("local_deep_research.database.library_init.seed_source_types")
|
|
def test_returns_error_on_ensure_failure(
|
|
self, mock_seed, mock_ensure, mock_history
|
|
):
|
|
"""Should include error message when ensure_default_library_collection fails."""
|
|
mock_ensure.side_effect = RuntimeError("Collection creation failed")
|
|
|
|
result = initialize_library_for_user("testuser")
|
|
|
|
assert result["success"] is False
|
|
assert result["source_types_seeded"] is True # Seeding succeeded
|
|
assert result["library_collection_id"] is None
|
|
assert "error" in result
|
|
assert "Collection creation failed" in result["error"]
|
|
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_research_history_collection"
|
|
)
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_default_library_collection"
|
|
)
|
|
@patch("local_deep_research.database.library_init.seed_source_types")
|
|
def test_calls_seed_and_ensure_in_order(
|
|
self, mock_seed, mock_ensure, mock_history
|
|
):
|
|
"""Should call both seed_source_types and ensure_default_library_collection."""
|
|
mock_ensure.return_value = "library-uuid"
|
|
call_order = []
|
|
mock_seed.side_effect = lambda *args, **kwargs: call_order.append(
|
|
"seed"
|
|
)
|
|
mock_ensure.side_effect = lambda *args, **kwargs: (
|
|
call_order.append("ensure"),
|
|
"library-uuid",
|
|
)[1]
|
|
|
|
initialize_library_for_user("testuser", "testpass")
|
|
|
|
assert call_order == ["seed", "ensure"]
|
|
mock_seed.assert_called_once_with("testuser", "testpass")
|
|
mock_ensure.assert_called_once_with("testuser", "testpass")
|
|
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_default_library_collection"
|
|
)
|
|
@patch("local_deep_research.database.library_init.seed_source_types")
|
|
def test_returns_all_expected_keys(self, mock_seed, mock_ensure):
|
|
"""Should return dict with all expected keys."""
|
|
mock_ensure.return_value = "lib-id"
|
|
|
|
result = initialize_library_for_user("testuser")
|
|
|
|
assert "source_types_seeded" in result
|
|
assert "library_collection_id" in result
|
|
assert "success" in result
|
|
|
|
|
|
class TestGetDefaultLibraryId:
|
|
"""Tests for get_default_library_id function."""
|
|
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_default_library_collection"
|
|
)
|
|
def test_returns_library_id(self, mock_ensure):
|
|
"""Should return the library collection ID."""
|
|
mock_ensure.return_value = "default-lib-uuid"
|
|
|
|
result = get_default_library_id("testuser", "testpass")
|
|
|
|
assert result == "default-lib-uuid"
|
|
mock_ensure.assert_called_once_with("testuser", "testpass")
|
|
|
|
@patch(
|
|
"local_deep_research.database.library_init.ensure_default_library_collection"
|
|
)
|
|
def test_creates_library_if_missing(self, mock_ensure):
|
|
"""Should create library if it doesn't exist (via ensure_default_library_collection)."""
|
|
mock_ensure.return_value = "new-lib-uuid"
|
|
|
|
result = get_default_library_id("testuser")
|
|
|
|
assert result == "new-lib-uuid"
|
|
|
|
|
|
class TestGetSourceTypeId:
|
|
"""Tests for get_source_type_id function."""
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_returns_id_for_valid_type(self, mock_get_session):
|
|
"""Should return ID for existing source type."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_source_type = MagicMock()
|
|
mock_source_type.id = "research-download-uuid"
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_source_type
|
|
|
|
result = get_source_type_id("testuser", "research_download", "testpass")
|
|
|
|
assert result == "research-download-uuid"
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_raises_value_error_for_unknown_type(self, mock_get_session):
|
|
"""Should raise ValueError for non-existent type."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
|
|
with pytest.raises(ValueError, match="Source type not found"):
|
|
get_source_type_id("testuser", "nonexistent_type")
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_raises_on_database_error(self, mock_get_session):
|
|
"""Should re-raise database errors after logging."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_session.query.side_effect = RuntimeError("Connection lost")
|
|
|
|
with pytest.raises(RuntimeError, match="Connection lost"):
|
|
get_source_type_id("testuser", "user_upload")
|
|
|
|
@patch("local_deep_research.database.library_init.get_user_db_session")
|
|
def test_password_is_optional(self, mock_get_session):
|
|
"""Should work when password is not provided."""
|
|
mock_session = MagicMock()
|
|
mock_get_session.return_value.__enter__.return_value = mock_session
|
|
mock_source_type = MagicMock()
|
|
mock_source_type.id = "type-uuid"
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_source_type
|
|
|
|
result = get_source_type_id("testuser", "manual_entry")
|
|
|
|
assert result == "type-uuid"
|
|
mock_get_session.assert_called_once_with("testuser", None)
|