Why:
- The isolation and pagination guarantees are the ones that fail silently, so
they need tests that would actually notice.
Changes:
- API tests over real Postgres and Qdrant together, because the invariant worth
testing spans both: the tenant Postgres derived is the only one Qdrant is ever
queried with.
- Pagination holds when a point is inserted behind the cursor mid-listing --
the defect an offset cursor would have.
- A route-order guard, since /{point_id} declared first turns count into a 422
and nothing else in the suite would catch it.
- Unit tests for the query service against the fake, including the Arabic to
Persian letterform fold and raising rather than returning None.
Impact:
- Suite goes to 325 passed, 3 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
"""`application/points/queries.py` against the fake repository (ADR-0016).
|
|
|
|
The HTTP-level behaviour is covered in `tests/integration/qdrant/`. What is
|
|
worth isolating here is the small amount of logic the service actually owns —
|
|
raising instead of returning `None`, and folding a query's Arabic letterforms —
|
|
because both are cheap to test directly and expensive to notice when broken.
|
|
"""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from src.application.points.point import PointNotFoundError
|
|
from src.application.points.queries import (
|
|
count_points,
|
|
get_point,
|
|
list_file_points,
|
|
search_points,
|
|
)
|
|
from tests.fakes import FakePointRepository
|
|
from tests.support.point_contract import SeedSpec, build_point, point_id_for
|
|
|
|
pytestmark = [pytest.mark.unit, pytest.mark.asyncio]
|
|
|
|
TENANT = uuid.UUID("11111111-1111-4111-8111-111111111111")
|
|
OTHER_TENANT = uuid.UUID("22222222-2222-4222-8222-222222222222")
|
|
FILE = uuid.UUID("33333333-3333-4333-8333-333333333333")
|
|
|
|
|
|
def _repository(*specs: SeedSpec) -> FakePointRepository:
|
|
repository = FakePointRepository()
|
|
for spec in specs:
|
|
repository.add(build_point(spec))
|
|
return repository
|
|
|
|
|
|
async def test_get_point_returns_the_point_for_its_owner() -> None:
|
|
repository = _repository(SeedSpec(TENANT, FILE, 0, "hello", 1.0))
|
|
|
|
point = await get_point(repository, tenant_id=TENANT, point_id=point_id_for(FILE, 0))
|
|
|
|
assert point.content == "hello"
|
|
|
|
|
|
async def test_get_point_raises_for_another_tenants_point() -> None:
|
|
"""Raising, not returning `None`, so a route cannot serve `200 null`."""
|
|
repository = _repository(SeedSpec(TENANT, FILE, 0, "hello", 1.0))
|
|
|
|
with pytest.raises(PointNotFoundError):
|
|
await get_point(repository, tenant_id=OTHER_TENANT, point_id=point_id_for(FILE, 0))
|
|
|
|
|
|
async def test_get_point_raises_for_an_unknown_point() -> None:
|
|
repository = _repository()
|
|
|
|
with pytest.raises(PointNotFoundError):
|
|
await get_point(repository, tenant_id=TENANT, point_id=uuid.uuid4())
|
|
|
|
|
|
async def test_search_points_folds_arabic_letterforms_before_matching() -> None:
|
|
"""The query is normalized the same way ingestion normalized the content.
|
|
|
|
Stored content carries Persian yeh (U+06CC) because `normalize_persian_text`
|
|
folded it at ingest. A query typed with Arabic yeh (U+064A) is a different
|
|
codepoint and would match nothing — silently, with no error to notice.
|
|
"""
|
|
persian = "مدیریت"
|
|
arabic = "مديريت"
|
|
assert persian != arabic
|
|
|
|
repository = _repository(SeedSpec(TENANT, FILE, 0, persian, 1.0))
|
|
|
|
found = await search_points(repository, tenant_id=TENANT, query=arabic, limit=10)
|
|
|
|
assert [point.content for point in found.points] == [persian]
|
|
|
|
|
|
async def test_search_points_does_not_reach_another_tenant() -> None:
|
|
repository = _repository(SeedSpec(OTHER_TENANT, FILE, 0, "secret", 1.0))
|
|
|
|
found = await search_points(repository, tenant_id=TENANT, query="secret", limit=10)
|
|
|
|
assert found.points == ()
|
|
|
|
|
|
async def test_list_file_points_returns_an_empty_page_for_a_foreign_file() -> None:
|
|
"""Indistinguishable from an unknown file — the same non-disclosure rule."""
|
|
repository = _repository(SeedSpec(OTHER_TENANT, FILE, 0, "secret", 1.0))
|
|
|
|
page = await list_file_points(repository, tenant_id=TENANT, file_id=FILE, limit=10)
|
|
|
|
assert page.points == ()
|
|
assert page.next_cursor is None
|
|
|
|
|
|
async def test_count_points_excludes_inactive_unless_asked() -> None:
|
|
repository = _repository(
|
|
SeedSpec(TENANT, FILE, 0, "live", 1.0),
|
|
SeedSpec(TENANT, FILE, 1, "gone", 2.0, is_active=False),
|
|
)
|
|
|
|
assert await count_points(repository, tenant_id=TENANT) == 1
|
|
assert await count_points(repository, tenant_id=TENANT, include_inactive=True) == 2
|