"""Admin document-upload toggle — enable/disable gate and enforcement on contract upload routes. ADMIN-006 style. The document_uploads_enabled setting gates every contract upload route for non-admins. Admins always bypass the lock. """ import pytest from app.extensions import db from app.models import AppSettings, Contract, User NON_ADMIN_ROLES = ['manager', 'coach'] def _redirected(response): return response.status_code in (301, 302) class TestDocumentUploadToggle: """Toggle logic — on/off round-trip and audit log.""" def test_toggle_from_enabled_to_disabled(self, client, as_role): as_role('admin') # Default is enabled client.post('/admin/toggle-document-uploads', data={}, follow_redirects=False) with client.application.app_context(): assert not AppSettings.get_bool('document_uploads_enabled', default=True) def test_toggle_from_disabled_to_enabled(self, client, as_role): as_role('admin') client.post('/admin/toggle-document-uploads', data={}, follow_redirects=False) client.post('/admin/toggle-document-uploads', data={}, follow_redirects=False) with client.application.app_context(): assert AppSettings.get_bool('document_uploads_enabled', default=True) def test_toggle_creates_audit_log(self, client, as_role): from app.models import AuditLog as_role('admin') client.post('/admin/toggle-document-uploads', data={}, follow_redirects=False) with client.application.app_context(): entry = AuditLog.query.filter_by(action='document_uploads_toggled').first() assert entry is not None def test_default_is_enabled(self, app): with app.app_context(): assert AppSettings.get_bool('document_uploads_enabled', default=True) class TestDocumentUploadGateEnforcement: """Non-admin roles are blocked when the toggle is off.""" def _contract_for(self, app, player_id): """Create a minimal contract for enforcement tests.""" from datetime import datetime with app.app_context(): admin = User.query.filter_by(role='admin').first() contract = Contract( player_id=player_id, uploaded_by_id=admin.id, original_filename='test.pdf', stored_filename='test.pdf', file_path='contrats signes/test.pdf', status='pending_player_signing', uploaded_at=datetime.utcnow(), ) db.session.add(contract) db.session.commit() return contract.id @pytest.mark.parametrize('role', NON_ADMIN_ROLES) def test_non_admin_cannot_upload_contract_when_disabled( self, app, client, as_role, make_user, role ): as_role('admin') client.post('/admin/toggle-document-uploads', data={}) # disable as_role(role) response = client.post( '/users/contracts/upload', data={'player_id': '', 'contract_file': ''}, follow_redirects=True, ) html = response.data.decode() # Default locale is French — the flash message says "désactivés" assert 'désactivés' in html or 'disabled' in html.lower() @pytest.mark.parametrize('role', NON_ADMIN_ROLES) def test_admin_can_always_upload_contract(self, client, as_role, role): as_role('admin') # Pre-create a player for the contract as_role('player') client.post('/admin/toggle-document-uploads', data={}) # disable # Admin should still be able to reach the upload page response = client.get('/users/contracts/upload', follow_redirects=False) assert _redirected(response) or response.status_code == 200 def test_player_cannot_upload_signed_when_disabled(self, app, client, as_role, make_user): as_role('admin') client.post('/admin/toggle-document-uploads', data={}) # disable while logged in as admin player_id = make_user('player') contract_id = self._contract_for(app, player_id) # Log in as the player with app.app_context(): username = db.session.get(User, player_id).username client.post('/auth/login', data={'username': username, 'password': 'Password123'}) response = client.post( f'/users/contracts/{contract_id}/upload_signed', data={'signed_file': ''}, follow_redirects=True, ) html = response.data.decode() assert 'désactivés' in html or 'disabled' in html.lower() def test_admin_can_always_upload(self, app, client, as_role): as_role('admin') client.post('/admin/toggle-document-uploads', data={}) # disable # Admin GET the upload page — should be allowed (not redirected away) response = client.get('/users/contracts/upload', follow_redirects=False) # The admin bypasses the lock so they see the upload form (200) # or get redirected (if no players exist, etc). Either way is fine # as long as it's not the flash about uploads being disabled. body = response.data.decode() if response.status_code == 200 else '' # The toggle flash ("désactivés") could still be present, but the # upload-disabled message ("désactivés") must not appear in the body. # We check for the French lock message specifically. assert 'actuellement désactivés' not in body