"""Admin season lifecycle — start/end and gate on team matches. ADMIN-007. The season_active setting gates regular-season match scheduling for non-admins. These tests verify start/end season and the gate. """ import pytest from app.extensions import db from app.models import AppSettings, OrgTeam, TeamMatch, User NON_ADMIN_ROLES = ['manager', 'coach'] def _redirected(response): return response.status_code in (301, 302) # --------------------------------------------------------------------------- # Season lifecycle # --------------------------------------------------------------------------- class TestSeasonLifecycle: def test_start_season_sets_fields(self, client, as_role): as_role('admin') client.post( '/admin/season/start', data={'season_name': 'Fall 2026', 'season_start': '2026-09-01'}, follow_redirects=False, ) with client.application.app_context(): assert AppSettings.get('season_name') == 'Fall 2026' assert AppSettings.get('season_start') == '2026-09-01' assert AppSettings.get_bool('season_active', default=False) def test_cannot_start_season_when_already_active(self, client, as_role): as_role('admin') client.post( '/admin/season/start', data={'season_name': 'Fall 2026', 'season_start': '2026-09-01'}, ) # Second start should be rejected response = client.post( '/admin/season/start', data={'season_name': 'Spring 2027', 'season_start': '2027-01-01'}, follow_redirects=True, ) html = response.data.decode() assert 'already active' in html.lower() def test_start_season_creates_audit_log(self, client, as_role): from app.models import AuditLog as_role('admin') client.post( '/admin/season/start', data={'season_name': 'Fall 2026', 'season_start': '2026-09-01'}, ) with client.application.app_context(): entry = AuditLog.query.filter_by(action='season_started').first() assert entry is not None def test_end_season_sets_end_date_and_deactivates(self, client, as_role): as_role('admin') client.post( '/admin/season/start', data={'season_name': 'Fall 2026', 'season_start': '2026-09-01'}, ) client.post( '/admin/season/end', data={'season_end': '2026-12-15'}, follow_redirects=False, ) with client.application.app_context(): assert AppSettings.get('season_end') == '2026-12-15' assert not AppSettings.get_bool('season_active', default=False) def test_cannot_end_season_when_inactive(self, client, as_role): as_role('admin') # No season started response = client.post( '/admin/season/end', data={'season_end': '2026-12-15'}, follow_redirects=True, ) html = response.data.decode() assert 'no season is currently active' in html.lower() def test_end_season_creates_audit_log(self, client, as_role): from app.models import AuditLog as_role('admin') client.post( '/admin/season/start', data={'season_name': 'Fall 2026', 'season_start': '2026-09-01'}, ) client.post('/admin/season/end', data={'season_end': '2026-12-15'}) with client.application.app_context(): entry = AuditLog.query.filter_by(action='season_ended').first() assert entry is not None # --------------------------------------------------------------------------- # Gate enforcement on team matches # --------------------------------------------------------------------------- class TestSeasonGateEnforcement: @pytest.fixture def org_team(self, app, client, as_role): as_role('admin') client.post( '/teams/create', data={'name': 'Varsity Test'}, follow_redirects=False, ) with app.app_context(): return OrgTeam.query.filter_by(name='Varsity Test').first().id @pytest.mark.parametrize('role', NON_ADMIN_ROLES) def test_non_admin_cannot_create_match_when_season_inactive( self, client, as_role, org_team, role ): as_role(role) response = client.get( f'/team-matches/{org_team}/create', follow_redirects=True, ) html = response.data.decode() assert 'season is not active' in html.lower() or 'begin a season' in html.lower() @pytest.mark.parametrize('role', NON_ADMIN_ROLES) def test_non_admin_cannot_delete_match_when_season_inactive( self, app, client, as_role, org_team, role, make_user ): # Create a match as admin (season active not required for admin) as_role('admin') # Start season so match can be created client.post( '/admin/season/start', data={'season_name': 'Fall', 'season_start': '2026-09-01'}, ) client.post( f'/team-matches/{org_team}/create', data={'title': 'Match Test', 'date': '2026-10-01', 'start_time': '10:00'}, follow_redirects=False, ) with app.app_context(): match_id = TeamMatch.query.filter_by(title='Match Test').first().id # End season client.post('/admin/season/end', data={'season_end': '2026-12-15'}) as_role(role) response = client.post( f'/team-matches/{match_id}/delete', data={}, follow_redirects=True, ) html = response.data.decode() assert 'season is not active' in html.lower() or 'begin a season' in html.lower() def test_admin_can_always_schedule_match(self, client, as_role, org_team): as_role('admin') # No season active response = client.post( f'/team-matches/{org_team}/create', data={'title': 'Admin Match', 'date': '2026-10-01', 'start_time': '10:00'}, follow_redirects=False, ) assert _redirected(response) # success, not blocked