Skip to main content

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​

TestEscenarioResultado esperadoMocks
should return only tickets that belong to TENANT_AListado del tenant A.Contiene A1 y A2 y no contiene B1.SocketBroadcastService
should return only tickets that belong to TENANT_BListado del tenant B.Contiene B1 y no contiene A1 ni A2.SocketBroadcastService
should return empty array for an unknown tenantTenant inexistente.results vacío.SocketBroadcastService

last message population​

TestEscenarioResultado esperadoMocks
should populate lastMessage as a contact message objectTicket 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 typeTicket A2 con último mensaje de tipo media.lastMessage.content es '' y type es 'media'.ninguno
exposes who sent the last messageTicket A2.lastMessage.senderId es el agente A.ninguno
should populate lastMessage as an agent message objectTicket A2.lastMessage con content: '', sendType: 'agent', type: 'media'.ninguno

contact population​

TestEscenarioResultado esperadoMocks
should populate contact infoTicket A1.contact.username es alice.ninguno
should populate contact labels with full label documentsContacto con un label.1 label con name: 'VIP' y color: '#ff0000'.ninguno
should return empty labels array when contact has no labelsTicket 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​

TestEscenarioResultado esperadoMocks
should count unread messages from contactA1 con 2 mensajes del contacto sin leer.unreadCount = 2.ninguno
should be 0 when all messages are readA2 con mensaje leído.unreadCount = 0.ninguno

assignedUser population​

TestEscenarioResultado esperadoMocks
should populate assigned agent infoA1 asignado a agent1.assignedUser no nulo con username y email del agente.ninguno
should include the assigned agent avatarUrlA1.assignedUser.avatarUrl igual a la URL del fixture.ninguno
should be null when no agent is assignedA2 sin agente.assignedUser es null.ninguno

filter: status​

TestEscenarioResultado esperadoMocks
should return only open ticketsFiltro status: 'open'.Todos con status open; A2 no aparece.ninguno
should return only closed ticketsFiltro status: 'closed'.Todos con status closed; A1 no aparece.ninguno

filter: assignedUserId​

TestEscenarioResultado esperadoMocks
should return only tickets assigned to the given agentFiltro por el agente A.Un solo resultado: A1.ninguno
should return empty when no tickets match the agentAgente inexistente.Sin resultados.ninguno

filter: contactId​

TestEscenarioResultado esperadoMocks
should return only tickets for the given contactFiltro por el contacto A1.Un solo resultado: A1.ninguno

filter: createdFrom / createdTo​

TestEscenarioResultado esperadoMocks
should return tickets created within the date rangeRango 2020-01-01 a 2099-12-31.Al menos 2 resultados.ninguno
should return empty when range is in the pastRango en el año 2000.Sin resultados.ninguno

filter: lastMessageType​

TestEscenarioResultado esperadoMocks
should return only tickets whose last message is type chatlastMessageType: 'chat'.Incluye A1 y excluye A2.ninguno
should return only tickets whose last message is type medialastMessageType: 'media'.Un solo resultado: A2.ninguno

filter: lastMessageSendType​

TestEscenarioResultado esperadoMocks
should return only tickets where last message was sent by contactlastMessageSendType: 'contact'.Incluye A1 y excluye A2.ninguno
should return only tickets where last message was sent by agentlastMessageSendType: 'agent'.Incluye A2 y excluye A1.ninguno

filter: lastMessageAck​

TestEscenarioResultado esperadoMocks
should return only tickets whose last message is still unreadlastMessageAck: 'sent'.Incluye A1 y excluye A2.ninguno
should return only tickets whose last message is readlastMessageAck: 'read'.Incluye A2 y excluye A1.ninguno

filter: labelId​

TestEscenarioResultado esperadoMocks
should return only tickets whose contact has the given labelFiltro por el label VIP.Un solo resultado: A1.ninguno
should return empty when no contact has the given labelLabel inexistente.Sin resultados.ninguno

filter: contactIsBlocked​

TestEscenarioResultado esperadoMocks
should return only tickets with blocked contactscontactIsBlocked: true.Todos los contactos tienen isBlocked === true.ninguno
should return only tickets with non-blocked contactscontactIsBlocked: false.Todos los contactos tienen isBlocked === false.ninguno

filter: hasUnread​

TestEscenarioResultado esperadoMocks
should return only tickets with at least one unread messagehasUnread: true.Todos con unreadCount > 0; A2 no aparece.ninguno
should return only tickets with no unread messageshasUnread: false.Todos con unreadCount === 0; A1 no aparece.ninguno

lockedByUser population​

TestEscenarioResultado esperadoMocks
should populate lockedByUser with the locking agent info for a locked ticketTicket A3 bloqueado por agent2.lockedBy es el agente que bloquea y lockedByUser.username es agent2.ninguno
should be null for an unlocked ticketTicket A1 sin bloqueo.lockedBy nulo y lockedByUser null.ninguno

filter: lockedOnly​

TestEscenarioResultado esperadoMocks
should return only locked ticketslockedOnly: true.Un solo resultado: A3.ninguno
should not filter by lock state when lockedOnly is omittedSin el filtro.Aparecen tanto A3 como A1.ninguno

sort order​

TestEscenarioResultado esperadoMocks
should return tickets sorted by last message date descendingListado sin filtros.Las fechas de último mensaje están en orden descendente.ninguno

combined filters​

TestEscenarioResultado esperadoMocks
should apply status + hasUnread simultaneouslystatus: 'open' y hasUnread: true.Todos open con unreadCount > 0.ninguno
should apply labelId + lastMessageSendType simultaneouslyLabel VIP y último mensaje de contacto.Un solo resultado: A1.ninguno

reactivateDueTickets​

TestEscenarioResultado esperadoMocks
reactivates a due postponed ticket, unsets the date and broadcasts onceTicket 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 yetpostponedUntil 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 ticketDos ejecuciones concurrentes de reactivateDueTickets.Solo un broadcast para ese ticket.broadcastTicketReactivated

deleteTicket​

TestEscenarioResultado esperadoMocks
deletes 0 orphaned messages: all messages for the deleted ticket are goneTicket con 2 mensajes y se elimina.Quedan 0 mensajes del ticket.SocketBroadcastService
deletes the ticket document itselfSe elimina un ticket.findById devuelve null.SocketBroadcastService
enforces tenant scoping: cascade never touches another tenant’s messages for a different ticketDos 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 untouchedSe 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)​

TestEscenarioResultado esperadoMocks
ticket without department is never auto-assigned, even with agents onlineTicket sin departamento y un agente conectado.Devuelve null y el ticket sigue sin UserId.ninguno
autoAssignment: false → does not assignDepartamento con autoAssignment: false y agente en cola.Devuelve null; no se asigna.ninguno
assigns least-loaded from the department queueDos agentes en cola; el agente 1 tiene 2 tickets activos.Elige al agente 2 (menos carga).ninguno
assignOfflineUsers: false + offline-only queue → does not assignAgente en cola pero no conectado y assignOfflineUsers: false.Devuelve null.ninguno
assignOfflineUsers: true → assigns a disconnected agentIgual, con assignOfflineUsers: true.Asigna al agente desconectado.ninguno
empty queue → no assignment and NO global fallbackDepartamento sin cola y un agente conectado del tenant ajeno a la cola.Devuelve null; no hay fallback global.ninguno

redistribution (redistributeTicket)​

TestEscenarioResultado esperadoMocks
reassigns when the agent is disconnected and redistributeChats is onTicket 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 agentredistributeChats: false.Devuelve null; el ticket conserva al agente 1.ninguno
connected agent → no redistributionEl agente asignado sigue conectado.Devuelve null.ninguno
empty pool (everyone disconnected) → keeps the agent, no loopNadie conectado.Devuelve null y mantiene al agente (sin bucle).ninguno

contact message entry point (assignOnContactMessage)​

TestEscenarioResultado esperadoMocks
unassigned department ticket → assigns only among online users that are activeDos 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 unassignedfilterAssignableByIds devuelve [].Devuelve null y el ticket queda sin asignar.filterAssignableByIds
unassigned ticket without department → stays unassigned even with users onlineTicket sin departamento.Devuelve null.getConnectedAgentIds
assignOfflineUsers: true never assigns an inactive queue memberassignOfflineUsers: 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 itAgente 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 agentAgente 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 agentAgente offline con redistributeChats: false.Devuelve null y conserva al agente.isUserOnline

no-user transfer​

TestEscenarioResultado esperadoMocks
department-only transfer queues the ticket and the engine assigns immediatelyAdmin 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 queuedDepartamento 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) ticketTicket 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 agentTicket con dueño y un agente que no es dueño ni admin.Rechaza con ForbiddenException.ninguno
transfer without userId nor departmentId → 400Transferencia 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​

TestEscenarioResultado esperadoMocks
union of two enabled department queues; ticket.departmentId = chosen agent departmentConexió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 orderEl 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 persistedTicket 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 departmentIddepartmentIds 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 agentCola 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 departmentVentas 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 departmentTicket 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 onlineConexión con departmentIds: [].Devuelve null y sin UserId.ninguno

cambio de conexión (resolveContactTicket)​

TestEscenarioResultado esperadoMocks
reusa el ticket abierto cuando el contacto vuelve a escribir por la MISMA conexiónTicket 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 OTRATicket 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 abiertosMismo escenario.El ticket anterior queda CLOSED y se emite broadcastTicketStatusChanged.broadcastTicketStatusChanged
deja anotado el motivo del cierre como mensaje internoMismo 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 resueltosTicket RESOLVED en otra conexión.Sigue RESOLVED.ninguno

tolerance-window reopen (resolveContactTicket)​

TestEscenarioResultado esperadoMocks
resolvedAt is stamped on resolve and cleared on manual reopenSe 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 ticketresolvedAt anterior a 16 minutos con tolerancia de 15.created: true y ticket distinto.ninguno
resolved ticket of ANOTHER connection is not reopenedTicket 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 candidateTicket 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​

TestEscenarioResultado esperadoMocks
sends the interpolated greeting when the connection has greetingMessageConexió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 emptyConexión con saludo vacío.No se crea mensaje ni se hace broadcast.broadcastConnectionMessage
does not send anything for a ticket without connectionIdTicket sin conexión.No se crea mensaje ni se hace broadcast.broadcastConnectionMessage

farewell on resolveTicket​

TestEscenarioResultado esperadoMocks
sends the interpolated farewell when resolving a ticket with a connection farewellMessageConexió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 farewellMessageConexió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 routeresolveTicket(..., 'Ana', false) con despedida configurada.No se envía despedida.broadcastConnectionMessage
does not send a farewell when resolving a ticket without connectionTicket 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)​

TestEscenarioResultado esperadoMocks
sends the interpolated greeting when transferring to a department with greetingMessageSe 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 departmentSe 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 departmentTransferencia 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 greetingMessageDepartamento con saludo vacío.Sin mensajes ni broadcast; greetedDepartmentId es null.broadcastDepartmentGreeting
does not send anything when transferring without departmentIdTransferencia a un agente sin departmentId.Sin mensajes ni broadcast; greetedDepartmentId es null.broadcastDepartmentGreeting
still emits the system transfer message alongside the greetingTransferencia 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)​

TestEscenarioResultado esperadoMocks
rejects the transfer when the agent is not a member of the departmentEl 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 falseMembresía con assignToDepartment: false.Rechaza con BadRequestException.ninguno
allows a department-only transfer (no agent) without membershipTransferencia solo a departamento (sin agente) y sin membresía.Se actualiza el ticket con el nuevo departmentId.ninguno