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]>
195 lines
5.5 KiB
Python
195 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 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()
|
|
try:
|
|
os.unlink(db_path)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
@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()
|
|
try:
|
|
os.unlink(db_path)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
@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
|