ARCH-005, seconde moitie. Meme forme que pour les matchs : des champs lus a la main sur request.form, deux verifies et le reste cru sur parole. Cote tryout : - game pilote la liste des postes et les champs de gamertag montres au joueur qui s inscrit. Il etait accepte tel quel : une faute de frappe produisait une selection pour laquelle personne ne pouvait etre evalue ; - max_players etait int(x) if x else None — un 500 sur « twelve », et un -3 accepte sans broncher ; - coach_ids etait charge par User.id.in_(...) sans filtre de role. Une soumission fabriquee a la main pouvait donc nommer un joueur coach d une selection, ce qui est une attribution de droits : gerer la selection et evaluer ses joueurs. Ce n est pas un formulaire que l interface propose, et ca marchait. Cote evaluation, validate_score transformait tout ce qui sortait de 1..10 — 11, 0, « bien » — en None. Le critere disparaissait de la moyenne et la page annoncait l evaluation enregistree. Rien ne distinguait « non evalue » de « evalue, refuse, et oublie ». Le calcul de la moyenne remonte sur le modele, en Evaluation.overall_from et apply_scores. Il vivait dans la route, additionnant neuf variables locales, et ne pouvait pas etre exerce sans requete HTTP, session authentifiee et base — c est TEST-002, et c est pourquoi le calcul des scores n avait aucun test. Il en a maintenant six, sans rien monter. Une precision qui compte : aucun critere rempli donne None, pas 0. La grille commence a 1, donc un zero serait une note qu aucun joueur ne peut recevoir, et qui le classerait sous tout le monde dans la liste. Les neuf criteres sont ecrits en toutes lettres dans le schema plutot que generes depuis le modele — un schema se lit — et un test verifie que les deux listes coincident. C est la garde qui empeche la derive, pas l astuce. Douze chaines traduites, dont trois que pybabel avait devinees en fuzzy : une entree fuzzy est ignoree a l execution, le piege est consigne dans docs/translations.md. 30 tests neufs. 477 au total.
120 lines
3.9 KiB
Python
120 lines
3.9 KiB
Python
"""Creating and editing a tryout, through the form (ARCH-005).
|
|
|
|
Same shape as the match routes: ten fields read off `request.form`, two of
|
|
them checked and the rest believed.
|
|
|
|
- `game` drives the position list and the gamertag fields a registering
|
|
player is shown. It was accepted as any string, so a typo produced a
|
|
tryout nobody could be evaluated for;
|
|
- `max_players` was `int(x) if x else None` — a 500 on 'twelve', and a
|
|
cheerful -3 otherwise;
|
|
- `coach_ids` were loaded with `User.id.in_(...)` and no role filter, so a
|
|
hand-made submission could name a player as coach of a tryout, which is
|
|
a permission grant.
|
|
"""
|
|
|
|
from datetime import date
|
|
|
|
import pytest
|
|
|
|
from app.extensions import db
|
|
from app.models import OrgTeam, Tryout
|
|
|
|
|
|
@pytest.fixture
|
|
def form_context(app, as_role, make_user):
|
|
admin_id = as_role('admin')
|
|
coach_id = make_user('coach')
|
|
player_id = make_user('player')
|
|
|
|
with app.app_context():
|
|
org_team = OrgTeam(name='Varsity', created_by=admin_id)
|
|
db.session.add(org_team)
|
|
db.session.commit()
|
|
return {'org_team_id': org_team.id, 'coach_id': coach_id, 'player_id': player_id}
|
|
|
|
|
|
VALID = {
|
|
'title': 'Spring tryout',
|
|
'game': 'Valorant',
|
|
'date': '2030-04-01',
|
|
'end_date': '2030-04-03',
|
|
'location': 'Arena',
|
|
'max_players': '12',
|
|
}
|
|
|
|
|
|
def _create(client, **overrides):
|
|
return client.post('/tryouts/create', data=dict(VALID, **overrides), follow_redirects=True)
|
|
|
|
|
|
def _only_tryout(app):
|
|
with app.app_context():
|
|
return Tryout.query.one_or_none()
|
|
|
|
|
|
class TestCreating:
|
|
def test_a_valid_tryout_is_stored_with_typed_values(self, app, client, form_context):
|
|
_create(client)
|
|
|
|
tryout = _only_tryout(app)
|
|
assert tryout is not None
|
|
assert tryout.date == date(2030, 4, 1)
|
|
assert tryout.end_date == date(2030, 4, 3)
|
|
assert tryout.max_players == 12
|
|
|
|
def test_no_end_date_is_allowed(self, app, client, form_context):
|
|
_create(client, end_date='')
|
|
|
|
assert _only_tryout(app).end_date is None
|
|
|
|
def test_no_player_limit_is_allowed(self, app, client, form_context):
|
|
_create(client, max_players='')
|
|
|
|
assert _only_tryout(app).max_players is None
|
|
|
|
def test_a_coach_can_be_attached(self, app, client, form_context):
|
|
_create(client, coach_ids=str(form_context['coach_id']))
|
|
|
|
with app.app_context():
|
|
tryout = Tryout.query.one()
|
|
assert [c.id for c in tryout.coaches] == [form_context['coach_id']]
|
|
|
|
|
|
class TestRefusals:
|
|
def test_an_unknown_game_is_refused(self, app, client, form_context):
|
|
_create(client, game='Pong')
|
|
|
|
assert _only_tryout(app) is None
|
|
|
|
def test_an_empty_title_is_refused(self, app, client, form_context):
|
|
_create(client, title='')
|
|
|
|
assert _only_tryout(app) is None
|
|
|
|
def test_a_non_numeric_player_limit_is_refused_not_crashed(self, app, client, form_context):
|
|
response = _create(client, max_players='twelve')
|
|
|
|
assert response.status_code == 200
|
|
assert _only_tryout(app) is None
|
|
|
|
def test_a_negative_player_limit_is_refused(self, app, client, form_context):
|
|
_create(client, max_players='-3')
|
|
|
|
assert _only_tryout(app) is None
|
|
|
|
def test_an_end_before_the_start_is_refused(self, app, client, form_context):
|
|
_create(client, date='2030-04-05', end_date='2030-04-01')
|
|
|
|
assert _only_tryout(app) is None
|
|
|
|
def test_a_player_cannot_be_slipped_in_as_a_coach(self, app, client, form_context):
|
|
"""Not a form the interface offers — the select lists coaches. It is
|
|
a hand-made submission, and it used to work: being a coach of a
|
|
tryout carries the right to manage it and evaluate its players."""
|
|
_create(client, coach_ids=str(form_context['player_id']))
|
|
|
|
with app.app_context():
|
|
tryout = Tryout.query.one()
|
|
assert list(tryout.coaches) == []
|