jurix-api / tests /test_services /test_article_reference.py
rahim237cm's picture
Claude Opus 5
feat(recherche): le titre passe devant le corps du texte, et les fautes de frappe passent
893a679
Raw History Blame Contribute Delete
7.03 kB
"""
« article 35 du code minier » ouvre le bon document sur le bon article.
Trois choses devaient exister ensemble : reconnaitre le numero, reconnaitre le
document, et verifier que l'article s'y trouve. Avant, la premiere seule etait
faite — `_parse_article_reference` extrayait bien `doc_hint`, mais personne ne
s'en servait, et `direct_navigation` etait conditionne a `total == 1`, ce qui ne
se produit presque jamais.
Usage:
pytest tests/test_services/test_article_reference.py -v
"""
from datetime import date
import pytest
from app.models.law import Article, Law
from app.schemas.search import SearchRequest
from app.services.article_reference import (
ArticleReference,
normalize_number,
parse_reference,
)
from app.services.search_service import SearchService
from app.services.search_vectors import REINDEX_ARTICLES_SQL, REINDEX_LAWS_SQL
CODE_MINIER = 300
CODE_TRAVAIL = 301
@pytest.fixture
async def codes(db_session, category_ids):
from sqlalchemy import text as sql_text
laws = [
Law(id=CODE_MINIER, reference="LOI-2023-014",
title="Loi N°2023/014 du 19 décembre 2023 portant Code Minier",
content="Code régissant les activités minières au Cameroun.",
type="loi", language="fr", status="published",
category_id=category_ids["Droit de l'Environnement et des Ressources Naturelles"],
publication_date=date(2023, 12, 19)),
Law(id=CODE_TRAVAIL, reference="LOI-1992-007",
title="Loi N°92/007 du 14 août 1992 portant Code du Travail",
content="Code régissant les relations de travail.",
type="loi", language="fr", status="published",
category_id=category_ids["Droit du Travail et Sécurité Sociale"],
publication_date=date(1992, 8, 14)),
]
db_session.add_all(laws)
await db_session.flush()
articles = []
for law_id, numbers in ((CODE_MINIER, ("1", "35", "36")), (CODE_TRAVAIL, ("1", "2"))):
for number in numbers:
articles.append(Article(
law_id=law_id, number=number,
content=(f"Dispositions de l'article {number}. "
"La superficie couverte par le permis de recherche est limitée."),
order=int(number), kind="article", embed=True,
))
db_session.add_all(articles)
await db_session.commit()
await db_session.execute(sql_text(REINDEX_LAWS_SQL))
await db_session.execute(sql_text(REINDEX_ARTICLES_SQL))
await db_session.commit()
return laws
@pytest.fixture
def service(db_session):
return SearchService(db_session, use_cache=False)
class TestParsing:
"""Le module unique remplace huit motifs « article N » divergents."""
@pytest.mark.parametrize("query,number,hint", [
("article 35 du code minier", "35", "code minier"),
("Article 35 de la Constitution", "35", "Constitution"),
("art. 35 du code du travail", "35", "code du travail"),
# « art.35 » sans espace : le motif du RAG exigeait `art\.?\s+`.
("art.35", "35", ""),
("article 35", "35", ""),
("article premier de la constitution", "1", "constitution"),
("article 1er du code minier", "1", "code minier"),
("article L 94 du code des impots", "L 94", "code des impots"),
])
def test_parses(self, query, number, hint):
reference = parse_reference(query)
assert reference == ArticleReference(number, hint), f"pour {query!r}"
@pytest.mark.parametrize("query", [
"code minier", "permis de recherche", "", "les articles du code",
])
def test_not_a_reference(self, query):
assert parse_reference(query) is None
@pytest.mark.parametrize("raw,expected", [
("Article 1er", "1"), ("PREMIER", "1"), ("1ÈRE", "1"),
("deuxieme", "2"), ("35", "35"), ("Art. 35", "35"), ("", ""),
])
def test_normalisation(self, raw, expected):
"""`articles.number` stocke « 1 » : rendre « PREMIER » ne matcherait rien."""
assert normalize_number(raw) == expected
class TestDirectNavigation:
@pytest.mark.asyncio
async def test_opens_the_right_document_on_the_right_article(self, service, codes):
response = await service.search(
SearchRequest(query="article 35 du code minier", mode="text", limit=10)
)
assert response.target_law_id == CODE_MINIER
assert response.target_article == "35"
assert response.direct_navigation is True
@pytest.mark.asyncio
async def test_the_document_hint_tolerates_a_typo(self, service, codes):
"""word_similarity('code miner', 'Loi ... portant Code Minier') = 0,727."""
response = await service.search(
SearchRequest(query="article 35 du code miner", mode="text", limit=10)
)
assert response.target_law_id == CODE_MINIER
assert response.direct_navigation is True
@pytest.mark.asyncio
async def test_the_hint_selects_between_two_codes(self, service, codes):
response = await service.search(
SearchRequest(query="article 1 du code du travail", mode="text", limit=10)
)
assert response.target_law_id == CODE_TRAVAIL
@pytest.mark.asyncio
async def test_an_absent_article_does_not_navigate(self, service, codes):
"""
Ouvrir un document sur un article qui n'y est pas serait pire que ne pas
l'ouvrir : l'utilisateur croirait avoir lu la reponse.
"""
response = await service.search(
SearchRequest(query="article 999 du code minier", mode="text", limit=10)
)
assert response.direct_navigation is False
assert response.target_article == "999"
assert response.results, "les resultats de recherche doivent rester"
@pytest.mark.asyncio
async def test_article_premier_resolves_to_the_stored_number(self, service, codes):
response = await service.search(
SearchRequest(query="article premier du code minier", mode="text", limit=10)
)
assert response.target_article == "1"
assert response.target_law_id == CODE_MINIER
@pytest.mark.asyncio
async def test_a_plain_query_does_not_navigate(self, service, codes):
response = await service.search(
SearchRequest(query="permis de recherche", mode="text", limit=10)
)
assert response.direct_navigation is False
assert response.target_article is None
class TestRemovedEndpoint:
@pytest.mark.asyncio
async def test_the_dead_article_endpoint_is_gone(self, client, db_session):
"""
Repondait 500 : il appelait `_parse_article_reference` ET
`_estimate_page_number`, aucun des deux defini dans ce module. Aucun
test ne le couvrait — c'est ce qui lui a permis de survivre.
"""
response = await client.get("/api/v1/search/article?q=article%205")
assert response.status_code == 404