Ticket — Casos de prueba de integración
Documenta los tests de integración del servicio de tickets, en src/app/modules/ticket/services/:
ticket.service.integration.spec.ts
ticket.service.assignment-engine.integration.spec.ts
ticket.service.connection-messages.integration.spec.ts
ticket.service.transfer-greeting.integration.spec.ts
Todos usan MongoMemoryServer (MongoDB en memoria) con modelos de Mongoose reales; solo se mockean el broadcast de sockets y servicios externos. No requieren .env con base de datos real.
Resumen: 4 specs, 35 suites (describe) y 108 tests (49 + 43 + 7 + 9).
ticket.service.integration.spec.ts
Suite TicketService — integration (mongodb-memory-server). Ejecuta getTicketsList, reactivateDueTickets y deleteTicket contra una instancia real de MongoDB en memoria (MongoMemoryServer) con pipelines de agregación reales; no usa valores mockeados para la base de datos.
Datos de prueba: dos tenants (A y B), contactos alice (con label VIP, offline, bloqueo falso) y bob (bloqueado), un agente agent1 y otro agent2 (el que bloquea), y tickets A1 (open, asignado, con 2 mensajes no leídos), A2 (closed, último mensaje de agente tipo media leído), A3 (inProgress, bloqueado por agent2) y B1 (tenant B). SocketBroadcastService y los demás servicios externos están mockeados.
tenant isolation
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets that belong to TENANT_A | Listado del tenant A. | Contiene A1 y A2 y no contiene B1. | SocketBroadcastService |
should return only tickets that belong to TENANT_B | Listado del tenant B. | Contiene B1 y no contiene A1 ni A2. | SocketBroadcastService |
should return empty array for an unknown tenant | Tenant inexistente. | results vacío. | SocketBroadcastService |
last message population
| Test | Escenario | Resultado esperado | Mocks |
|---|
should populate lastMessage as a contact message object | Ticket A1 con último mensaje del contacto. | lastMessage con content: 'Hola necesito ayuda', sendType: 'contact', type: 'chat'. | ninguno |
leaves a media preview empty so the list can label it by type | Ticket A2 con último mensaje de tipo media. | lastMessage.content es '' y type es 'media'. | ninguno |
exposes who sent the last message | Ticket A2. | lastMessage.senderId es el agente A. | ninguno |
should populate lastMessage as an agent message object | Ticket A2. | lastMessage con content: '', sendType: 'agent', type: 'media'. | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
should populate contact info | Ticket A1. | contact.username es alice. | ninguno |
should populate contact labels with full label documents | Contacto con un label. | 1 label con name: 'VIP' y color: '#ff0000'. | ninguno |
should return empty labels array when contact has no labels | Ticket A2 (contacto sin labels). | contact.labels vacío. | ninguno |
should populate presence fields (isOnline, lastSeen, lastDisconnectReason) | Ticket A1. | isOnline false, lastSeen igual a 2024-01-01T10:00:00.000Z, lastDisconnectReason network-loss. | ninguno |
unreadCount
| Test | Escenario | Resultado esperado | Mocks |
|---|
should count unread messages from contact | A1 con 2 mensajes del contacto sin leer. | unreadCount = 2. | ninguno |
should be 0 when all messages are read | A2 con mensaje leído. | unreadCount = 0. | ninguno |
assignedUser population
| Test | Escenario | Resultado esperado | Mocks |
|---|
should populate assigned agent info | A1 asignado a agent1. | assignedUser no nulo con username y email del agente. | ninguno |
should include the assigned agent avatarUrl | A1. | assignedUser.avatarUrl igual a la URL del fixture. | ninguno |
should be null when no agent is assigned | A2 sin agente. | assignedUser es null. | ninguno |
filter: status
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only open tickets | Filtro status: 'open'. | Todos con status open; A2 no aparece. | ninguno |
should return only closed tickets | Filtro status: 'closed'. | Todos con status closed; A1 no aparece. | ninguno |
filter: assignedUserId
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets assigned to the given agent | Filtro por el agente A. | Un solo resultado: A1. | ninguno |
should return empty when no tickets match the agent | Agente inexistente. | Sin resultados. | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets for the given contact | Filtro por el contacto A1. | Un solo resultado: A1. | ninguno |
filter: createdFrom / createdTo
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return tickets created within the date range | Rango 2020-01-01 a 2099-12-31. | Al menos 2 resultados. | ninguno |
should return empty when range is in the past | Rango en el año 2000. | Sin resultados. | ninguno |
filter: lastMessageType
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets whose last message is type chat | lastMessageType: 'chat'. | Incluye A1 y excluye A2. | ninguno |
should return only tickets whose last message is type media | lastMessageType: 'media'. | Un solo resultado: A2. | ninguno |
filter: lastMessageSendType
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets where last message was sent by contact | lastMessageSendType: 'contact'. | Incluye A1 y excluye A2. | ninguno |
should return only tickets where last message was sent by agent | lastMessageSendType: 'agent'. | Incluye A2 y excluye A1. | ninguno |
filter: lastMessageAck
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets whose last message is still unread | lastMessageAck: 'sent'. | Incluye A1 y excluye A2. | ninguno |
should return only tickets whose last message is read | lastMessageAck: 'read'. | Incluye A2 y excluye A1. | ninguno |
filter: labelId
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets whose contact has the given label | Filtro por el label VIP. | Un solo resultado: A1. | ninguno |
should return empty when no contact has the given label | Label inexistente. | Sin resultados. | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets with blocked contacts | contactIsBlocked: true. | Todos los contactos tienen isBlocked === true. | ninguno |
should return only tickets with non-blocked contacts | contactIsBlocked: false. | Todos los contactos tienen isBlocked === false. | ninguno |
filter: hasUnread
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only tickets with at least one unread message | hasUnread: true. | Todos con unreadCount > 0; A2 no aparece. | ninguno |
should return only tickets with no unread messages | hasUnread: false. | Todos con unreadCount === 0; A1 no aparece. | ninguno |
lockedByUser population
| Test | Escenario | Resultado esperado | Mocks |
|---|
should populate lockedByUser with the locking agent info for a locked ticket | Ticket A3 bloqueado por agent2. | lockedBy es el agente que bloquea y lockedByUser.username es agent2. | ninguno |
should be null for an unlocked ticket | Ticket A1 sin bloqueo. | lockedBy nulo y lockedByUser null. | ninguno |
filter: lockedOnly
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return only locked tickets | lockedOnly: true. | Un solo resultado: A3. | ninguno |
should not filter by lock state when lockedOnly is omitted | Sin el filtro. | Aparecen tanto A3 como A1. | ninguno |
sort order
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return tickets sorted by last message date descending | Listado sin filtros. | Las fechas de último mensaje están en orden descendente. | ninguno |
combined filters
| Test | Escenario | Resultado esperado | Mocks |
|---|
should apply status + hasUnread simultaneously | status: 'open' y hasUnread: true. | Todos open con unreadCount > 0. | ninguno |
should apply labelId + lastMessageSendType simultaneously | Label VIP y último mensaje de contacto. | Un solo resultado: A1. | ninguno |
reactivateDueTickets
| Test | Escenario | Resultado esperado | Mocks |
|---|
reactivates a due postponed ticket, unsets the date and broadcasts once | Ticket postponed con postponedUntil vencida hace un minuto. | Se devuelve con sendType open y sin postponedUntil; broadcastTicketReactivated(tenantId, { ticketId }) una sola vez. | broadcastTicketReactivated |
does not touch a postponed ticket whose date has not arrived yet | postponedUntil en el futuro. | No aparece en el resultado y no hay broadcast. | broadcastTicketReactivated |
CAS: only broadcasts once even if two concurrent runs race on the same due ticket | Dos ejecuciones concurrentes de reactivateDueTickets. | Solo un broadcast para ese ticket. | broadcastTicketReactivated |
deleteTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
deletes 0 orphaned messages: all messages for the deleted ticket are gone | Ticket con 2 mensajes y se elimina. | Quedan 0 mensajes del ticket. | SocketBroadcastService |
deletes the ticket document itself | Se elimina un ticket. | findById devuelve null. | SocketBroadcastService |
enforces tenant scoping: cascade never touches another tenant’s messages for a different ticket | Dos tickets del mismo tenant; se elimina uno. | El mensaje del otro ticket sobrevive y los del eliminado quedan en 0. | SocketBroadcastService |
rejects a delete request for a ticket owned by a different tenant, leaving it untouched | Se elimina con el tenant B un ticket del tenant A. | Rechaza con ticket.notFound y el ticket sigue existiendo. | SocketBroadcastService |
ticket.service.assignment-engine.integration.spec.ts
Suite TicketService — assignment engine (integration, SPEC 06). Cubre la matriz completa del motor de asignación: colas por departamento, enrutamiento por conexión (unión de colas y departmentId persistido), transferencias sin usuario, redistribución y reapertura dentro de la ventana de tolerancia. Usa MongoDB en memoria con modelos y repositorios reales.
Mocks: SocketBroadcastService (broadcastTicketAssigned, broadcastSystemMessage, broadcastDepartmentGreeting, broadcastTicketStatusChanged, broadcastTicketReactivated, getConnectedAgentIds, isUserOnline) y UserRepository (findByMongoId, filterAssignableByIds) para controlar qué agentes están conectados y asignables. TenantSettingsService con horario deshabilitado.
Fixtures: departamentos con flags autoAssignment, assignOfflineUsers y redistributeChats; membresías en cola (enqueue) para los agentes AGENT_1, AGENT_2, AGENT_3; conexiones web con departmentIds.
department queue (autoAssignTicket)
| Test | Escenario | Resultado esperado | Mocks |
|---|
ticket without department is never auto-assigned, even with agents online | Ticket sin departamento y un agente conectado. | Devuelve null y el ticket sigue sin UserId. | ninguno |
autoAssignment: false → does not assign | Departamento con autoAssignment: false y agente en cola. | Devuelve null; no se asigna. | ninguno |
assigns least-loaded from the department queue | Dos agentes en cola; el agente 1 tiene 2 tickets activos. | Elige al agente 2 (menos carga). | ninguno |
assignOfflineUsers: false + offline-only queue → does not assign | Agente en cola pero no conectado y assignOfflineUsers: false. | Devuelve null. | ninguno |
assignOfflineUsers: true → assigns a disconnected agent | Igual, con assignOfflineUsers: true. | Asigna al agente desconectado. | ninguno |
empty queue → no assignment and NO global fallback | Departamento sin cola y un agente conectado del tenant ajeno a la cola. | Devuelve null; no hay fallback global. | ninguno |
redistribution (redistributeTicket)
| Test | Escenario | Resultado esperado | Mocks |
|---|
reassigns when the agent is disconnected and redistributeChats is on | Ticket asignado al agente 1 (desconectado); solo el agente 2 conectado; redistributeChats: true. | Reasigna al agente 2; un broadcastTicketAssigned y un broadcastSystemMessage. | broadcastTicketAssigned, broadcastSystemMessage |
redistributeChats: false → keeps the agent | redistributeChats: false. | Devuelve null; el ticket conserva al agente 1. | ninguno |
connected agent → no redistribution | El agente asignado sigue conectado. | Devuelve null. | ninguno |
empty pool (everyone disconnected) → keeps the agent, no loop | Nadie conectado. | Devuelve null y mantiene al agente (sin bucle). | ninguno |
contact message entry point (assignOnContactMessage)
| Test | Escenario | Resultado esperado | Mocks |
|---|
unassigned department ticket → assigns only among online users that are active | Dos agentes conectados; filterAssignableByIds deja solo al agente 2. | Consulta getConnectedAgentIds(tenant) y filterAssignableByIds; asigna al agente 2 y lo persiste. | getConnectedAgentIds, filterAssignableByIds |
unassigned department ticket with no active user online → stays unassigned | filterAssignableByIds devuelve []. | Devuelve null y el ticket queda sin asignar. | filterAssignableByIds |
unassigned ticket without department → stays unassigned even with users online | Ticket sin departamento. | Devuelve null. | getConnectedAgentIds |
assignOfflineUsers: true never assigns an inactive queue member | assignOfflineUsers: true, agente 1 inactivo y agente 2 con 3 tickets. | Asigna al agente 2 (nunca al miembro inactivo). | filterAssignableByIds |
assigned ticket whose agent went offline → redistributes when the department allows it | Agente asignado offline, redistributeChats: true, agente 2 conectado. | Consulta isUserOnline, redistribuye y asigna al agente 2. | isUserOnline, getConnectedAgentIds |
assigned ticket whose agent is still online but not assignable (admin or paused) → keeps the agent | Agente asignado aún en línea. | Devuelve null, no consulta getConnectedAgentIds y conserva al agente 1. | isUserOnline |
assigned ticket whose agent went offline with redistributeChats off → keeps the agent | Agente offline con redistributeChats: false. | Devuelve null y conserva al agente. | isUserOnline |
no-user transfer
| Test | Escenario | Resultado esperado | Mocks |
|---|
department-only transfer queues the ticket and the engine assigns immediately | Admin transfiere solo a un departamento con el agente 2 conectado en cola. | El ticket recibe el departmentId y queda asignado al agente 2; dos mensajes de sistema (transferencia y asignación). | getConnectedAgentIds, broadcastSystemMessage |
department-only transfer with empty queue stays queued | Departamento sin cola ni agentes conectados. | El ticket queda sin UserId pero con el nuevo departmentId. | getConnectedAgentIds |
a non-admin agent can claim a queued (ownerless) ticket | Ticket en cola (sin dueño) y un agente no admin que se lo transfiere a sí mismo. | El ticket queda asignado al agente 2. | ninguno |
an owned ticket still rejects a non-owner, non-admin agent | Ticket con dueño y un agente que no es dueño ni admin. | Rechaza con ForbiddenException. | ninguno |
transfer without userId nor departmentId → 400 | Transferencia sin usuario ni departamento. | Rechaza con BadRequestException. | ninguno |
department-only transfer still sends the department greeting (SPEC 02) | Transferencia a departamento con saludo Hola {{username}} y sin agentes conectados. | broadcastDepartmentGreeting una vez. | broadcastDepartmentGreeting |
connection routing
| Test | Escenario | Resultado esperado | Mocks |
|---|
union of two enabled department queues; ticket.departmentId = chosen agent department | Conexión con departamentos Ventas y Soporte; el agente 1 tiene 2 tickets activos. | Elige al agente 2 y el ticket queda con el departamento Soporte. | ninguno |
agent in several connection departments → first by connection order | El agente 1 está en ambos departamentos. | El departmentId es el primero en el orden de la conexión (Ventas). | ninguno |
all connection departments with autoAssignment: false → unassigned, no departmentId | Único departamento con autoAssignment: false. | Devuelve null; sin UserId ni departmentId. | ninguno |
prod repro: connected agent, assignOfflineUsers off, ticket born via resolveContactTicket → departmentId persisted | Ticket nacido por resolveContactTicket y agente conectado. | created: true; asigna al agente 1 y persiste el departmentId. | ninguno |
prod repro 2: departmentIds stored as raw STRINGS still routes and persists departmentId | departmentIds guardado como strings planos (documento sucio de producción). | Enruta, asigna al agente 1 y persiste el departamento. | ninguno |
ticket born via connection inherits the department even with NO assignable agent | Cola con un agente offline y assignOfflineUsers: false. | El ticket nace con el departamento de la conexión; autoAssignTicket devuelve null y el departamento se mantiene. | ninguno |
birth department is the first declared; engine re-points it to the chosen agent department | Ventas sin cola y Soporte con el agente 2. | Nace con Ventas; tras asignar, el departamento pasa a Soporte. | ninguno |
ticket transferred outside the connection departments keeps that department | Ticket movido manualmente a un departamento ajeno a la conexión. | El pool sale del departamento "Otro" y su departmentId no se sobrescribe. | ninguno |
connection without departmentIds → stays unassigned even with agents online | Conexión con departmentIds: []. | Devuelve null y sin UserId. | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
reusa el ticket abierto cuando el contacto vuelve a escribir por la MISMA conexión | Ticket abierto de la conexión y el contacto escribe por la misma. | created: false y devuelve el mismo ticket. | ninguno |
abre un ticket nuevo, atado a la nueva conexión, cuando escribe por OTRA | Ticket abierto en la conexión 1 y el contacto escribe por la 2. | created: true, ticket distinto con connectionId de la conexión 2. | ninguno |
cierra el ticket de la conexión anterior en vez de dejar dos abiertos | Mismo escenario. | El ticket anterior queda CLOSED y se emite broadcastTicketStatusChanged. | broadcastTicketStatusChanged |
deja anotado el motivo del cierre como mensaje interno | Mismo escenario. | Mensaje interno Ticket cerrado automáticamente: el contacto escribió por otra conexión (Conexión 2) con isInternal: true. | ninguno |
no toca tickets de otras conexiones que ya estaban cerrados o resueltos | Ticket RESOLVED en otra conexión. | Sigue RESOLVED. | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
resolvedAt is stamped on resolve and cleared on manual reopen | Se resuelve y luego se llama returnTicket. | resolvedAt es Date tras resolver; se limpia y el ticket vuelve a OPEN al reabrir. | ninguno |
message within tolerance reopens the SAME ticket (no new one, reactivation emitted) | Ticket resuelto y mensaje dentro de 15 min con returnToLastAgent: true. | created: false, mismo ticket OPEN, conserva al agente, resolvedAt vacío y broadcastTicketReactivated. | broadcastTicketReactivated |
returnToLastAgent: false → reopens without agent (engine reassigns on the same message) | returnToLastAgent: false. | Reabre el mismo ticket sin agente. | ninguno |
message after the tolerance window → new ticket | resolvedAt anterior a 16 minutos con tolerancia de 15. | created: true y ticket distinto. | ninguno |
resolved ticket of ANOTHER connection is not reopened | Ticket resuelto de la conexión A y mensaje por la conexión B. | created: true y ticket distinto. | ninguno |
active ticket wins over any reopen candidate | Ticket activo en la misma conexión. | created: false y devuelve el ticket activo. | ninguno |
ticket.service.connection-messages.integration.spec.ts
Suite TicketService — connection greeting/farewell (integration). Usa MongoDB en memoria con TicketRepository, ContactRepository y ConnectionRepository reales y verifica los mensajes de bienvenida y despedida configurados en la conexión (canal web). Mensajes de sistema se buscan con sendType SYSTEM y messageType CHAT.
Mocks: SocketBroadcastService (broadcastSystemMessage, broadcastConnectionMessage, broadcastTicketStatusChanged), TenantSettingsService.getOrCreate (sendFarewellMessage: true, horario deshabilitado), OutboundRouterService.route, BusinessHoursEvaluatorService, ScheduledMessageCancellationService.
sendConnectionGreeting
| Test | Escenario | Resultado esperado | Mocks |
|---|
sends the interpolated greeting when the connection has greetingMessage | Conexión con saludo HTML que usa {{username}} y {{identifier}}; se invoca sendConnectionGreeting. | Se persiste 1 mensaje con el texto interpolado y sin HTML, sin sendUserId; broadcastConnectionMessage una vez con senderName del sistema. | broadcastConnectionMessage |
does not send anything when greetingMessage is empty | Conexión con saludo vacío. | No se crea mensaje ni se hace broadcast. | broadcastConnectionMessage |
does not send anything for a ticket without connectionId | Ticket sin conexión. | No se crea mensaje ni se hace broadcast. | broadcastConnectionMessage |
farewell on resolveTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
sends the interpolated farewell when resolving a ticket with a connection farewellMessage | Conexión con despedida Gracias {{username}}, ¡hasta pronto!; se resuelve el ticket. | El ticket queda RESOLVED; se persiste la despedida interpolada y se emite por broadcastConnectionMessage; el mensaje de sistema de resolución se emite una vez. | broadcastConnectionMessage, broadcastSystemMessage |
does not send a farewell when the connection has no farewellMessage | Conexión con saludo pero despedida vacía. | No se crean mensajes de despedida ni broadcast de conexión. | broadcastConnectionMessage |
does not send a farewell when resolving through the no-farewell route | resolveTicket(..., 'Ana', false) con despedida configurada. | No se envía despedida. | broadcastConnectionMessage |
does not send a farewell when resolving a ticket without connection | Ticket sin conexión. | No se envía despedida. | broadcastConnectionMessage |
ticket.service.transfer-greeting.integration.spec.ts
Suite TicketService — department greeting on transfer (integration) (SPEC 02). MongoDB en memoria con modelos y repositorios reales (TicketRepository, DepartmentRepository, DepartmentUserRepository, ContactRepository); solo se mockea el broadcast de sockets y UserRepository.
Mocks: SocketBroadcastService (broadcastSystemMessage, broadcastDepartmentGreeting, etc.), UserRepository.findByMongoId (agente agent1), TenantSettingsService, OutboundRouterService. El agente debe ser miembro del departamento destino (assignToDepartment: true) para que transferTicket acepte el par usuario/departamento.
TicketService — department greeting on transfer (integration)
| Test | Escenario | Resultado esperado | Mocks |
|---|
sends the interpolated greeting when transferring to a department with greetingMessage | Se transfiere un ticket a un departamento con saludo HTML {{username}} / {{identifier}}. | Se persiste 1 mensaje de sistema con el saludo interpolado, tipo CHAT y sin sendUserId; broadcastDepartmentGreeting una vez con departmentName: 'Soporte'; greetedDepartmentId queda con el departamento. | broadcastDepartmentGreeting |
does not resend the greeting when re-transferring to the same department | Se transfiere dos veces al mismo departamento. | Solo 1 mensaje de saludo y 1 broadcast. | broadcastDepartmentGreeting |
sends a new greeting and updates the guard when transferring to a different department | Transferencia a Ventas y luego a Cobros. | Dos saludos en orden (Saludo A wolf77, Saludo B wolf77), 2 broadcasts y greetedDepartmentId apunta al segundo departamento. | broadcastDepartmentGreeting |
does not send anything when the department has no greetingMessage | Departamento con saludo vacío. | Sin mensajes ni broadcast; greetedDepartmentId es null. | broadcastDepartmentGreeting |
does not send anything when transferring without departmentId | Transferencia a un agente sin departmentId. | Sin mensajes ni broadcast; greetedDepartmentId es null. | broadcastDepartmentGreeting |
still emits the system transfer message alongside the greeting | Transferencia con saludo configurado. | broadcastSystemMessage una vez y existe 1 mensaje de sistema que contiene Ticket asignado a. | broadcastSystemMessage |
membership guard (agent must belong to the target department)
| Test | Escenario | Resultado esperado | Mocks |
|---|
rejects the transfer when the agent is not a member of the department | El agente no tiene membresía en el departamento destino. | Rechaza con BadRequestException; el ticket no cambia (UserId y departmentId vacíos) y no hay saludo. | broadcastDepartmentGreeting |
rejects when the membership exists but assignToDepartment is false | Membresía con assignToDepartment: false. | Rechaza con BadRequestException. | ninguno |
allows a department-only transfer (no agent) without membership | Transferencia solo a departamento (sin agente) y sin membresía. | Se actualiza el ticket con el nuevo departmentId. | ninguno |