""" 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