2026-02-24 14:36:31 +00:00
|
|
|
"""Pytest configuration and fixtures"""
|
2026-02-25 18:32:57 +00:00
|
|
|
import logging
|
2026-04-12 10:01:45 +00:00
|
|
|
import time
|
2026-03-20 17:17:01 +00:00
|
|
|
from io import BytesIO
|
2026-02-24 16:19:15 +00:00
|
|
|
|
|
|
|
|
import pytest
|
2026-02-24 14:36:31 +00:00
|
|
|
from faker import Faker
|
2026-03-20 17:17:01 +00:00
|
|
|
from PIL import Image
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
from app import create_app, db
|
2026-04-12 10:01:45 +00:00
|
|
|
from app.models import Board, Card, Comment, List, User
|
2026-02-24 14:36:31 +00:00
|
|
|
|
2026-02-25 18:32:57 +00:00
|
|
|
log = logging.getLogger(__name__)
|
2026-02-24 14:36:31 +00:00
|
|
|
fake = Faker()
|
|
|
|
|
|
|
|
|
|
|
2026-05-01 15:03:00 +00:00
|
|
|
@pytest.fixture(scope="session")
|
2026-02-24 14:36:31 +00:00
|
|
|
def app():
|
2026-02-25 18:32:57 +00:00
|
|
|
"""Create application for testing with PostgreSQL database (session scope)"""
|
2026-02-24 16:19:15 +00:00
|
|
|
app = create_app(config_name="test")
|
|
|
|
|
app.config.update(
|
|
|
|
|
{
|
|
|
|
|
"TESTING": True,
|
2026-02-25 18:32:57 +00:00
|
|
|
# fmt: off
|
2026-02-24 16:19:15 +00:00
|
|
|
"WTF_CSRF_ENABLED": False,
|
2026-02-25 18:32:57 +00:00
|
|
|
"JWT_SECRET_KEY":
|
|
|
|
|
"test-secret-keytest-secret-keytest-secret-keytest-secret-key",
|
2026-02-24 16:19:15 +00:00
|
|
|
"SERVER_NAME": "localhost.localdomain",
|
2026-02-25 18:32:57 +00:00
|
|
|
# fmt: on
|
2026-02-24 16:19:15 +00:00
|
|
|
}
|
|
|
|
|
)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
2026-02-25 18:32:57 +00:00
|
|
|
# Create tables once per session
|
2026-02-24 14:36:31 +00:00
|
|
|
with app.app_context():
|
2026-02-27 12:41:44 +00:00
|
|
|
print("--------db.create_all()------")
|
2026-02-24 14:36:31 +00:00
|
|
|
db.create_all()
|
|
|
|
|
yield app
|
2026-02-25 18:32:57 +00:00
|
|
|
# Cleanup after all tests
|
2026-02-24 14:36:31 +00:00
|
|
|
db.session.remove()
|
|
|
|
|
db.drop_all()
|
2026-02-27 20:26:25 +00:00
|
|
|
db.engine.dispose()
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def client(app):
|
|
|
|
|
"""Test client for making requests"""
|
2026-02-25 18:32:57 +00:00
|
|
|
|
|
|
|
|
log.debug("Test finished - session dirty: %s", db.session.dirty)
|
|
|
|
|
log.debug("Test finished - session new: %s", db.session.new)
|
2026-02-24 14:36:31 +00:00
|
|
|
return app.test_client()
|
|
|
|
|
|
|
|
|
|
|
2026-02-25 18:32:57 +00:00
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
@pytest.fixture
|
|
|
|
|
def runner(app):
|
|
|
|
|
"""Test CLI runner"""
|
|
|
|
|
return app.test_cli_runner()
|
|
|
|
|
|
|
|
|
|
|
2026-02-25 18:32:57 +00:00
|
|
|
@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()
|
|
|
|
|
|
|
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
@pytest.fixture
|
|
|
|
|
def db_session(app):
|
2026-02-25 18:32:57 +00:00
|
|
|
"""Database session for tests (function scope)"""
|
|
|
|
|
"""Provide the shared Flask-SQLAlchemy db.session for tests"""
|
2026-02-24 14:36:31 +00:00
|
|
|
with app.app_context():
|
|
|
|
|
yield db.session
|
2026-02-25 18:32:57 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@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,
|
2026-02-24 16:19:15 +00:00
|
|
|
is_active=True,
|
2026-02-24 14:36:31 +00:00
|
|
|
)
|
2026-02-24 16:19:15 +00:00
|
|
|
user.set_password("password123")
|
2026-02-24 14:36:31 +00:00
|
|
|
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(),
|
2026-04-12 10:01:45 +00:00
|
|
|
username=f"{fake.user_name()}_{int(time.time() * 1000)}",
|
2026-02-24 14:36:31 +00:00
|
|
|
first_name=fake.first_name(),
|
|
|
|
|
last_name=fake.last_name(),
|
|
|
|
|
is_admin=False,
|
2026-02-24 16:19:15 +00:00
|
|
|
is_active=True,
|
2026-02-24 14:36:31 +00:00
|
|
|
)
|
2026-02-24 16:19:15 +00:00
|
|
|
user.set_password("password123")
|
2026-02-24 14:36:31 +00:00
|
|
|
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,
|
2026-02-24 16:19:15 +00:00
|
|
|
is_active=False,
|
2026-02-24 14:36:31 +00:00
|
|
|
)
|
2026-02-24 16:19:15 +00:00
|
|
|
user.set_password("password123")
|
2026-02-24 14:36:31 +00:00
|
|
|
db_session.add(user)
|
|
|
|
|
db_session.commit()
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def auth_headers(client, regular_user):
|
|
|
|
|
"""Get authentication headers for a regular user"""
|
2026-02-24 16:19:15 +00:00
|
|
|
response = client.post(
|
|
|
|
|
"/api/auth/login", json={"email": regular_user.email, "password": "password123"}
|
|
|
|
|
)
|
2026-02-24 14:36:31 +00:00
|
|
|
data = response.get_json()
|
2026-02-25 18:32:57 +00:00
|
|
|
response.close()
|
2026-02-24 16:19:15 +00:00
|
|
|
token = data["access_token"]
|
2026-02-24 14:36:31 +00:00
|
|
|
print(f"Auth headers token for user {regular_user.email}: {token[:50]}...")
|
2026-02-24 16:19:15 +00:00
|
|
|
return {"Authorization": f"Bearer {token}"}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def admin_headers(client, admin_user):
|
|
|
|
|
"""Get authentication headers for an admin user"""
|
2026-02-24 16:19:15 +00:00
|
|
|
response = client.post(
|
|
|
|
|
"/api/auth/login", json={"email": admin_user.email, "password": "password123"}
|
|
|
|
|
)
|
2026-02-24 14:36:31 +00:00
|
|
|
data = response.get_json()
|
2026-02-24 16:19:15 +00:00
|
|
|
token = data["access_token"]
|
2026-02-24 14:36:31 +00:00
|
|
|
print(f"Admin headers token for user {admin_user.email}: {token[:50]}...")
|
2026-02-24 16:19:15 +00:00
|
|
|
return {"Authorization": f"Bearer {token}"}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
|
2026-03-20 17:17:01 +00:00
|
|
|
# ============ 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
|