"""Bibliothèque de sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY, screens/labels/animal_sprite_library.py) — réservée au rôle admin (licence CraftPix, voir core/sprite_gate.py et le plan "Bibliothèque de sprites animaux CraftPix"). Un compte "user" ne doit ni la voir dans la galerie de l'éditeur, ni pouvoir l'assigner par un POST direct.""" import json from collections.abc import Callable from pathlib import Path from typing import Any from flask.testing import FlaskClient import screens from tests.conftest import not_none def test_admin_sprite_library_loaded_from_manifest() -> None: assert len(screens.ADMIN_SPRITE_LIBRARY) > 0 bunny = screens.ADMIN_SPRITE_LIBRARY["animal-bunny-01"] assert bunny["family"] == "bunny" assert bunny["animations"]["idle"][0] == "/static/characters/animals/bunny/01/idle0.png" assert len(bunny["animations"]["idle"]) == 20 def test_sprite_library_is_public_plus_admin_merged() -> None: assert set(screens.SPRITE_LIBRARY) == set(screens.PUBLIC_SPRITE_LIBRARY) | set(screens.ADMIN_SPRITE_LIBRARY) assert set(screens.ADMIN_SPRITE_LIBRARY) == screens.ADMIN_ONLY_CHARACTER_SLUGS assert "male-adventurer" not in screens.ADMIN_ONLY_CHARACTER_SLUGS def test_sprite_gallery_families_shows_one_tile_per_animal() -> None: families = screens.sprite_gallery_families(screens.SPRITE_LIBRARY) slugs = [slug for slug, _ in families] assert len(slugs) == len(screens.PUBLIC_SPRITE_LIBRARY) + 14 # 6 Kenney + 14 familles animales assert "animal-bunny-01" in slugs assert "animal-bunny-02" not in slugs def _create_jeu2d_game_as(post_client: FlaskClient, name: str, slug_owner_client: FlaskClient | None = None) -> Any: # Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) : un # jeu n'a plus de type propre — seul l'écran créé ensuite par chaque # test (screens.create_screen(..., kind="jeu_2d")) est une scène. resp = post_client.post("/games/new", data={"name": name}, follow_redirects=False) assert resp.status_code == 302 return resp.headers["Location"].rstrip("/").split("/")[-1] def test_scene_editor_hides_animal_characters_from_a_user_account( client: FlaskClient, user_client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_user")) with user_client.session_transaction() as sess: import auth auth.set_project_slug(sess["user_id"], slug) screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d") html = user_client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True) assert "animal-bunny-01" not in html def test_scene_object_add_rejects_animal_character_for_a_user_account( client: FlaskClient, user_client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_gate")) with user_client.session_transaction() as sess: import auth auth.set_project_slug(sess["user_id"], slug) screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d") resp = user_client.post( f"/game/{slug}/screens/{screen_id}/scene-objects/add", data={"kind": "personnage", "forge_character": "animal-bunny-01"}, ) assert resp.status_code == 403 def test_scene_object_add_allows_animal_character_for_admin( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_admin")) screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d") resp = client.post( f"/game/{slug}/screens/{screen_id}/scene-objects/add", data={"kind": "personnage", "forge_character": "animal-bunny-01"}, follow_redirects=False, ) assert resp.status_code == 302 obj_id = int(resp.headers["Location"].rsplit("selected=", 1)[1]) obj = not_none(screens.get_scene_object(slug, obj_id)) assert screens.resolve_personnage_data(obj)["forge_character"] == "animal-bunny-01" def test_scene_object_personnage_swap_rejects_animal_character_for_a_user_account( client: FlaskClient, user_client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_swap_gate")) with user_client.session_transaction() as sess: import auth auth.set_project_slug(sess["user_id"], slug) screen_id = screens.create_screen(slug, "Scène 1") obj_id = screens.add_scene_object(slug, screen_id) resp = user_client.post( f"/game/{slug}/scene-objects/{obj_id}/personnage", data=json.dumps({"source": "forge", "forge_character": "animal-bunny-01"}), content_type="application/json", ) assert resp.status_code == 403 def test_list_used_forge_characters_scans_elements_and_scene_objects( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_used_characters")) screen_id = screens.create_screen(slug, "Scène 1") screens.add_scene_object(slug, screen_id, forge_character="animal-bunny-01") screens.add_scene_object(slug, screen_id, kind="decor") used = screens.list_used_forge_characters(slug) assert used == {"animal-bunny-01"} def test_build_scorm_package_only_copies_used_animal_character_folders( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str], tmp_path: Path ) -> None: slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_scorm_animals")) screen_id = screens.create_screen(slug, "Scène 1") screens.add_scene_object(slug, screen_id, forge_character="animal-bunny-01") with client.application.test_request_context(): from publish import build_scorm_package zip_path, _name = build_scorm_package.build_scorm_zip(slug) try: import zipfile with zipfile.ZipFile(zip_path) as zf: names = zf.namelist() animal_paths = {n for n in names if "static/characters/animals/" in n and not n.endswith("manifest.json")} animals_used = {n.split("static/characters/animals/")[1].split("/")[0] for n in animal_paths} assert animals_used == {"bunny"} assert any("static/characters/animals/bunny/01/" in n for n in animal_paths) assert not any("static/characters/animals/bear/" in n for n in animal_paths) assert any(n.endswith("static/characters/animals/manifest.json") for n in names) assert any("static/characters/male-adventurer/" in n for n in names) finally: import os os.remove(zip_path)