423 lines
14 KiB
Python
423 lines
14 KiB
Python
"""
|
|
End-to-end tests for the Word Bank Pack feature.
|
|
|
|
Admin endpoints require a user whose email matches ADMIN_USER_EMAILS (admin@test.com).
|
|
User endpoints require any authenticated user.
|
|
"""
|
|
|
|
import uuid
|
|
import httpx
|
|
import pytest
|
|
|
|
ADMIN_EMAIL = "admin@test.com"
|
|
ADMIN_PASSWORD = "adminpassword123"
|
|
USER_EMAIL = "packuser@example.com"
|
|
USER_PASSWORD = "userpassword123"
|
|
|
|
|
|
# ── Auth helpers ──────────────────────────────────────────────────────────────
|
|
|
|
def _register_and_login(client: httpx.Client, email: str, password: str) -> str:
|
|
"""Return a Bearer token for the given credentials, registering first if needed."""
|
|
client.post("/auth/register", json={"email": email, "password": password})
|
|
resp = client.post("/auth/login", json={"email": email, "password": password})
|
|
return resp.json()["access_token"]
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_client(client: httpx.Client) -> httpx.Client:
|
|
token = _register_and_login(client, ADMIN_EMAIL, ADMIN_PASSWORD)
|
|
client.headers["Authorization"] = f"Bearer {token}"
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def user_client(client: httpx.Client) -> httpx.Client:
|
|
token = _register_and_login(client, USER_EMAIL, USER_PASSWORD)
|
|
client.headers["Authorization"] = f"Bearer {token}"
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def unauthed_client(client: httpx.Client) -> httpx.Client:
|
|
return client
|
|
|
|
|
|
# ── Admin: create / list / update / publish ───────────────────────────────────
|
|
|
|
def test_admin_creates_pack(admin_client: httpx.Client):
|
|
resp = admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": "Food & Drink",
|
|
"name_target": "La Nourriture et les Boissons",
|
|
"description": "Common food and drink vocabulary.",
|
|
"description_target": "Vocabulaire courant de nourriture et de boissons.",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": ["A1", "A2"],
|
|
},
|
|
)
|
|
assert resp.status_code == 201
|
|
body = resp.json()
|
|
assert body["name"] == "Food & Drink"
|
|
assert body["is_published"] is False
|
|
assert body["proficiencies"] == ["A1", "A2"]
|
|
assert "id" in body
|
|
|
|
|
|
def test_non_admin_cannot_create_pack(user_client: httpx.Client):
|
|
resp = user_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": "Sneaky Pack",
|
|
"name_target": "Pack Sournois",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": [],
|
|
},
|
|
)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_admin_lists_packs_including_unpublished(admin_client: httpx.Client):
|
|
admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": f"Draft Pack {uuid.uuid4()}",
|
|
"name_target": "Paquet Brouillon",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": [],
|
|
},
|
|
)
|
|
resp = admin_client.get("/api/admin/packs")
|
|
assert resp.status_code == 200
|
|
packs = resp.json()
|
|
assert isinstance(packs, list)
|
|
assert len(packs) >= 1
|
|
|
|
|
|
def test_admin_updates_pack(admin_client: httpx.Client):
|
|
create_resp = admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": "Original Name",
|
|
"name_target": "Nom Original",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": ["A1"],
|
|
},
|
|
)
|
|
pack_id = create_resp.json()["id"]
|
|
|
|
resp = admin_client.patch(
|
|
f"/api/admin/packs/{pack_id}",
|
|
json={"name": "Updated Name", "proficiencies": ["A1", "A2"]},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "Updated Name"
|
|
assert resp.json()["proficiencies"] == ["A1", "A2"]
|
|
|
|
|
|
def test_admin_publishes_pack(admin_client: httpx.Client):
|
|
create_resp = admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": "Soon Published",
|
|
"name_target": "Bientôt Publié",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": [],
|
|
},
|
|
)
|
|
pack_id = create_resp.json()["id"]
|
|
|
|
resp = admin_client.post(f"/api/admin/packs/{pack_id}/publish")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["is_published"] is True
|
|
|
|
|
|
# ── Admin: entries and flashcard templates ────────────────────────────────────
|
|
|
|
def _create_published_pack(admin_client: httpx.Client) -> str:
|
|
resp = admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": f"Test Pack {uuid.uuid4()}",
|
|
"name_target": "Paquet Test",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": ["A1"],
|
|
},
|
|
)
|
|
pack_id = resp.json()["id"]
|
|
admin_client.post(f"/api/admin/packs/{pack_id}/publish")
|
|
return pack_id
|
|
|
|
|
|
def test_admin_adds_entry_to_pack(admin_client: httpx.Client):
|
|
pack_id = _create_published_pack(admin_client)
|
|
resp = admin_client.post(
|
|
f"/api/admin/packs/{pack_id}/entries",
|
|
json={"surface_text": "bonjour"},
|
|
)
|
|
assert resp.status_code == 201
|
|
assert resp.json()["surface_text"] == "bonjour"
|
|
assert resp.json()["pack_id"] == pack_id
|
|
|
|
|
|
def test_admin_adds_flashcard_template_to_entry(admin_client: httpx.Client):
|
|
pack_id = _create_published_pack(admin_client)
|
|
entry_resp = admin_client.post(
|
|
f"/api/admin/packs/{pack_id}/entries",
|
|
json={"surface_text": "aller"},
|
|
)
|
|
entry_id = entry_resp.json()["id"]
|
|
|
|
resp = admin_client.post(
|
|
f"/api/admin/packs/{pack_id}/entries/{entry_id}/flashcards",
|
|
json={
|
|
"card_direction": "target_to_source",
|
|
"prompt_text": "aller",
|
|
"answer_text": "to go",
|
|
"prompt_context_text": "il veut [aller] au cinéma",
|
|
"answer_context_text": "he wants [to go] to the cinema",
|
|
},
|
|
)
|
|
assert resp.status_code == 201
|
|
body = resp.json()
|
|
assert body["card_direction"] == "target_to_source"
|
|
assert body["prompt_context_text"] == "il veut [aller] au cinéma"
|
|
|
|
|
|
def test_admin_get_pack_detail_includes_entries_and_templates(admin_client: httpx.Client):
|
|
pack_id = _create_published_pack(admin_client)
|
|
entry_resp = admin_client.post(
|
|
f"/api/admin/packs/{pack_id}/entries",
|
|
json={"surface_text": "maison"},
|
|
)
|
|
entry_id = entry_resp.json()["id"]
|
|
admin_client.post(
|
|
f"/api/admin/packs/{pack_id}/entries/{entry_id}/flashcards",
|
|
json={
|
|
"card_direction": "source_to_target",
|
|
"prompt_text": "house",
|
|
"answer_text": "maison",
|
|
},
|
|
)
|
|
|
|
resp = admin_client.get(f"/api/admin/packs/{pack_id}")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert len(body["entries"]) == 1
|
|
assert body["entries"][0]["surface_text"] == "maison"
|
|
assert len(body["entries"][0]["flashcard_templates"]) == 1
|
|
|
|
|
|
def test_admin_removes_entry_from_pack(admin_client: httpx.Client):
|
|
pack_id = _create_published_pack(admin_client)
|
|
entry_resp = admin_client.post(
|
|
f"/api/admin/packs/{pack_id}/entries",
|
|
json={"surface_text": "chat"},
|
|
)
|
|
entry_id = entry_resp.json()["id"]
|
|
|
|
del_resp = admin_client.delete(f"/api/admin/packs/{pack_id}/entries/{entry_id}")
|
|
assert del_resp.status_code == 204
|
|
|
|
detail = admin_client.get(f"/api/admin/packs/{pack_id}")
|
|
assert all(e["id"] != entry_id for e in detail.json()["entries"])
|
|
|
|
|
|
# ── User: browse published packs ──────────────────────────────────────────────
|
|
|
|
def test_user_only_sees_published_packs(admin_client: httpx.Client, user_client: httpx.Client):
|
|
# Create and leave unpublished
|
|
admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": f"Hidden {uuid.uuid4()}",
|
|
"name_target": "Caché",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": [],
|
|
},
|
|
)
|
|
# Create and publish
|
|
create_resp = admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": f"Visible {uuid.uuid4()}",
|
|
"name_target": "Visible",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": [],
|
|
},
|
|
)
|
|
visible_id = create_resp.json()["id"]
|
|
admin_client.post(f"/api/admin/packs/{visible_id}/publish")
|
|
|
|
resp = user_client.get("/api/packs", params={"source_lang": "en", "target_lang": "fr"})
|
|
assert resp.status_code == 200
|
|
ids = [p["id"] for p in resp.json()]
|
|
assert visible_id in ids
|
|
|
|
|
|
def test_user_cannot_see_unpublished_pack_by_id(
|
|
admin_client: httpx.Client, user_client: httpx.Client
|
|
):
|
|
create_resp = admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": "Secret Draft",
|
|
"name_target": "Brouillon Secret",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": [],
|
|
},
|
|
)
|
|
pack_id = create_resp.json()["id"]
|
|
|
|
resp = user_client.get(f"/api/packs/{pack_id}")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_user_sees_surface_texts_in_pack_detail(
|
|
admin_client: httpx.Client, user_client: httpx.Client
|
|
):
|
|
pack_id = _create_published_pack(admin_client)
|
|
admin_client.post(
|
|
f"/api/admin/packs/{pack_id}/entries", json={"surface_text": "chat"}
|
|
)
|
|
admin_client.post(
|
|
f"/api/admin/packs/{pack_id}/entries", json={"surface_text": "chien"}
|
|
)
|
|
|
|
resp = user_client.get(f"/api/packs/{pack_id}")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["entry_count"] == 2
|
|
assert set(body["surface_texts"]) == {"chat", "chien"}
|
|
|
|
|
|
# ── User: add pack to bank ────────────────────────────────────────────────────
|
|
|
|
def _setup_fresh_user(client: httpx.Client) -> None:
|
|
"""Register and log in as a fresh user (sets Authorization header on client)."""
|
|
email = f"packtest-{uuid.uuid4()}@example.com"
|
|
client.post("/auth/register", json={"email": email, "password": "password123"})
|
|
token_resp = client.post("/auth/login", json={"email": email, "password": "password123"})
|
|
client.headers["Authorization"] = f"Bearer {token_resp.json()['access_token']}"
|
|
|
|
|
|
def test_add_pack_to_bank_creates_bank_entries(
|
|
admin_client: httpx.Client, client: httpx.Client
|
|
):
|
|
pack_id = _create_published_pack(admin_client)
|
|
admin_client.post(f"/api/admin/packs/{pack_id}/entries", json={"surface_text": "bonjour"})
|
|
admin_client.post(f"/api/admin/packs/{pack_id}/entries", json={"surface_text": "merci"})
|
|
|
|
_setup_fresh_user(client)
|
|
|
|
resp = client.post(
|
|
f"/api/packs/{pack_id}/add-to-bank",
|
|
json={"source_lang": "en", "target_lang": "fr"},
|
|
)
|
|
assert resp.status_code == 201
|
|
body = resp.json()
|
|
assert set(body["added"]) == {"bonjour", "merci"}
|
|
|
|
|
|
def test_add_unpublished_pack_to_bank_returns_404(
|
|
admin_client: httpx.Client, client: httpx.Client
|
|
):
|
|
create_resp = admin_client.post(
|
|
"/api/admin/packs",
|
|
json={
|
|
"name": "Draft Only",
|
|
"name_target": "Brouillon Seulement",
|
|
"description": "d",
|
|
"description_target": "d",
|
|
"source_lang": "en",
|
|
"target_lang": "fr",
|
|
"proficiencies": [],
|
|
},
|
|
)
|
|
pack_id = create_resp.json()["id"]
|
|
_setup_fresh_user(client)
|
|
|
|
resp = client.post(
|
|
f"/api/packs/{pack_id}/add-to-bank",
|
|
json={"source_lang": "en", "target_lang": "fr"},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_add_pack_duplicate_plain_card_returns_409(
|
|
admin_client: httpx.Client, client: httpx.Client
|
|
):
|
|
"""Adding a pack whose plain-card entry the user already has returns 409."""
|
|
pack_id = _create_published_pack(admin_client)
|
|
admin_client.post(f"/api/admin/packs/{pack_id}/entries", json={"surface_text": "maison"})
|
|
|
|
_setup_fresh_user(client)
|
|
|
|
# Add the pack once — succeeds
|
|
client.post(
|
|
f"/api/packs/{pack_id}/add-to-bank",
|
|
json={"source_lang": "en", "target_lang": "fr"},
|
|
)
|
|
|
|
# Add it again — same plain card, should 409
|
|
resp = client.post(
|
|
f"/api/packs/{pack_id}/add-to-bank",
|
|
json={"source_lang": "en", "target_lang": "fr"},
|
|
)
|
|
assert resp.status_code == 409
|
|
assert "maison" in resp.json()["detail"]
|
|
|
|
|
|
# ── BFF: pack selection screen ────────────────────────────────────────────────
|
|
|
|
def test_bff_packs_shows_already_added_flag(
|
|
admin_client: httpx.Client, client: httpx.Client
|
|
):
|
|
pack_id = _create_published_pack(admin_client)
|
|
admin_client.post(f"/api/admin/packs/{pack_id}/entries", json={"surface_text": "eau"})
|
|
|
|
_setup_fresh_user(client)
|
|
|
|
# Before adding
|
|
resp = client.get("/bff/packs", params={"source_lang": "en", "target_lang": "fr"})
|
|
assert resp.status_code == 200
|
|
pack_item = next((p for p in resp.json() if p["id"] == pack_id), None)
|
|
assert pack_item is not None
|
|
assert pack_item["already_added"] is False
|
|
|
|
# Add the pack
|
|
client.post(
|
|
f"/api/packs/{pack_id}/add-to-bank",
|
|
json={"source_lang": "en", "target_lang": "fr"},
|
|
)
|
|
|
|
# After adding
|
|
resp = client.get("/bff/packs", params={"source_lang": "en", "target_lang": "fr"})
|
|
pack_item = next((p for p in resp.json() if p["id"] == pack_id), None)
|
|
assert pack_item["already_added"] is True
|