animetix-web / tests /api /games /test_classic_coverage.py
MissawB's picture
Upload folder using huggingface_hub (part 3)
d483b58 verified
Raw
History Blame Contribute Delete
18.4 kB
"""Coverage tests for backend/api/animetix/api/games/classic.py.
These exercise the Classic mode endpoints (state / start / guess / reveal)
against the real Django session (the session state port reads request.session),
overriding the DI catalog/game services via the dependency_injector container.
"""
from unittest.mock import MagicMock
import pytest
from animetix.containers import container
from dependency_injector import providers
from django.contrib.auth.models import User
from django.urls import reverse
# NB: the per-test wiring guard lives in conftest.py (``_rewire_game_modules``).
# We never call ``container.reset_override()`` -- on this dependency_injector
# version it detaches the ``core -> persistence`` sub-container link and breaks the
# views. Each test scopes its overrides with ``with`` blocks, which self-clean.
@pytest.fixture
def mock_catalog():
return {
"title_to_full_data": {
"Naruto": {
"id": 1,
"title": "Naruto",
"description": "Ninja anime " * 30,
"image": "http://img.jpg",
"title_english": "Naruto EN",
"title_native": "Naruto JP",
"origin": "Japan",
"year": 2002,
"tags": ["action", "ninja", "shonen", "adventure", "battle", "extra"],
"metadata": {"studio": "Pierrot"},
},
"One Piece": {
"id": 2,
"title": "One Piece",
"description": "Pirate anime",
"image": "http://img2.jpg",
},
"Bleach": {
"id": 3,
"title": "Bleach",
"description": "Soul reaper anime",
"image": "http://img3.jpg",
},
},
"titles": ["Naruto", "One Piece", "Bleach"],
"title_to_index": {"Naruto": 1, "One Piece": 2, "Bleach": 3},
"lookup": [
{"id": 1, "title": "Naruto"},
{"id": 2, "title": "One Piece"},
{"id": 3, "title": "Bleach"},
],
}
def _override(catalog=None, game=None):
"""Return the list of container override context managers for the test."""
overrides = []
if catalog is not None:
overrides.append(
container.core.catalog_service.override(providers.Object(catalog))
)
if game is not None:
overrides.append(container.core.game_service.override(providers.Object(game)))
return overrides
# ---------------------------------------------------------------------------
# ClassicGameStateView
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_state_no_game_in_progress(api_client, mock_catalog):
"""GET state with empty session -> 400 No game in progress."""
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
with container.core.catalog_service.override(providers.Object(cat)):
resp = api_client.get(reverse("api_classic_state"))
assert resp.status_code == 400
assert resp.json()["error"] == "No game in progress"
@pytest.mark.django_db
def test_start_then_state_ok(api_client, mock_catalog):
"""Start (staff override) seeds the session, then state returns hints/guesses."""
user = User.objects.create_user("staff1", password="x", is_staff=True)
api_client.force_authenticate(user=user)
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
game.select_secret.return_value = "Naruto"
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
start = api_client.post(
reverse("api_classic_start"),
{"media_type": "Anime", "override_secret": "Naruto"},
format="json",
)
assert start.status_code == 200
assert start.json()["status"] == "started"
resp = api_client.get(reverse("api_classic_state"))
assert resp.status_code == 200
data = resp.json()
assert data["media_type"] == "Anime"
assert data["game_over"] is False
# game not over -> secret hidden
assert data["secret_title"] is None
assert "origin" in data["hints"]
@pytest.mark.django_db
def test_state_catalog_not_found(api_client):
"""State when catalog_service returns falsy -> 404 Catalog not found."""
user = User.objects.create_user("staff2", password="x", is_staff=True)
api_client.force_authenticate(user=user)
full_catalog = {
"title_to_full_data": {"Naruto": {"title": "Naruto"}},
"titles": ["Naruto"],
"title_to_index": {"Naruto": 1},
"lookup": [{"id": 1, "title": "Naruto"}],
}
cat = MagicMock()
game = MagicMock()
# Start succeeds with a real catalog, then state sees an empty catalog.
cat.get_catalog.side_effect = [full_catalog, None]
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
api_client.post(
reverse("api_classic_start"),
{"media_type": "Anime", "override_secret": "Naruto"},
format="json",
)
resp = api_client.get(reverse("api_classic_state"))
assert resp.status_code == 404
assert resp.json()["error"] == "Catalog not found"
@pytest.mark.django_db
def test_state_secret_data_not_found(api_client):
"""State when secret title missing from catalog data -> 404."""
user = User.objects.create_user("staff3", password="x", is_staff=True)
api_client.force_authenticate(user=user)
start_catalog = {
"title_to_full_data": {"Naruto": {"title": "Naruto"}},
"titles": ["Naruto"],
"title_to_index": {"Naruto": 1},
"lookup": [{"id": 1, "title": "Naruto"}],
}
state_catalog = {
"title_to_full_data": {}, # secret no longer present
"titles": [],
"title_to_index": {},
"lookup": [],
}
cat = MagicMock()
cat.get_catalog.side_effect = [start_catalog, state_catalog]
game = MagicMock()
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
api_client.post(
reverse("api_classic_start"),
{"media_type": "Anime", "override_secret": "Naruto"},
format="json",
)
resp = api_client.get(reverse("api_classic_state"))
assert resp.status_code == 404
assert resp.json()["error"] == "Secret title data not found"
# ---------------------------------------------------------------------------
# ClassicGameStartView
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_start_catalog_not_found(api_client, mock_catalog):
cat = MagicMock()
cat.get_catalog.return_value = None
game = MagicMock()
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
resp = api_client.post(
reverse("api_classic_start"), {"media_type": "Anime"}, format="json"
)
assert resp.status_code == 404
assert resp.json()["error"] == "Catalog not found"
@pytest.mark.django_db
def test_start_select_secret_via_game_service(api_client, mock_catalog):
"""Non-staff start: secret chosen by game_service.select_secret."""
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
game.select_secret.return_value = "Naruto"
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
resp = api_client.post(
reverse("api_classic_start"),
{"media_type": "Anime", "difficulty": "Hard"},
format="json",
)
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "started"
assert data["difficulty"] == "Hard"
assert data["is_ranked"] is False
game.select_secret.assert_called_once()
@pytest.mark.django_db
def test_start_select_secret_failure(api_client, mock_catalog):
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
game.select_secret.return_value = None
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
resp = api_client.post(
reverse("api_classic_start"), {"media_type": "Anime"}, format="json"
)
assert resp.status_code == 500
assert resp.json()["error"] == "Failed to select secret title"
@pytest.mark.django_db
def test_start_non_staff_override_ignored(api_client, mock_catalog):
"""A non-staff user passing override_secret is logged and ignored."""
user = User.objects.create_user("plain", password="x")
api_client.force_authenticate(user=user)
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
game.select_secret.return_value = "Bleach"
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
resp = api_client.post(
reverse("api_classic_start"),
{"media_type": "Anime", "override_secret": "Naruto"},
format="json",
)
assert resp.status_code == 200
# game_service was used because override was ignored
game.select_secret.assert_called_once()
# ---------------------------------------------------------------------------
# ClassicGameGuessView
# ---------------------------------------------------------------------------
def _seed_started_game(api_client, cat, game, media_type="Anime"):
"""Start a game as staff with a fixed secret so the session has state."""
return api_client.post(
reverse("api_classic_start"),
{"media_type": media_type, "override_secret": "Naruto"},
format="json",
)
@pytest.mark.django_db
def test_guess_no_game(api_client, mock_catalog):
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
resp = api_client.post(
reverse("api_classic_guess"), {"guess": "Naruto"}, format="json"
)
assert resp.status_code == 400
assert resp.json()["error"] == "No game in progress"
@pytest.mark.django_db
def test_guess_missing_guess_field(api_client, mock_catalog):
user = User.objects.create_user("staffg1", password="x", is_staff=True)
api_client.force_authenticate(user=user)
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
_seed_started_game(api_client, cat, game)
resp = api_client.post(reverse("api_classic_guess"), {}, format="json")
assert resp.status_code == 400
assert resp.json()["error"] == "Guess is required"
@pytest.mark.django_db
def test_guess_title_not_in_catalog(api_client, mock_catalog):
user = User.objects.create_user("staffg2", password="x", is_staff=True)
api_client.force_authenticate(user=user)
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
_seed_started_game(api_client, cat, game)
resp = api_client.post(
reverse("api_classic_guess"), {"guess": "Unknown Title"}, format="json"
)
assert resp.status_code == 400
assert "not in catalog" in resp.json()["error"]
@pytest.mark.django_db
def test_guess_incorrect(api_client, mock_catalog):
"""A wrong guess returns a scored guess and keeps the game running."""
user = User.objects.create_user("staffg3", password="x", is_staff=True)
api_client.force_authenticate(user=user)
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
game.check_title_match.return_value = False
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
_seed_started_game(api_client, cat, game)
resp = api_client.post(
reverse("api_classic_guess"), {"guess": "Bleach"}, format="json"
)
assert resp.status_code == 200
data = resp.json()
assert data["is_correct"] is False
assert data["game_over"] is False
assert data["latest_guess"]["title"] == "Bleach"
assert data["guess_count"] == 1
@pytest.mark.django_db
def test_guess_correct_authenticated_creates_session(api_client, mock_catalog):
"""A correct guess by an authenticated user ends the game, awards a win,
and persists a GameplaySession row."""
from animetix.models import GameplaySession
user = User.objects.create_user("winner", password="x", is_staff=True)
api_client.force_authenticate(user=user)
# profile.add_win returns an achievement with attributes used by the view
ach = MagicMock()
ach.name = "First Win"
ach.description = "You won"
ach.xp_reward = 10
ach.badge_url = "http://badge.png"
# The view accesses request.user.profile.add_win; ensure a profile exists.
profile = getattr(user, "profile", None)
if profile is None:
pytest.skip("User has no profile relation configured")
profile.add_win = MagicMock(return_value=[ach])
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
game.check_title_match.return_value = True
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
_seed_started_game(api_client, cat, game)
resp = api_client.post(
reverse("api_classic_guess"), {"guess": "Naruto"}, format="json"
)
assert resp.status_code == 200
data = resp.json()
assert data["is_correct"] is True
assert data["game_over"] is True
assert data["secret_title"] == "Naruto"
assert data["latest_guess"]["score"] == 100.0
assert GameplaySession.objects.filter(target_item="Naruto", was_won=True).exists()
@pytest.mark.django_db
def test_guess_when_game_over(api_client, mock_catalog):
"""A second correct guess after game_over returns 400 Game already over."""
user = User.objects.create_user("staffg4", password="x", is_staff=True)
api_client.force_authenticate(user=user)
profile = getattr(user, "profile", None)
if profile is not None:
profile.add_win = MagicMock(return_value=[])
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
game.check_title_match.return_value = True
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
_seed_started_game(api_client, cat, game)
api_client.post(
reverse("api_classic_guess"), {"guess": "Naruto"}, format="json"
)
resp = api_client.post(
reverse("api_classic_guess"), {"guess": "Naruto"}, format="json"
)
assert resp.status_code == 400
assert resp.json()["error"] == "Game already over"
# ---------------------------------------------------------------------------
# ClassicGameRevealView
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_reveal_no_game(api_client, mock_catalog):
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
with container.core.catalog_service.override(providers.Object(cat)):
resp = api_client.post(
reverse("api_classic_reveal"), {"hint_type": "origin"}, format="json"
)
assert resp.status_code == 400
assert resp.json()["error"] == "No game in progress"
@pytest.mark.django_db
def test_reveal_missing_hint_type(api_client, mock_catalog):
user = User.objects.create_user("staffr1", password="x", is_staff=True)
api_client.force_authenticate(user=user)
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
game.select_secret.return_value = "Naruto"
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
_seed_started_game(api_client, cat, game)
resp = api_client.post(reverse("api_classic_reveal"), {}, format="json")
assert resp.status_code == 400
# Missing and unknown hint types share one rejection message.
assert resp.json()["error"] == "Invalid hint type"
@pytest.mark.django_db
def test_reveal_ok(api_client, mock_catalog):
user = User.objects.create_user("staffr2", password="x", is_staff=True)
api_client.force_authenticate(user=user)
cat = MagicMock()
cat.get_catalog.return_value = mock_catalog
game = MagicMock()
with (
container.core.catalog_service.override(providers.Object(cat)),
container.core.game_service.override(providers.Object(game)),
):
_seed_started_game(api_client, cat, game)
# Hints unlock progressively (n-th hint after n*step guesses, step=5);
# "origin" is the 2nd hint, so seed 10 guesses to make it revealable.
session = api_client.session
session["guesses"] = [{"title": "x", "is_correct": False} for _ in range(10)]
session.save()
resp = api_client.post(
reverse("api_classic_reveal"), {"hint_type": "origin"}, format="json"
)
assert resp.status_code == 200
data = resp.json()
assert "origin" in data["revealed_hints"]
assert "hints" in data