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]>
This commit is contained in:
GGThed
2026-08-08 15:53:10 -04:00
co-authored by Claude Opus 5
parent 2f40290f00
commit 7cec18c139
72 changed files with 2658 additions and 1449 deletions
+1
View File
@@ -183,6 +183,7 @@ def as_role(app, client, make_user, login):
user_id = make_user(role, **kwargs)
with app.app_context():
from app.models import User
username = _db.session.get(User, user_id).username
response = login(username)
assert response.status_code in (301, 302), (
+60 -22
View File
@@ -106,10 +106,17 @@ class TestAdministrativeEvents:
def test_account_creation_is_recorded(self, client, as_role, auth_log):
as_role('admin')
client.post('/users/create', data={
'username': 'newcoach', 'email': '[email protected]',
'password': 'Password123', 'full_name': 'New Coach', 'role': 'coach',
}, follow_redirects=True)
client.post(
'/users/create',
data={
'username': 'newcoach',
'email': '[email protected]',
'password': 'Password123',
'full_name': 'New Coach',
'role': 'coach',
},
follow_redirects=True,
)
assert _has_event(auth_log, 'account.created_by_admin')
@@ -117,10 +124,16 @@ class TestAdministrativeEvents:
target_id = make_user('player')
as_role('admin')
client.post(f'/users/{target_id}/edit', data={
'full_name': 'Promoted', 'email': '[email protected]t',
'role': 'coach', 'is_active_account': 'on',
}, follow_redirects=True)
client.post(
f'/users/{target_id}/edit',
data={
'full_name': 'Promoted',
'email': '[email protected]',
'role': 'coach',
'is_active_account': 'on',
},
follow_redirects=True,
)
message = next(m for m in _events(auth_log) if 'event=account.role_changed' in m)
assert 'previous_role=player' in message
@@ -135,17 +148,21 @@ class TestAdministrativeEvents:
assert _has_event(auth_log, 'account.deleted')
def test_a_self_service_password_change_is_recorded(
self, app, client, as_role, auth_log
):
def test_a_self_service_password_change_is_recorded(self, app, client, as_role, auth_log):
user_id = as_role('player')
with app.app_context():
username = db.session.get(User, user_id).username
client.post('/users/profile/edit', data={
'username': username, 'full_name': 'Same Name',
'email': '[email protected]', 'password': 'BrandNew123',
}, follow_redirects=True)
client.post(
'/users/profile/edit',
data={
'username': username,
'full_name': 'Same Name',
'email': '[email protected]',
'password': 'BrandNew123',
},
follow_redirects=True,
)
assert _has_event(auth_log, 'account.password_changed')
@@ -162,8 +179,13 @@ class TestRedaction:
from app.logging_config import SensitiveDataFilter
record = logging.LogRecord(
'test', logging.ERROR, 'x.py', 1,
'Database failure: %s', ('password=hunter2 host=db.internal',), None,
'test',
logging.ERROR,
'x.py',
1,
'Database failure: %s',
('password=hunter2 host=db.internal',),
None,
)
SensitiveDataFilter().filter(record)
@@ -175,8 +197,13 @@ class TestRedaction:
from app.logging_config import SensitiveDataFilter
record = logging.LogRecord(
'test', logging.INFO, 'x.py', 1,
'Calling Discord with %s', ('Bearer abcdef123456',), None,
'test',
logging.INFO,
'x.py',
1,
'Calling Discord with %s',
('Bearer abcdef123456',),
None,
)
SensitiveDataFilter().filter(record)
@@ -186,8 +213,13 @@ class TestRedaction:
from app.logging_config import SensitiveDataFilter
record = logging.LogRecord(
'test', logging.INFO, 'x.py', 1,
'event=login.success username=%s', ('alice',), None,
'test',
logging.INFO,
'x.py',
1,
'event=login.success username=%s',
('alice',),
None,
)
SensitiveDataFilter().filter(record)
@@ -197,6 +229,12 @@ class TestRedaction:
from app.logging_config import SensitiveDataFilter
record = logging.LogRecord(
'test', logging.INFO, 'x.py', 1, 'broken %d', ('not-a-number',), None,
'test',
logging.INFO,
'x.py',
1,
'broken %d',
('not-a-number',),
None,
)
assert SensitiveDataFilter().filter(record) is True
+22 -19
View File
@@ -121,20 +121,26 @@ class TestLogout:
with app_with_csrf.app_context():
from app.extensions import hash_password
from app.models import Player
user = Player(username='navtest', password_hash=hash_password('Password123'),
role='player', full_name='Nav Test', email='[email protected]')
user = Player(
username='navtest',
password_hash=hash_password('Password123'),
role='player',
full_name='Nav Test',
email='[email protected]',
)
db.session.add(user)
db.session.commit()
page = client.get('/auth/login').get_data(as_text=True)
token = re.search(r'name="csrf_token" value="([^"]+)"', page).group(1)
client.post('/auth/login', data={'username': 'navtest',
'password': 'Password123',
'csrf_token': token})
client.post(
'/auth/login',
data={'username': 'navtest', 'password': 'Password123', 'csrf_token': token},
)
body = client.get('/users/profile').get_data(as_text=True)
form = re.search(
r'<form method="POST" action="/auth/logout".*?</form>', body, re.S)
form = re.search(r'<form method="POST" action="/auth/logout".*?</form>', body, re.S)
assert form is not None, 'no logout form in the navigation'
assert 'name="csrf_token"' in form.group(0)
@@ -150,9 +156,7 @@ class TestLoginRejection:
response = client.get('/users/profile', follow_redirects=False)
assert response.status_code in (301, 302)
def test_login_failure_message_does_not_reveal_account_existence(
self, client, make_user, app
):
def test_login_failure_message_does_not_reveal_account_existence(self, client, make_user, app):
"""Compares the rendered flash messages rather than looking for a
known substring: the site is served in French by default, so an
English marker would silently match nothing on both sides and make
@@ -171,9 +175,7 @@ class TestLoginRejection:
assert failure_message(username) == failure_message('no-such-account')
def test_the_message_stays_the_same_past_the_attempt_threshold(
self, client, make_user, app
):
def test_the_message_stays_the_same_past_the_attempt_threshold(self, client, make_user, app):
"""The tally used to be counted out loud — "3 attempt(s) remaining"
— which is the same disclosure, spread over five requests."""
user_id = make_user('player')
@@ -200,13 +202,13 @@ class TestFailedAttemptThrottle:
@staticmethod
def _exhaust(client, username, times=6):
for _ in range(times):
client.post('/auth/login',
data={'username': username, 'password': 'WrongPassword1'},
follow_redirects=True)
client.post(
'/auth/login',
data={'username': username, 'password': 'WrongPassword1'},
follow_redirects=True,
)
def test_the_owner_still_gets_in_after_the_threshold(
self, app, client, make_user, login
):
def test_the_owner_still_gets_in_after_the_threshold(self, app, client, make_user, login):
user_id = make_user('player')
with app.app_context():
username = db.session.get(User, user_id).username
@@ -294,6 +296,7 @@ class TestRedirectValidation:
with app.test_request_context('/auth/login'):
from flask import request
assert is_safe_url(f'http://{request.host}/dashboard')
def test_the_login_redirect_refuses_to_leave_the_site(self, client, make_user, app):
+103 -66
View File
@@ -61,9 +61,7 @@ class TestVerticalAccess:
def test_only_admin_reaches_user_management(self, client, as_role, role, route):
as_role(role)
response = client.get(route, follow_redirects=False)
assert _redirected(response), (
f'{role} reached {route}, which is meant to be admin-only'
)
assert _redirected(response), f'{role} reached {route}, which is meant to be admin-only'
def test_admin_reaches_user_management(self, client, as_role):
as_role('admin')
@@ -106,8 +104,10 @@ class TestHorizontalAccess:
owner_id = make_user('player')
with app.app_context():
slot = PlayerDisponibility(
player_id=owner_id, day_of_week=1,
start_time=time(10, 0), end_time=time(10, 30),
player_id=owner_id,
day_of_week=1,
start_time=time(10, 0),
end_time=time(10, 30),
)
db.session.add(slot)
db.session.commit()
@@ -134,8 +134,11 @@ class TestNestedResourceOwnership:
with app.app_context():
tryout = Tryout(
title=title, game='Valorant', date=date(2030, 1, 1),
created_by=owner_id, status='upcoming',
title=title,
game='Valorant',
date=date(2030, 1, 1),
created_by=owner_id,
status='upcoming',
)
db.session.add(tryout)
db.session.flush()
@@ -144,9 +147,7 @@ class TestNestedResourceOwnership:
db.session.commit()
return tryout.id, team.id
def test_cannot_add_a_player_to_a_team_of_another_tryout(
self, app, client, as_role, make_user
):
def test_cannot_add_a_player_to_a_team_of_another_tryout(self, app, client, as_role, make_user):
from app.models import TeamMember, TryoutRegistration
other_admin = make_user('admin')
@@ -157,8 +158,7 @@ class TestNestedResourceOwnership:
player_id = make_user('player')
with app.app_context():
db.session.add(TryoutRegistration(
tryout_id=own_tryout_id, player_id=player_id))
db.session.add(TryoutRegistration(tryout_id=own_tryout_id, player_id=player_id))
db.session.commit()
response = client.post(
@@ -167,15 +167,11 @@ class TestNestedResourceOwnership:
follow_redirects=False,
)
assert response.status_code == 404, (
'a team belonging to another tryout was accepted'
)
assert response.status_code == 404, 'a team belonging to another tryout was accepted'
with app.app_context():
assert TeamMember.query.filter_by(team_id=foreign_team_id).count() == 0
def test_cannot_add_a_player_who_is_not_registered(
self, app, client, as_role, make_user
):
def test_cannot_add_a_player_who_is_not_registered(self, app, client, as_role, make_user):
from app.models import TeamMember
manager_id = as_role('manager')
@@ -191,9 +187,7 @@ class TestNestedResourceOwnership:
with app.app_context():
assert TeamMember.query.filter_by(team_id=team_id).count() == 0
def test_a_registered_player_can_still_be_added(
self, app, client, as_role, make_user
):
def test_a_registered_player_can_still_be_added(self, app, client, as_role, make_user):
"""Guard against over-correcting: the normal path must keep working."""
from app.models import TeamMember, TryoutRegistration
@@ -243,11 +237,15 @@ class TestInputValidation:
user_id = as_role('player')
payload = '<img src=x onerror=alert(1)>'
client.post('/users/profile/edit', data={
'username': payload,
'full_name': 'Legit Name',
'email': '[email protected]',
}, follow_redirects=True)
client.post(
'/users/profile/edit',
data={
'username': payload,
'full_name': 'Legit Name',
'email': '[email protected]',
},
follow_redirects=True,
)
with app.app_context():
assert db.session.get(User, user_id).username != payload
@@ -257,12 +255,16 @@ class TestInputValidation:
with app.app_context():
before = db.session.get(User, user_id).password_hash
client.post('/users/profile/edit', data={
'username': _username(app, user_id),
'full_name': 'Legit Name',
'email': '[email protected]',
'password': 'a',
}, follow_redirects=True)
client.post(
'/users/profile/edit',
data={
'username': _username(app, user_id),
'full_name': 'Legit Name',
'email': '[email protected]',
'password': 'a',
},
follow_redirects=True,
)
with app.app_context():
assert db.session.get(User, user_id).password_hash == before, (
@@ -272,13 +274,17 @@ class TestInputValidation:
def test_create_user_enforces_the_password_policy(self, app, client, as_role):
as_role('admin')
client.post('/users/create', data={
'username': 'weakling',
'email': '[email protected]',
'password': 'a',
'full_name': 'Weak Account',
'role': 'admin',
}, follow_redirects=True)
client.post(
'/users/create',
data={
'username': 'weakling',
'email': 'weak@example.test',
'password': 'a',
'full_name': 'Weak Account',
'role': 'admin',
},
follow_redirects=True,
)
with app.app_context():
created = User.query.filter_by(username='weakling').first()
@@ -292,11 +298,15 @@ class TestInputValidation:
with app.app_context():
taken = db.session.get(User, other_id).email
response = client.post(f'/users/{target_id}/edit', data={
'full_name': 'Target',
'email': taken,
'role': 'player',
}, follow_redirects=False)
response = client.post(
f'/users/{target_id}/edit',
data={
'full_name': 'Target',
'email': taken,
'role': 'player',
},
follow_redirects=False,
)
assert response.status_code < 500, 'duplicate email produced a server error'
@@ -305,18 +315,21 @@ class TestAdminSafety:
def test_the_last_admin_cannot_demote_itself(self, app, client, as_role):
admin_id = as_role('admin')
client.post(f'/users/{admin_id}/edit', data={
'full_name': 'Admin',
'email': '[email protected]',
'role': 'player',
}, follow_redirects=True)
client.post(
f'/users/{admin_id}/edit',
data={
'full_name': 'Admin',
'email': '[email protected]',
'role': 'player',
},
follow_redirects=True,
)
with app.app_context():
assert db.session.get(User, admin_id).role == 'admin', (
'the only administrator demoted itself; no interface can undo this'
)
def test_an_admin_cannot_change_its_own_role_even_with_others_present(
self, app, client, as_role, make_user
):
@@ -324,9 +337,15 @@ class TestAdminSafety:
make_user('admin')
admin_id = as_role('admin')
client.post(f'/users/{admin_id}/edit', data={
'full_name': 'Admin', 'email': '[email protected]', 'role': 'player',
}, follow_redirects=True)
client.post(
f'/users/{admin_id}/edit',
data={
'full_name': 'Admin',
'email': '[email protected]',
'role': 'player',
},
follow_redirects=True,
)
with app.app_context():
assert db.session.get(User, admin_id).role == 'admin'
@@ -336,10 +355,16 @@ class TestAdminSafety:
other_id = make_user('admin')
as_role('admin')
client.post(f'/users/{other_id}/edit', data={
'full_name': 'Other', 'email': '[email protected]t',
'role': 'coach', 'is_active_account': 'on',
}, follow_redirects=True)
client.post(
f'/users/{other_id}/edit',
data={
'full_name': 'Other',
'email': '[email protected]',
'role': 'coach',
'is_active_account': 'on',
},
follow_redirects=True,
)
with app.app_context():
assert db.session.get(User, other_id).role == 'coach'
@@ -350,11 +375,16 @@ class TestCsrf:
"""CSRFProtect is global. This pins that down so a future
@csrf.exempt cannot slip in unnoticed."""
client = app_with_csrf.test_client()
response = client.post('/auth/login', data={
'username': 'someone', 'password': 'Password123',
})
response = client.post(
'/auth/login',
data={
'username': 'someone',
'password': 'Password123',
},
)
assert response.status_code == 400
class TestCorsPolicy:
"""SEC-WEB-003 — with no origins configured, flask-cors defaulted to '*'
and, credentials being allowed, echoed back the caller's Origin."""
@@ -367,12 +397,19 @@ class TestCorsPolicy:
def test_configured_origins_are_still_honoured(self, app_with_csrf):
from app.app import create_app
application = create_app({
'SECRET_KEY': 'test', 'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
'TESTING': True, 'FORCE_HTTPS': False, 'ENABLE_DISCORD_BOT': False,
'AUTO_CREATE_TABLES': False, 'CORS_ALLOWED_ORIGINS': 'https://trusted.test',
})
application = create_app(
{
'SECRET_KEY': 'test',
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
'TESTING': True,
'FORCE_HTTPS': False,
'ENABLE_DISCORD_BOT': False,
'AUTO_CREATE_TABLES': False,
'CORS_ALLOWED_ORIGINS': 'https://trusted.test',
}
)
response = application.test_client().get(
'/auth/login', headers={'Origin': 'https://trusted.test'})
'/auth/login', headers={'Origin': 'https://trusted.test'}
)
assert response.headers.get('Access-Control-Allow-Origin') == 'https://trusted.test'
+2 -4
View File
@@ -37,8 +37,7 @@ class TestUrlParsing:
def test_the_sqlalchemy_dialect_suffix_is_accepted(self):
"""SQLAlchemy writes postgresql+psycopg://, which pg_dump rejects."""
conn = parse_database_url(
'postgresql+psycopg://u:p@localhost/tryouts')
conn = parse_database_url('postgresql+psycopg://u:p@localhost/tryouts')
assert conn['dbname'] == 'tryouts'
def test_the_default_port_is_applied(self):
@@ -119,5 +118,4 @@ class TestExitCodes:
assert backup_module.main([]) == 1
def test_verifying_a_missing_archive_fails(self, tmp_path):
assert backup_module.main(
['--verify-only', str(tmp_path / 'nope.dump')]) == 1
assert backup_module.main(['--verify-only', str(tmp_path / 'nope.dump')]) == 1
+22 -18
View File
@@ -25,7 +25,10 @@ from app.extensions import db
def match_factory(app):
"""Create a tryout with one player_scrim match and one participant."""
from app.models import (
Match, MatchParticipant, Tryout, TryoutRegistration,
Match,
MatchParticipant,
Tryout,
TryoutRegistration,
)
def _make(owner_id, player_id, *, description=None, username=None):
@@ -39,24 +42,30 @@ def match_factory(app):
player.username = username
tryout = Tryout(
title='Spring tryout', game='Valorant', date=date(2030, 5, 1),
created_by=owner_id, status='upcoming',
title='Spring tryout',
game='Valorant',
date=date(2030, 5, 1),
created_by=owner_id,
status='upcoming',
)
db.session.add(tryout)
db.session.flush()
db.session.add(TryoutRegistration(
tryout_id=tryout.id, player_id=player_id))
db.session.add(TryoutRegistration(tryout_id=tryout.id, player_id=player_id))
match = Match(
tryout_id=tryout.id, title='Scrim A', description=description,
date=date(2030, 5, 2), start_time=time(18, 0), end_time=time(19, 0),
match_type='player_scrim', created_by=owner_id,
tryout_id=tryout.id,
title='Scrim A',
description=description,
date=date(2030, 5, 2),
start_time=time(18, 0),
end_time=time(19, 0),
match_type='player_scrim',
created_by=owner_id,
)
db.session.add(match)
db.session.flush()
db.session.add(MatchParticipant(
match_id=match.id, player_id=player_id))
db.session.add(MatchParticipant(match_id=match.id, player_id=player_id))
db.session.commit()
return tryout.id, match.id
@@ -68,9 +77,7 @@ def _match_event(payload):
class TestCalendarEventPayload:
def test_description_is_returned_verbatim(
self, app, client, as_role, make_user, match_factory
):
def test_description_is_returned_verbatim(self, app, client, as_role, make_user, match_factory):
player_id = make_user('player')
admin_id = as_role('admin')
match_factory(admin_id, player_id, description='Bring your own peripherals')
@@ -92,9 +99,7 @@ class TestCalendarEventPayload:
assert '<br>' not in props['description']
assert props['participants'] not in props['description']
def test_an_empty_description_stays_empty(
self, app, client, as_role, make_user, match_factory
):
def test_an_empty_description_stays_empty(self, app, client, as_role, make_user, match_factory):
player_id = make_user('player')
admin_id = as_role('admin')
match_factory(admin_id, player_id, description=None)
@@ -132,8 +137,7 @@ class TestLegacyHostileData:
):
player_id = make_user('player')
admin_id = as_role('admin')
match_factory(admin_id, player_id,
description='Normal text', username=self.PAYLOAD)
match_factory(admin_id, player_id, description='Normal text', username=self.PAYLOAD)
props = _match_event(client.get('/matches/api/events').get_json())['extendedProps']
+14 -12
View File
@@ -32,8 +32,10 @@ def contract_for(app, tmp_path):
path.write_bytes(PDF_BYTES)
with app.app_context():
contract = Contract(
player_id=player_id, uploaded_by_id=uploader_id,
original_filename='contract.pdf', stored_filename=stored,
player_id=player_id,
uploaded_by_id=uploader_id,
original_filename='contract.pdf',
stored_filename=stored,
file_path=str(path),
)
db.session.add(contract)
@@ -54,7 +56,8 @@ class TestSignedUpload:
client.post(
f'/users/contracts/{contract_id}/upload_signed',
data={'signed_file': (io.BytesIO(NOT_PDF_BYTES), 'signed.pdf')},
content_type='multipart/form-data', follow_redirects=True,
content_type='multipart/form-data',
follow_redirects=True,
)
with app.app_context():
@@ -63,9 +66,7 @@ class TestSignedUpload:
assert contract.signed_file_path is None
assert not os.path.exists(directory / 'signed_deadbeef.pdf')
def test_a_foreign_extension_is_refused(
self, app, client, as_role, make_user, contract_for
):
def test_a_foreign_extension_is_refused(self, app, client, as_role, make_user, contract_for):
player_id = as_role('player')
admin_id = make_user('admin')
contract_id, _directory = contract_for(player_id, admin_id)
@@ -73,15 +74,14 @@ class TestSignedUpload:
client.post(
f'/users/contracts/{contract_id}/upload_signed',
data={'signed_file': (io.BytesIO(b'<?php system($_GET[0]); ?>'), 'shell.php')},
content_type='multipart/form-data', follow_redirects=True,
content_type='multipart/form-data',
follow_redirects=True,
)
with app.app_context():
assert db.session.get(Contract, contract_id).status != 'signed'
def test_a_real_pdf_still_goes_through(
self, app, client, as_role, make_user, contract_for
):
def test_a_real_pdf_still_goes_through(self, app, client, as_role, make_user, contract_for):
"""Guard against over-correcting: signing a contract is the point."""
player_id = as_role('player')
admin_id = make_user('admin')
@@ -90,7 +90,8 @@ class TestSignedUpload:
client.post(
f'/users/contracts/{contract_id}/upload_signed',
data={'signed_file': (io.BytesIO(PDF_BYTES), 'signed.pdf')},
content_type='multipart/form-data', follow_redirects=True,
content_type='multipart/form-data',
follow_redirects=True,
)
with app.app_context():
@@ -110,7 +111,8 @@ class TestSignedUpload:
client.post(
f'/users/contracts/{contract_id}/upload_signed',
data={'signed_file': (io.BytesIO(PDF_BYTES), 'signed.pdf')},
content_type='multipart/form-data', follow_redirects=True,
content_type='multipart/form-data',
follow_redirects=True,
)
with app.app_context():
+3 -4
View File
@@ -21,7 +21,8 @@ from app.app import build_csp
TEMPLATE_ROOT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'app', 'templates',
'app',
'templates',
)
#: Attributes a nonce can never authorise.
@@ -126,9 +127,7 @@ class TestInlineHandlerRatchet:
continue
offenders.append(f'{relative}: {tag}')
assert not offenders, (
'inline <script> without nonce="{{ csp_nonce }}": ' + str(offenders)
)
assert not offenders, 'inline <script> without nonce="{{ csp_nonce }}": ' + str(offenders)
def test_the_shared_layout_is_free_of_them(self):
"""base.html and macros.html render on every single page."""
+17 -11
View File
@@ -19,23 +19,29 @@ from app.app import normalise_database_url
class TestNormalisation:
@pytest.mark.parametrize('given', [
'postgresql://user:pass@host:5432/tryouts',
'postgres://user:pass@host:5432/tryouts',
])
@pytest.mark.parametrize(
'given',
[
'postgresql://user:pass@host:5432/tryouts',
'postgres://user:pass@host:5432/tryouts',
],
)
def test_a_driverless_postgres_url_gets_psycopg(self, given):
assert normalise_database_url(given).startswith('postgresql+psycopg://')
def test_the_rest_of_the_url_is_untouched(self):
result = normalise_database_url(
'postgresql://u:p%40s[email protected]:5432/tryouts?sslmode=require')
assert result == (
'postgresql+psycopg://u:p%40s[email protected]:5432/tryouts?sslmode=require')
'postgresql://u:p%40s[email protected]:5432/tryouts?sslmode=require'
)
assert result == ('postgresql+psycopg://u:p%40s[email protected]:5432/tryouts?sslmode=require')
@pytest.mark.parametrize('given', [
'postgresql+psycopg://user:pass@host/db',
'postgresql+psycopg2://user:pass@host/db',
])
@pytest.mark.parametrize(
'given',
[
'postgresql+psycopg://user:pass@host/db',
'postgresql+psycopg2://user:pass@host/db',
],
)
def test_an_explicit_driver_is_left_alone(self, given):
"""Naming psycopg2 stays possible for an environment that has it."""
assert normalise_database_url(given) == given
+66 -39
View File
@@ -17,8 +17,17 @@ import pytest
from app.extensions import db
from app.models import (
Match, MatchParticipant, OrgTeam, PersonalNote, Team, TeamMatch,
TeamMember, TeamNote, TeamPlayer, Tryout, TryoutRegistration,
Match,
MatchParticipant,
OrgTeam,
PersonalNote,
Team,
TeamMatch,
TeamMember,
TeamNote,
TeamPlayer,
Tryout,
TryoutRegistration,
)
@@ -38,8 +47,13 @@ def world(app, make_user):
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)
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()
@@ -47,36 +61,57 @@ def world(app, make_user):
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)
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.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,
'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)
@@ -89,8 +124,7 @@ class TestDeleteMatch:
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)
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():
@@ -102,8 +136,7 @@ class TestDeleteMatch:
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
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;
@@ -125,8 +158,7 @@ class TestDeleteTryout:
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)
response = client.post(f"/tryouts/{world['tryout_id']}/delete", follow_redirects=False)
assert response.status_code < 500
with app.app_context():
@@ -140,8 +172,7 @@ class TestDeleteTryout:
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
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)
@@ -164,8 +195,7 @@ class TestDeleteTeam:
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)
response = client.post(f"/teams/{world['org_team_id']}/delete", follow_redirects=False)
assert response.status_code < 500
with app.app_context():
@@ -177,12 +207,9 @@ class TestDeleteTeam:
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
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."""
+18 -16
View File
@@ -24,7 +24,8 @@ def discord_configured(monkeypatch):
monkeypatch.setattr(auth_module, 'DISCORD_CLIENT_ID', '123456789012345678')
monkeypatch.setattr(auth_module, 'DISCORD_CLIENT_SECRET', 'not-a-real-secret')
monkeypatch.setattr(
auth_module, 'DISCORD_REDIRECT_URI',
auth_module,
'DISCORD_REDIRECT_URI',
'https://example.test/auth/discord/callback',
)
@@ -50,16 +51,17 @@ class TestAuthorizationRequest:
assert sess[auth_module.DISCORD_STATE_KEY] == sent
def test_two_requests_get_different_states(self, client, discord_configured):
first = _authorize_params(
client.get('/auth/discord/login', follow_redirects=False))['state'][0]
second = _authorize_params(
client.get('/auth/discord/login', follow_redirects=False))['state'][0]
first = _authorize_params(client.get('/auth/discord/login', follow_redirects=False))[
'state'
][0]
second = _authorize_params(client.get('/auth/discord/login', follow_redirects=False))[
'state'
][0]
assert first != second
def test_scopes_and_redirect_are_preserved(self, client, discord_configured):
params = _authorize_params(
client.get('/auth/discord/login', follow_redirects=False))
params = _authorize_params(client.get('/auth/discord/login', follow_redirects=False))
assert params['scope'][0] == 'identify connections'
assert params['response_type'][0] == 'code'
@@ -87,8 +89,7 @@ class TestCallbackStateValidation:
def test_a_callback_without_state_is_rejected(self, client, discord_configured):
client.get('/auth/discord/login', follow_redirects=False)
response = client.get(
'/auth/discord/callback?code=attacker-code', follow_redirects=False)
response = client.get('/auth/discord/callback?code=attacker-code', follow_redirects=False)
assert '/auth/register' in response.headers['Location']
@@ -96,8 +97,8 @@ class TestCallbackStateValidation:
client.get('/auth/discord/login', follow_redirects=False)
response = client.get(
'/auth/discord/callback?code=attacker-code&state=forged',
follow_redirects=False)
'/auth/discord/callback?code=attacker-code&state=forged', follow_redirects=False
)
assert '/auth/register' in response.headers['Location']
with client.session_transaction() as sess:
@@ -106,18 +107,19 @@ class TestCallbackStateValidation:
def test_a_callback_without_a_prior_request_is_rejected(self, client, discord_configured):
"""No /discord/login beforehand: nothing to match against."""
response = client.get(
'/auth/discord/callback?code=x&state=anything', follow_redirects=False)
'/auth/discord/callback?code=x&state=anything', follow_redirects=False
)
assert '/auth/register' in response.headers['Location']
def test_the_state_is_single_use(self, client, discord_configured):
"""Consumed on the first callback, valid or not, so it cannot be
replayed."""
state = _authorize_params(
client.get('/auth/discord/login', follow_redirects=False))['state'][0]
state = _authorize_params(client.get('/auth/discord/login', follow_redirects=False))[
'state'
][0]
client.get(f'/auth/discord/callback?code=x&state={state}',
follow_redirects=False)
client.get(f'/auth/discord/callback?code=x&state={state}', follow_redirects=False)
with client.session_transaction() as sess:
assert auth_module.DISCORD_STATE_KEY not in sess
+66 -44
View File
@@ -28,28 +28,27 @@ class TestDefaults:
assert 'Se connecter' in body
def test_the_html_lang_attribute_follows_the_locale(self, client):
english = client.get('/auth/login',
headers={'Accept-Language': 'en-CA,en;q=0.9'})
english = client.get('/auth/login', headers={'Accept-Language': 'en-CA,en;q=0.9'})
assert 'lang="en"' in english.get_data(as_text=True)
class TestBrowserNegotiation:
def test_an_english_browser_is_served_english(self, client):
body = client.get('/auth/login',
headers={'Accept-Language': 'en-CA,en;q=0.9'}
).get_data(as_text=True)
body = client.get('/auth/login', headers={'Accept-Language': 'en-CA,en;q=0.9'}).get_data(
as_text=True
)
assert 'Sign In' in body
def test_an_unsupported_language_falls_back_to_french(self, client):
body = client.get('/auth/login',
headers={'Accept-Language': 'de-DE,de;q=0.9'}
).get_data(as_text=True)
body = client.get('/auth/login', headers={'Accept-Language': 'de-DE,de;q=0.9'}).get_data(
as_text=True
)
assert 'Se connecter' in body
def test_a_french_browser_is_served_french(self, client):
body = client.get('/auth/login',
headers={'Accept-Language': 'fr-CA,fr;q=0.9'}
).get_data(as_text=True)
body = client.get('/auth/login', headers={'Accept-Language': 'fr-CA,fr;q=0.9'}).get_data(
as_text=True
)
assert 'Se connecter' in body
@@ -65,9 +64,9 @@ class TestExplicitSwitch:
"""Someone on an English machine who picks French must keep French."""
client.get('/lang/fr')
body = client.get('/auth/login',
headers={'Accept-Language': 'en-CA,en;q=0.9'}
).get_data(as_text=True)
body = client.get('/auth/login', headers={'Accept-Language': 'en-CA,en;q=0.9'}).get_data(
as_text=True
)
assert 'Se connecter' in body
assert 'lang="fr"' in body
@@ -85,16 +84,18 @@ class TestExplicitSwitch:
assert 'Sign In' in body, 'an unsupported code must not change the locale'
def test_the_switcher_redirects_back_to_the_referring_page(self, client):
response = client.get('/lang/en',
headers={'Referer': 'http://localhost/auth/register'},
follow_redirects=False)
response = client.get(
'/lang/en',
headers={'Referer': 'http://localhost/auth/register'},
follow_redirects=False,
)
assert response.headers['Location'].endswith('/auth/register')
def test_an_external_referer_is_not_followed(self, client):
"""An unchecked Referer would make this an open redirect."""
response = client.get('/lang/en',
headers={'Referer': 'https://evil.test/phishing'},
follow_redirects=False)
response = client.get(
'/lang/en', headers={'Referer': 'https://evil.test/phishing'}, follow_redirects=False
)
assert 'evil.test' not in response.headers['Location']
@@ -135,13 +136,14 @@ class TestTranslatedContent:
body = client.get('/no-such-page').get_data(as_text=True)
assert 'Page introuvable' in body
@pytest.mark.parametrize('locale,expected', [
('fr', 'Ce compte a été désactivé.'),
('en', 'This account has been deactivated.'),
])
def test_flash_messages_are_translated(
self, app, client, make_user, login, locale, expected
):
@pytest.mark.parametrize(
'locale,expected',
[
('fr', 'Ce compte a été désactivé.'),
('en', 'This account has been deactivated.'),
],
)
def test_flash_messages_are_translated(self, app, client, make_user, login, locale, expected):
from app.extensions import db
from app.models import User
@@ -168,11 +170,14 @@ class TestCatalogueIntegrity:
path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'app', 'translations', locale, 'LC_MESSAGES', 'messages.mo',
'app',
'translations',
locale,
'LC_MESSAGES',
'messages.mo',
)
assert os.path.exists(path), (
f'{locale} catalogue is not compiled: run '
'`pybabel compile -d app/translations`'
f'{locale} catalogue is not compiled: run `pybabel compile -d app/translations`'
)
@pytest.mark.parametrize('locale', SUPPORTED_LOCALES)
@@ -184,15 +189,18 @@ class TestCatalogueIntegrity:
path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'app', 'translations', locale, 'LC_MESSAGES', 'messages.po',
'app',
'translations',
locale,
'LC_MESSAGES',
'messages.po',
)
with io.open(path, encoding='utf-8') as handle:
catalog = read_po(handle, locale=locale)
untranslated = [m.id for m in catalog if m.id and not m.string]
assert not untranslated, (
f'{len(untranslated)} untranslated string(s) in {locale}: '
f'{untranslated[:5]}'
f'{len(untranslated)} untranslated string(s) in {locale}: {untranslated[:5]}'
)
@@ -233,9 +241,9 @@ class TestLocaleSurvivesSessionRotation:
client.get('/lang/fr')
as_role('player')
body = client.get('/users/profile',
headers={'Accept-Language': 'en-CA,en;q=0.9'}
).get_data(as_text=True)
body = client.get('/users/profile', headers={'Accept-Language': 'en-CA,en;q=0.9'}).get_data(
as_text=True
)
assert 'lang="fr"' in body
def test_the_csrf_token_is_still_preserved(self, app, client, make_user, login):
@@ -273,20 +281,34 @@ class TestFlashMessagesAreTranslated:
built at import time, before any request exists."""
as_role('admin')
body = client.post('/users/create', data={
'username': 'x', 'email': 'not-an-email',
'password': 'a', 'full_name': 'X', 'role': 'coach',
}, follow_redirects=True).get_data(as_text=True)
body = client.post(
'/users/create',
data={
'username': 'x',
'email': 'not-an-email',
'password': 'a',
'full_name': 'X',
'role': 'coach',
},
follow_redirects=True,
).get_data(as_text=True)
assert 'Le nom d' in body and 'utilisateur doit compter' in body
def test_a_message_with_a_value_keeps_it(self, client, as_role):
as_role('admin')
body = client.post('/users/create', data={
'username': 'recrue', 'email': '[email protected]',
'password': 'Password123', 'full_name': 'Nouvelle Recrue', 'role': 'coach',
}, follow_redirects=True).get_data(as_text=True)
body = client.post(
'/users/create',
data={
'username': 'recrue',
'email': '[email protected]',
'password': 'Password123',
'full_name': 'Nouvelle Recrue',
'role': 'coach',
},
follow_redirects=True,
).get_data(as_text=True)
assert 'Nouvelle Recrue' in body
assert 'créé avec le rôle coach' in body
+86 -59
View File
@@ -19,11 +19,19 @@ import pytest
from app.extensions import db
from app.models import (
Contract, OrgTeam, PersonalNote, TeamPlayer, Tryout, TryoutRegistration,
Contract,
OrgTeam,
PersonalNote,
TeamPlayer,
Tryout,
TryoutRegistration,
)
from app.permissions import (
can_manage_player_contract, coach_can_access_player, coach_org_team_ids,
coach_player_ids, visible_org_teams,
can_manage_player_contract,
coach_can_access_player,
coach_org_team_ids,
coach_player_ids,
visible_org_teams,
)
@@ -33,8 +41,9 @@ def team_factory(app):
with app.app_context():
from app.models import User
team = OrgTeam(name=name, created_by=coach_id or legacy_coach_id,
coach_id=legacy_coach_id)
team = OrgTeam(
name=name, created_by=coach_id or legacy_coach_id, coach_id=legacy_coach_id
)
db.session.add(team)
db.session.flush()
if coach_id:
@@ -48,25 +57,23 @@ def team_factory(app):
class TestTeamResolution:
def test_a_coach_attached_by_the_relationship_is_found(
self, app, make_user, team_factory
):
def test_a_coach_attached_by_the_relationship_is_found(self, app, make_user, team_factory):
coach_id = make_user('coach')
team_id = team_factory('Varsity', coach_id=coach_id)
with app.app_context():
from app.models import User
coach = db.session.get(User, coach_id)
assert coach_org_team_ids(coach) == [team_id]
def test_a_coach_attached_by_the_legacy_column_is_found(
self, app, make_user, team_factory
):
def test_a_coach_attached_by_the_legacy_column_is_found(self, app, make_user, team_factory):
coach_id = make_user('coach')
team_id = team_factory('JV', legacy_coach_id=coach_id)
with app.app_context():
from app.models import User
coach = db.session.get(User, coach_id)
assert coach_org_team_ids(coach) == [team_id]
@@ -74,6 +81,7 @@ class TestTeamResolution:
coach_id = make_user('coach')
with app.app_context():
from app.models import User
assert coach_org_team_ids(db.session.get(User, coach_id)) == []
@@ -85,47 +93,45 @@ class TestPlayerAccess:
with app.app_context():
from app.models import User
assert coach_can_access_player(db.session.get(User, coach_id), player_id)
def test_a_second_coach_of_the_team_also_reaches_the_player(
self, app, make_user, team_factory
):
def test_a_second_coach_of_the_team_also_reaches_the_player(self, app, make_user, team_factory):
"""The case that used to fail everywhere users.py looked at coach_id."""
first_coach = make_user('coach')
second_coach = make_user('coach')
player_id = make_user('player')
team_id = team_factory('Varsity', legacy_coach_id=first_coach,
player_ids=[player_id])
team_id = team_factory('Varsity', legacy_coach_id=first_coach, player_ids=[player_id])
with app.app_context():
from app.models import User
team = db.session.get(OrgTeam, team_id)
team.coaches.append(db.session.get(User, second_coach))
db.session.commit()
assert coach_can_access_player(db.session.get(User, second_coach), player_id)
def test_a_coach_does_not_reach_an_unrelated_player(
self, app, make_user, team_factory
):
def test_a_coach_does_not_reach_an_unrelated_player(self, app, make_user, team_factory):
coach_id = make_user('coach')
stranger_id = make_user('player')
team_factory('Varsity', coach_id=coach_id)
with app.app_context():
from app.models import User
assert not coach_can_access_player(db.session.get(User, coach_id), stranger_id)
def test_a_coach_reaches_a_player_registered_in_their_tryout(
self, app, make_user
):
def test_a_coach_reaches_a_player_registered_in_their_tryout(self, app, make_user):
coach_id = make_user('coach')
player_id = make_user('player')
with app.app_context():
from app.models import User
tryout = Tryout(title='Open tryout', game='Valorant',
date=date(2030, 3, 1), created_by=coach_id)
tryout = Tryout(
title='Open tryout', game='Valorant', date=date(2030, 3, 1), created_by=coach_id
)
db.session.add(tryout)
db.session.flush()
tryout.coaches.append(db.session.get(User, coach_id))
@@ -138,6 +144,7 @@ class TestPlayerAccess:
coach_id = make_user('coach')
with app.app_context():
from app.models import User
assert not coach_can_access_player(db.session.get(User, coach_id), None)
@@ -152,9 +159,14 @@ class TestPersonalNoteRoutes:
coach_id = as_role('coach')
team_factory('Varsity', coach_id=coach_id)
client.post('/users/personal-notes/manage', data={
'player_id': stranger_id, 'content': 'Unrelated observation',
}, follow_redirects=True)
client.post(
'/users/personal-notes/manage',
data={
'player_id': stranger_id,
'content': 'Unrelated observation',
},
follow_redirects=True,
)
with app.app_context():
assert PersonalNote.query.filter_by(player_id=stranger_id).count() == 0
@@ -167,9 +179,14 @@ class TestPersonalNoteRoutes:
coach_id = as_role('coach')
team_factory('Varsity', coach_id=coach_id, player_ids=[player_id])
client.post('/users/personal-notes/manage', data={
'player_id': player_id, 'content': 'Good positioning today',
}, follow_redirects=True)
client.post(
'/users/personal-notes/manage',
data={
'player_id': player_id,
'content': 'Good positioning today',
},
follow_redirects=True,
)
with app.app_context():
note = PersonalNote.query.filter_by(player_id=player_id).one()
@@ -184,17 +201,18 @@ class TestContractVisibility:
def _contract(app, player_id, uploader_id, team_id=None):
with app.app_context():
contract = Contract(
player_id=player_id, team_id=team_id, uploaded_by_id=uploader_id,
original_filename='c.pdf', stored_filename='uuid.pdf',
player_id=player_id,
team_id=team_id,
uploaded_by_id=uploader_id,
original_filename='c.pdf',
stored_filename='uuid.pdf',
file_path='/tmp/uuid.pdf',
)
db.session.add(contract)
db.session.commit()
return contract.id
def test_a_teamless_contract_is_not_visible_to_every_coach(
self, app, make_user, team_factory
):
def test_a_teamless_contract_is_not_visible_to_every_coach(self, app, make_user, team_factory):
coach_id = make_user('coach')
stranger_id = make_user('player')
admin_id = make_user('admin')
@@ -203,12 +221,11 @@ class TestContractVisibility:
with app.app_context():
from app.models import User
contract = db.session.get(Contract, contract_id)
assert not contract.can_view(db.session.get(User, coach_id))
def test_a_coach_sees_the_contract_of_their_own_player(
self, app, make_user, team_factory
):
def test_a_coach_sees_the_contract_of_their_own_player(self, app, make_user, team_factory):
coach_id = make_user('coach')
player_id = make_user('player')
admin_id = make_user('admin')
@@ -217,6 +234,7 @@ class TestContractVisibility:
with app.app_context():
from app.models import User
contract = db.session.get(Contract, contract_id)
assert contract.can_view(db.session.get(User, coach_id))
@@ -227,6 +245,7 @@ class TestContractVisibility:
with app.app_context():
from app.models import User
contract = db.session.get(Contract, contract_id)
assert contract.can_view(db.session.get(User, player_id))
@@ -237,6 +256,7 @@ class TestContractVisibility:
with app.app_context():
from app.models import User
contract = db.session.get(Contract, contract_id)
assert contract.can_view(db.session.get(User, admin_id))
@@ -251,9 +271,7 @@ class TestSecondTeam:
attached by the relationship only reached none of it. Both defects were
live, and silent: the pages rendered, just empty."""
def test_a_coach_of_two_teams_reaches_both_squads(
self, app, make_user, team_factory
):
def test_a_coach_of_two_teams_reaches_both_squads(self, app, make_user, team_factory):
coach_id = make_user('coach')
first_player = make_user('player')
second_player = make_user('player')
@@ -262,6 +280,7 @@ class TestSecondTeam:
with app.app_context():
from app.models import User
coach = db.session.get(User, coach_id)
assert sorted(coach_player_ids(coach)) == sorted([first_player, second_player])
@@ -275,6 +294,7 @@ class TestSecondTeam:
with app.app_context():
from app.models import User
coach = db.session.get(User, coach_id)
assert can_manage_player_contract(coach, second_player)
@@ -287,6 +307,7 @@ class TestSecondTeam:
with app.app_context():
from app.models import User
coach = db.session.get(User, coach_id)
assert not can_manage_player_contract(coach, stranger_id)
@@ -326,6 +347,7 @@ class TestTeamVisibility:
with app.app_context():
from app.models import User
teams = visible_org_teams(db.session.get(User, coach_id))
assert [t.name for t in teams] == ['Varsity']
@@ -337,6 +359,7 @@ class TestTeamVisibility:
with app.app_context():
from app.models import User
teams = visible_org_teams(db.session.get(User, admin_id))
assert [t.name for t in teams] == ['JV', 'Varsity']
@@ -347,11 +370,10 @@ class TestTeamVisibility:
with app.app_context():
from app.models import User
assert visible_org_teams(db.session.get(User, scout_id)) == []
def test_the_team_listing_shows_the_relationship_team(
self, app, client, as_role, team_factory
):
def test_the_team_listing_shows_the_relationship_team(self, app, client, as_role, team_factory):
coach_id = as_role('coach')
team_factory('Northern Lights', coach_id=coach_id)
@@ -366,49 +388,54 @@ class TestTryoutVisibility:
@staticmethod
def _tryout(app, *, creator_id, title, target_team_id=None, legacy_coach_id=None):
with app.app_context():
tryout = Tryout(title=title, game='Valorant', date=date(2030, 5, 1),
created_by=creator_id, target_org_team_id=target_team_id,
coach_id=legacy_coach_id)
tryout = Tryout(
title=title,
game='Valorant',
date=date(2030, 5, 1),
created_by=creator_id,
target_org_team_id=target_team_id,
coach_id=legacy_coach_id,
)
db.session.add(tryout)
db.session.commit()
return tryout.id
def test_a_tryout_targeting_a_legacy_team_is_visible(
self, app, make_user, team_factory
):
def test_a_tryout_targeting_a_legacy_team_is_visible(self, app, make_user, team_factory):
coach_id = make_user('coach')
team_id = team_factory('Varsity', legacy_coach_id=coach_id)
self._tryout(app, creator_id=coach_id, title='Spring intake',
target_team_id=team_id)
self._tryout(app, creator_id=coach_id, title='Spring intake', target_team_id=team_id)
with app.app_context():
from app.models import User
coach = db.session.get(User, coach_id)
assert [t.title for t in coach.get_visible_tryouts()] == ['Spring intake']
def test_the_same_tryout_is_manageable(self, app, make_user, team_factory):
coach_id = make_user('coach')
team_id = team_factory('Varsity', legacy_coach_id=coach_id)
tryout_id = self._tryout(app, creator_id=coach_id, title='Spring intake',
target_team_id=team_id)
tryout_id = self._tryout(
app, creator_id=coach_id, title='Spring intake', target_team_id=team_id
)
with app.app_context():
from app.models import User
coach = db.session.get(User, coach_id)
assert coach.can_manage_this_tryout(db.session.get(Tryout, tryout_id))
def test_another_coachs_tryout_stays_out_of_reach(
self, app, make_user, team_factory
):
def test_another_coachs_tryout_stays_out_of_reach(self, app, make_user, team_factory):
coach_id = make_user('coach')
other_id = make_user('coach')
team_factory('Varsity', coach_id=coach_id)
other_team = team_factory('JV', coach_id=other_id)
tryout_id = self._tryout(app, creator_id=other_id, title='Their intake',
target_team_id=other_team)
tryout_id = self._tryout(
app, creator_id=other_id, title='Their intake', target_team_id=other_team
)
with app.app_context():
from app.models import User
coach = db.session.get(User, coach_id)
assert coach.get_visible_tryouts() == []
assert not coach.can_manage_this_tryout(db.session.get(Tryout, tryout_id))
+2 -6
View File
@@ -44,9 +44,7 @@ class TestPolymorphicIdentity:
with app.app_context():
assert isinstance(db.session.get(User, target_id), Coach)
def test_the_new_role_grants_its_pages_right_away(
self, app, client, as_role, make_user, login
):
def test_the_new_role_grants_its_pages_right_away(self, app, client, as_role, make_user, login):
"""isinstance() is how this application authorises; a stale class in
the identity map is a stale permission set."""
target_id = make_user('player')
@@ -61,9 +59,7 @@ class TestPolymorphicIdentity:
# Coach-only, and it is a coach's own page rather than a redirect.
assert client.get('/users/notes-dashboard').status_code == 200
def test_the_other_fields_of_the_edit_land_too(
self, app, client, as_role, make_user
):
def test_the_other_fields_of_the_edit_land_too(self, app, client, as_role, make_user):
target_id = make_user('player')
as_role('admin')
+1 -3
View File
@@ -1,6 +1,5 @@
"""HTTP hardening, error disclosure, and template escaping."""
from app.app import nl2br
@@ -35,8 +34,7 @@ class TestErrorDisclosure:
def boom(*args, **kwargs):
raise RuntimeError(
'FATAL: password authentication failed for user "app" '
'host=db.internal port=5432'
'FATAL: password authentication failed for user "app" host=db.internal port=5432'
)
original = db.session.execute