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/domain/test_collections.py
T

122 lines
4.2 KiB
Python

"""Tests for collection identity and domain behavior."""
from __future__ import annotations
import re
from dataclasses import dataclass, field
import pytest
from recoll_modern.domain.collections import (
CollectionPage,
CollectionPaths,
CollectionRepository,
CollectionService,
normalize_name,
)
from recoll_modern.domain.errors import AppError
from recoll_modern.domain.models import Collection, CollectionCreate, CollectionPatch, _Unset
@dataclass
class MemoryCollectionRepository:
"""Small real-behavior repository used to isolate service behavior."""
collections: dict[str, Collection] = field(default_factory=dict)
created: list[CollectionCreate] = field(default_factory=list)
operations: list[str] = field(default_factory=list)
def create(self, collection: CollectionCreate) -> Collection:
self.operations.append("create")
identifier = f"c{len(self.collections):010x}"
result = Collection(id=identifier, name=collection.name, description=collection.description)
self.collections[identifier] = result
self.created.append(collection)
return result
def get(self, collection_id: str) -> Collection | None:
self.operations.append("get")
return self.collections.get(collection_id)
def list(self, limit: int, offset: int) -> CollectionPage:
self.operations.append("list")
values = list(self.collections.values())
return values[offset : offset + limit], len(values)
def patch(self, collection_id: str, patch: CollectionPatch) -> Collection:
self.operations.append("patch")
collection = self.collections[collection_id]
description = (
collection.description
if isinstance(patch.description, _Unset)
else patch.description
)
result = Collection(
id=collection.id,
name=patch.name if patch.name is not None else collection.name,
description=description,
)
self.collections[collection_id] = result
return result
def delete(self, collection_id: str) -> CollectionPaths:
self.operations.append("delete")
del self.collections[collection_id]
return []
@pytest.fixture
def repository() -> CollectionRepository:
"""Return an in-memory repository for service-level behavior tests."""
return MemoryCollectionRepository()
@pytest.fixture
def service(repository: CollectionRepository) -> CollectionService:
"""Create the service under test."""
return CollectionService(repository)
def test_collection_ids_are_opaque(service: CollectionService) -> None:
"""Fail if a collection ID is not the required opaque server value."""
collection = service.create(CollectionCreate(name="Tech Books"))
assert re.fullmatch(r"c[0-9a-f]{10}", collection.id)
def test_normalize_name_preserves_display_name_and_casefolds_nfc() -> None:
"""Fail if display names are altered or equivalent Unicode names get different keys."""
name, name_key = normalize_name("Stra\u00dfe")
assert name == "Stra\u00dfe"
assert name_key == "strasse"
def test_normalize_name_rejects_whitespace_only_name() -> None:
"""Fail if a name without a visible character can reach storage."""
with pytest.raises(AppError) as caught:
normalize_name(" \t\n ")
assert caught.value.code == "validation_failed"
def test_patch_rejects_unknown_collection(service: CollectionService) -> None:
"""Fail if an update silently succeeds for a missing collection."""
with pytest.raises(AppError) as caught:
service.patch("c000000000", CollectionPatch(name="Reference"))
assert caught.value.code == "collection_not_found"
def test_rename_does_not_enqueue_work() -> None:
"""Fail if a metadata-only rename performs work beyond repository lookup and patch."""
repository = MemoryCollectionRepository()
service = CollectionService(repository)
collection = service.create(CollectionCreate(name="Tech Books"))
repository.operations.clear()
changed = service.patch(collection.id, CollectionPatch(name="Reference"))
assert changed.name == "Reference"
assert repository.operations == ["get", "patch"]