Spaces:
Running
Running
| """Coverage tests for ``animetix.api.games.animinator`` (solo Animinator oracle | |
| HTTP API: ask + guess). | |
| The async SSE variant (``AniminatorStreamView`` in streams.py) is covered by | |
| tests/api/test_animinator_stream_view.py — nothing here drives it. These tests | |
| exercise the two synchronous DRF views end-to-end through the real Django | |
| session. ``animinator_service`` / ``catalog_service`` are replaced with | |
| instance-level container overrides; ``deduct_berrix`` is patched at the module | |
| namespace so the GPU-turn charge doesn't need a seeded wallet (one test lets it | |
| raise to prove the 402 is not swallowed into a 500). | |
| """ | |
| from unittest.mock import MagicMock, patch | |
| import animetix.api.games.animinator as animinator_mod | |
| import pytest | |
| from animetix.api.billing import PaymentRequired | |
| from animetix.containers import container | |
| from animetix.models import GameplaySession | |
| from dependency_injector import providers | |
| from django.contrib.auth.models import User | |
| from django.urls import reverse | |
| from rest_framework.test import APIClient | |
| def _wire_animinator(): | |
| container.wire(modules=[animinator_mod]) | |
| yield | |
| def api_client(): | |
| return APIClient() | |
| def auth_client(db): | |
| user = User.objects.create_user(username="oracle-player", password="pw") | |
| client = APIClient() | |
| client.force_authenticate(user=user) | |
| return client | |
| def _oracle_service(): | |
| svc = MagicMock() | |
| svc.ask_oracle_stream.return_value = iter(["Il ", "est ", "un ninja."]) | |
| return svc | |
| def _catalog_service(data): | |
| cs = MagicMock() | |
| cs.load_data.return_value = data | |
| return cs | |
| def _catalog_data(): | |
| return { | |
| "lookup": [{"title": "Naruto"}, {"title": "Luffy"}], | |
| "title_to_full_data": {"Naruto": {}, "Luffy": {}}, | |
| } | |
| def _override_core(**services): | |
| return [ | |
| getattr(container.core, name).override(providers.Object(svc)) | |
| for name, svc in services.items() | |
| ] | |
| def _seed_ask_session(client, secret="Naruto", questions_left=20, chat=None): | |
| session = client.session | |
| session["animinator_secret"] = secret | |
| session["animinator_questions_left"] = questions_left | |
| session["animinator_chat"] = chat if chat is not None else [] | |
| session["media_type"] = "Anime" | |
| session.save() | |
| # --------------------------------------------------------------------------- # | |
| # AniminatorAskView (IsAuthenticated, consumes Berrix) | |
| # --------------------------------------------------------------------------- # | |
| def test_ask_requires_authentication(api_client): | |
| resp = api_client.post( | |
| reverse("api_animinator_ask"), {"question": "?"}, format="json" | |
| ) | |
| assert resp.status_code in (401, 403) | |
| def test_ask_missing_question_returns_400(auth_client): | |
| resp = auth_client.post(reverse("api_animinator_ask"), {}, format="json") | |
| assert resp.status_code == 400 | |
| assert resp.json()["error"] == "No question provided." | |
| def test_ask_new_game_catalog_not_found_returns_404(auth_client): | |
| cat = _catalog_service(None) | |
| _override_core(catalog_service=cat, animinator_service=_oracle_service()) | |
| try: | |
| with patch.object(animinator_mod, "deduct_berrix"): | |
| resp = auth_client.post( | |
| reverse("api_animinator_ask"), {"question": "ninja?"}, format="json" | |
| ) | |
| finally: | |
| container.core.catalog_service.reset_last_overriding() | |
| container.core.animinator_service.reset_last_overriding() | |
| assert resp.status_code == 404 | |
| assert resp.json()["error"] == "Catalog not found" | |
| def test_ask_new_game_seeds_secret_and_returns_answer(auth_client): | |
| cat = _catalog_service(_catalog_data()) | |
| oracle = _oracle_service() | |
| _override_core(catalog_service=cat, animinator_service=oracle) | |
| try: | |
| with patch.object(animinator_mod, "deduct_berrix") as deduct: | |
| resp = auth_client.post( | |
| reverse("api_animinator_ask"), | |
| {"question": "Est-ce un ninja ?", "media_type": "Anime"}, | |
| format="json", | |
| ) | |
| finally: | |
| container.core.catalog_service.reset_last_overriding() | |
| container.core.animinator_service.reset_last_overriding() | |
| assert resp.status_code == 200 | |
| body = resp.json() | |
| # Streamed tokens are joined and HTML-sanitized. | |
| assert body["answer"] == "Il est un ninja." | |
| assert body["questions_left"] == 19 | |
| deduct.assert_called_once() | |
| # A secret was chosen from the catalog pool and stored in the session. | |
| assert auth_client.session["animinator_secret"] in ("Naruto", "Luffy") | |
| oracle.ask_oracle_stream.assert_called_once() | |
| def test_ask_last_question_ends_game_and_records_loss(auth_client): | |
| _seed_ask_session(auth_client, secret="Naruto", questions_left=1) | |
| oracle = _oracle_service() | |
| _override_core( | |
| catalog_service=_catalog_service(_catalog_data()), animinator_service=oracle | |
| ) | |
| try: | |
| with patch.object(animinator_mod, "deduct_berrix"): | |
| resp = auth_client.post( | |
| reverse("api_animinator_ask"), {"question": "dernier ?"}, format="json" | |
| ) | |
| finally: | |
| container.core.catalog_service.reset_last_overriding() | |
| container.core.animinator_service.reset_last_overriding() | |
| assert resp.status_code == 200 | |
| assert resp.json()["questions_left"] == 0 | |
| lost = GameplaySession.objects.filter(game_mode="animinator", was_won=False) | |
| assert lost.exists() | |
| assert auth_client.session.get("animinator_game_over") is True | |
| def test_ask_insufficient_berrix_returns_402_not_500(auth_client): | |
| _seed_ask_session(auth_client, secret="Naruto") | |
| _override_core( | |
| catalog_service=_catalog_service(_catalog_data()), | |
| animinator_service=_oracle_service(), | |
| ) | |
| try: | |
| with patch.object( | |
| animinator_mod, "deduct_berrix", side_effect=PaymentRequired() | |
| ): | |
| resp = auth_client.post( | |
| reverse("api_animinator_ask"), {"question": "ninja?"}, format="json" | |
| ) | |
| finally: | |
| container.core.catalog_service.reset_last_overriding() | |
| container.core.animinator_service.reset_last_overriding() | |
| # PaymentRequired is raised OUTSIDE the try/except, so it surfaces as 402 | |
| # rather than being swallowed into a generic 500. | |
| assert resp.status_code == 402 | |
| def test_ask_oracle_failure_returns_500(auth_client): | |
| _seed_ask_session(auth_client, secret="Naruto") | |
| oracle = MagicMock() | |
| oracle.ask_oracle_stream.side_effect = RuntimeError("oracle down") | |
| _override_core( | |
| catalog_service=_catalog_service(_catalog_data()), animinator_service=oracle | |
| ) | |
| try: | |
| with patch.object(animinator_mod, "deduct_berrix"): | |
| resp = auth_client.post( | |
| reverse("api_animinator_ask"), {"question": "ninja?"}, format="json" | |
| ) | |
| finally: | |
| container.core.catalog_service.reset_last_overriding() | |
| container.core.animinator_service.reset_last_overriding() | |
| assert resp.status_code == 500 | |
| assert resp.json()["error"] == "Internal server error" | |
| # --------------------------------------------------------------------------- # | |
| # AniminatorGuessView (AllowAny, CPU comparison) | |
| # --------------------------------------------------------------------------- # | |
| def test_guess_no_game_in_progress_returns_400(api_client): | |
| resp = api_client.post( | |
| reverse("api_animinator_guess"), {"guess": "Naruto"}, format="json" | |
| ) | |
| assert resp.status_code == 400 | |
| assert resp.json()["error"] == "No game in progress" | |
| def test_guess_missing_guess_returns_400(api_client): | |
| _seed_ask_session(api_client, secret="Naruto") | |
| _override_core(animinator_service=_oracle_service()) | |
| try: | |
| resp = api_client.post(reverse("api_animinator_guess"), {}, format="json") | |
| finally: | |
| container.core.animinator_service.reset_last_overriding() | |
| assert resp.status_code == 400 | |
| assert resp.json()["error"] == "guess is required" | |
| def test_guess_correct_ends_game_and_reveals_secret(auth_client): | |
| _seed_ask_session(auth_client, secret="Naruto", chat=[{"q": "a", "a": "b"}]) | |
| oracle = MagicMock() | |
| oracle.check_guess.return_value = True | |
| _override_core(animinator_service=oracle) | |
| try: | |
| resp = auth_client.post( | |
| reverse("api_animinator_guess"), {"guess": "Naruto"}, format="json" | |
| ) | |
| finally: | |
| container.core.animinator_service.reset_last_overriding() | |
| body = resp.json() | |
| assert body["correct"] is True | |
| assert body["game_over"] is True | |
| assert body["secret"] == "Naruto" | |
| assert GameplaySession.objects.filter(game_mode="animinator", was_won=True).exists() | |
| # Secret cleared for replay. | |
| assert auth_client.session["animinator_secret"] == "" | |
| def test_guess_wrong_keeps_secret_hidden(api_client): | |
| _seed_ask_session(api_client, secret="Naruto") | |
| oracle = MagicMock() | |
| oracle.check_guess.return_value = False | |
| _override_core(animinator_service=oracle) | |
| try: | |
| resp = api_client.post( | |
| reverse("api_animinator_guess"), {"guess": "Luffy"}, format="json" | |
| ) | |
| finally: | |
| container.core.animinator_service.reset_last_overriding() | |
| body = resp.json() | |
| assert body["correct"] is False | |
| assert body["game_over"] is False | |
| assert body["secret"] is None | |