fix(audit): fermer les frontieres restantes
This commit is contained in:
@@ -119,3 +119,15 @@ class TestExitCodes:
|
||||
|
||||
def test_verifying_a_missing_archive_fails(self, tmp_path):
|
||||
assert backup_module.main(['--verify-only', str(tmp_path / 'nope.dump')]) == 1
|
||||
|
||||
def test_a_missing_document_store_makes_an_otherwise_valid_run_incomplete(
|
||||
self, monkeypatch, tmp_path
|
||||
):
|
||||
monkeypatch.setenv('DATABASE_URL', URL)
|
||||
monkeypatch.setenv('DOCUMENTS_ROOT', str(tmp_path / 'missing-documents'))
|
||||
monkeypatch.setattr(backup_module, 'BACKUP_DIR', str(tmp_path / 'backups'))
|
||||
monkeypatch.setattr(backup_module, 'backup_database', lambda conn: 'database.dump')
|
||||
monkeypatch.setattr(backup_module, 'verify_backup', lambda path: True)
|
||||
monkeypatch.setattr(backup_module, 'cleanup_old_backups', lambda: None)
|
||||
|
||||
assert backup_module.main([]) == 1
|
||||
|
||||
@@ -117,6 +117,23 @@ class TestInlineHandlerRatchet:
|
||||
|
||||
assert _count_handlers(template) == 1
|
||||
|
||||
def test_dynamic_player_names_are_escaped_before_html_insertion(self):
|
||||
template = os.path.join(TEMPLATE_ROOT, 'pages', 'match_form.html')
|
||||
with open(template, encoding='utf-8') as handle:
|
||||
content = handle.read()
|
||||
|
||||
assert 'html += playerName;' not in content
|
||||
assert "' + playerName + '" not in content
|
||||
assert content.count('escapeHtml(playerName)') == 6
|
||||
|
||||
def test_api_messages_are_written_as_text(self):
|
||||
template = os.path.join(TEMPLATE_ROOT, 'pages', 'coach_availability.html')
|
||||
with open(template, encoding='utf-8') as handle:
|
||||
content = handle.read()
|
||||
|
||||
assert 'text.textContent = message' in content
|
||||
assert "alert.innerHTML = '<span>' + message" not in content
|
||||
|
||||
@pytest.mark.parametrize('relative,full', list(_templates()))
|
||||
def test_a_template_never_gains_an_inline_handler(self, relative, full):
|
||||
allowed = HANDLER_BUDGET.get(relative, 0)
|
||||
|
||||
@@ -9,9 +9,8 @@ in the project directory.
|
||||
|
||||
The document store was fixed in wave G. The other two were not, and the gap
|
||||
that opened between them is the reason this file exists: `backup.py` kept
|
||||
archiving `./documents` while the application wrote to `DOCUMENTS_ROOT`, and
|
||||
the script's answer to a missing directory is to print a line and exit 0.
|
||||
Following the deployment documentation was what broke it.
|
||||
archiving `./documents` while the application wrote to `DOCUMENTS_ROOT`.
|
||||
The script now resolves the shared root and fails the run when it is absent.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -106,16 +105,15 @@ class TestTheBackupScriptAgreesWithTheApplication:
|
||||
with zipfile.ZipFile(archive) as zf:
|
||||
assert any(name.endswith('contrat.pdf') for name in zf.namelist())
|
||||
|
||||
def test_a_missing_store_names_the_path_it_looked_in(self, tmp_path, monkeypatch, capsys):
|
||||
""" "No documents directory found" read as "there are no documents"
|
||||
rather than "I am looking in the wrong place"."""
|
||||
def test_a_missing_store_names_the_path_it_looked_in(self, tmp_path, monkeypatch):
|
||||
"""A missing configured store is an actionable failure, not a skip."""
|
||||
from app.supporting_scripts import backup
|
||||
|
||||
missing = tmp_path / 'not-here'
|
||||
monkeypatch.setenv('DOCUMENTS_ROOT', str(missing))
|
||||
|
||||
assert backup.backup_documents() is None
|
||||
assert str(missing) in capsys.readouterr().out
|
||||
with pytest.raises(backup.BackupError, match=str(missing).replace('\\', '\\\\')):
|
||||
backup.backup_documents()
|
||||
|
||||
|
||||
class TestLogsFollowTheSameRule:
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
"""Regression tests for compact POST forms that bypassed the shared schemas."""
|
||||
|
||||
from datetime import date, time
|
||||
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from app.models import (
|
||||
Match,
|
||||
OneOnOneRequest,
|
||||
OrgTeam,
|
||||
PersonalNote,
|
||||
Team,
|
||||
TeamMember,
|
||||
TeamPlayer,
|
||||
Tryout,
|
||||
TryoutRegistration,
|
||||
UserGamertag,
|
||||
)
|
||||
|
||||
|
||||
def _tryout(db, owner_id, *, coach_id=None):
|
||||
row = Tryout(
|
||||
title='Boundary tryout',
|
||||
game='Valorant',
|
||||
date=date(2030, 4, 1),
|
||||
created_by=owner_id,
|
||||
coach_id=coach_id,
|
||||
)
|
||||
db.session.add(row)
|
||||
db.session.commit()
|
||||
return row.id
|
||||
|
||||
|
||||
def _give_coach_a_player(db, coach_id, player_id, owner_id):
|
||||
org_team = OrgTeam(
|
||||
name=f'Org {coach_id}-{player_id}',
|
||||
created_by=owner_id,
|
||||
coach_id=coach_id,
|
||||
)
|
||||
db.session.add(org_team)
|
||||
db.session.flush()
|
||||
db.session.add(TeamPlayer(org_team_id=org_team.id, player_id=player_id))
|
||||
db.session.commit()
|
||||
return org_team.id
|
||||
|
||||
|
||||
def test_tryout_team_name_is_bounded(app, client, as_role):
|
||||
admin_id = as_role('admin')
|
||||
from app.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
tryout_id = _tryout(db, admin_id)
|
||||
|
||||
response = client.post(
|
||||
f'/tryouts/{tryout_id}/team/create',
|
||||
data={'team_name': 'x' * 101},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
with app.app_context():
|
||||
assert Team.query.filter_by(tryout_id=tryout_id).count() == 0
|
||||
|
||||
|
||||
def test_registration_decisions_lock_the_tryout_row():
|
||||
from app.routes.tryouts import registration_lock_statement
|
||||
|
||||
sql = str(registration_lock_statement(42).compile(dialect=postgresql.dialect()))
|
||||
|
||||
assert 'FOR UPDATE' in sql
|
||||
|
||||
|
||||
def test_tryout_team_position_is_bounded(app, client, as_role, make_user):
|
||||
admin_id = as_role('admin')
|
||||
player_id = make_user('player')
|
||||
from app.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
tryout_id = _tryout(db, admin_id)
|
||||
team = Team(tryout_id=tryout_id, name='Blue', created_by=admin_id)
|
||||
db.session.add(team)
|
||||
db.session.flush()
|
||||
team_id = team.id
|
||||
db.session.add(TryoutRegistration(tryout_id=tryout_id, player_id=player_id))
|
||||
db.session.commit()
|
||||
|
||||
response = client.post(
|
||||
f'/tryouts/{tryout_id}/team/{team_id}/add',
|
||||
data={'player_id': player_id, 'position': 'x' * 51},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
with app.app_context():
|
||||
assert TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first() is None
|
||||
|
||||
|
||||
def test_a_coach_cannot_open_an_unrelated_tryout_note_form(app, client, as_role, make_user):
|
||||
coach_id = as_role('coach')
|
||||
other_coach_id = make_user('coach')
|
||||
admin_id = make_user('admin')
|
||||
player_id = make_user('player')
|
||||
from app.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
tryout_id = _tryout(db, admin_id, coach_id=other_coach_id)
|
||||
db.session.add(TryoutRegistration(tryout_id=tryout_id, player_id=player_id))
|
||||
db.session.commit()
|
||||
|
||||
response = client.get(f'/users/personal-notes/tryout/{tryout_id}')
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.headers['Location'].endswith('/users/notes-dashboard')
|
||||
assert coach_id != other_coach_id
|
||||
|
||||
|
||||
def test_a_note_cannot_claim_a_team_that_does_not_contain_the_player(
|
||||
app, client, as_role, make_user
|
||||
):
|
||||
coach_id = as_role('coach')
|
||||
admin_id = make_user('admin')
|
||||
player_id = make_user('player')
|
||||
from app.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
_give_coach_a_player(db, coach_id, player_id, admin_id)
|
||||
tryout_id = _tryout(db, admin_id, coach_id=coach_id)
|
||||
team = Team(tryout_id=tryout_id, name='No player here', created_by=admin_id)
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
team_id = team.id
|
||||
|
||||
response = client.post(
|
||||
'/users/personal-notes/add',
|
||||
data={'player_id': player_id, 'content': 'Private note', 'team_id': team_id},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
with app.app_context():
|
||||
assert PersonalNote.query.count() == 0
|
||||
|
||||
|
||||
def test_a_personal_note_is_bounded(app, client, as_role, make_user):
|
||||
coach_id = as_role('coach')
|
||||
admin_id = make_user('admin')
|
||||
player_id = make_user('player')
|
||||
from app.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
_give_coach_a_player(db, coach_id, player_id, admin_id)
|
||||
|
||||
response = client.post(
|
||||
'/users/personal-notes/manage',
|
||||
data={'player_id': player_id, 'content': 'x' * 5001},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
with app.app_context():
|
||||
assert PersonalNote.query.count() == 0
|
||||
|
||||
|
||||
def test_a_rejection_reason_is_bounded(app, client, as_role, make_user):
|
||||
coach_id = as_role('coach')
|
||||
player_id = make_user('player')
|
||||
from app.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
request = OneOnOneRequest(
|
||||
player_id=player_id,
|
||||
coach_id=coach_id,
|
||||
date=date(2030, 4, 2),
|
||||
start_time=time(18, 0),
|
||||
end_time=time(18, 30),
|
||||
)
|
||||
db.session.add(request)
|
||||
db.session.commit()
|
||||
request_id = request.id
|
||||
|
||||
response = client.post(
|
||||
f'/users/one-on-one/{request_id}/reject',
|
||||
data={'rejection_reason': 'x' * 2001},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
with app.app_context():
|
||||
assert db.session.get(OneOnOneRequest, request_id).status == 'pending'
|
||||
|
||||
|
||||
def test_a_match_context_must_contain_the_player(app, client, as_role, make_user):
|
||||
coach_id = as_role('coach')
|
||||
admin_id = make_user('admin')
|
||||
player_id = make_user('player')
|
||||
from app.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
_give_coach_a_player(db, coach_id, player_id, admin_id)
|
||||
tryout_id = _tryout(db, admin_id, coach_id=coach_id)
|
||||
match = Match(
|
||||
tryout_id=tryout_id,
|
||||
title='Scrim',
|
||||
date=date(2030, 4, 2),
|
||||
match_type='player_vs_player',
|
||||
created_by=coach_id,
|
||||
)
|
||||
db.session.add(match)
|
||||
db.session.commit()
|
||||
match_id = match.id
|
||||
|
||||
response = client.post(
|
||||
'/users/personal-notes/add',
|
||||
data={'player_id': player_id, 'content': 'Private note', 'match_id': match_id},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
with app.app_context():
|
||||
assert PersonalNote.query.count() == 0
|
||||
|
||||
|
||||
def test_the_note_dashboard_lists_tryout_teams_not_org_teams(app, client, as_role, make_user):
|
||||
coach_id = as_role('coach')
|
||||
admin_id = make_user('admin')
|
||||
player_id = make_user('player')
|
||||
from app.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
_give_coach_a_player(db, coach_id, player_id, admin_id)
|
||||
tryout_id = _tryout(db, admin_id, coach_id=coach_id)
|
||||
team = Team(tryout_id=tryout_id, name='Tryout Alpha', created_by=admin_id)
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
team_id = team.id
|
||||
|
||||
body = client.get('/users/notes-dashboard').get_data(as_text=True)
|
||||
|
||||
assert f'<option value="{team_id}">Tryout Alpha</option>' in body
|
||||
assert f'>Org {coach_id}-{player_id}</option>' not in body
|
||||
|
||||
|
||||
def test_an_oversized_dynamic_gamertag_is_rejected(app, client, as_role):
|
||||
player_id = as_role('player')
|
||||
|
||||
response = client.post(
|
||||
'/users/profile/edit',
|
||||
data={
|
||||
'username': 'player1',
|
||||
'full_name': 'Player One',
|
||||
'email': '[email protected]',
|
||||
'games': 'Valorant',
|
||||
'gamertag_Valorant': 'x' * 121,
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
with app.app_context():
|
||||
assert UserGamertag.query.filter_by(user_id=player_id).count() == 0
|
||||
|
||||
|
||||
def test_a_platform_must_belong_to_the_selected_game(app, client, as_role):
|
||||
player_id = as_role('player')
|
||||
|
||||
response = client.post(
|
||||
'/users/profile/edit',
|
||||
data={
|
||||
'username': 'player1',
|
||||
'full_name': 'Player One',
|
||||
'email': '[email protected]',
|
||||
'games': 'Apex Legends',
|
||||
'gamertag_Apex Legends': 'LegitName',
|
||||
'platform_Apex Legends': 'Forged platform',
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
with app.app_context():
|
||||
assert UserGamertag.query.filter_by(user_id=player_id).count() == 0
|
||||
@@ -233,6 +233,42 @@ class TestPendingEvaluations:
|
||||
assert self._pending(client) == 1
|
||||
|
||||
|
||||
class TestRegisteredPlayersForMatchForm:
|
||||
"""The match forms used to issue one or two user lookups per registration."""
|
||||
|
||||
def test_the_result_is_unique_ordered_and_constant_cost(self, app, make_user, count_queries):
|
||||
admin_id = make_user('admin')
|
||||
player_ids = [make_user('player', username=name) for name in ('zulu', 'alpha', 'mike')]
|
||||
|
||||
with app.app_context():
|
||||
tryout = Tryout(
|
||||
title='Match form',
|
||||
game='Valorant',
|
||||
date=date(2030, 3, 1),
|
||||
created_by=admin_id,
|
||||
)
|
||||
db.session.add(tryout)
|
||||
db.session.flush()
|
||||
for player_id in player_ids:
|
||||
db.session.add(TryoutRegistration(tryout_id=tryout.id, player_id=player_id))
|
||||
# DB-006 is pending, so prove the UI remains unique even when the
|
||||
# current database already contains a duplicate registration.
|
||||
db.session.add(TryoutRegistration(tryout_id=tryout.id, player_id=player_ids[0]))
|
||||
db.session.commit()
|
||||
tryout_id = tryout.id
|
||||
|
||||
from app.routes.matches import registered_players
|
||||
|
||||
counter = count_queries()
|
||||
try:
|
||||
players = registered_players(tryout_id)
|
||||
finally:
|
||||
counter.stop()
|
||||
|
||||
assert [player.username for player in players] == ['alpha', 'mike', 'zulu']
|
||||
assert counter.total == 1
|
||||
|
||||
|
||||
class TestViewTryout:
|
||||
"""PERF-001 — the most-visited page in the application ran one query
|
||||
per registration, one per player evaluated, one per team, and one per
|
||||
|
||||
Reference in New Issue
Block a user