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
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.
321 lines
8.8 KiB
Python
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
|
|
|