""" 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={ "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["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={ "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