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]>
192 lines
5.5 KiB
Python
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
|