docling-studio/document-parser/tests/test_api_stores.py
Pier-Jean Malandrino 245ef40a24 feat(#251): /api/stores router + wiring + schemas Pydantic + tests API
- Schemas camelCase : StoreInfoResponse, StoreResponse, StoreCreate/UpdateRequest, StoreDocEntryResponse
- Router /api/stores : GET (list), POST (create 201), GET/{slug}, PATCH/{slug}, DELETE/{slug} (204)
- Endpoints documents-in-store : GET /{slug}/documents, DELETE /{slug}/documents/{docId}
- Mapping erreurs StoreServiceError → HTTPException via http_status hint
- StoreService étendu : list_documents/remove_document avec injection optionnelle de document_repo (filename humain)
- Wiring lifespan : SqliteStoreRepository + SqliteDocumentStoreLinkRepository + StoreService sur app.state
- include_router(stores_router) après documents/analyses
- Tests API : 18 cas couvrant 200/201/204/404/409/422 + camelCase + cycle complet
2026-05-05 09:38:39 +02:00

200 lines
6.9 KiB
Python

"""Tests for `/api/stores/*` HTTP endpoints (#251)."""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from domain.models import Document, DocumentStoreLink
from domain.value_objects import DocumentStoreLinkState
from main import app
from persistence.database import init_db
from persistence.document_repo import SqliteDocumentRepository
from persistence.document_store_link_repo import SqliteDocumentStoreLinkRepository
from persistence.store_repo import SqliteStoreRepository
from services.store_service import StoreService
@pytest.fixture(autouse=True)
async def setup_db_and_state(monkeypatch, tmp_path):
db_path = str(tmp_path / "test.db")
monkeypatch.setattr("persistence.database.DB_PATH", db_path)
await init_db()
store_repo = SqliteStoreRepository()
link_repo = SqliteDocumentStoreLinkRepository()
document_repo = SqliteDocumentRepository()
original = getattr(app.state, "store_service", None)
app.state.store_service = StoreService(store_repo, link_repo, document_repo)
yield
app.state.store_service = original
@pytest.fixture
def client():
return TestClient(app, raise_server_exceptions=False)
VALID_OS = {
"name": "rh",
"slug": "rh",
"kind": "opensearch",
"embedder": "bge-m3",
"config": {"indexName": "rh"},
}
class TestList:
def test_list_returns_seeded_default(self, client):
resp = client.get("/api/stores")
assert resp.status_code == 200
slugs = [s["slug"] for s in resp.json()]
assert "default" in slugs
def test_list_uses_camel_case(self, client):
resp = client.get("/api/stores")
body = resp.json()[0]
assert "documentCount" in body
assert "isDefault" in body
class TestCreate:
def test_create_201(self, client):
resp = client.post("/api/stores", json=VALID_OS)
assert resp.status_code == 201, resp.text
body = resp.json()
assert body["slug"] == "rh"
assert body["isDefault"] is False
def test_create_duplicate_slug_409(self, client):
client.post("/api/stores", json=VALID_OS)
resp = client.post("/api/stores", json={**VALID_OS, "name": "rh-2"})
assert resp.status_code == 409
def test_create_invalid_kind_422(self, client):
resp = client.post("/api/stores", json={**VALID_OS, "kind": "pinecone"})
assert resp.status_code == 422
def test_create_missing_index_422(self, client):
resp = client.post("/api/stores", json={**VALID_OS, "config": {}})
assert resp.status_code == 422
def test_create_bad_slug_422(self, client):
resp = client.post("/api/stores", json={**VALID_OS, "slug": "RH Corp"})
assert resp.status_code == 422
class TestRead:
def test_get_by_slug(self, client):
client.post("/api/stores", json=VALID_OS)
resp = client.get("/api/stores/rh")
assert resp.status_code == 200
body = resp.json()
assert body["slug"] == "rh"
assert body["embedder"] == "bge-m3"
def test_get_unknown_404(self, client):
resp = client.get("/api/stores/missing")
assert resp.status_code == 404
class TestUpdate:
def test_patch_embedder(self, client):
client.post("/api/stores", json=VALID_OS)
resp = client.patch("/api/stores/rh", json={"embedder": "bge-large"})
assert resp.status_code == 200
assert resp.json()["embedder"] == "bge-large"
def test_patch_rename_slug(self, client):
client.post("/api/stores", json=VALID_OS)
resp = client.patch("/api/stores/rh", json={"slug": "rh-v2"})
assert resp.status_code == 200
assert resp.json()["slug"] == "rh-v2"
assert client.get("/api/stores/rh").status_code == 404
assert client.get("/api/stores/rh-v2").status_code == 200
def test_patch_unknown_404(self, client):
resp = client.patch("/api/stores/missing", json={"embedder": "x"})
assert resp.status_code == 404
class TestDelete:
def test_delete_204(self, client):
client.post("/api/stores", json=VALID_OS)
resp = client.delete("/api/stores/rh")
assert resp.status_code == 204
assert client.get("/api/stores/rh").status_code == 404
def test_delete_default_409(self, client):
resp = client.delete("/api/stores/default")
assert resp.status_code == 409
async def test_delete_with_links_409(self, client):
client.post("/api/stores", json=VALID_OS)
# Seed a link from outside (service-level access).
await SqliteDocumentRepository().insert(
Document(id="d-1", filename="t.pdf", storage_path="/tmp/t.pdf")
)
store = await SqliteStoreRepository().find_by_slug("rh")
assert store is not None
await SqliteDocumentStoreLinkRepository().upsert(
DocumentStoreLink(
id="l-1",
document_id="d-1",
store_id=store.id,
state=DocumentStoreLinkState.INGESTED,
)
)
resp = client.delete("/api/stores/rh")
assert resp.status_code == 409
class TestStoreDocuments:
async def test_list_documents_for_store(self, client):
client.post("/api/stores", json=VALID_OS)
await SqliteDocumentRepository().insert(
Document(id="d-1", filename="hr.pdf", storage_path="/tmp/hr.pdf")
)
store = await SqliteStoreRepository().find_by_slug("rh")
assert store is not None
await SqliteDocumentStoreLinkRepository().upsert(
DocumentStoreLink(
id="l-1",
document_id="d-1",
store_id=store.id,
state=DocumentStoreLinkState.INGESTED,
)
)
resp = client.get("/api/stores/rh/documents")
assert resp.status_code == 200
body = resp.json()
assert len(body) == 1
assert body[0]["docId"] == "d-1"
assert body[0]["filename"] == "hr.pdf"
async def test_remove_document_204(self, client):
client.post("/api/stores", json=VALID_OS)
await SqliteDocumentRepository().insert(
Document(id="d-1", filename="hr.pdf", storage_path="/tmp/hr.pdf")
)
store = await SqliteStoreRepository().find_by_slug("rh")
assert store is not None
await SqliteDocumentStoreLinkRepository().upsert(
DocumentStoreLink(
id="l-1",
document_id="d-1",
store_id=store.id,
state=DocumentStoreLinkState.INGESTED,
)
)
resp = client.delete("/api/stores/rh/documents/d-1")
assert resp.status_code == 204
# Now empty.
listing = client.get("/api/stores/rh/documents")
assert listing.json() == []
def test_remove_unknown_doc_404(self, client):
client.post("/api/stores", json=VALID_OS)
resp = client.delete("/api/stores/rh/documents/missing")
assert resp.status_code == 404