This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
recoll-modern/tests/storage/test_collection_repository.py
T

158 lines
6.0 KiB
Python

"""Tests for SQLite collection persistence."""
from pathlib import Path
import pytest
from recoll_modern.domain.errors import AppError
from recoll_modern.domain.models import CollectionCreate, CollectionPatch
from recoll_modern.storage.collections import SQLiteCollectionRepository
from recoll_modern.storage.database import Database
@pytest.fixture
def database(tmp_path: Path) -> Database:
"""Provide a migrated database for each repository test."""
result = Database(tmp_path / "recoll-modern.sqlite3")
result.migrate()
return result
@pytest.fixture
def repository(database: Database) -> SQLiteCollectionRepository:
"""Create the repository under test."""
return SQLiteCollectionRepository(database)
def test_create_assigns_opaque_id_and_persists_optional_description(
repository: SQLiteCollectionRepository,
) -> None:
"""Fail if creation leaks a caller-controlled ID or loses the description."""
collection = repository.create(CollectionCreate(name="Tech Books", description="Shelf one"))
assert collection.id.startswith("c")
assert len(collection.id) == 11
assert collection.name == "Tech Books"
assert collection.description == "Shelf one"
assert repository.get(collection.id) == collection
def test_create_maps_normalized_name_conflict_to_app_error(
repository: SQLiteCollectionRepository,
) -> None:
"""Fail if equivalent Unicode names expose a raw SQLite uniqueness error."""
repository.create(CollectionCreate(name="Stra\u00dfe"))
with pytest.raises(AppError) as caught:
repository.create(CollectionCreate(name="STRASSE"))
assert caught.value.code == "collection_name_taken"
assert caught.value.status == 409
def test_list_returns_page_and_total_count(repository: SQLiteCollectionRepository) -> None:
"""Fail if list pagination omits the full collection count or returns the wrong page."""
first = repository.create(CollectionCreate(name="One"))
second = repository.create(CollectionCreate(name="Two"))
repository.create(CollectionCreate(name="Three"))
page, total = repository.list(limit=1, offset=1)
assert total == 3
assert page == [second]
assert first not in page
def test_patch_updates_only_requested_fields(repository: SQLiteCollectionRepository) -> None:
"""Fail if a rename alters an unchanged description or does not persist the new name."""
collection = repository.create(CollectionCreate(name="Tech Books", description="Shelf one"))
changed = repository.patch(collection.id, CollectionPatch(name="Reference"))
assert changed.name == "Reference"
assert changed.description == "Shelf one"
assert repository.get(collection.id) == changed
def test_patch_can_clear_description(repository: SQLiteCollectionRepository) -> None:
"""Fail if an explicit null description cannot remove existing metadata."""
collection = repository.create(CollectionCreate(name="Tech Books", description="Shelf one"))
changed = repository.patch(collection.id, CollectionPatch(description=None))
assert changed.description is None
assert repository.get(collection.id) == changed
def test_delete_returns_canonical_paths_before_memberships_cascade(
database: Database, repository: SQLiteCollectionRepository
) -> None:
"""Fail if deletion loses the paths that must later be reindexed."""
collection = repository.create(CollectionCreate(name="Tech Books"))
with database.transaction() as connection:
connection.execute(
"INSERT INTO documents VALUES (?, ?, ?, ?, ?)",
("a" * 64, 1, "present", "2026-09-18T00:00:00+00:00", "2026-09-18T00:00:00+00:00"),
)
connection.execute(
"INSERT INTO document_paths "
"(document_hash, relative_path, is_canonical, last_seen_at, missing_at) "
"VALUES (?, ?, ?, ?, ?)",
("a" * 64, "books/a.pdf", 1, "2026-09-18T00:00:00+00:00", None),
)
connection.execute(
"INSERT INTO direct_memberships VALUES (?, ?, ?)",
(collection.id, "a" * 64, "2026-09-18T00:00:00+00:00"),
)
paths = repository.delete(collection.id)
assert paths == ["books/a.pdf"]
assert repository.get(collection.id) is None
with database.connect() as connection:
assert connection.execute("SELECT COUNT(*) FROM direct_memberships").fetchone()[0] == 0
def test_delete_includes_non_excluded_rule_memberships(
database: Database, repository: SQLiteCollectionRepository
) -> None:
"""Fail if deletion misses canonical paths contributed by persistent rules."""
collection = repository.create(CollectionCreate(name="Tech Books"))
with database.transaction() as connection:
connection.execute(
"INSERT INTO documents VALUES (?, ?, ?, ?, ?)",
("b" * 64, 1, "present", "2026-09-18T00:00:00+00:00", "2026-09-18T00:00:00+00:00"),
)
connection.execute(
"INSERT INTO document_paths "
"(document_hash, relative_path, is_canonical, last_seen_at, missing_at) "
"VALUES (?, ?, ?, ?, ?)",
("b" * 64, "books/b.pdf", 1, "2026-09-18T00:00:00+00:00", None),
)
connection.execute(
"INSERT INTO directory_rules VALUES (?, ?, ?, ?, ?)",
(
"r1",
collection.id,
"books",
"2026-09-18T00:00:00+00:00",
"2026-09-18T00:00:00+00:00",
),
)
connection.execute(
"INSERT INTO rule_memberships VALUES (?, ?, ?)",
("r1", "b" * 64, "2026-09-18T00:00:00+00:00"),
)
assert repository.delete(collection.id) == ["books/b.pdf"]
def test_repository_does_not_leak_sqlite_error_for_name_conflicts(
repository: SQLiteCollectionRepository,
) -> None:
"""Fail if the domain boundary exposes database implementation failures."""
repository.create(CollectionCreate(name="Same"))
with pytest.raises(AppError):
repository.create(CollectionCreate(name="same"))