Files
PecHub/backend/tests/integration/conftest.py
T
mgiustini 58a233236c feat: Fase 1 – Fondamenta complete (backend FastAPI + auth + permessi)
- docker-compose.yml: PostgreSQL 16, Redis 7, MinIO, Nginx
- backend FastAPI: struttura monorepo, config pydantic-settings
- modelli SQLAlchemy: tutti i modelli (tenants, users, mailboxes, messages, archival, permissions, labels, audit_log)
- migrazione Alembic 0001: schema completo in pure SQL
- auth API: login JWT, refresh token rotation, logout, 2FA TOTP (setup/verify/disable)
- CRUD utenti: lista, crea, modifica, reset password, soft delete
- permessi granulari (Fase 1-A): mailbox_permissions, assegna/revoca/lista
- CRUD tenant: gestione super-admin
- sicurezza: AES-256-GCM cifratura credenziali IMAP/SMTP, bcrypt password
- RLS PostgreSQL: isolamento multi-tenant per request
- seed sviluppo: tenant demo + admin + operator
- test unit: security (bcrypt, JWT, AES), auth_service
- test integration: auth endpoints, users endpoints
- CI GitHub Actions: lint (ruff), test (pytest), build Docker, security scan
- infra: nginx.conf, redis.conf
- Makefile con comandi make dev/test/migrate/seed

Definition of Done:
 Login, refresh token e TOTP funzionanti
 make dev porta in piedi tutto lo stack locale
 CI configurata
2026-03-18 16:42:01 +01:00

145 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Fixtures per test di integrazione DB in-memory SQLite + app FastAPI.
Per i test di integrazione si usa SQLite async invece di PostgreSQL per
semplicità e velocità. In CI si può aggiungere un servizio PostgreSQL reale.
"""
import os
import uuid
from typing import AsyncGenerator
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
# Override variabili d'ambiente prima di importare l'app
os.environ["ENCRYPTION_KEY"] = "b" * 64
os.environ["SECRET_KEY"] = "integration-test-secret-key-only-for-tests"
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///./test_integration.db"
os.environ["DATABASE_URL_SYNC"] = "sqlite:///./test_integration.db"
os.environ["APP_ENV"] = "development"
os.environ["APP_DEBUG"] = "false"
from app.database import Base
from app.main import app
# Engine SQLite per test
TEST_DATABASE_URL = "sqlite+aiosqlite:///./test_integration.db"
test_engine = create_async_engine(
TEST_DATABASE_URL,
echo=False,
connect_args={"check_same_thread": False},
)
TestAsyncSessionLocal = async_sessionmaker(
bind=test_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
@pytest_asyncio.fixture(scope="session", autouse=True)
async def setup_database():
"""Crea tutte le tabelle nel DB di test."""
# Import modelli per registrarli nel metadata
import app.models # noqa: F401
async with test_engine.begin() as conn:
# SQLite non supporta tutti i tipi PostgreSQL, usiamo tabelle semplici
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
yield
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await test_engine.dispose()
# Pulisci file DB
import os
if os.path.exists("test_integration.db"):
os.remove("test_integration.db")
@pytest_asyncio.fixture
async def db_session() -> AsyncGenerator[AsyncSession, None]:
"""Session DB isolata per ogni test (rollback automatico)."""
async with TestAsyncSessionLocal() as session:
yield session
await session.rollback()
@pytest_asyncio.fixture
async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
"""HTTP client per test API con override del DB."""
from app.database import get_db
async def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
) as c:
yield c
app.dependency_overrides.clear()
@pytest_asyncio.fixture
async def demo_tenant(db_session: AsyncSession):
"""Crea un tenant di test."""
from app.models.tenant import Tenant
tenant = Tenant(
id=uuid.UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
slug="test-tenant",
name="Test Tenant",
plan="pro",
max_mailboxes=10,
max_users=10,
)
db_session.add(tenant)
await db_session.commit()
await db_session.refresh(tenant)
return tenant
@pytest_asyncio.fixture
async def admin_user(db_session: AsyncSession, demo_tenant):
"""Crea un utente admin nel tenant di test."""
from app.core.security import hash_password
from app.models.user import User
user = User(
tenant_id=demo_tenant.id,
email="admin@test.com",
password_hash=hash_password("AdminPass1!"),
full_name="Test Admin",
role="admin",
is_active=True,
)
db_session.add(user)
await db_session.commit()
await db_session.refresh(user)
return user
@pytest_asyncio.fixture
async def admin_token(client: AsyncClient, admin_user, db_session: AsyncSession) -> str:
"""
Token JWT per l'utente admin.
Nota: il login usa il DB sovrapposto dalla fixture, quindi
il seed_user deve già esistere.
"""
from app.core.security import create_access_token
token = create_access_token(
subject=admin_user.id,
tenant_id=admin_user.tenant_id,
role=admin_user.role,
)
return token