punimtag/tests/conftest.py
Tanya 77ffbdcc50
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 2m5s
CI / python-lint (pull_request) Successful in 1m51s
CI / test-backend (pull_request) Successful in 2m44s
CI / build (pull_request) Successful in 2m24s
CI / secret-scanning (pull_request) Successful in 1m39s
CI / dependency-scan (pull_request) Successful in 1m33s
CI / sast-scan (pull_request) Successful in 2m45s
CI / workflow-summary (pull_request) Successful in 1m26s
chore: Update CI workflow and testing setup with new dependencies and test plan documentation
This commit enhances the CI workflow by adding steps to create test databases and install new testing dependencies, including `pytest`, `httpx`, and `pytest-cov`. Additionally, comprehensive test plan documentation is introduced to outline the structure and best practices for backend API tests. These changes improve the testing environment and contribute to a more robust CI process.
2026-01-07 14:53:26 -05:00

321 lines
8.8 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
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