punimtag/tests/conftest.py
Tanya 364974141d
Some checks failed
CI / skip-ci-check (push) Successful in 1m27s
CI / lint-and-type-check (push) Has been cancelled
CI / python-lint (push) Has been cancelled
CI / test-backend (push) Has been cancelled
CI / build (push) Has been cancelled
CI / secret-scanning (push) Has been cancelled
CI / dependency-scan (push) Has been cancelled
CI / sast-scan (push) Has been cancelled
CI / workflow-summary (push) Has been cancelled
CI / skip-ci-check (pull_request) Successful in 1m27s
CI / lint-and-type-check (pull_request) Successful in 2m6s
CI / python-lint (pull_request) Successful in 1m54s
CI / test-backend (pull_request) Successful in 3m39s
CI / build (pull_request) Successful in 2m24s
CI / secret-scanning (pull_request) Successful in 1m40s
CI / dependency-scan (pull_request) Successful in 1m33s
CI / sast-scan (pull_request) Successful in 2m47s
CI / workflow-summary (pull_request) Successful in 1m26s
chore: Add pytest configuration and update CI to skip DeepFace during tests
This commit introduces a new `pytest.ini` configuration file for backend tests, specifying test discovery patterns and output options. Additionally, the CI workflow is updated to set an environment variable that prevents DeepFace and TensorFlow from loading during tests, avoiding illegal instruction errors on certain CPUs. The face service and pose detection modules are modified to conditionally import DeepFace and RetinaFace based on this environment variable, enhancing test reliability. These changes improve the testing setup and contribute to a more robust CI process.
2026-01-07 15:02:41 -05:00

325 lines
9.1 KiB
Python

"""Test configuration and fixtures for PunimTag backend tests."""
from __future__ import annotations
import os
from typing import Generator
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
# Prevent DeepFace/TensorFlow from loading during tests (causes illegal instruction on some CPUs)
# Set environment variable BEFORE any backend imports that might trigger DeepFace/TensorFlow
os.environ["SKIP_DEEPFACE_IN_TESTS"] = "1"
from backend.app import create_app
from backend.db.base import Base
from backend.db.session import get_db
# Test database URL - use environment variable or default
TEST_DATABASE_URL = os.getenv(
"DATABASE_URL",
"postgresql+psycopg2://postgres:postgres@localhost:5432/punimtag_test"
)
@pytest.fixture(scope="session")
def test_db_engine():
"""Create test database engine and initialize schema."""
engine = create_engine(TEST_DATABASE_URL, future=True)
# Create all tables
Base.metadata.create_all(bind=engine)
yield engine
# Cleanup: drop all tables after tests
Base.metadata.drop_all(bind=engine)
engine.dispose()
@pytest.fixture(scope="function")
def test_db_session(test_db_engine) -> Generator[Session, None, None]:
"""Create a test database session with transaction rollback.
Each test gets a fresh session that rolls back after the test completes.
"""
connection = test_db_engine.connect()
transaction = connection.begin()
session = sessionmaker(bind=connection, autoflush=False, autocommit=False)()
yield session
# Rollback transaction and close connection
session.close()
transaction.rollback()
connection.close()
@pytest.fixture(scope="function")
def test_client(test_db_session: Session) -> Generator[TestClient, None, None]:
"""Create a test client with test database dependency override."""
app = create_app()
def override_get_db() -> Generator[Session, None, None]:
yield test_db_session
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as client:
yield client
# Clear dependency overrides after test
app.dependency_overrides.clear()
@pytest.fixture
def admin_user(test_db_session: Session):
"""Create an admin user for testing."""
from backend.db.models import User
from backend.utils.password import hash_password
from backend.constants.roles import DEFAULT_ADMIN_ROLE
user = User(
username="testadmin",
email="testadmin@example.com",
password_hash=hash_password("testpass"),
is_admin=True,
is_active=True,
role=DEFAULT_ADMIN_ROLE,
)
test_db_session.add(user)
test_db_session.commit()
test_db_session.refresh(user)
return user
@pytest.fixture
def regular_user(test_db_session: Session):
"""Create a regular user for testing."""
from backend.db.models import User
from backend.utils.password import hash_password
from backend.constants.roles import DEFAULT_USER_ROLE
user = User(
username="testuser",
email="testuser@example.com",
password_hash=hash_password("testpass"),
is_admin=False,
is_active=True,
role=DEFAULT_USER_ROLE,
)
test_db_session.add(user)
test_db_session.commit()
test_db_session.refresh(user)
return user
@pytest.fixture
def inactive_user(test_db_session: Session):
"""Create an inactive user for testing."""
from backend.db.models import User
from backend.utils.password import hash_password
from backend.constants.roles import DEFAULT_USER_ROLE
user = User(
username="inactiveuser",
email="inactiveuser@example.com",
password_hash=hash_password("testpass"),
is_admin=False,
is_active=False,
role=DEFAULT_USER_ROLE,
)
test_db_session.add(user)
test_db_session.commit()
test_db_session.refresh(user)
return user
@pytest.fixture
def auth_token(test_client: TestClient, admin_user) -> str:
"""Get authentication token for admin user."""
response = test_client.post(
"/api/v1/auth/login",
json={"username": "testadmin", "password": "testpass"}
)
assert response.status_code == 200
return response.json()["access_token"]
@pytest.fixture
def regular_auth_token(test_client: TestClient, regular_user) -> str:
"""Get authentication token for regular user."""
response = test_client.post(
"/api/v1/auth/login",
json={"username": "testuser", "password": "testpass"}
)
assert response.status_code == 200
return response.json()["access_token"]
@pytest.fixture
def auth_headers(auth_token: str) -> dict[str, str]:
"""Get authentication headers for admin user."""
return {"Authorization": f"Bearer {auth_token}"}
@pytest.fixture
def regular_auth_headers(regular_auth_token: str) -> dict[str, str]:
"""Get authentication headers for regular user."""
return {"Authorization": f"Bearer {regular_auth_token}"}
@pytest.fixture
def test_photo(test_db_session: Session):
"""Create a test photo."""
from backend.db.models import Photo
from datetime import date
photo = Photo(
path="/test/path/photo1.jpg",
filename="photo1.jpg",
date_taken=date(2024, 1, 15),
processed=True,
media_type="image",
)
test_db_session.add(photo)
test_db_session.commit()
test_db_session.refresh(photo)
return photo
@pytest.fixture
def test_photo_2(test_db_session: Session):
"""Create a second test photo."""
from backend.db.models import Photo
from datetime import date
photo = Photo(
path="/test/path/photo2.jpg",
filename="photo2.jpg",
date_taken=date(2024, 1, 16),
processed=True,
media_type="image",
)
test_db_session.add(photo)
test_db_session.commit()
test_db_session.refresh(photo)
return photo
@pytest.fixture
def test_face(test_db_session: Session, test_photo):
"""Create a test face (unidentified)."""
from backend.db.models import Face
import numpy as np
# Create a dummy encoding (128-dimensional vector like DeepFace)
encoding = np.random.rand(128).astype(np.float32).tobytes()
face = Face(
photo_id=test_photo.id,
person_id=None, # Unidentified
encoding=encoding,
location='{"x": 100, "y": 100, "w": 200, "h": 200}',
quality_score=0.85,
face_confidence=0.95,
detector_backend="retinaface",
model_name="VGG-Face",
pose_mode="frontal",
excluded=False,
)
test_db_session.add(face)
test_db_session.commit()
test_db_session.refresh(face)
return face
@pytest.fixture
def test_face_2(test_db_session: Session, test_photo_2):
"""Create a second test face (unidentified)."""
from backend.db.models import Face
import numpy as np
# Create a similar encoding (for similarity testing)
encoding = np.random.rand(128).astype(np.float32).tobytes()
face = Face(
photo_id=test_photo_2.id,
person_id=None, # Unidentified
encoding=encoding,
location='{"x": 150, "y": 150, "w": 200, "h": 200}',
quality_score=0.80,
face_confidence=0.90,
detector_backend="retinaface",
model_name="VGG-Face",
pose_mode="frontal",
excluded=False,
)
test_db_session.add(face)
test_db_session.commit()
test_db_session.refresh(face)
return face
@pytest.fixture
def test_person(test_db_session: Session):
"""Create a test person."""
from backend.db.models import Person
from datetime import date, datetime
person = Person(
first_name="John",
last_name="Doe",
middle_name="Middle",
maiden_name=None,
date_of_birth=date(1990, 1, 1),
created_date=datetime.utcnow(),
)
test_db_session.add(person)
test_db_session.commit()
test_db_session.refresh(person)
return person
@pytest.fixture
def identified_face(test_db_session: Session, test_photo, test_person):
"""Create an identified face (already linked to a person)."""
from backend.db.models import Face, PersonEncoding
import numpy as np
# Create encoding
encoding = np.random.rand(128).astype(np.float32).tobytes()
face = Face(
photo_id=test_photo.id,
person_id=test_person.id,
encoding=encoding,
location='{"x": 200, "y": 200, "w": 200, "h": 200}',
quality_score=0.90,
face_confidence=0.98,
detector_backend="retinaface",
model_name="VGG-Face",
pose_mode="frontal",
excluded=False,
)
test_db_session.add(face)
test_db_session.flush()
# Create person encoding
person_encoding = PersonEncoding(
person_id=test_person.id,
face_id=face.id,
encoding=encoding,
quality_score=0.90,
detector_backend="retinaface",
model_name="VGG-Face",
)
test_db_session.add(person_encoding)
test_db_session.commit()
test_db_session.refresh(face)
return face