- Extraccion de helpers en backend: audit_helpers.py, helpers.py - Modularizacion de schemas en archivos individuales por dominio - Reduccion de audit.py en 953 lineas (74% del archivo) - Reduccion de tickets.py en 655 lineas (60% del archivo) - Expansion de auth.py con recuperacion de contrasenia y tokens - Nuevos modulos: core/email.py, core/cache.py - Reorganizacion de scripts a backend/scripts/ - Frontend: refactorizacion de audit page con array-driven components - Frontend: correccion de 11 errores ortograficos en tickets page - Frontend: proxy Docker corregido en vite.config.js - Frontend: nuevas rutas forgot-password, reset-password, organization, profile - Nuevas utilidades TS: colorUtils.ts, dateFormats.ts - 5 nuevos archivos de tests unitarios en backend/tests/unit/ - Eliminacion de 3 scripts temporales de prueba - Documentacion tecnica: CAMBIOS_v1.10.0.md, OPTIMIZACIONES_RENDIMIENTO.md
192 lines
6.1 KiB
Python
192 lines
6.1 KiB
Python
"""
|
|
Unit Tests - Audit Service - ServiceManagerWeb
|
|
|
|
Tests para app.services.audit_service usando mocks de BD.
|
|
No requieren base de datos real ni red.
|
|
"""
|
|
|
|
import pytest
|
|
import uuid
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
|
|
class TestAuditServiceLog:
|
|
"""Tests para AuditService.log()."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_creates_audit_entry(self):
|
|
"""AuditService.log() debe crear un registro en la BD."""
|
|
from app.services.audit_service import AuditService
|
|
|
|
mock_db = AsyncMock()
|
|
mock_db.add = MagicMock()
|
|
mock_db.commit = AsyncMock()
|
|
mock_db.refresh = AsyncMock()
|
|
|
|
tenant_id = uuid.uuid4()
|
|
user_id = uuid.uuid4()
|
|
resource_id = uuid.uuid4()
|
|
|
|
result = await AuditService.log(
|
|
db=mock_db,
|
|
tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
action="ticket.create",
|
|
resource_type="ticket",
|
|
resource_id=resource_id,
|
|
new_values={"subject": "Test ticket", "status": "NEW"},
|
|
)
|
|
|
|
# Se debe haber llamado a db.add con el AuditLog
|
|
mock_db.add.assert_called_once()
|
|
# El resultado debe ser un AuditLog
|
|
assert result is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_without_user_id(self):
|
|
"""AuditService.log() funciona sin user_id (acciones del sistema)."""
|
|
from app.services.audit_service import AuditService
|
|
|
|
mock_db = AsyncMock()
|
|
mock_db.add = MagicMock()
|
|
mock_db.commit = AsyncMock()
|
|
mock_db.refresh = AsyncMock()
|
|
|
|
result = await AuditService.log(
|
|
db=mock_db,
|
|
tenant_id=uuid.uuid4(),
|
|
action="system.startup",
|
|
resource_type="system",
|
|
)
|
|
|
|
mock_db.add.assert_called_once()
|
|
assert result is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_with_old_and_new_values(self):
|
|
"""AuditService.log() acepta old_values y new_values para auditoría de cambios."""
|
|
from app.services.audit_service import AuditService
|
|
|
|
mock_db = AsyncMock()
|
|
mock_db.add = MagicMock()
|
|
mock_db.commit = AsyncMock()
|
|
mock_db.refresh = AsyncMock()
|
|
|
|
await AuditService.log(
|
|
db=mock_db,
|
|
tenant_id=uuid.uuid4(),
|
|
user_id=uuid.uuid4(),
|
|
action="ticket.update",
|
|
resource_type="ticket",
|
|
resource_id=uuid.uuid4(),
|
|
old_values={"status": "NEW", "priority": "LOW"},
|
|
new_values={"status": "IN_PROGRESS", "priority": "HIGH"},
|
|
)
|
|
|
|
mock_db.add.assert_called_once()
|
|
# Verificar que el AuditLog tiene old_values y new_values
|
|
audit_log = mock_db.add.call_args[0][0]
|
|
assert audit_log.old_values == {"status": "NEW", "priority": "LOW"}
|
|
assert audit_log.new_values == {"status": "IN_PROGRESS", "priority": "HIGH"}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_action_stored_correctly(self):
|
|
"""AuditService.log() almacena la acción correctamente."""
|
|
from app.services.audit_service import AuditService
|
|
|
|
mock_db = AsyncMock()
|
|
mock_db.add = MagicMock()
|
|
mock_db.commit = AsyncMock()
|
|
mock_db.refresh = AsyncMock()
|
|
|
|
await AuditService.log(
|
|
db=mock_db,
|
|
tenant_id=uuid.uuid4(),
|
|
action="user.login",
|
|
resource_type="user",
|
|
)
|
|
|
|
audit_log = mock_db.add.call_args[0][0]
|
|
assert audit_log.action == "user.login"
|
|
assert audit_log.resource_type == "user"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_tenant_id_stored_correctly(self):
|
|
"""AuditService.log() almacena el tenant_id correctamente."""
|
|
from app.services.audit_service import AuditService
|
|
|
|
mock_db = AsyncMock()
|
|
mock_db.add = MagicMock()
|
|
mock_db.commit = AsyncMock()
|
|
mock_db.refresh = AsyncMock()
|
|
|
|
tenant_id = uuid.uuid4()
|
|
|
|
await AuditService.log(
|
|
db=mock_db,
|
|
tenant_id=tenant_id,
|
|
action="ticket.delete",
|
|
resource_type="ticket",
|
|
)
|
|
|
|
audit_log = mock_db.add.call_args[0][0]
|
|
assert audit_log.tenant_id == tenant_id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_with_request_extracts_ip(self):
|
|
"""AuditService.log() extrae información del request si se provee."""
|
|
from app.services.audit_service import AuditService
|
|
|
|
mock_db = AsyncMock()
|
|
mock_db.add = MagicMock()
|
|
mock_db.commit = AsyncMock()
|
|
mock_db.refresh = AsyncMock()
|
|
|
|
mock_request = MagicMock()
|
|
mock_request.client.host = "192.168.1.100"
|
|
mock_request.headers = {"user-agent": "TestBrowser/1.0"}
|
|
mock_request.state.correlation_id = "test-correlation-id"
|
|
|
|
await AuditService.log(
|
|
db=mock_db,
|
|
tenant_id=uuid.uuid4(),
|
|
action="ticket.view",
|
|
resource_type="ticket",
|
|
request=mock_request,
|
|
)
|
|
|
|
mock_db.add.assert_called_once()
|
|
|
|
|
|
class TestAuditServiceMetadata:
|
|
"""Tests para metadata adicional en registros de auditoría."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_with_custom_metadata(self):
|
|
"""AuditService.log() almacena metadata personalizada en extra_metadata.
|
|
|
|
Nota: El campo Python es 'extra_metadata' (no 'metadata') porque
|
|
SQLAlchemy reserva el atributo 'metadata' para MetaData de la tabla.
|
|
La columna en BD sí se llama 'metadata'.
|
|
"""
|
|
from app.services.audit_service import AuditService
|
|
|
|
mock_db = AsyncMock()
|
|
mock_db.add = MagicMock()
|
|
mock_db.commit = AsyncMock()
|
|
mock_db.refresh = AsyncMock()
|
|
|
|
metadata = {"source": "api", "version": "1.9.0", "client_ip": "10.0.0.1"}
|
|
|
|
await AuditService.log(
|
|
db=mock_db,
|
|
tenant_id=uuid.uuid4(),
|
|
action="tenant.update",
|
|
resource_type="tenant",
|
|
metadata=metadata,
|
|
)
|
|
|
|
audit_log = mock_db.add.call_args[0][0]
|
|
# El atributo Python es extra_metadata (columna BD: metadata)
|
|
assert audit_log.extra_metadata == metadata
|