"""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