Backend: - conftest with async SQLite DB, factory fixtures for all models - pytest-asyncio config in pyproject.toml - Tests: auth (JWT, dev login), RBAC (access service), audit (query, export), brand enforcement (colors, fonts, logos, contrast), retention (cleanup, purge), content intelligence (regex classifiers), slide mapping, review workflow, analytics data queries Frontend: - Cypress E2E config with baseUrl and viewport settings - Custom commands (devLogin, createPresentation) - E2E specs: login flow, wizard navigation, admin panel, review workflow - Test scripts in package.json Infrastructure: - Makefile: test-e2e and test-all targets Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
215 lines
6.8 KiB
Python
215 lines
6.8 KiB
Python
"""Test fixtures: async SQLite database, factory functions for models."""
|
|
import os
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
|
from sqlmodel import SQLModel
|
|
|
|
# Force SQLite for tests (must be set before any app imports)
|
|
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///:memory:")
|
|
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key")
|
|
os.environ.setdefault("DEV_AUTH_PASSWORD", "testpass")
|
|
|
|
# Import all SQLModel table classes so metadata is populated
|
|
from models.sql.user import UserModel # noqa: E402
|
|
from models.sql.client import ClientModel # noqa: E402
|
|
from models.sql.team import TeamModel # noqa: E402
|
|
from models.sql.team_membership import TeamMembershipModel # noqa: E402
|
|
from models.sql.brand_config import BrandConfigModel # noqa: E402
|
|
from models.sql.audit_log import AuditLogModel # noqa: E402
|
|
from models.sql.presentation import PresentationModel # noqa: E402
|
|
from models.sql.job import JobModel # noqa: E402
|
|
from models.sql.master_deck import MasterDeckModel # noqa: E402
|
|
|
|
|
|
_test_engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
|
|
_test_session_maker = async_sessionmaker(_test_engine, expire_on_commit=False)
|
|
|
|
|
|
@pytest_asyncio.fixture(autouse=True)
|
|
async def setup_database():
|
|
"""Create all tables before each test, drop after."""
|
|
async with _test_engine.begin() as conn:
|
|
await conn.run_sync(SQLModel.metadata.create_all)
|
|
yield
|
|
async with _test_engine.begin() as conn:
|
|
await conn.run_sync(SQLModel.metadata.drop_all)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def session() -> AsyncSession:
|
|
async with _test_session_maker() as s:
|
|
yield s
|
|
|
|
|
|
# --------------- Factory Fixtures ---------------
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def make_user(session: AsyncSession):
|
|
async def _factory(
|
|
email: str = "test@oliver.com",
|
|
role: str = "user",
|
|
display_name: str = "Test User",
|
|
is_active: bool = True,
|
|
azure_oid: str | None = None,
|
|
) -> UserModel:
|
|
user = UserModel(
|
|
email=email,
|
|
display_name=display_name,
|
|
role=role,
|
|
is_active=is_active,
|
|
azure_oid=azure_oid,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
)
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
return user
|
|
return _factory
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def make_client(session: AsyncSession):
|
|
async def _factory(
|
|
name: str = "Acme Corp",
|
|
slug: str | None = None,
|
|
retention_days: int | None = None,
|
|
is_active: bool = True,
|
|
) -> ClientModel:
|
|
client = ClientModel(
|
|
name=name,
|
|
slug=slug or name.lower().replace(" ", "-"),
|
|
retention_days=retention_days,
|
|
is_active=is_active,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
)
|
|
session.add(client)
|
|
await session.commit()
|
|
await session.refresh(client)
|
|
return client
|
|
return _factory
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def make_team(session: AsyncSession):
|
|
async def _factory(
|
|
name: str = "Dev Team",
|
|
client_id: uuid.UUID | None = None,
|
|
is_default: bool = False,
|
|
) -> TeamModel:
|
|
team = TeamModel(
|
|
name=name,
|
|
client_id=client_id,
|
|
is_default=is_default,
|
|
created_at=datetime.now(timezone.utc),
|
|
)
|
|
session.add(team)
|
|
await session.commit()
|
|
await session.refresh(team)
|
|
return team
|
|
return _factory
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def make_membership(session: AsyncSession):
|
|
async def _factory(user_id: uuid.UUID, team_id: uuid.UUID) -> TeamMembershipModel:
|
|
membership = TeamMembershipModel(
|
|
user_id=user_id,
|
|
team_id=team_id,
|
|
assigned_at=datetime.now(timezone.utc),
|
|
)
|
|
session.add(membership)
|
|
await session.commit()
|
|
await session.refresh(membership)
|
|
return membership
|
|
return _factory
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def make_brand_config(session: AsyncSession):
|
|
async def _factory(
|
|
client_id: uuid.UUID,
|
|
primary_colors: list | None = None,
|
|
secondary_colors: list | None = None,
|
|
fonts: dict | None = None,
|
|
logo_paths: list | None = None,
|
|
voice_rules: str | None = None,
|
|
voice_examples: list | None = None,
|
|
) -> BrandConfigModel:
|
|
brand = BrandConfigModel(
|
|
client_id=client_id,
|
|
primary_colors=primary_colors or ["#5146E5", "#3D35B0"],
|
|
secondary_colors=secondary_colors or ["#E9E8F8"],
|
|
fonts=fonts or {"heading": "Inter", "body": "Roboto"},
|
|
logo_paths=logo_paths,
|
|
voice_rules=voice_rules,
|
|
voice_examples=voice_examples,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
)
|
|
session.add(brand)
|
|
await session.commit()
|
|
await session.refresh(brand)
|
|
return brand
|
|
return _factory
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def make_presentation(session: AsyncSession):
|
|
async def _factory(
|
|
owner_id: uuid.UUID | None = None,
|
|
client_id: uuid.UUID | None = None,
|
|
title: str = "Test Deck",
|
|
status: str = "draft",
|
|
created_at: datetime | None = None,
|
|
deleted_at: datetime | None = None,
|
|
) -> PresentationModel:
|
|
pres = PresentationModel(
|
|
content="Test content",
|
|
n_slides=5,
|
|
language="English",
|
|
title=title,
|
|
owner_id=owner_id,
|
|
client_id=client_id,
|
|
status=status,
|
|
created_at=created_at or datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
deleted_at=deleted_at,
|
|
)
|
|
session.add(pres)
|
|
await session.commit()
|
|
await session.refresh(pres)
|
|
return pres
|
|
return _factory
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def make_job(session: AsyncSession):
|
|
async def _factory(
|
|
user_id: uuid.UUID,
|
|
client_id: uuid.UUID,
|
|
job_type: str = "generate_presentation",
|
|
status: str = "queued",
|
|
progress: int = 0,
|
|
presentation_id: uuid.UUID | None = None,
|
|
) -> JobModel:
|
|
job = JobModel(
|
|
user_id=user_id,
|
|
client_id=client_id,
|
|
job_type=job_type,
|
|
status=status,
|
|
progress=progress,
|
|
presentation_id=presentation_id,
|
|
created_at=datetime.now(timezone.utc),
|
|
)
|
|
session.add(job)
|
|
await session.commit()
|
|
await session.refresh(job)
|
|
return job
|
|
return _factory
|