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
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.
325 lines
9.1 KiB
Python
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
|
|
|