feat: Implementar suite completa de tests de integración v1.9.0
- Agregar 46 tests de integración (auth, multi-tenancy, tickets) - Crear estructura organizada tests/integration/ y tests/unit/ - Implementar fixtures completas para testing con BD separada - Agregar conftest_integration.py con setup async - Mover scripts PowerShell de testing a tests/scripts/ - Actualizar pytest.ini con markers y configuración - Crear run_tests.sh script ejecutable para testing - Documentación completa en README_TESTS.md - Fix: Remover opciones obsoletas de TypeScript (importsNotUsedAsValues) Tests implementados: - Authentication: 15 tests (login, refresh, permisos, seguridad) - Multi-tenancy: 13 tests (aislamiento, validaciones, seguridad B2B) - Tickets: 18 tests (CRUD, filtros, permisos por rol) - Unit: 10 tests básicos - Verificación: 8 tests de setup Base de datos de testing: servicemanager_test (separada de producción) Cobertura estimada: ~40% (desde 5%) Próximos pasos: Agregar tests de SLA, attachments, auditoría
This commit is contained in:
613
backend/tests/integration/test_tickets_integration.py
Normal file
613
backend/tests/integration/test_tickets_integration.py
Normal file
@@ -0,0 +1,613 @@
|
||||
"""
|
||||
Tickets Integration Tests - ServiceManagerWeb
|
||||
|
||||
Tests completos del CRUD de tickets y funcionalidad relacionada.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import uuid
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.ticket import Ticket, TicketStatus, TicketPriority
|
||||
from app.models.system import System
|
||||
from app.models.category import Category
|
||||
|
||||
pytest_plugins = ['tests.conftest_integration']
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
class TestTicketCreation:
|
||||
"""Tests de creación de tickets."""
|
||||
|
||||
async def test_create_ticket_success(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
test_tenant: Tenant,
|
||||
test_category: Category,
|
||||
auth_headers_client: dict
|
||||
):
|
||||
"""Test crear ticket con datos válidos."""
|
||||
|
||||
response = await client.post(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_client,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
},
|
||||
json={
|
||||
"title": "Test ticket",
|
||||
"description": "This is a test ticket description",
|
||||
"priority": "MEDIUM",
|
||||
"category_id": str(test_category.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
|
||||
assert data["title"] == "Test ticket"
|
||||
assert data["description"] == "This is a test ticket description"
|
||||
assert data["priority"] == "MEDIUM"
|
||||
assert data["status"] == "NEW"
|
||||
assert data["category_id"] == str(test_category.id)
|
||||
|
||||
async def test_create_ticket_with_all_fields(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
test_tenant: Tenant,
|
||||
test_category: Category,
|
||||
test_system: System,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test crear ticket con todos los campos opcionales."""
|
||||
|
||||
response = await client.post(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
},
|
||||
json={
|
||||
"title": "Complete ticket",
|
||||
"description": "Full ticket with all fields",
|
||||
"priority": "HIGH",
|
||||
"category_id": str(test_category.id),
|
||||
"system_id": str(test_system.id),
|
||||
"contact_email": "contact@test.com",
|
||||
"contact_phone": "+1234567890"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
|
||||
assert data["priority"] == "HIGH"
|
||||
assert data["system_id"] == str(test_system.id)
|
||||
assert data["contact_email"] == "contact@test.com"
|
||||
|
||||
async def test_create_ticket_missing_required_fields(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
test_tenant: Tenant,
|
||||
auth_headers_client: dict
|
||||
):
|
||||
"""Test crear ticket sin campos requeridos."""
|
||||
|
||||
response = await client.post(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_client,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
},
|
||||
json={
|
||||
"description": "Missing title"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
async def test_create_ticket_invalid_priority(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
test_tenant: Tenant,
|
||||
test_category: Category,
|
||||
auth_headers_client: dict
|
||||
):
|
||||
"""Test crear ticket con prioridad inválida."""
|
||||
|
||||
response = await client.post(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_client,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
},
|
||||
json={
|
||||
"title": "Test ticket",
|
||||
"description": "Description",
|
||||
"priority": "SUPER_URGENT", # Inválido
|
||||
"category_id": str(test_category.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
class TestTicketRetrieval:
|
||||
"""Tests de consulta de tickets."""
|
||||
|
||||
async def test_list_tickets_empty(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
test_tenant: Tenant,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test listar tickets cuando no hay ninguno."""
|
||||
|
||||
response = await client.get(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
tickets = response.json()
|
||||
assert isinstance(tickets, list)
|
||||
|
||||
async def test_list_tickets_with_data(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_admin_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test listar tickets cuando existen."""
|
||||
|
||||
# Crear algunos tickets
|
||||
for i in range(3):
|
||||
ticket = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title=f"Test ticket {i+1}",
|
||||
description=f"Description {i+1}",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
db_session.add(ticket)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.get(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
tickets = response.json()
|
||||
assert len(tickets) == 3
|
||||
|
||||
async def test_get_ticket_by_id(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_admin_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test obtener ticket específico por ID."""
|
||||
|
||||
ticket = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Specific ticket",
|
||||
description="Get this ticket",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.HIGH,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
db_session.add(ticket)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(ticket)
|
||||
|
||||
response = await client.get(
|
||||
f"/v1/tickets/{ticket.id}",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == str(ticket.id)
|
||||
assert data["title"] == "Specific ticket"
|
||||
|
||||
async def test_get_nonexistent_ticket(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
test_tenant: Tenant,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test obtener ticket inexistente."""
|
||||
|
||||
fake_id = str(uuid.uuid4())
|
||||
|
||||
response = await client.get(
|
||||
f"/v1/tickets/{fake_id}",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
class TestTicketUpdate:
|
||||
"""Tests de actualización de tickets."""
|
||||
|
||||
async def test_update_ticket_status(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_admin_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test actualizar status de ticket."""
|
||||
|
||||
ticket = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Ticket to update",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
db_session.add(ticket)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(ticket)
|
||||
|
||||
response = await client.patch(
|
||||
f"/v1/tickets/{ticket.id}",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
},
|
||||
json={
|
||||
"status": "IN_PROGRESS"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "IN_PROGRESS"
|
||||
|
||||
async def test_update_ticket_priority(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_admin_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test actualizar prioridad de ticket."""
|
||||
|
||||
ticket = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Ticket priority test",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.LOW,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
db_session.add(ticket)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(ticket)
|
||||
|
||||
response = await client.patch(
|
||||
f"/v1/tickets/{ticket.id}",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
},
|
||||
json={
|
||||
"priority": "URGENT"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["priority"] == "URGENT"
|
||||
|
||||
async def test_update_ticket_assignment(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_admin_user: User,
|
||||
test_agent_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test asignar ticket a un agente."""
|
||||
|
||||
ticket = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Ticket to assign",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
db_session.add(ticket)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(ticket)
|
||||
|
||||
response = await client.patch(
|
||||
f"/v1/tickets/{ticket.id}",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
},
|
||||
json={
|
||||
"assigned_to": str(test_agent_user.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["assigned_to"] == str(test_agent_user.id)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
class TestTicketFilters:
|
||||
"""Tests de filtros de tickets."""
|
||||
|
||||
async def test_filter_by_status(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_admin_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test filtrar tickets por status."""
|
||||
|
||||
# Crear tickets con diferentes status
|
||||
ticket_new = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="New ticket",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
ticket_progress = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="In progress ticket",
|
||||
description="Description",
|
||||
status=TicketStatus.IN_PROGRESS,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
db_session.add_all([ticket_new, ticket_progress])
|
||||
await db_session.commit()
|
||||
|
||||
# Filtrar por status NEW
|
||||
response = await client.get(
|
||||
"/v1/tickets/?status=NEW",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
tickets = response.json()
|
||||
assert all(t["status"] == "NEW" for t in tickets)
|
||||
|
||||
async def test_filter_by_priority(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_admin_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_admin: dict
|
||||
):
|
||||
"""Test filtrar tickets por prioridad."""
|
||||
|
||||
# Crear tickets con diferentes prioridades
|
||||
ticket_low = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Low priority",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.LOW,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
ticket_urgent = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Urgent priority",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.URGENT,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
db_session.add_all([ticket_low, ticket_urgent])
|
||||
await db_session.commit()
|
||||
|
||||
# Filtrar por URGENT
|
||||
response = await client.get(
|
||||
"/v1/tickets/?priority=URGENT",
|
||||
headers={
|
||||
**auth_headers_admin,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
tickets = response.json()
|
||||
assert all(t["priority"] == "URGENT" for t in tickets)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.db
|
||||
class TestTicketPermissions:
|
||||
"""Tests de permisos en tickets."""
|
||||
|
||||
async def test_client_can_create_ticket(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
test_tenant: Tenant,
|
||||
test_category: Category,
|
||||
auth_headers_client: dict
|
||||
):
|
||||
"""Test que cliente puede crear tickets."""
|
||||
|
||||
response = await client.post(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_client,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
},
|
||||
json={
|
||||
"title": "Client ticket",
|
||||
"description": "Created by client",
|
||||
"priority": "MEDIUM",
|
||||
"category_id": str(test_category.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
async def test_client_can_only_see_own_tickets(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_client_user: User,
|
||||
test_admin_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_client: dict
|
||||
):
|
||||
"""Test que cliente solo ve sus propios tickets."""
|
||||
|
||||
# Ticket del cliente
|
||||
ticket_own = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="My ticket",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_client_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
|
||||
# Ticket de otro usuario
|
||||
ticket_other = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Other ticket",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
|
||||
db_session.add_all([ticket_own, ticket_other])
|
||||
await db_session.commit()
|
||||
|
||||
# Cliente lista tickets
|
||||
response = await client.get(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_client,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
tickets = response.json()
|
||||
|
||||
# Solo debe ver su propio ticket
|
||||
ticket_ids = [t["id"] for t in tickets]
|
||||
assert str(ticket_own.id) in ticket_ids
|
||||
assert str(ticket_other.id) not in ticket_ids
|
||||
|
||||
async def test_agent_can_see_all_tenant_tickets(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_tenant: Tenant,
|
||||
test_agent_user: User,
|
||||
test_admin_user: User,
|
||||
test_category: Category,
|
||||
auth_headers_agent: dict
|
||||
):
|
||||
"""Test que agente ve todos los tickets del tenant."""
|
||||
|
||||
# Crear tickets de diferentes usuarios
|
||||
ticket_1 = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Ticket 1",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_agent_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
ticket_2 = Ticket(
|
||||
tenant_id=test_tenant.id,
|
||||
title="Ticket 2",
|
||||
description="Description",
|
||||
status=TicketStatus.NEW,
|
||||
priority=TicketPriority.MEDIUM,
|
||||
created_by=test_admin_user.id,
|
||||
category_id=test_category.id
|
||||
)
|
||||
|
||||
db_session.add_all([ticket_1, ticket_2])
|
||||
await db_session.commit()
|
||||
|
||||
# Agente lista tickets
|
||||
response = await client.get(
|
||||
"/v1/tickets/",
|
||||
headers={
|
||||
**auth_headers_agent,
|
||||
"X-Tenant-ID": str(test_tenant.id)
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
tickets = response.json()
|
||||
|
||||
# Debe ver ambos tickets
|
||||
assert len(tickets) >= 2
|
||||
Reference in New Issue
Block a user