Files
Forge-Engine/tests/test_bornage.py
T
williamandClaude Sonnet 5 c57420c8c9 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>
2026-09-15 16:06:15 +02:00

167 lines
5.8 KiB
Python

"""Tests du bornage automatique des champs numériques (2.2)."""
import json
import re
from typing import Any
from flask.testing import FlaskClient
import db
from tests.conftest import not_none
def _create_screen(client: FlaskClient, slug: str, name: str = "Accueil") -> Any:
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
return int(not_none(re.search(r"/screens/(\d+)/edit", resp.headers["Location"])).group(1))
def _create_numeric_object(
client: FlaskClient,
slug: str,
field_name: str = "reputation",
min_v: str = "0",
max_v: str = "100",
start: str = "50",
) -> Any:
field: dict[str, Any] = {"name": field_name, "type": "nombre_entier"}
if min_v != "":
field["min_value"] = min_v
if max_v != "":
field["max_value"] = max_v
def_id = db.create_definition(slug, "Partie", [field])
definition = not_none(db.get_definition(slug, def_id))
db.insert_row(slug, definition, {field_name: start})
return def_id
def _get_row_value(client: FlaskClient, slug: str, def_id: int, field_name: str) -> Any:
import db
definition = not_none(db.get_definition(slug, def_id))
rows = db.list_rows(slug, definition)
return rows[0].get(db.slugify(field_name).replace("-", "_"))
def test_field_bounds_are_stored_at_creation(client: FlaskClient, game: str) -> None:
def_id = _create_numeric_object(client, game, "reputation", "0", "100", "50")
import db
definition = not_none(db.get_definition(game, def_id))
field = definition["fields"][0]
assert field["min_value"] == 0
assert field["max_value"] == 100
def test_incrementer_clamps_to_max(client: FlaskClient, game: str) -> None:
"""Régression 2.2 : une action "Augmenter de..." ne doit jamais faire
dépasser la borne maxi réglée sur le champ, même si le montant ajouté
dépasse largement ce qu'il reste avant la limite."""
def_id = _create_numeric_object(client, game, "reputation", "0", "100", "95")
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(
{
"node_type": "action",
"action_type": "modifier_donnee",
"target_definition_id": def_id,
"target_row_id": 1,
"target_field": "reputation",
"data_operation": "incrementer",
"data_value": "50",
}
),
content_type="application/json",
)
node = resp.get_json()
resp = client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data")
assert resp.get_json() == {"ok": True}
new_value = _get_row_value(client, game, def_id, "reputation")
assert new_value == 100, f"la valeur aurait dû être bornée à 100, vaut {new_value}"
def test_decrementer_clamps_to_min(client: FlaskClient, game: str) -> None:
"""Régression 2.2 : symétrique pour "Diminuer de..." et la borne mini."""
def_id = _create_numeric_object(client, game, "reputation", "0", "100", "5")
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(
{
"node_type": "action",
"action_type": "modifier_donnee",
"target_definition_id": def_id,
"target_row_id": 1,
"target_field": "reputation",
"data_operation": "decrementer",
"data_value": "50",
}
),
content_type="application/json",
)
node = resp.get_json()
client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data")
new_value = _get_row_value(client, game, def_id, "reputation")
assert new_value == 0, f"la valeur aurait dû être bornée à 0, vaut {new_value}"
def test_definir_nombre_also_clamped(client: FlaskClient, game: str) -> None:
"""Régression 2.2 : "Donner cette valeur (nombre)" doit aussi être
bornée, pas seulement les incréments/décréments."""
def_id = _create_numeric_object(client, game, "reputation", "0", "100", "50")
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(
{
"node_type": "action",
"action_type": "modifier_donnee",
"target_definition_id": def_id,
"target_row_id": 1,
"target_field": "reputation",
"data_operation": "definir_nombre",
"data_value": "9999",
}
),
content_type="application/json",
)
node = resp.get_json()
client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data")
new_value = _get_row_value(client, game, def_id, "reputation")
assert new_value == 100
def test_no_bounds_set_means_unlimited(client: FlaskClient, game: str) -> None:
"""Sans bornes réglées (champ laissé vide), le comportement doit rester
celui d'avant 2.2 : aucune limite appliquée."""
def_id = _create_numeric_object(client, game, "reputation", "", "", "50")
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(
{
"node_type": "action",
"action_type": "modifier_donnee",
"target_definition_id": def_id,
"target_row_id": 1,
"target_field": "reputation",
"data_operation": "incrementer",
"data_value": "500",
}
),
content_type="application/json",
)
node = resp.get_json()
client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data")
new_value = _get_row_value(client, game, def_id, "reputation")
assert new_value == 550