kanban-app/backend/tests/conftest.py

258 lines
6.5 KiB
Python

"""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="function")
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