Archived
122 lines
4.2 KiB
Python
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"]
|