170 lines
6.1 KiB
Python
170 lines
6.1 KiB
Python
"""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 |