"""Integration Test Configuration - ServiceManagerWeb Fixtures y utilidades para tests de integración con BD real. Este conftest vive dentro de tests/integration para que sus fixtures (client, db_session, test_tenant, tokens, etc.) apliquen solo a los tests de integración y no colisionen con los fixtures SQLite del conftest global. """ import os import pytest from typing import AsyncGenerator from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker from sqlalchemy.pool import NullPool from sqlalchemy import text from httpx import AsyncClient from app.main import app from app.core.database import Base, get_db from app.core.security import SecurityUtils from app.models.tenant import Tenant, TenantStatus from app.models.user import User, UserRole from app.models.system import System from app.models.category import Category _DEFAULT_TEST_DATABASE_URL = "postgresql+asyncpg://servicemanager:servicemanager123@localhost:5432/servicemanager_test" _ENV_TEST_DATABASE_URL = os.getenv("TEST_DATABASE_URL") _ENV_DATABASE_URL = os.getenv("DATABASE_URL") if _ENV_TEST_DATABASE_URL: TEST_DATABASE_URL = _ENV_TEST_DATABASE_URL elif _ENV_DATABASE_URL and "@postgres:" in _ENV_DATABASE_URL: TEST_DATABASE_URL = _ENV_DATABASE_URL.rsplit("/", 1)[0] + "/servicemanager_test" else: TEST_DATABASE_URL = _DEFAULT_TEST_DATABASE_URL @pytest.fixture(scope="session") async def test_engine(): """Create test database engine.""" engine = create_async_engine( TEST_DATABASE_URL, echo=False, poolclass=NullPool, ) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() @pytest.fixture async def db_session(test_engine) -> AsyncGenerator[AsyncSession, None]: """Create a fresh database session for each integration test.""" async_session = async_sessionmaker( test_engine, class_=AsyncSession, expire_on_commit=False, ) async with async_session() as session: try: yield session finally: # Rollback any open transaction await session.rollback() # Hard reset DB state for next test (tests commit, so rollback alone isn't enough) table_names = [t.name for t in Base.metadata.sorted_tables] if table_names: quoted = ", ".join(f'"{name}"' for name in table_names) await session.execute(text(f"TRUNCATE TABLE {quoted} RESTART IDENTITY CASCADE")) await session.commit() @pytest.fixture async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]: """Create test client with overridden database dependency.""" # Disable login rate limiting during integration tests to avoid flakiness # (tests perform many logins quickly from the same IP). import app.api.v1.endpoints.auth as auth_endpoint old_rate_limit_enabled = getattr(auth_endpoint.settings, "RATE_LIMIT_ENABLED", None) old_testing = getattr(auth_endpoint.settings, "TESTING", None) auth_endpoint.settings.RATE_LIMIT_ENABLED = False auth_endpoint.settings.TESTING = True async def override_get_db(): yield db_session app.dependency_overrides[get_db] = override_get_db async with AsyncClient(app=app, base_url="http://test") as ac: yield ac app.dependency_overrides.clear() # Restore settings if old_rate_limit_enabled is not None: auth_endpoint.settings.RATE_LIMIT_ENABLED = old_rate_limit_enabled if old_testing is not None: auth_endpoint.settings.TESTING = old_testing # =================================== # FIXTURES DE DATOS DE TEST # =================================== @pytest.fixture async def test_tenant(db_session: AsyncSession) -> Tenant: tenant = Tenant( name="Test Company", slug="test-company", domain="test.company.com", status=TenantStatus.ACTIVE, contact_email="admin@test.company.com", contact_phone="+1234567890", ) db_session.add(tenant) await db_session.commit() await db_session.refresh(tenant) return tenant @pytest.fixture async def test_tenant_2(db_session: AsyncSession) -> Tenant: tenant = Tenant( name="Test Company 2", slug="test-company-2", domain="test2.company.com", status=TenantStatus.ACTIVE, contact_email="admin@test2.company.com", contact_phone="+9876543210", ) db_session.add(tenant) await db_session.commit() await db_session.refresh(tenant) return tenant @pytest.fixture async def test_admin_user(db_session: AsyncSession, test_tenant: Tenant) -> User: user = User( tenant_id=test_tenant.id, email="admin@test.com", first_name="Admin", last_name="User", password_hash=SecurityUtils.hash_password("AdminPass123!"), role=UserRole.ADMIN, is_active=True, email_verified=True, ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user @pytest.fixture async def test_agent_user(db_session: AsyncSession, test_tenant: Tenant) -> User: user = User( tenant_id=test_tenant.id, email="agent@test.com", first_name="Agent", last_name="User", password_hash=SecurityUtils.hash_password("AgentPass123!"), role=UserRole.AGENT, is_active=True, email_verified=True, ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user @pytest.fixture async def test_client_user(db_session: AsyncSession, test_tenant: Tenant) -> User: user = User( tenant_id=test_tenant.id, email="client@test.com", first_name="Client", last_name="User", password_hash=SecurityUtils.hash_password("ClientPass123!"), role=UserRole.CLIENT_USER, is_active=True, email_verified=True, ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user @pytest.fixture async def test_system(db_session: AsyncSession, test_tenant: Tenant) -> System: system = System( name="Test System", description="Test system description", tenant_id=test_tenant.id, is_active=True, ) db_session.add(system) await db_session.commit() await db_session.refresh(system) return system @pytest.fixture async def test_category(db_session: AsyncSession, test_tenant: Tenant) -> Category: category = Category( name="Test Category", description="Test category description", tenant_id=test_tenant.id, is_active=True, sla_response_hours=24, sla_resolution_hours=72, ) db_session.add(category) await db_session.commit() await db_session.refresh(category) return category @pytest.fixture async def admin_token(client: AsyncClient, test_admin_user: User, test_tenant: Tenant) -> str: response = await client.post( "/v1/auth/login", json={ "email": test_admin_user.email, "password": "AdminPass123!", "tenant_slug": test_tenant.slug, }, ) assert response.status_code == 200 return response.json()["access_token"] @pytest.fixture async def agent_token(client: AsyncClient, test_agent_user: User, test_tenant: Tenant) -> str: response = await client.post( "/v1/auth/login", json={ "email": test_agent_user.email, "password": "AgentPass123!", "tenant_slug": test_tenant.slug, }, ) assert response.status_code == 200 return response.json()["access_token"] @pytest.fixture async def client_token(client: AsyncClient, test_client_user: User, test_tenant: Tenant) -> str: response = await client.post( "/v1/auth/login", json={ "email": test_client_user.email, "password": "ClientPass123!", "tenant_slug": test_tenant.slug, }, ) assert response.status_code == 200 return response.json()["access_token"] @pytest.fixture def auth_headers_admin(admin_token: str) -> dict: return {"Authorization": f"Bearer {admin_token}"} @pytest.fixture def auth_headers_agent(agent_token: str) -> dict: return {"Authorization": f"Bearer {agent_token}"} @pytest.fixture def auth_headers_client(client_token: str) -> dict: return {"Authorization": f"Bearer {client_token}"}