|
|
""" |
|
|
Test configuration and fixtures for the Cidadão.AI Backend. |
|
|
Provides comprehensive test setup with database, Redis, and API client fixtures. |
|
|
""" |
|
|
|
|
|
import pytest |
|
|
import asyncio |
|
|
import os |
|
|
from typing import AsyncGenerator, Generator |
|
|
from httpx import AsyncClient |
|
|
from testcontainers.postgres import PostgresContainer |
|
|
from testcontainers.redis import RedisContainer |
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession |
|
|
from unittest.mock import AsyncMock, patch, Mock |
|
|
|
|
|
|
|
|
os.environ["ENVIRONMENT"] = "testing" |
|
|
os.environ["TESTING"] = "true" |
|
|
|
|
|
from src.api.app import create_app |
|
|
from src.core.database import get_db_session |
|
|
from src.core.config import Settings, get_settings |
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session") |
|
|
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: |
|
|
"""Create event loop for async tests.""" |
|
|
policy = asyncio.get_event_loop_policy() |
|
|
loop = policy.new_event_loop() |
|
|
yield loop |
|
|
loop.close() |
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session") |
|
|
async def test_database() -> AsyncGenerator[str, None]: |
|
|
"""Integration test database using testcontainers.""" |
|
|
with PostgresContainer("postgres:15-alpine") as postgres: |
|
|
database_url = postgres.get_connection_url().replace( |
|
|
"postgresql://", "postgresql+asyncpg://" |
|
|
) |
|
|
|
|
|
|
|
|
engine = create_async_engine(database_url) |
|
|
|
|
|
|
|
|
from src.core.database import Base |
|
|
async with engine.begin() as conn: |
|
|
await conn.run_sync(Base.metadata.create_all) |
|
|
|
|
|
yield database_url |
|
|
|
|
|
|
|
|
async with engine.begin() as conn: |
|
|
await conn.run_sync(Base.metadata.drop_all) |
|
|
|
|
|
await engine.dispose() |
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session") |
|
|
async def test_redis() -> AsyncGenerator[str, None]: |
|
|
"""Test Redis instance using testcontainers.""" |
|
|
with RedisContainer("redis:7-alpine") as redis_container: |
|
|
redis_url = redis_container.get_connection_url() |
|
|
yield redis_url |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
async def db_session(test_database: str) -> AsyncGenerator[AsyncSession, None]: |
|
|
"""Database session for individual tests.""" |
|
|
engine = create_async_engine(test_database) |
|
|
|
|
|
async with AsyncSession(engine) as session: |
|
|
try: |
|
|
yield session |
|
|
await session.rollback() |
|
|
finally: |
|
|
await session.close() |
|
|
|
|
|
await engine.dispose() |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
async def test_settings(test_database: str, test_redis: str) -> Settings: |
|
|
"""Test application settings.""" |
|
|
return Settings( |
|
|
database_url=test_database, |
|
|
redis_url=test_redis, |
|
|
testing=True, |
|
|
secret_key="test-secret-key-do-not-use-in-production", |
|
|
transparency_api_key="test-api-key", |
|
|
environment="testing" |
|
|
) |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
async def app(test_settings: Settings): |
|
|
"""FastAPI application for testing.""" |
|
|
app = create_app(test_settings) |
|
|
return app |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
async def client(app, db_session: AsyncSession, test_settings: Settings) -> AsyncGenerator[AsyncClient, None]: |
|
|
"""Test client with database session override.""" |
|
|
|
|
|
async def get_test_db(): |
|
|
yield db_session |
|
|
|
|
|
app.dependency_overrides[get_db_session] = get_test_db |
|
|
app.dependency_overrides[get_settings] = lambda: test_settings |
|
|
|
|
|
async with AsyncClient(app=app, base_url="http://testserver") as client: |
|
|
yield client |
|
|
|
|
|
|
|
|
app.dependency_overrides.clear() |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
async def authenticated_client(client: AsyncClient) -> AsyncGenerator[AsyncClient, None]: |
|
|
"""Authenticated test client with JWT token.""" |
|
|
|
|
|
test_user_data = { |
|
|
"email": "test@example.com", |
|
|
"password": "testpassword123" |
|
|
} |
|
|
|
|
|
|
|
|
await client.post("/auth/register", json=test_user_data) |
|
|
|
|
|
|
|
|
response = await client.post("/auth/login", data={ |
|
|
"username": test_user_data["email"], |
|
|
"password": test_user_data["password"] |
|
|
}) |
|
|
|
|
|
token_data = response.json() |
|
|
access_token = token_data["access_token"] |
|
|
|
|
|
|
|
|
client.headers.update({"Authorization": f"Bearer {access_token}"}) |
|
|
|
|
|
yield client |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def mock_transparency_api(): |
|
|
"""Mock for transparency API calls.""" |
|
|
with patch('src.services.transparency_service.TransparencyService') as mock: |
|
|
|
|
|
mock.return_value.get_contracts.return_value = { |
|
|
"data": [ |
|
|
{ |
|
|
"id": "123", |
|
|
"objeto": "Test contract", |
|
|
"valor": 100000.00, |
|
|
"dataInicioVigencia": "2024-01-01", |
|
|
"dataFimVigencia": "2024-12-31", |
|
|
"fornecedor": {"nome": "Test Supplier"} |
|
|
} |
|
|
], |
|
|
"total": 1 |
|
|
} |
|
|
|
|
|
mock.return_value.get_expenses.return_value = { |
|
|
"data": [ |
|
|
{ |
|
|
"id": "456", |
|
|
"orgaoSuperior": {"nome": "Test Ministry"}, |
|
|
"valor": 50000.00, |
|
|
"dataCompetencia": "2024-01-01", |
|
|
"modalidadeAplicacao": {"nome": "Direct Application"} |
|
|
} |
|
|
], |
|
|
"total": 1 |
|
|
} |
|
|
|
|
|
yield mock |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def mock_ai_service(): |
|
|
"""Mock for AI service calls.""" |
|
|
with patch('src.services.ai_service.AIService') as mock: |
|
|
|
|
|
mock.return_value.classify_text.return_value = { |
|
|
"label": "corruption", |
|
|
"confidence": 0.85, |
|
|
"explanation": "High probability of corruption indicators" |
|
|
} |
|
|
|
|
|
mock.return_value.analyze_anomalies.return_value = { |
|
|
"anomalies": [ |
|
|
{ |
|
|
"type": "price_anomaly", |
|
|
"severity": "high", |
|
|
"description": "Price 300% above market average" |
|
|
} |
|
|
], |
|
|
"risk_score": 0.78 |
|
|
} |
|
|
|
|
|
yield mock |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def mock_agent_system(): |
|
|
"""Mock for agent system.""" |
|
|
with patch('src.agents.abaporu.MasterAgent') as mock: |
|
|
|
|
|
async def mock_process_task(task): |
|
|
return { |
|
|
"task_id": task.get("id", "test-task"), |
|
|
"status": "completed", |
|
|
"result": { |
|
|
"analysis": "Test analysis result", |
|
|
"recommendations": ["Test recommendation 1", "Test recommendation 2"], |
|
|
"confidence": 0.9 |
|
|
}, |
|
|
"agents_used": ["investigator", "analyst", "reporter"], |
|
|
"processing_time": 2.5 |
|
|
} |
|
|
|
|
|
mock.return_value.process_task = mock_process_task |
|
|
yield mock |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def sample_analysis_data(): |
|
|
"""Sample data for analysis tests.""" |
|
|
return { |
|
|
"text": "Contrato de fornecimento de equipamentos de informática no valor de R$ 1.000.000,00", |
|
|
"type": "analyze", |
|
|
"options": { |
|
|
"includeMetrics": True, |
|
|
"includeVisualization": False, |
|
|
"language": "pt" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def sample_contract_data(): |
|
|
"""Sample contract data for tests.""" |
|
|
return { |
|
|
"numero": "123456/2024", |
|
|
"objeto": "Fornecimento de equipamentos de informática", |
|
|
"valor": 1000000.00, |
|
|
"dataAssinatura": "2024-01-15", |
|
|
"dataInicioVigencia": "2024-02-01", |
|
|
"dataFimVigencia": "2025-01-31", |
|
|
"fornecedor": { |
|
|
"cnpj": "12.345.678/0001-90", |
|
|
"nome": "Tech Solutions LTDA", |
|
|
"endereco": "Rua das Tecnologias, 123" |
|
|
}, |
|
|
"orgao": { |
|
|
"codigo": "26000", |
|
|
"nome": "Ministério da Educação", |
|
|
"sigla": "MEC" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def sample_expense_data(): |
|
|
"""Sample expense data for tests.""" |
|
|
return { |
|
|
"codigo": "789012", |
|
|
"valor": 50000.00, |
|
|
"dataCompetencia": "2024-01-01", |
|
|
"orgaoSuperior": { |
|
|
"codigo": "20000", |
|
|
"nome": "Presidência da República", |
|
|
"sigla": "PR" |
|
|
}, |
|
|
"funcao": { |
|
|
"codigo": "04", |
|
|
"nome": "Administração" |
|
|
}, |
|
|
"subfuncao": { |
|
|
"codigo": "122", |
|
|
"nome": "Administração Geral" |
|
|
}, |
|
|
"modalidadeAplicacao": { |
|
|
"codigo": "90", |
|
|
"nome": "Aplicação Direta" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
pytest.mark.unit = pytest.mark.unit |
|
|
pytest.mark.integration = pytest.mark.integration |
|
|
pytest.mark.e2e = pytest.mark.e2e |
|
|
pytest.mark.slow = pytest.mark.slow |
|
|
pytest.mark.security = pytest.mark.security |
|
|
pytest.mark.performance = pytest.mark.performance |
|
|
|
|
|
|
|
|
|
|
|
def pytest_configure(config): |
|
|
"""Configure pytest with custom markers.""" |
|
|
config.addinivalue_line("markers", "unit: Unit tests") |
|
|
config.addinivalue_line("markers", "integration: Integration tests") |
|
|
config.addinivalue_line("markers", "e2e: End-to-end tests") |
|
|
config.addinivalue_line("markers", "slow: Slow running tests") |
|
|
config.addinivalue_line("markers", "security: Security-related tests") |
|
|
config.addinivalue_line("markers", "performance: Performance tests") |
|
|
|
|
|
|
|
|
def pytest_collection_modifyitems(config, items): |
|
|
"""Modify test collection to add markers automatically.""" |
|
|
for item in items: |
|
|
|
|
|
if not any(marker.name in ["integration", "e2e", "slow", "security", "performance"] |
|
|
for marker in item.iter_markers()): |
|
|
item.add_marker(pytest.mark.unit) |
|
|
|
|
|
|
|
|
if any(keyword in item.name.lower() for keyword in ["database", "redis", "ai", "agent"]): |
|
|
item.add_marker(pytest.mark.slow) |