SPB Git forge

spb/api-ka

Public

API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com

48commits 1branches 0releases
5.9 MBsize
maindefault branch
19 days agolast push
Python 60.9% HTML 21% TypeScript 7.3% JavaScript 5.2% CSS 4.8% Shell 0.8%
5.9 KB · 175 lines python
Raw Blame History
1# ============================================2# Projet   : API-KA3# Fichier  : tests/test_search.py4# Node     : m3u96b5# Author   : Simon-Pierre Boucher6# Contact  : contact@spboucher.ai7# Date     : 2026-08-238# ============================================9"""Tests des routes /api/v1/search et /api/v1/suggest (proxy Trouve·Ka).1011Le moteur amont est mocké (monkeypatch de ``search._get``) : aucun appel12réseau réel pendant la suite de tests.13"""1415from __future__ import annotations1617from typing import Any1819import httpx20import pytest21from fastapi.testclient import TestClient2223from src.api.main import app24from src.api.routes import search as search_module2526FAKE_SEARCH = {27    "query": "montreal",28    "total": 42,29    "took_ms": 12,30    "page": 1,31    "limit": 3,32    "semantic": True,33    "reranked": True,34    "related": ["hôtel montreal"],35    "results": [36        {37            "title": "Montréal",38            "url": "https://example.com/montreal",39            "display_url": "example.com › montreal",40            "snippet": "La métropole du Québec.",41            "domain": "example.com",42            "language": "fr",43            "quebec_score": 0.9,44            "badges": [],45            "published_at": None,46            "image": None,47        }48    ],49}5051FAKE_SUGGEST = {"suggestions": ["logement montreal", "logement quebec"]}525354@pytest.fixture(scope="module")55def client():56    """Client de test FastAPI avec cycle de vie (lifespan) actif."""57    with TestClient(app) as test_client:58        yield test_client596061def _mock_upstream(monkeypatch: pytest.MonkeyPatch, captured: dict[str, Any]) -> None:62    """Remplace le GET amont par un faux moteur qui capture url + params."""6364    async def fake_get(url: str, params: dict[str, Any]) -> Any:65        captured["url"] = url66        captured["params"] = params67        if "suggest" in url:68            return FAKE_SUGGEST69        return FAKE_SEARCH7071    monkeypatch.setattr(search_module, "_get", fake_get)727374def test_search_envelope(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:75    """GET /api/v1/search proxifie le moteur et renvoie l'enveloppe uniforme."""76    captured: dict[str, Any] = {}77    _mock_upstream(monkeypatch, captured)78    response = client.get("/api/v1/search", params={"q": "montreal", "limit": 3})79    assert response.status_code == 20080    body = response.json()81    assert body["success"] is True82    assert body["data"] == FAKE_SEARCH["results"]83    assert body["meta"]["total"] == 4284    assert body["meta"]["page"] == 185    assert body["meta"]["limit"] == 386    assert body["meta"]["query"] == "montreal"87    assert body["meta"]["took_ms"] == 1288    assert body["meta"]["related"] == ["hôtel montreal"]89    assert body["meta"]["source"] == "trouve-ka"90    assert captured["params"] == {"q": "montreal", "page": 1, "limit": 3}919293def test_search_forwards_optional_filters(94    client: TestClient, monkeypatch: pytest.MonkeyPatch95) -> None:96    """Les filtres optionnels (language, site, category, freshness) sont transmis."""97    captured: dict[str, Any] = {}98    _mock_upstream(monkeypatch, captured)99    response = client.get(100        "/api/v1/search",101        params={102            "q": "logement",103            "language": "fr",104            "site": "www.lou-ka.com",105            "category": "immobilier",106            "freshness": "week",107        },108    )109    assert response.status_code == 200110    assert captured["params"]["language"] == "fr"111    assert captured["params"]["site"] == "www.lou-ka.com"112    assert captured["params"]["category"] == "immobilier"113    assert captured["params"]["freshness"] == "week"114115116def test_search_validation(client: TestClient) -> None:117    """q est requis, limit plafonné à 50, freshness/language contraints."""118    assert client.get("/api/v1/search").status_code == 422119    assert client.get("/api/v1/search", params={"q": ""}).status_code == 422120    assert (121        client.get("/api/v1/search", params={"q": "x", "limit": 51}).status_code == 422122    )123    assert (124        client.get("/api/v1/search", params={"q": "x", "language": "es"}).status_code125        == 422126    )127    assert (128        client.get("/api/v1/search", params={"q": "x", "freshness": "hour"}).status_code129        == 422130    )131132133def test_search_upstream_error_is_502(134    client: TestClient, monkeypatch: pytest.MonkeyPatch135) -> None:136    """Une erreur httpx du moteur amont devient un 502 explicite."""137138    async def broken(url: str, params: dict[str, Any]) -> Any:139        raise httpx.ConnectError("moteur injoignable")140141    monkeypatch.setattr(search_module, "_get", broken)142    response = client.get("/api/v1/search", params={"q": "montreal"})143    assert response.status_code == 502144    assert "Trouve·Ka" in response.json()["detail"]145146147def test_suggest_envelope(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:148    """GET /api/v1/suggest renvoie les suggestions dans l'enveloppe uniforme."""149    captured: dict[str, Any] = {}150    _mock_upstream(monkeypatch, captured)151    response = client.get("/api/v1/suggest", params={"q": "logem"})152    assert response.status_code == 200153    body = response.json()154    assert body["success"] is True155    assert body["data"] == FAKE_SUGGEST["suggestions"]156    assert body["meta"]["source"] == "trouve-ka"157    assert captured["params"] == {"q": "logem"}158159160def test_suggest_min_length(client: TestClient) -> None:161    """q exige au moins 2 caractères."""162    assert client.get("/api/v1/suggest", params={"q": "l"}).status_code == 422163164165def test_search_not_captured_by_service_catchall(client: TestClient) -> None:166    """/api/v1/search ne doit PAS tomber dans le catch-all /api/v1/{service}.167168    Sans paramètres, la route search répond 422 (q requis) ; si le catch-all169    l'attrapait, on obtiendrait le 404 « Service inconnu : search ».170    """171    response = client.get("/api/v1/search")172    assert response.status_code == 422173    response = client.get("/api/v1/notaservice")174    assert response.status_code == 404175