Casos de prueba: mensajería auxiliar
Cubre los módulos starred-message (mensajes destacados), quick-response (respuestas rápidas), label (etiquetas) y system-message (mensajes de sistema).
Specs fuente (src/app/modules/):
starred-message/controllers/starred-message.controller.spec.ts
starred-message/services/starred-message.service.spec.ts
starred-message/repositories/starred-message.repository.spec.ts
starred-message/dtos/query-starred-messages.dto.spec.ts
starred-message/dtos/toggle-starred-message.dto.spec.ts
starred-message/models/starred-message.model.spec.ts
quick-response/controllers/quick-response.controller.spec.ts
quick-response/services/quick-response.service.spec.ts
quick-response/repositories/quick-response.repository.spec.ts
quick-response/repositories/quick-response.repository.integration.spec.ts
label/controllers/label.controller.spec.ts
label/services/label.service.spec.ts
system-message/services/system-message.service.spec.ts
Resumen: 13 archivos de spec y 132 tests it(): starred-message 46, quick-response 46, label 32, system-message 8.
Módulo: Starred message
starred-message.controller.spec.ts
Suite raíz: StarredMessageController. Servicio simulado.
toggle
| Test | Escenario | Resultado esperado | Mocks |
|---|
delegates to the service with user, messageId, dto and lang | Un agente destaca/quita un mensaje | Llama a service.toggle con usuario, messageId, DTO e idioma y responde con starred: true en el sobre estándar | mockService.toggle |
propagates NotFoundException for a cross-tenant message | El servicio rechaza por mensaje de otro tenant | La NotFoundException se propaga | mockService.toggle |
| Test | Escenario | Resultado esperado | Mocks |
|---|
delegates to the service with user, contactId and query | Destacados de un contacto | Delegación con usuario, contactId y query; respuesta paginada en el sobre | mockService.findByContact |
findByInternalConversation
| Test | Escenario | Resultado esperado | Mocks |
|---|
delegates to the service with user, conversationId and query | Destacados de una conversación interna | Delegación con usuario, conversationId y query | mockService.findByInternalConversation |
findGlobal
| Test | Escenario | Resultado esperado | Mocks |
|---|
delegates to the service with user and query | Destacados globales del agente | Delegación con usuario y query | mockService.findGlobal |
findIds
| Test | Escenario | Resultado esperado | Mocks |
|---|
delegates to the service and wraps the id list under data.ids | Se piden solo los ids destacados | Llama a findIdsByScope y envuelve la lista en data.ids | mockService.findIdsByScope |
starred-message.service.spec.ts
Suite raíz: StarredMessageService. Mocks: repositorio de favoritos, repositorio de tickets y modelos de mensaje y mensaje interno.
| Test | Escenario | Resultado esperado | Mocks |
|---|
resolves contactId from the message ticket and delegates to the repository | Mensaje de contacto del tenant | Resuelve el contactId desde el ticket del mensaje, llama a repository.toggle y devuelve { starred: true } | modelo de mensaje, findByIdAndTenant, toggle |
throws NotFoundException (not 500) when the message belongs to another tenant | El mensaje no se encuentra en el tenant | Lanza NotFoundException y no llama a toggle | modelo de mensaje (findOne devuelve null) |
scopes the message lookup by tenantId so a cross-tenant messageId never resolves | Id de mensaje de otro tenant | La consulta incluye el tenantId correcto (no el ajeno) | modelo de mensaje |
toggle — internal messages
| Test | Escenario | Resultado esperado | Mocks |
|---|
resolves conversationId from the internal message and delegates to the repository | Mensaje interno del tenant | Resuelve conversationId, delega y devuelve { starred: false } | modelo de mensaje interno, toggle |
throws NotFoundException when the internal message does not exist in the tenant | El mensaje interno no existe | Lanza NotFoundException y no llama a toggle | modelo de mensaje interno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
computes pagination meta from the repository total | El repositorio devuelve resultados y total | Calcula meta (página, límite, total, totalPages) a partir del total | repository.findByContact |
findIdsByScope
| Test | Escenario | Resultado esperado | Mocks |
|---|
delegates to the repository scoped by agent and tenant | Consulta de ids | Delegación con agente y tenant; devuelve los ids | repository.findIdsByScope |
starred-message.repository.spec.ts
Suite raíz: StarredMessageRepository. Modelo Mongoose simulado y StorageService falso.
toggle
| Test | Escenario | Resultado esperado | Mocks |
|---|
creates a StarredMessage and returns true (starred) when none exists yet | No existe el favorito | Crea el registro y devuelve true | findOne, create |
removes the existing StarredMessage and returns false (unstarred) when one already exists | Ya existe el favorito | Elimina con deleteOne, no crea y devuelve false | findOne, deleteOne |
treats a concurrent duplicate-key error (E11000) as already-starred success, not a crash | create falla con clave duplicada por concurrencia | Devuelve true sin lanzar | create rechaza con E11000 |
rethrows non-duplicate-key errors from create | create falla con otro error | Relanza el error | create rechaza |
| Test | Escenario | Resultado esperado | Mocks |
|---|
scopes the aggregation match stage by agentId, tenantId and contactId | Consulta por contacto | El primer $match filtra por agente, tenant y contacto | aggregate |
returns hydrated results and total from the facet, defaulting to 0 when the facet is empty | Facet con filas y total | Devuelve { results, total } con total 1 | aggregate |
defaults to empty results and 0 total when the facet returns no document | Sin documento de facet | Devuelve { results: [], total: 0 } | aggregate |
findByInternalConversation
| Test | Escenario | Resultado esperado | Mocks |
|---|
scopes the aggregation match stage by agentId, tenantId and conversationId | Consulta por conversación interna | El primer $match filtra por agente, tenant y conversación | aggregate |
findGlobal
| Test | Escenario | Resultado esperado | Mocks |
|---|
matches only by agentId and tenantId, with no contact/conversation scope | Consulta global | El primer $match usa solo agente y tenant | aggregate |
drops orphaned rows whose message no longer exists via a $match on the joined message | Favoritos cuyo mensaje fue eliminado | El pipeline incluye $match con message distinto de null | aggregate |
list hydration (WU5 — sender + file, fixes verify-report W1)
| Test | Escenario | Resultado esperado | Mocks |
|---|
adds sender + file $lookup stages scoped to Contacts/Users/Files inside the facet results branch | Hidratación de la lista | Agrega $lookup de remitente y archivo dentro de la rama de resultados | aggregate |
projects sender and file alongside the existing row fields | Etapa de proyección | $project incluye sender y file junto a los campos existentes | aggregate |
resolves a fresh signed URL from file.storageKey and strips storageKey from the response | Fila con archivo | Firma una URL desde storageKey y elimina storageKey de la respuesta | aggregate, getSignedUrl |
does nothing when a row has no file (text-only starred message) | Fila sin archivo | No se llama a getSignedUrl | aggregate, getSignedUrl |
removeAllByMessage
| Test | Escenario | Resultado esperado | Mocks |
|---|
deletes every StarredMessage row matching the messageId and messageType, across all agents | Se elimina un mensaje | deleteMany con messageId y messageType, sin filtrar por agente | deleteMany |
returns the deleteMany result | Resultado del borrado | Devuelve el resultado de deleteMany | deleteMany |
findIdsByScope
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns the messageId strings for the agent, scoped by tenant | Ids del agente en el tenant | Devuelve los messageId como texto; find filtra por agente y tenant | find |
adds the contactId filter to the query when provided | Se indica contactId | El filtro de find incluye contactId | find |
query-starred-messages.dto.spec.ts
Suite raíz: QueryStarredMessagesDto. Validación con class-validator y class-transformer, sin mocks.
| Test | Escenario | Resultado esperado | Mocks |
|---|
passes validation with no fields set (all optional) | DTO vacío | Sin errores | Ninguno |
coerces page/limit query strings to integers | page y limit como texto | Se convierten a 2 y 25 | Ninguno |
rejects a limit above PAGINATION.MAX_LIMIT (100) | limit mayor a 100 | Un error en la propiedad limit | Ninguno |
rejects a messageType outside the contact/internal enum | messageType inválido | Un error en messageType | Ninguno |
rejects a contactId that is not a valid Mongo id | contactId no es id Mongo | Un error en contactId | Ninguno |
accepts a valid Mongo-id contactId and conversationId | Ids Mongo válidos | Sin errores | Ninguno |
toggle-starred-message.dto.spec.ts
Suite raíz: ToggleStarredMessageDto.
| Test | Escenario | Resultado esperado | Mocks |
|---|
passes validation for a valid contact messageType | messageType contacto | Sin errores | Ninguno |
passes validation for a valid internal messageType | messageType interno | Sin errores | Ninguno |
rejects a messageType outside the contact/internal enum | Valor fuera del enum | Un error en messageType | Ninguno |
rejects a missing messageType | Falta messageType | Un error en messageType | Ninguno |
starred-message.model.spec.ts
Suite raíz: StarredMessageSchema. Inspecciona el esquema Mongoose sin base de datos.
| Test | Escenario | Resultado esperado | Mocks |
|---|
defines a unique compound index on agentId, messageId and messageType | Índices del esquema | Existe un índice compuesto único por agente, mensaje y tipo | Ninguno |
defines the three non-unique scoped indexes from the design (contact scope, conversation scope, global scope) | Índices por alcance | Existen los índices de alcance contacto, conversación y global | Ninguno |
requires tenantId, agentId, messageType, messageRefModel and messageId | Campos obligatorios | Esos cinco campos son required | Ninguno |
does not require contactId or conversationId (scope-dependent optional fields) | Campos opcionales | contactId y conversationId no son obligatorios | Ninguno |
restricts messageType to the contact/internal enum values | Enum de tipo | Valores permitidos: contact e internal | Ninguno |
Módulo: Quick response
quick-response.controller.spec.ts
Suite raíz: QuickResponseController. Servicio simulado; el tenant sale del usuario autenticado.
create
| Test | Escenario | Resultado esperado | Mocks |
|---|
should pass user.tenantId and return created record | Crear respuesta rápida | Llama a service.create con el tenant del usuario y devuelve el registro en el sobre | mockService.create |
should propagate ConflictException when shortcut already exists | El atajo ya existe | La ConflictException se propaga | mockService.create |
findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return quick responses scoped to user tenant | Listado | Llama a findAll(tenantId, {}) y devuelve los registros | mockService.findAll |
should return empty array when no records exist | Sin registros | Devuelve lista vacía | mockService.findAll |
findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return record scoped to user tenant | Registro existente | Delegación con tenant del usuario y devuelve el registro | mockService.findOne |
should propagate NotFoundException when not found in this tenant | No existe en el tenant | La excepción se propaga | mockService.findOne |
update
| Test | Escenario | Resultado esperado | Mocks |
|---|
should update record scoped to user tenant | Actualización válida | Delegación con tenant y devuelve el registro actualizado | mockService.update |
should propagate ConflictException when shortcut already used | Atajo en uso | La ConflictException se propaga | mockService.update |
should propagate NotFoundException when record not found in this tenant | No existe en el tenant | La excepción se propaga | mockService.update |
remove
| Test | Escenario | Resultado esperado | Mocks |
|---|
should remove record scoped to user tenant | Eliminación válida | Delegación con tenant y devuelve el registro eliminado | mockService.remove |
should propagate NotFoundException when record not found in this tenant | No existe en el tenant | La excepción se propaga | mockService.remove |
quick-response.service.spec.ts
Suite raíz: QuickResponseService. Mocks: repositorio de respuestas rápidas, repositorio de departamentos, repositorio de tickets y traducción.
create
| Test | Escenario | Resultado esperado | Mocks |
|---|
should throw ConflictException when shortcut already exists for this tenant | Atajo existente | Lanza ConflictException y no crea | findByShortcutAndTenant |
should create the quick response with its departments as ObjectIds scoped to the tenant | Atajo libre y departamentos válidos | Valida departamentos del tenant y crea con los ids como ObjectId | findByShortcutAndTenant, departmentRepository.findAll, create |
should collapse repeated department ids before validating them | Ids de departamento repetidos | Los deduplica antes de validar y crear | departmentRepository.findAll, create |
should throw BadRequestException when a department does not belong to the tenant | Departamento ajeno al tenant | Lanza BadRequestException con mensaje traducido y no crea | departmentRepository.findAll, traducción |
should throw BadRequestException when no department is given | Sin departamentos | Lanza BadRequestException y no crea | departmentRepository.findAll |
findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return quick responses scoped to the tenant | Sin ticketId | Llama a findAll(tenantId, undefined) sin consultar tickets | repository.findAll |
should return empty array when no records exist for the tenant | Sin registros | Devuelve [] | repository.findAll |
should return only active responses of the ticket department when a ticketId is given | Con ticketId con departamento | Filtra respuestas activas del departamento del ticket | ticketRepository.findByIdAndTenant, findAll |
should return nothing when the ticket has no department | Ticket sin departamento | Devuelve [] y no consulta el repositorio | ticketRepository.findByIdAndTenant |
should return nothing when the ticket does not belong to the tenant | Ticket de otro tenant | Devuelve [] y no consulta el repositorio | ticketRepository.findByIdAndTenant (null) |
findAllPaginated
| Test | Escenario | Resultado esperado | Mocks |
|---|
should paginate across the whole tenant when no ticketId is given | Sin ticketId | Pagina en todo el tenant y devuelve { results, total } | repository.findAllPaginated |
should paginate only active responses of the ticket department when a ticketId is given | Con ticketId | Pagina solo las activas del departamento del ticket | findByIdAndTenant, findAllPaginated |
should return an empty page when the ticket has no department | Ticket sin departamento | Devuelve { results: [], total: 0 } sin consultar el repositorio | findByIdAndTenant |
findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return quick response when found within the tenant | Registro existente | Busca por identificador y tenant y lo devuelve | findByIdentifierAndTenant |
should throw NotFoundException when not found in this tenant | No existe | Lanza NotFoundException | findByIdentifierAndTenant (null) |
update
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return updated record when found | Actualización sin departamentos | Actualiza y devuelve; no valida departamentos | findByShortcutAndTenant, update |
should validate and persist the departments when they are part of the update | La actualización incluye departamentos | Valida con departmentRepository.findAll y los persiste | departmentRepository.findAll, update |
should throw BadRequestException when an updated department does not belong to the tenant | Departamento ajeno | Lanza BadRequestException y no actualiza | departmentRepository.findAll ([]) |
should throw ConflictException when updating shortcut to one already used by another record | Atajo usado por otro registro | Lanza ConflictException y no actualiza | findByShortcutAndTenant |
should allow updating shortcut to its own current value | El atajo pertenece al mismo registro | Permite actualizar y devuelve el registro | findByShortcutAndTenant, update |
should throw NotFoundException when record does not belong to this tenant | update devuelve null | Lanza NotFoundException | findByShortcutAndTenant, update |
remove
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return removed record when found within tenant | Registro del tenant | Llama a remove(id, tenantId) y lo devuelve | repository.remove |
should throw NotFoundException when record does not belong to this tenant | remove devuelve null | Lanza NotFoundException | repository.remove |
quick-response.repository.spec.ts
Suite raíz: QuickResponseRepository — buildFilter. Pruebas puras del armado de filtros.
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns only the tenant scope when no filter is provided | Sin filtro | La consulta es { tenantId } | Ninguno |
builds an $or of case-insensitive regex on shortcut and message when search is present | Con search | Arma un $or de regex insensible a mayúsculas sobre atajo y mensaje | Ninguno |
keeps the existing shortcut-only behavior unchanged when search is absent | Filtro por atajo sin search | Se mantiene el filtro solo por atajo | Ninguno |
still applies the isActive filter alongside a search filter | isActive junto con search | Ambos filtros se aplican | Ninguno |
restricts the query to quick responses assigned to the given department | Filtro por departamento | La consulta restringe a las asignadas a ese departamento | Ninguno |
ignores the ticketId filter field, which the service resolves into a department | Filtro con ticketId | Se ignora (el servicio lo resuelve a un departamento); queda solo el tenant | Ninguno |
quick-response.repository.integration.spec.ts
Suite raíz: QuickResponseRepository — integration (mongodb-memory-server). MongoDB real en memoria.
findAll with search
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns a quick response whose message body matches, even when the shortcut does not | search coincide solo con el cuerpo del mensaje | Devuelve 1 resultado (/saludo) | MongoDB en memoria |
returns no results when the search value matches neither shortcut nor message | search sin coincidencias | Devuelve 0 resultados | MongoDB en memoria |
findAll scoped to a department
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns only the responses assigned to that department within the tenant | Filtro por departamento | Solo las asignadas a ese departamento en el tenant | MongoDB en memoria |
excludes inactive responses, responses without departments and other tenants | Datos mezclados | Excluye inactivas, sin departamentos y de otros tenants; queda 1 (/saludo) del tenant A | MongoDB en memoria |
persists department ids as ObjectIds so the department filter matches them | Se guarda un registro con departamentos | departmentIds[0] es ObjectId | MongoDB en memoria |
paginates the department scope with a matching total | Paginación con 2 coincidencias y límite 1 | Devuelve 1 resultado y total 2 | MongoDB en memoria |
Módulo: Label
label.controller.spec.ts
Suite raíz: LabelController. Servicio simulado.
create
| Test | Escenario | Resultado esperado | Mocks |
|---|
should pass user.tenantId and return created label | Crear etiqueta | Delegación con tenant del usuario; responde { message: 'label.created', data } | mockService.create |
should propagate ConflictException when label name already exists for tenant | Nombre duplicado | La ConflictException se propaga | mockService.create |
findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return labels scoped to user tenant without pagination | Sin page/limit | Llama a findAll(tenantId, {}) y devuelve la lista | mockService.findAll |
should return empty array when no labels exist for tenant | Sin etiquetas | Lista vacía | mockService.findAll |
should forward filter to service when provided without pagination | Filtro sin paginación | El filtro se reenvía a findAll | mockService.findAll |
should call findAllPaginated and return meta when page and limit are provided | Con page y limit | Usa findAllPaginated y devuelve meta | mockService.findAllPaginated |
should call findAllPaginated with filter when all params provided | Filtro, page y limit | findAllPaginated recibe el filtro; responde con resultados vacíos y meta | mockService.findAllPaginated |
findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return label scoped to user tenant | Etiqueta existente | Delegación con tenant y devuelve la etiqueta | mockService.findOne |
should propagate NotFoundException when label not found in this tenant | No existe | La excepción se propaga | mockService.findOne |
update
| Test | Escenario | Resultado esperado | Mocks |
|---|
should update label scoped to user tenant | Actualización válida | Responde label.updated con la etiqueta actualizada | mockService.update |
should propagate NotFoundException when label not found in this tenant | No existe | La excepción se propaga | mockService.update |
remove
| Test | Escenario | Resultado esperado | Mocks |
|---|
should remove label scoped to user tenant | Eliminación válida | Responde label.deleted con la etiqueta | mockService.remove |
should propagate NotFoundException when label not found in this tenant | No existe | La excepción se propaga | mockService.remove |
label.service.spec.ts
Suite raíz: LabelService. Repositorio de etiquetas y traducción simulados.
create
| Test | Escenario | Resultado esperado | Mocks |
|---|
should throw ConflictException when label with same name exists for tenant | Nombre existente | Lanza ConflictException y no crea | findByNameAndTenant |
should create and return label when valid | Nombre libre | Crea con el tenant y devuelve la etiqueta | findByNameAndTenant, create |
findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return labels scoped to the tenant | Sin filtro | Llama a findAll(tenantId, undefined) | repository.findAll |
should return empty array when no labels exist for the tenant | Sin etiquetas | Devuelve [] | repository.findAll |
should forward filter to repository | Con filtro | El filtro llega al repositorio | repository.findAll |
findAllPaginated
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return paginated results scoped to the tenant | Paginación | Delegación con tenant y devuelve el resultado paginado | repository.findAllPaginated |
should return empty results when no labels match for the tenant | Sin coincidencias | Devuelve { results: [], total: 0 } | repository.findAllPaginated |
should forward filter to repository | Con filtro | El filtro llega al repositorio | repository.findAllPaginated |
findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return label when found within the tenant | Etiqueta del tenant | Busca por id y tenant y la devuelve | findByIdAndTenant |
should throw NotFoundException when label does not exist in this tenant | No existe | Lanza NotFoundException | findByIdAndTenant (null) |
update
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return updated label when found within tenant | Nuevo nombre libre | Actualiza y devuelve la etiqueta | findByNameAndTenant, update |
should throw ConflictException when new name is taken by another label in the tenant | Nombre usado por otra etiqueta | Lanza ConflictException y no actualiza | findByNameAndTenant |
should allow update when the same label keeps its own name | El nombre es de la misma etiqueta | Permite actualizar | findByNameAndTenant, update |
should throw NotFoundException when label does not belong to this tenant | update devuelve null | Lanza NotFoundException | findByNameAndTenant, update |
remove
| Test | Escenario | Resultado esperado | Mocks |
|---|
should return removed label when found within tenant | Etiqueta del tenant | Llama a remove con id y tenant y la devuelve | repository.remove |
should throw NotFoundException when label does not belong to this tenant | remove devuelve null | Lanza NotFoundException | repository.remove |
validateLabelsBelongToTenant
| Test | Escenario | Resultado esperado | Mocks |
|---|
should resolve without error when all labels belong to tenant | Todas las etiquetas son del tenant | Resuelve sin error | findManyByIdsAndTenant |
should throw NotFoundException when some labels do not belong to tenant | Se piden más ids de los que existen | Lanza NotFoundException | findManyByIdsAndTenant |
should skip validation when labelIds array is empty | Arreglo de ids vacío | Resuelve sin consultar el repositorio | findManyByIdsAndTenant |
Módulo: System message
system-message.service.spec.ts
Suite raíz: SystemMessageService. Mocks: modelo de mensaje, repositorio de contactos, MessageTemplateService, broadcast de sockets y OutboundRouterService.
| Test | Escenario | Resultado esperado | Mocks |
|---|
builds, persists, emits and delivers a plain system message through the channel | Mensaje de sistema simple a un contacto | Busca el contacto, interpola la plantilla, persiste, emite por socket y despacha por el canal; devuelve el mensaje guardado | repositorio de contactos, plantilla, Message.create, broadcast, router |
persists a connection message as chat and emits it authored by "Sistema" | Mensaje de conexión | Se persiste como chat, se emite con autor "Sistema" (no usa broadcastSystemMessage) y se despacha al canal | Message.create, broadcast, router |
persists a department greeting as chat and emits it authored by the department | Saludo de departamento | Se persiste como chat, se emite con el departamento como autor y se despacha | Message.create, broadcast, router |
omits connectionId when the ticket has no connection | Ticket sin conexión | El mensaje se crea sin connectionId | Message.create |
does nothing when the contact does not belong to the tenant | Contacto no encontrado en el tenant | Devuelve null; no persiste, no emite ni despacha | findByIdAndTenant (null) |
does nothing when the template interpolates to an empty body | La plantilla queda vacía | Devuelve null; no persiste ni despacha | interpolate devuelve vacío |
keeps the message persisted and emitted when the channel delivery fails | El router rechaza con canal caído | La promesa resuelve con el mensaje; se persistió y se emitió igualmente | route rechaza |
recordInternal
| Test | Escenario | Resultado esperado | Mocks |
|---|
persists an internal message, emits it to agents only and never delivers it to the channel | Registro de mensaje interno | Persiste, emite solo a agentes, no busca contacto, no interpola y no despacha al canal; devuelve el mensaje | Message.create, broadcast, repositorio de contactos, router |