"""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 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 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