Files
Forge-Engine/tests/test_publish.py
T
williamandClaude Sonnet 5 76a50fa87a
Build and deploy / test-python (push) Failing after 1m48s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Onboarding guidé systématique + tableau de bord simplifié
Comportement SYSTÉMATIQUE à chaque création de jeu (admin compris), pas
une formalité réservée à l'inscription : /onboarding (routes/onboarding/)
devient le point d'entrée unique — 4 cartes retournables (survol =
explication au dos), défilement horizontal animé vers le nom du jeu. Un
admin y repasse à volonté (pas de project_slug dédié, jamais bloqué/
redirigé vers un projet précédent) ; un compte "user" n'en a plus qu'un
créé d'office (routes/auth/register_2fa.py), guidé ici à la place.

- db/games/game_type_catalog.py : catalogue des 4 types (Quiz/
  Embranchement-escape game/RPG/Créer mon jeu de A à Z), _meta['onboarding_type']
  décide du "kind" du premier écran créé et si le tableau de bord complet
  reste accessible.
- routes/games/game_dashboard.py, templates/game_dashboard_simple.html :
  un type restreint (quiz/embranchement/rpg) voit désormais SON tableau
  de bord (même route que "custom"), rendu en version simplifiée — juste
  ses écrans en cartes avec un aperçu RÉEL du contenu (scène mise à
  l'échelle par container query CSS, adaptée à la largeur réelle de la
  carte). "+ Ajouter un écran" n'y propose pas de choix de type : imposé
  par le projet (routes/screens/screens_new.py), verrouillé aussi côté
  serveur.
- core/auth_guard.py : plus de blocage de game_dashboard par type — la
  restriction se fait au rendu, pas à l'accès à la route.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 06:22:45 +02:00

93 lines
3.8 KiB
Python

"""Publier un jeu en exécutable Windows autonome (bouton "Publier", voir
routes/publish/publish_game.py et publish/build_package.py). Le vrai
téléchargement du Python portable + Flask (publish/vendor_runtime.py)
n'a JAMAIS lieu ici : `ensure_vendor_ready` est monkeypatché vers un
dossier factice minuscule — ces tests vérifient l'ASSEMBLAGE du zip
(fichiers présents, structure), jamais un vrai lancement du serveur
embarqué (nécessite un environnement Windows réel, voir le plan)."""
import os
import zipfile
import pytest
import publish.build_package as build_package
from tests.test_auth import anon_client # noqa: F401
@pytest.fixture
def fake_vendor(tmp_path, monkeypatch):
"""Un python-embed/pylibs factice, juste assez pour que l'assemblage
du zip (copie de fichiers) fonctionne sans jamais toucher au réseau."""
python_dir = tmp_path / "python-embed-amd64"
python_dir.mkdir()
(python_dir / "python.exe").write_bytes(b"fake-exe")
pylibs_dir = tmp_path / "pylibs"
pylibs_dir.mkdir()
(pylibs_dir / "flask").mkdir()
(pylibs_dir / "flask" / "__init__.py").write_text("# fake", encoding="utf-8")
def _fake_ensure_vendor_ready():
return str(python_dir), str(pylibs_dir)
monkeypatch.setattr(build_package.vendor_runtime, "ensure_vendor_ready", _fake_ensure_vendor_ready)
return python_dir, pylibs_dir
def test_build_game_zip_contains_the_expected_files(fake_vendor, game):
zip_path, game_name = build_package.build_game_zip(game)
try:
assert game_name # nom du jeu (pytest_test_game par défaut, voir conftest.py)
with zipfile.ZipFile(zip_path) as zf:
names = set(zf.namelist())
for expected in [
"run.bat", "player_app.py",
f"projects/{game}/game.db",
"screens/__init__.py", "db/__init__.py", "filters/__init__.py",
"core/__init__.py", "core/flask_app.py", "core/jinja_filters.py",
"templates/play.html",
"static/style.css", "static/csrf_fetch.js",
"python-embed/python.exe", "python-embed/pylibs/flask/__init__.py",
]:
assert expected in names, f"{expected} manquant du zip"
finally:
if os.path.isfile(zip_path):
os.remove(zip_path)
def test_build_game_zip_bakes_the_slug_into_player_app(fake_vendor, game):
zip_path, _ = build_package.build_game_zip(game)
try:
with zipfile.ZipFile(zip_path) as zf:
player_app_src = zf.read("player_app.py").decode("utf-8")
assert f'SLUG = "{game}"' in player_app_src
assert "_SLUG_PLACEHOLDER" not in player_app_src
finally:
if os.path.isfile(zip_path):
os.remove(zip_path)
def test_publish_route_returns_a_zip_attachment(fake_vendor, client, game):
resp = client.post(f"/game/{game}/publish")
assert resp.status_code == 200
assert resp.headers["Content-Type"] == "application/zip"
assert "attachment" in resp.headers["Content-Disposition"]
assert resp.data[:2] == b"PK" # signature d'un fichier zip
def test_publish_route_is_isolated_like_other_game_routes(fake_vendor, anon_client):
"""Réutilise la garde d'accès existante (core/auth_guard.py) sans
code supplémentaire — un compte non-admin ne doit pas pouvoir publier
le projet d'un autre (même vérification que /game/<slug> lui-même,
voir tests/test_auth.py::test_non_admin_user_is_isolated_to_their_own_project)."""
from tests.test_auth import _register, _confirm_2fa, _complete_onboarding, _cleanup_project
_register(anon_client, "publishisolation@example.com")
_confirm_2fa(anon_client)
_complete_onboarding(anon_client)
try:
resp = anon_client.post("/game/un-projet-qui-nest-pas-le-sien/publish")
assert resp.status_code == 403
finally:
_cleanup_project("publishisolation@example.com")