test(points): cover the read paths and the query service
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>
This commit is contained in:
103
tests/unit/application/points/test_queries.py
Normal file
103
tests/unit/application/points/test_queries.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""`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
|
||||
Reference in New Issue
Block a user