Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
+80
-46
@@ -1,15 +1,19 @@
|
||||
""""Mes assets" (voir plan Phase 1, §2) — bibliothèque d'images PAR
|
||||
""" "Mes assets" (voir plan Phase 1, §2) — bibliothèque d'images PAR
|
||||
COMPTE, distincte du dossier d'un jeu (routes/uploads/upload_file.py) :
|
||||
réutilisable d'un projet à l'autre du MÊME compte, jamais visible par un
|
||||
autre."""
|
||||
|
||||
import io
|
||||
import struct
|
||||
import wave
|
||||
from typing import Any
|
||||
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
import auth
|
||||
|
||||
|
||||
def _png_bytes(width, height):
|
||||
def _png_bytes(width: int, height: int) -> Any:
|
||||
"""Un PNG minimal — seule la signature + l'en-tête IHDR (largeur/
|
||||
hauteur) comptent pour auth.image_dimensions, le reste du fichier
|
||||
n'a pas besoin d'être une image RÉELLEMENT décodable (voir
|
||||
@@ -19,11 +23,11 @@ def _png_bytes(width, height):
|
||||
return b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\x0d" + b"IHDR" + ihdr + b"\x00\x00\x00\x00"
|
||||
|
||||
|
||||
def _mp4_box(box_type, payload):
|
||||
def _mp4_box(box_type: Any, payload: Any) -> Any:
|
||||
return struct.pack(">I4s", len(payload) + 8, box_type) + payload
|
||||
|
||||
|
||||
def _mp4_bytes(seconds, timescale=1000):
|
||||
def _mp4_bytes(seconds: int, timescale: Any = 1000) -> Any:
|
||||
"""Un MP4 minimal — juste "ftyp" + "moov/mvhd" (durée/timescale),
|
||||
suffisant pour que mutagen (auth/validate_video_duration.py) lise sa
|
||||
durée, sans piste vidéo/audio réelle ni "mdat" (jamais décodé, seule
|
||||
@@ -31,14 +35,13 @@ def _mp4_bytes(seconds, timescale=1000):
|
||||
ftyp = _mp4_box(b"ftyp", b"isom" + struct.pack(">I", 0) + b"isom" + b"mp42")
|
||||
duration = int(seconds * timescale)
|
||||
mvhd_payload = (
|
||||
struct.pack(">I", 0) + struct.pack(">II", 0, 0)
|
||||
+ struct.pack(">II", timescale, duration) + b"\x00" * 80
|
||||
struct.pack(">I", 0) + struct.pack(">II", 0, 0) + struct.pack(">II", timescale, duration) + b"\x00" * 80
|
||||
)
|
||||
moov = _mp4_box(b"moov", _mp4_box(b"mvhd", mvhd_payload))
|
||||
return ftyp + moov
|
||||
|
||||
|
||||
def _wav_bytes(seconds, framerate=8000):
|
||||
def _wav_bytes(seconds: int, framerate: Any = 8000) -> Any:
|
||||
"""Un WAV de silence, durée EXACTE (au sample près) — 8kHz mono 8-bit
|
||||
pour rester petit même sur 3+ minutes (voir test de la limite de
|
||||
durée d'un son, auth/validate_audio_duration.py)."""
|
||||
@@ -51,29 +54,31 @@ def _wav_bytes(seconds, framerate=8000):
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def test_create_and_list_user_asset(client):
|
||||
def test_create_and_list_user_asset(client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(user_id, "abc123.png", original_name="photo.png", source="upload")
|
||||
assets = auth.list_user_assets(user_id)
|
||||
assert any(a["id"] == asset_id and a["filename"] == "abc123.png" for a in assets)
|
||||
auth.delete_user_asset(asset_id, user_id)
|
||||
|
||||
|
||||
def test_list_user_assets_is_isolated_per_account(client, user_client):
|
||||
def test_list_user_assets_is_isolated_per_account(client: FlaskClient, user_client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
admin_id = sess["user_id"]
|
||||
with user_client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
auth.create_user_asset(admin_id, "admin_only.png")
|
||||
admin_asset_id = auth.create_user_asset(admin_id, "admin_only.png")
|
||||
assets = auth.list_user_assets(user_id)
|
||||
assert all(a["filename"] != "admin_only.png" for a in assets)
|
||||
auth.delete_user_asset(admin_asset_id, admin_id)
|
||||
|
||||
|
||||
def test_get_user_asset_returns_none_for_unknown_id():
|
||||
def test_get_user_asset_returns_none_for_unknown_id() -> None:
|
||||
assert auth.get_user_asset(999999) is None
|
||||
|
||||
|
||||
def test_delete_user_asset_refuses_when_not_the_owner(client, user_client):
|
||||
def test_delete_user_asset_refuses_when_not_the_owner(client: FlaskClient, user_client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
admin_id = sess["user_id"]
|
||||
with user_client.session_transaction() as sess:
|
||||
@@ -81,9 +86,10 @@ def test_delete_user_asset_refuses_when_not_the_owner(client, user_client):
|
||||
asset_id = auth.create_user_asset(admin_id, "admin_only.png")
|
||||
assert auth.delete_user_asset(asset_id, user_id) is False
|
||||
assert auth.get_user_asset(asset_id) is not None
|
||||
auth.delete_user_asset(asset_id, admin_id)
|
||||
|
||||
|
||||
def test_upload_user_asset_route_stores_the_file_and_returns_a_url(client):
|
||||
def test_upload_user_asset_route_stores_the_file_and_returns_a_url(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(b"fake-image-bytes"), "photo.png")},
|
||||
@@ -97,9 +103,15 @@ def test_upload_user_asset_route_stores_the_file_and_returns_a_url(client):
|
||||
dl = client.get(body["url"])
|
||||
assert dl.status_code == 200
|
||||
assert dl.data == b"fake-image-bytes"
|
||||
# libere le descripteur ouvert par send_from_directory avant de le
|
||||
# supprimer (Windows : os.remove echoue tant qu'il reste ouvert).
|
||||
dl.close()
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
auth.delete_user_asset(body["id"], user_id)
|
||||
|
||||
|
||||
def test_upload_user_asset_route_requires_a_file(client):
|
||||
def test_upload_user_asset_route_requires_a_file(client: FlaskClient) -> None:
|
||||
resp = client.post("/assets/upload", data={}, content_type="multipart/form-data")
|
||||
assert resp.status_code == 400
|
||||
|
||||
@@ -107,25 +119,28 @@ def test_upload_user_asset_route_requires_a_file(client):
|
||||
# ---------- scene_kind ("fond"/"decor" — demande explicite : "un bouton
|
||||
# d'import séparé pour les images de fond et les objets") ----------
|
||||
|
||||
def test_create_user_asset_stores_the_scene_kind(client):
|
||||
|
||||
def test_create_user_asset_stores_the_scene_kind(client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(user_id, "ciel.png", source="upload", scene_kind="fond")
|
||||
assets = auth.list_user_assets(user_id)
|
||||
asset = next(a for a in assets if a["id"] == asset_id)
|
||||
assert asset["scene_kind"] == "fond"
|
||||
auth.delete_user_asset(asset_id, user_id)
|
||||
|
||||
|
||||
def test_create_user_asset_scene_kind_defaults_to_none(client):
|
||||
def test_create_user_asset_scene_kind_defaults_to_none(client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(user_id, "ancienne.png", source="upload")
|
||||
assets = auth.list_user_assets(user_id)
|
||||
asset = next(a for a in assets if a["id"] == asset_id)
|
||||
assert asset["scene_kind"] is None
|
||||
auth.delete_user_asset(asset_id, user_id)
|
||||
|
||||
|
||||
def test_upload_route_stores_the_posted_scene_kind_for_an_image(client):
|
||||
def test_upload_route_stores_the_posted_scene_kind_for_an_image(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(b"fake-image-bytes"), "ciel.png"), "scene_kind": "fond"},
|
||||
@@ -137,9 +152,10 @@ def test_upload_route_stores_the_posted_scene_kind_for_an_image(client):
|
||||
assets = auth.list_user_assets(user_id)
|
||||
asset = next(a for a in assets if a["id"] == resp.get_json()["id"])
|
||||
assert asset["scene_kind"] == "fond"
|
||||
auth.delete_user_asset(asset["id"], user_id)
|
||||
|
||||
|
||||
def test_upload_route_defaults_an_image_without_scene_kind_to_decor(client):
|
||||
def test_upload_route_defaults_an_image_without_scene_kind_to_decor(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(b"fake-image-bytes"), "photo.png")},
|
||||
@@ -150,9 +166,10 @@ def test_upload_route_defaults_an_image_without_scene_kind_to_decor(client):
|
||||
assets = auth.list_user_assets(user_id)
|
||||
asset = next(a for a in assets if a["id"] == resp.get_json()["id"])
|
||||
assert asset["scene_kind"] == "decor"
|
||||
auth.delete_user_asset(asset["id"], user_id)
|
||||
|
||||
|
||||
def test_upload_route_ignores_scene_kind_for_a_non_image_file(client):
|
||||
def test_upload_route_ignores_scene_kind_for_a_non_image_file(client: FlaskClient) -> None:
|
||||
"""scene_kind n'a de sens que pour une IMAGE (voir
|
||||
auth/user_asset_kind.py) — un son/une vidéo ne "posent" rien sur la
|
||||
scène, peu importe ce que le client aurait posté."""
|
||||
@@ -166,9 +183,10 @@ def test_upload_route_ignores_scene_kind_for_a_non_image_file(client):
|
||||
assets = auth.list_user_assets(user_id)
|
||||
asset = next(a for a in assets if a["id"] == resp.get_json()["id"])
|
||||
assert asset["scene_kind"] is None
|
||||
auth.delete_user_asset(asset["id"], user_id)
|
||||
|
||||
|
||||
def test_serve_user_asset_route_403s_for_another_account(client, user_client):
|
||||
def test_serve_user_asset_route_403s_for_another_account(client: FlaskClient, user_client: FlaskClient) -> None:
|
||||
# Un compte "user" fraîchement inscrit (sans projet) est redirigé vers
|
||||
# l'onboarding pour toute route hors _REACHABLE_WITHOUT_PROJECT (voir
|
||||
# core/auth_guard.py) — lui assigner un projet ici isole ce test sur
|
||||
@@ -186,35 +204,39 @@ def test_serve_user_asset_route_403s_for_another_account(client, user_client):
|
||||
url = resp.get_json()["url"]
|
||||
resp2 = user_client.get(url)
|
||||
assert resp2.status_code == 403
|
||||
with client.session_transaction() as sess:
|
||||
admin_id = sess["user_id"]
|
||||
auth.delete_user_asset(resp.get_json()["id"], admin_id)
|
||||
|
||||
|
||||
# ---------- "Mes assets" — upload d'un son, limité à 3 minutes (demande
|
||||
# explicite) ----------
|
||||
|
||||
def test_user_asset_kind_classifies_by_extension():
|
||||
|
||||
def test_user_asset_kind_classifies_by_extension() -> None:
|
||||
assert auth.user_asset_kind("photo.PNG") == "image"
|
||||
assert auth.user_asset_kind("bip.mp3") == "audio"
|
||||
assert auth.user_asset_kind("bip.WAV") == "audio"
|
||||
assert auth.user_asset_kind("notes.txt") == "other"
|
||||
|
||||
|
||||
def test_validate_audio_duration_accepts_a_short_wav():
|
||||
def test_validate_audio_duration_accepts_a_short_wav() -> None:
|
||||
content = _wav_bytes(2)
|
||||
assert auth.validate_audio_duration(content) is None
|
||||
|
||||
|
||||
def test_validate_audio_duration_rejects_a_wav_over_the_limit():
|
||||
def test_validate_audio_duration_rejects_a_wav_over_the_limit() -> None:
|
||||
content = _wav_bytes(200) # > 180s (auth.MAX_AUDIO_SECONDS)
|
||||
error = auth.validate_audio_duration(content)
|
||||
assert error is not None
|
||||
assert "3 minutes" in error
|
||||
|
||||
|
||||
def test_validate_audio_duration_rejects_unrecognized_content():
|
||||
def test_validate_audio_duration_rejects_unrecognized_content() -> None:
|
||||
assert auth.validate_audio_duration(b"pas-un-vrai-fichier-audio") is not None
|
||||
|
||||
|
||||
def test_upload_route_accepts_a_short_audio_file(client):
|
||||
def test_upload_route_accepts_a_short_audio_file(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(_wav_bytes(2)), "bip.wav")},
|
||||
@@ -225,9 +247,10 @@ def test_upload_route_accepts_a_short_audio_file(client):
|
||||
user_id = sess["user_id"]
|
||||
assets = auth.list_user_assets(user_id)
|
||||
assert any(a["filename"].endswith(".wav") for a in assets)
|
||||
auth.delete_user_asset(resp.get_json()["id"], user_id)
|
||||
|
||||
|
||||
def test_upload_route_rejects_an_audio_file_over_three_minutes(client):
|
||||
def test_upload_route_rejects_an_audio_file_over_three_minutes(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(_wav_bytes(200)), "trop-long.wav")},
|
||||
@@ -244,21 +267,22 @@ def test_upload_route_rejects_an_audio_file_over_three_minutes(client):
|
||||
# ---------- "Mes assets" — upload d'une vidéo, format MP4 uniquement,
|
||||
# limité à 5 minutes (demande explicite) ----------
|
||||
|
||||
def test_validate_video_duration_accepts_a_short_mp4():
|
||||
|
||||
def test_validate_video_duration_accepts_a_short_mp4() -> None:
|
||||
assert auth.validate_video_duration(_mp4_bytes(30)) is None
|
||||
|
||||
|
||||
def test_validate_video_duration_rejects_an_mp4_over_the_limit():
|
||||
def test_validate_video_duration_rejects_an_mp4_over_the_limit() -> None:
|
||||
error = auth.validate_video_duration(_mp4_bytes(400))
|
||||
assert error is not None
|
||||
assert "5 minutes" in error
|
||||
|
||||
|
||||
def test_validate_video_duration_rejects_unrecognized_content():
|
||||
def test_validate_video_duration_rejects_unrecognized_content() -> None:
|
||||
assert auth.validate_video_duration(b"pas-une-vraie-video") is not None
|
||||
|
||||
|
||||
def test_upload_route_accepts_a_short_mp4_video(client):
|
||||
def test_upload_route_accepts_a_short_mp4_video(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(_mp4_bytes(30)), "clip.mp4")},
|
||||
@@ -269,9 +293,10 @@ def test_upload_route_accepts_a_short_mp4_video(client):
|
||||
user_id = sess["user_id"]
|
||||
assets = auth.list_user_assets(user_id)
|
||||
assert any(a["filename"].endswith(".mp4") for a in assets)
|
||||
auth.delete_user_asset(resp.get_json()["id"], user_id)
|
||||
|
||||
|
||||
def test_upload_route_rejects_an_mp4_video_over_five_minutes(client):
|
||||
def test_upload_route_rejects_an_mp4_video_over_five_minutes(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(_mp4_bytes(400)), "trop-long.mp4")},
|
||||
@@ -285,7 +310,7 @@ def test_upload_route_rejects_an_mp4_video_over_five_minutes(client):
|
||||
assert not any(a["original_name"] == "trop-long.mp4" for a in assets)
|
||||
|
||||
|
||||
def test_upload_route_rejects_a_non_mp4_video_format(client):
|
||||
def test_upload_route_rejects_a_non_mp4_video_format(client: FlaskClient) -> None:
|
||||
"""Demande explicite : "une vidéo au format MP4" — webm/mov/avi sont
|
||||
reconnus comme "vidéo" par auth/user_asset_kind.py (rétrocompatibilité
|
||||
de l'organisation "Mes assets"), mais plus ACCEPTÉS à l'upload."""
|
||||
@@ -298,7 +323,7 @@ def test_upload_route_rejects_a_non_mp4_video_format(client):
|
||||
assert "MP4" in resp.get_json()["error"]
|
||||
|
||||
|
||||
def test_upload_route_rejects_unrecognized_mp4_content(client):
|
||||
def test_upload_route_rejects_unrecognized_mp4_content(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(b"pas-une-vraie-video"), "faux.mp4")},
|
||||
@@ -310,7 +335,8 @@ def test_upload_route_rejects_unrecognized_mp4_content(client):
|
||||
# ---------- Suppression d'un asset (demande explicite : "voir, utiliser
|
||||
# ou supprimer des son, image et vidéo") ----------
|
||||
|
||||
def test_delete_user_asset_route_removes_it(client):
|
||||
|
||||
def test_delete_user_asset_route_removes_it(client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(user_id, "abc123.png", original_name="photo.png", source="upload")
|
||||
@@ -319,12 +345,12 @@ def test_delete_user_asset_route_removes_it(client):
|
||||
assert auth.get_user_asset(asset_id) is None
|
||||
|
||||
|
||||
def test_delete_user_asset_route_404s_for_an_unknown_id(client):
|
||||
def test_delete_user_asset_route_404s_for_an_unknown_id(client: FlaskClient) -> None:
|
||||
resp = client.delete("/assets/999999")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_delete_user_asset_route_refuses_another_accounts_asset(client, user_client):
|
||||
def test_delete_user_asset_route_refuses_another_accounts_asset(client: FlaskClient, user_client: FlaskClient) -> None:
|
||||
# Un compte "user" fraîchement inscrit (sans projet) est redirigé vers
|
||||
# l'onboarding pour toute route hors _REACHABLE_WITHOUT_PROJECT (voir
|
||||
# core/auth_guard.py) — lui assigner un projet isole ce test sur le
|
||||
@@ -338,6 +364,7 @@ def test_delete_user_asset_route_refuses_another_accounts_asset(client, user_cli
|
||||
resp = user_client.delete(f"/assets/{asset_id}")
|
||||
assert resp.status_code == 404
|
||||
assert auth.get_user_asset(asset_id) is not None
|
||||
auth.delete_user_asset(asset_id, admin_id)
|
||||
|
||||
|
||||
# ---------- Reclasser un asset DÉJÀ importé entre "Fonds" et
|
||||
@@ -345,33 +372,36 @@ def test_delete_user_asset_route_refuses_another_accounts_asset(client, user_cli
|
||||
# image importée par le mauvais bouton, ou avant la séparation fond/décor,
|
||||
# restait bloquée dans la mauvaise section) ----------
|
||||
|
||||
def test_update_user_asset_scene_kind_reclassifies_the_asset(client):
|
||||
|
||||
def test_update_user_asset_scene_kind_reclassifies_the_asset(client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(user_id, "ciel.png", source="upload", scene_kind="decor")
|
||||
assert auth.update_user_asset_scene_kind(asset_id, user_id, "fond") is True
|
||||
asset = auth.get_user_asset(asset_id)
|
||||
assets = auth.list_user_assets(user_id)
|
||||
assert next(a for a in assets if a["id"] == asset_id)["scene_kind"] == "fond"
|
||||
auth.delete_user_asset(asset_id, user_id)
|
||||
|
||||
|
||||
def test_update_user_asset_scene_kind_refuses_when_not_the_owner(client, user_client):
|
||||
def test_update_user_asset_scene_kind_refuses_when_not_the_owner(client: FlaskClient, user_client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
admin_id = sess["user_id"]
|
||||
with user_client.session_transaction() as sess:
|
||||
other_user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(admin_id, "ciel.png", source="upload", scene_kind="decor")
|
||||
assert auth.update_user_asset_scene_kind(asset_id, other_user_id, "fond") is False
|
||||
auth.delete_user_asset(asset_id, admin_id)
|
||||
|
||||
|
||||
def test_update_user_asset_scene_kind_refuses_a_non_image_asset(client):
|
||||
def test_update_user_asset_scene_kind_refuses_a_non_image_asset(client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(user_id, "bip.mp3", source="upload")
|
||||
assert auth.update_user_asset_scene_kind(asset_id, user_id, "fond") is False
|
||||
auth.delete_user_asset(asset_id, user_id)
|
||||
|
||||
|
||||
def test_set_scene_kind_route_reclassifies_the_asset(client):
|
||||
def test_set_scene_kind_route_reclassifies_the_asset(client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(user_id, "ciel.png", source="upload", scene_kind="decor")
|
||||
@@ -379,17 +409,19 @@ def test_set_scene_kind_route_reclassifies_the_asset(client):
|
||||
assert resp.status_code == 200
|
||||
assets = auth.list_user_assets(user_id)
|
||||
assert next(a for a in assets if a["id"] == asset_id)["scene_kind"] == "fond"
|
||||
auth.delete_user_asset(asset_id, user_id)
|
||||
|
||||
|
||||
def test_set_scene_kind_route_rejects_an_invalid_value(client):
|
||||
def test_set_scene_kind_route_rejects_an_invalid_value(client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
asset_id = auth.create_user_asset(user_id, "ciel.png", source="upload", scene_kind="decor")
|
||||
resp = client.post(f"/assets/{asset_id}/scene-kind", json={"scene_kind": "autre"})
|
||||
assert resp.status_code == 400
|
||||
auth.delete_user_asset(asset_id, user_id)
|
||||
|
||||
|
||||
def test_set_scene_kind_route_404s_for_another_accounts_asset(client, user_client):
|
||||
def test_set_scene_kind_route_404s_for_another_accounts_asset(client: FlaskClient, user_client: FlaskClient) -> None:
|
||||
with client.session_transaction() as sess:
|
||||
admin_id = sess["user_id"]
|
||||
with user_client.session_transaction() as sess:
|
||||
@@ -398,21 +430,23 @@ def test_set_scene_kind_route_404s_for_another_accounts_asset(client, user_clien
|
||||
asset_id = auth.create_user_asset(admin_id, "ciel.png", source="upload", scene_kind="decor")
|
||||
resp = user_client.post(f"/assets/{asset_id}/scene-kind", json={"scene_kind": "fond"})
|
||||
assert resp.status_code == 404
|
||||
auth.delete_user_asset(asset_id, admin_id)
|
||||
|
||||
|
||||
# ---------- Dimensions réelles d'une image importée (bug : "je ne peux
|
||||
# pas poser un objet hors du champ de la caméra quand j'utilise un fond
|
||||
# de scène importé" — voir auth/image_dimensions.py) ----------
|
||||
|
||||
def test_image_dimensions_reads_a_png_header():
|
||||
|
||||
def test_image_dimensions_reads_a_png_header() -> None:
|
||||
assert auth.image_dimensions(_png_bytes(1920, 1080)) == (1920, 1080)
|
||||
|
||||
|
||||
def test_image_dimensions_returns_none_for_unrecognized_content():
|
||||
def test_image_dimensions_returns_none_for_unrecognized_content() -> None:
|
||||
assert auth.image_dimensions(b"pas-une-image") is None
|
||||
|
||||
|
||||
def test_upload_route_rejects_unrecognized_audio_content(client):
|
||||
def test_upload_route_rejects_unrecognized_audio_content(client: FlaskClient) -> None:
|
||||
resp = client.post(
|
||||
"/assets/upload",
|
||||
data={"file": (io.BytesIO(b"pas-un-vrai-son"), "faux.mp3")},
|
||||
|
||||
Reference in New Issue
Block a user