Files
team-tryouts/tests/test_deletions.py
GGThedandClaude Opus 5 7cec18c139 style: formater le depot avec ruff format
QUA-002, premiere moitie. **Ce commit ne fait que reformater** : aucun
changement de comportement, aucune ligne de logique touchee. 72 fichiers,
4 restaient deja conformes. Il est isole exprès, pour que `git log -p` sur
les commits voisins reste lisible.

`quote-style = "preserve"` etait deja pose dans pyproject.toml, ce qui
evite le brassage guillemets simples / doubles : le diff porte sur les
retours a la ligne, l indentation des appels longs et les virgules
finales, pas sur le style de chaine.

Verification : 263 tests passent avant et apres, ruff check propre.

L activation en CI arrive dans le commit suivant, separement, pour que ce
diff-ci ne contienne rien d autre.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 15:53:10 -04:00

224 lines
8.1 KiB
Python

"""Deletion of entities that other rows point at.
DATA-004, DATA-005, DATA-006. Three delete routes clean up some of their
dependants and ignore others. Every ignored one is a foreign key pointing
at the row being removed, so the delete raises an IntegrityError, which
surfaces as a 500 and rolls back — the entity simply cannot be deleted once
it has been used.
These are reachable through the interface by any manager or admin, and the
audit could only rate them "likely" without executing them. That is what
this file is for.
"""
from datetime import date, time
import pytest
from app.extensions import db
from app.models import (
Match,
MatchParticipant,
OrgTeam,
PersonalNote,
Team,
TeamMatch,
TeamMember,
TeamNote,
TeamPlayer,
Tryout,
TryoutRegistration,
)
@pytest.fixture
def world(app, make_user):
"""A tryout with a team, a match, a participant and a note on each.
Deliberately mirrors what an actual tryout looks like after a session:
the empty case already worked, the populated one is what breaks.
"""
admin_id = make_user('admin')
coach_id = make_user('coach')
player_id = make_user('player')
with app.app_context():
org_team = OrgTeam(name='Varsity', created_by=admin_id, coach_id=coach_id)
db.session.add(org_team)
db.session.flush()
tryout = Tryout(
title='Spring',
game='Valorant',
date=date(2030, 4, 1),
created_by=admin_id,
target_org_team_id=org_team.id,
)
db.session.add(tryout)
db.session.flush()
team = Team(tryout_id=tryout.id, name='Alpha', created_by=admin_id)
db.session.add(team)
db.session.flush()
match = Match(
tryout_id=tryout.id,
title='Scrim',
date=date(2030, 4, 2),
start_time=time(18, 0),
match_type='player_scrim',
created_by=admin_id,
)
db.session.add(match)
db.session.flush()
db.session.add_all(
[
TryoutRegistration(tryout_id=tryout.id, player_id=player_id),
TeamMember(team_id=team.id, player_id=player_id),
MatchParticipant(match_id=match.id, player_id=player_id),
TeamPlayer(player_id=player_id, org_team_id=org_team.id),
TeamNote(org_team_id=org_team.id, coach_id=coach_id, content='Team note'),
TeamMatch(
org_team_id=org_team.id,
title='Season match',
date=date(2030, 4, 5),
created_by=admin_id,
),
# A note referencing all three contexts at once.
PersonalNote(
player_id=player_id,
coach_id=coach_id,
content='Watch the entries',
match_id=match.id,
team_id=team.id,
tryout_id=tryout.id,
),
]
)
db.session.commit()
return {
'admin_id': admin_id,
'coach_id': coach_id,
'player_id': player_id,
'org_team_id': org_team.id,
'tryout_id': tryout.id,
'team_id': team.id,
'match_id': match.id,
}
def _login_admin(client, app, admin_id, login):
from app.models import User
with app.app_context():
username = db.session.get(User, admin_id).username
login(username)
class TestDeleteMatch:
"""DATA-004 — Match.participants had no cascade, and PersonalNote.match_id
references matches.id."""
def test_a_populated_match_can_be_deleted(self, app, client, world, login):
_login_admin(client, app, world['admin_id'], login)
response = client.post(f"/matches/{world['match_id']}/delete", follow_redirects=False)
assert response.status_code < 500, 'deleting a used match raised'
with app.app_context():
assert db.session.get(Match, world['match_id']) is None
def test_its_participants_go_with_it(self, app, client, world, login):
_login_admin(client, app, world['admin_id'], login)
client.post(f"/matches/{world['match_id']}/delete", follow_redirects=True)
with app.app_context():
assert MatchParticipant.query.filter_by(match_id=world['match_id']).count() == 0
def test_notes_survive_but_lose_their_match_context(self, app, client, world, login):
"""A coach's observation keeps its value once the match is gone;
deleting it with the match would destroy unrelated content."""
_login_admin(client, app, world['admin_id'], login)
client.post(f"/matches/{world['match_id']}/delete", follow_redirects=True)
with app.app_context():
note = PersonalNote.query.filter_by(player_id=world['player_id']).one()
assert note.content == 'Watch the entries'
assert note.match_id is None
class TestDeleteTryout:
"""DATA-006 — delete_tryout removed matches, teams, registrations and
evaluations, but not the PersonalNote rows pointing at any of them."""
def test_a_populated_tryout_can_be_deleted(self, app, client, world, login):
_login_admin(client, app, world['admin_id'], login)
response = client.post(f"/tryouts/{world['tryout_id']}/delete", follow_redirects=False)
assert response.status_code < 500
with app.app_context():
assert db.session.get(Tryout, world['tryout_id']) is None
def test_its_matches_and_teams_go_with_it(self, app, client, world, login):
_login_admin(client, app, world['admin_id'], login)
client.post(f"/tryouts/{world['tryout_id']}/delete", follow_redirects=True)
with app.app_context():
assert db.session.get(Match, world['match_id']) is None
assert db.session.get(Team, world['team_id']) is None
assert TryoutRegistration.query.filter_by(tryout_id=world['tryout_id']).count() == 0
def test_notes_survive_the_tryout(self, app, client, world, login):
_login_admin(client, app, world['admin_id'], login)
client.post(f"/tryouts/{world['tryout_id']}/delete", follow_redirects=True)
with app.app_context():
note = PersonalNote.query.filter_by(player_id=world['player_id']).one()
assert note.tryout_id is None
assert note.match_id is None
assert note.team_id is None
class TestDeleteTeam:
"""DATA-005 — delete_team handled tryouts and TeamPlayer, but not
TeamNote or TeamMatch, both of which are NOT NULL foreign keys. It also
committed three times, so a failure at the third step left the tryouts
detached and the players removed without deleting the team."""
def test_a_populated_team_can_be_deleted(self, app, client, world, login):
_login_admin(client, app, world['admin_id'], login)
response = client.post(f"/teams/{world['org_team_id']}/delete", follow_redirects=False)
assert response.status_code < 500
with app.app_context():
assert db.session.get(OrgTeam, world['org_team_id']) is None
def test_its_notes_and_season_matches_go_with_it(self, app, client, world, login):
_login_admin(client, app, world['admin_id'], login)
client.post(f"/teams/{world['org_team_id']}/delete", follow_redirects=True)
with app.app_context():
assert TeamNote.query.filter_by(org_team_id=world['org_team_id']).count() == 0
assert TeamMatch.query.filter_by(org_team_id=world['org_team_id']).count() == 0
assert TeamPlayer.query.filter_by(org_team_id=world['org_team_id']).count() == 0
def test_the_tryout_survives_and_is_detached(self, app, client, world, login):
"""A tryout outlives the team it was aimed at."""
_login_admin(client, app, world['admin_id'], login)
client.post(f"/teams/{world['org_team_id']}/delete", follow_redirects=True)
with app.app_context():
tryout = db.session.get(Tryout, world['tryout_id'])
assert tryout is not None
assert tryout.target_org_team_id is None