"""Pytest configuration and fixtures""" import logging import time from io import BytesIO import pytest from faker import Faker from PIL import Image from app import create_app, db from app.models import Board, Card, Comment, List, User log = logging.getLogger(__name__) fake = Faker() @pytest.fixture(scope="session") def app(): """Create application for testing with PostgreSQL database (session scope)""" app = create_app(config_name="test") app.config.update( { "TESTING": True, # fmt: off "WTF_CSRF_ENABLED": False, "JWT_SECRET_KEY": "test-secret-keytest-secret-keytest-secret-keytest-secret-key", "SERVER_NAME": "localhost.localdomain", # fmt: on } ) # Create tables once per session with app.app_context(): print("--------db.create_all()------") db.create_all() yield app # Cleanup after all tests db.session.remove() db.drop_all() db.engine.dispose() @pytest.fixture def client(app): """Test client for making requests""" log.debug("Test finished - session dirty: %s", db.session.dirty) log.debug("Test finished - session new: %s", db.session.new) return app.test_client() @pytest.fixture(autouse=True) def _cleanup_db_after_test(app): """ Automatically rollback and remove DB session after EACH test function. - Runs after every test function (default scope='function') - Depends on 'app' to ensure app context is available - Rollback prevents data leakage between tests - remove() returns connection to pool (prevents exhaustion) """ yield # Let the test run with app.app_context(): db.session.rollback() # Undo all changes from the test db.session.remove() # Return connection to pool @pytest.fixture def runner(app): """Test CLI runner""" return app.test_cli_runner() @pytest.fixture(autouse=True) def _rollback_db_session(app): """Automatically rollback db.session after each test for isolation. This fixture uses the shared Flask-SQLAlchemy session but ensures tests don't interfere with each other by rolling back after each test. """ yield with app.app_context(): db.session.rollback() db.session.remove() @pytest.fixture def db_session(app): """Database session for tests (function scope)""" """Provide the shared Flask-SQLAlchemy db.session for tests""" with app.app_context(): yield db.session @pytest.fixture def admin_user(db_session): """Create an admin user for testing""" user = User( email=fake.email(), username=fake.user_name(), first_name=fake.first_name(), last_name=fake.last_name(), is_admin=True, is_active=True, ) user.set_password("password123") db_session.add(user) db_session.commit() return user @pytest.fixture def regular_user(db_session): """Create a regular user for testing""" user = User( email=fake.email(), username=f"{fake.user_name()}_{int(time.time() * 1000)}", first_name=fake.first_name(), last_name=fake.last_name(), is_admin=False, is_active=True, ) user.set_password("password123") db_session.add(user) db_session.commit() return user @pytest.fixture def inactive_user(db_session): """Create an inactive user for testing""" user = User( email=fake.email(), username=fake.user_name(), first_name=fake.first_name(), last_name=fake.last_name(), is_admin=False, is_active=False, ) user.set_password("password123") db_session.add(user) db_session.commit() return user @pytest.fixture def auth_headers(client, regular_user): """Get authentication headers for a regular user""" response = client.post( "/api/auth/login", json={"email": regular_user.email, "password": "password123"} ) data = response.get_json() response.close() token = data["access_token"] print(f"Auth headers token for user {regular_user.email}: {token[:50]}...") return {"Authorization": f"Bearer {token}"} @pytest.fixture def admin_headers(client, admin_user): """Get authentication headers for an admin user""" response = client.post( "/api/auth/login", json={"email": admin_user.email, "password": "password123"} ) data = response.get_json() token = data["access_token"] print(f"Admin headers token for user {admin_user.email}: {token[:50]}...") return {"Authorization": f"Bearer {token}"} # ============ File Upload Fixtures ============ @pytest.fixture def test_image_file(): """Create a test image file as BytesIO""" # Create a simple red PNG image using PIL img = Image.new("RGB", (10, 10), color="red") img_io = BytesIO() img.save(img_io, format="PNG") img_io.seek(0) return img_io @pytest.fixture def test_large_image_file(): """Create a larger test image (50KB)""" # Create a larger image using PIL img = Image.new("RGB", (200, 200), color="blue") img_io = BytesIO() img.save(img_io, format="PNG") img_io.seek(0) return img_io @pytest.fixture def test_pdf_file(): """Create a test PDF file as BytesIO""" # Minimal valid PDF pdf_data = b"%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n>>\nendobj\n%%EOF" return BytesIO(pdf_data) @pytest.fixture def test_board(db_session, regular_user): """Create a test board""" board = Board( user_id=regular_user.id, name=fake.sentence(nb_words=4)[:-1], description=fake.paragraph(), ) db_session.add(board) db_session.commit() return board @pytest.fixture def test_list(db_session, test_board): """Create a test list""" lst = List( board_id=test_board.id, name=fake.sentence(nb_words=3)[:-1], pos=0, ) db_session.add(lst) db_session.commit() return lst @pytest.fixture def test_card(db_session, test_list): """Create a test card""" card = Card( list_id=test_list.id, name=fake.sentence(nb_words=4)[:-1], description=fake.paragraph(), pos=0, due=None, board_id=test_list.board_id, ) db_session.add(card) db_session.commit() return card @pytest.fixture def test_comment(db_session, test_card, regular_user): """Create a test comment""" comment = Comment( card_id=test_card.id, user_id=regular_user.id, text=fake.paragraph(), ) db_session.add(comment) db_session.commit() return comment