- 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
614 lines
18 KiB
Python
614 lines
18 KiB
Python
"""
|
|
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
|