"""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"), full_name="Test Admin", 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"), full_name="Test User", 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"), full_name="Inactive User", 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