Files
team-tryouts/tests/conftest.py
T
GGThedandClaude Opus 5 8e3865f557 chore(lint): elargir les regles ruff et rendre le format bloquant en CI
QUA-002, seconde moitie. Le depot etant formate, l elargissement porte sur
des defauts et non sur du brassage.

Ajoute a la selection : B (bugbear), C4, RET, SIM, UP. Le lot entier n a
produit que 24 signalements sur 76 fichiers -- le code etait plus propre
que l audit ne le craignait. Neuf corriges automatiquement, quinze a la
main.

SIM108 est ignore : forcer un ternaire se lit moins bien que le if/else
qu il remplace, au seul endroit ou il se declenche.

isort (I) n est PAS active. Il reordonnerait les imports de 48 fichiers,
soit une seconde passe de pur brassage juste apres le commit de formatage.
A faire, mais seul.

Deux vrais defauts trouves par les nouvelles regles
  - team_matches.edit_match faisait `except ValueError: pass` sur l heure de
    debut et l heure de fin, trois lignes sous un champ date qui, lui,
    signale et redirige. Une heure mal saisie etait donc acceptee par le
    formulaire, jetee, l ancienne valeur conservee -- et la page annoncait
    la reussite. Meme traitement que la date desormais.
  - backup.py levait BackupError depuis deux blocs `except` sans `from`,
    ce qui perdait la cause d origine dans la trace.

Ainsi que : un `return` explicite dans force_https, `%`-formatage remplace
dans log_auth_event (operations de chaine avant journalisation, pas des
gabarits de logger -- la redaction n est pas affectee), une compréhension
inutile, un `set(...)` en compréhension d ensemble, `open(..., 'r')`, une
variable de boucle inutilisee, et `contextlib.suppress` dans conftest.

CI : `ruff format --check` remplace le commentaire qui expliquait pourquoi
il etait absent.

263 tests passent. Les deux nouveaux messages sont traduits ; attention,
pybabel les avait apparies en `fuzzy` avec des entrees « date » existantes,
et une entree fuzzy est ignoree a l execution.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 15:59:40 -04:00

192 lines
5.5 KiB
Python

"""Shared pytest fixtures for the Team Tryouts test suite.
The application factory is driven entirely through the ``config`` argument
here: no environment variable is required to run the suite, no database
server is needed, and the Discord bot never starts.
"""
import contextlib
import os
import sys
import tempfile
import pytest
# Make the project root importable as the 'app' package.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import event # noqa: E402
from sqlalchemy.engine import Engine # noqa: E402
from app.app import create_app # noqa: E402
from app.extensions import db as _db # noqa: E402
from app.models import Admin, Coach, Manager, Player, Scout # noqa: E402
@event.listens_for(Engine, 'connect')
def _enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
"""Make SQLite behave like PostgreSQL about foreign keys.
SQLite ignores foreign key constraints unless asked not to. Production
runs on PostgreSQL, which always enforces them, so without this a test
suite could pass over a deletion that fails in production — exactly the
class of bug DATA-004 and DATA-005 turned out to be.
"""
import sqlite3
if isinstance(dbapi_connection, sqlite3.Connection):
cursor = dbapi_connection.cursor()
cursor.execute('PRAGMA foreign_keys=ON')
cursor.close()
ROLE_CLASSES = {
'admin': Admin,
'manager': Manager,
'coach': Coach,
'player': Player,
'scout': Scout,
}
#: Satisfies the documented policy (8+ chars, upper, lower, digit).
VALID_PASSWORD = 'Password123'
def _base_config(db_path, csrf=False):
return {
'SECRET_KEY': 'test-secret-not-used-anywhere-real',
'SQLALCHEMY_DATABASE_URI': f'sqlite:///{db_path}',
'TESTING': True,
'WTF_CSRF_ENABLED': csrf,
# Without these three the suite would 301 every request, start a
# Discord bot, and refuse to issue cookies over the test client.
'FORCE_HTTPS': False,
'SESSION_COOKIE_SECURE': False,
'ENABLE_DISCORD_BOT': False,
# The schema still comes from create_all() until Alembic lands (DB-002).
'AUTO_CREATE_TABLES': True,
'CORS_ALLOWED_ORIGINS': '',
'RATELIMIT_ENABLED': False,
}
@pytest.fixture
def app():
"""A fully configured application backed by a throwaway SQLite file.
A file rather than :memory: because Flask-SQLAlchemy hands out a
connection per thread, and an in-memory database is not shared between
them — tables created on one connection would be invisible to the next.
"""
fd, db_path = tempfile.mkstemp(suffix='.sqlite')
os.close(fd)
application = create_app(_base_config(db_path))
yield application
with application.app_context():
_db.session.remove()
_db.engine.dispose()
with contextlib.suppress(OSError):
os.unlink(db_path)
@pytest.fixture
def app_with_csrf():
"""Same application, with CSRF protection left switched on."""
fd, db_path = tempfile.mkstemp(suffix='.sqlite')
os.close(fd)
application = create_app(_base_config(db_path, csrf=True))
yield application
with application.app_context():
_db.session.remove()
_db.engine.dispose()
with contextlib.suppress(OSError):
os.unlink(db_path)
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def db(app):
"""Database handle bound to an active application context."""
with app.app_context():
yield _db
@pytest.fixture
def make_user(app):
"""Factory creating a user of a given role and returning its id.
Returns the primary key rather than the instance: the object would be
detached once the fixture's application context is popped, and every
caller wants to look it up inside its own context anyway.
"""
counter = {'n': 0}
def _make(role='player', password=VALID_PASSWORD, **kwargs):
from app.extensions import hash_password
counter['n'] += 1
n = counter['n']
cls = ROLE_CLASSES[role]
with app.app_context():
user = cls(
username=kwargs.pop('username', f'{role}{n}'),
password_hash=hash_password(password),
role=role,
full_name=kwargs.pop('full_name', f'{role.title()} {n}'),
email=kwargs.pop('email', f'{role}{n}@example.test'),
**kwargs,
)
_db.session.add(user)
_db.session.commit()
return user.id
return _make
@pytest.fixture
def login(client):
"""Log a user in through the real login form.
Deliberately exercises the actual authentication path rather than
poking flask_login's session key, so that session handling itself
stays under test.
"""
def _login(username, password=VALID_PASSWORD):
return client.post(
'/auth/login',
data={'username': username, 'password': password},
follow_redirects=False,
)
return _login
@pytest.fixture
def as_role(app, client, make_user, login):
"""Create a user of the given role, log in, and return its id."""
def _as(role='player', **kwargs):
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), (
f'login for {username} did not redirect: {response.status_code}'
)
return user_id
return _as