Skip to main content

Casos de prueba: módulo Message (controladores y repositorios)

Documenta los tests de la capa HTTP y de persistencia del módulo message.

Specs fuente (src/app/modules/message/):

  • controllers/message.controller.spec.ts
  • controllers/message.controller.permissions.spec.ts
  • repositories/message.repository.spec.ts
  • repositories/message.repository.ack.integration.spec.ts
  • repositories/message.repository.media.integration.spec.ts

La lógica de servicios está en Message (servicios).

Resumen: 5 archivos de spec, 6 suites raíz, 117 tests it() (28 + 2 de controladores, 58 unitarios de repositorio y 27 + 2 de integración con MongoDB en memoria).


message.controller.spec.ts​

Suite raíz: MessageController. El servicio se reemplaza por un mock; las respuestas siguen el sobre { message, data }.

create​

TestEscenarioResultado esperadoMocks
should return message and created messageSe crea un mensaje con un DTOLlama a service.create(dto) y responde { message: 'message.created', data }mockService.create

findAll​

TestEscenarioResultado esperadoMocks
should return all messages when no ticketId filterSin ticketIdUsa findAll y no findAllByTicketmockService.findAll
should return paginated messages with meta when ticketId providedCon ticketId, page y limitUsa findAllByTicket, no findAll, y devuelve resultados con metamockService.findAllByTicket
should use default page=1 and limit=10ticketId sin paginaciónSe usan page 1 y limit 10mockService.findAllByTicket
should cap limit to MAX_LIMIT (100)Límite mayor a 100El límite se recorta a 100mockService.findAllByTicket
should return empty results when no messages existServicio devuelve []Respuesta con datos vacíosmockService.findAll

findOne​

TestEscenarioResultado esperadoMocks
should return message and found messageEl mensaje existeLlama a findOne(id, lang) y responde con el mensajemockService.findOne
should propagate NotFoundException when message not foundEl servicio rechaza con NotFoundExceptionLa excepción se propagamockService.findOne

update​

TestEscenarioResultado esperadoMocks
should return message and updated messageActualización válidaLlama a update(id, dto, lang) y responde message.updatedmockService.update
should propagate NotFoundException when message not foundEl servicio rechazaLa excepción se propagamockService.update

remove​

TestEscenarioResultado esperadoMocks
should return message and removed messageEliminación válidaResponde message.deleted con el mensaje eliminadomockService.remove
should propagate NotFoundException when message not foundEl servicio rechazaLa excepción se propagamockService.remove

markAsRead​

TestEscenarioResultado esperadoMocks
should return translated message and updated documentMarcar un mensaje como leídoResponde message.markedRead con el documento actualizadomockService.markAsRead
should propagate NotFoundException when message not foundEl servicio rechazaLa excepción se propagamockService.markAsRead

markTicketMessagesAsRead​

TestEscenarioResultado esperadoMocks
should return translated message without dataMarcar todos los mensajes de un ticketResponde solo { message: 'message.markedRead' } sin datamockService.markTicketMessagesAsRead

getWindow​

TestEscenarioResultado esperadoMocks
should delegate to the service with id, tenantId, radius and langVentana alrededor de un mensajeLlama a getMessageWindow con id, tenantId, radio e idiomamockService.getMessageWindow
should default radius to 15 when not providedNo se envía radioEl radio por defecto es 15mockService.getMessageWindow
should return standard envelope with the window resultsVentana válidaDevuelve el sobre estándar con los resultadosmockService.getMessageWindow
should propagate NotFoundException when the anchor message is not foundEl ancla no existeLa excepción se propagamockService.getMessageWindow

getContactTickets​

TestEscenarioResultado esperadoMocks
should delegate to service with contactId, tenantId and filterTickets de un contactoDelegación con contactId, tenantId y filtromockService.getTicketsByContact
should return standard envelope with findAll message keyResultado normalSobre con la clave message.findAllmockService.getTicketsByContact
should return empty results when contact has no ticketsContacto sin ticketsDevuelve resultados vacíos en el sobremockService.getTicketsByContact

getContactMessages​

TestEscenarioResultado esperadoMocks
should delegate to service with contactId, tenantId and filterMensajes de un contactoDelegación con contactId, tenantId y filtromockService.findAllByContact
should return standard envelope with findAll message keyResultado normalSobre con la clave message.findAllmockService.findAllByContact
should return empty results when contact has no messagesContacto sin mensajesDevuelve resultados vacíosmockService.findAllByContact

getContactTicketMessages​

TestEscenarioResultado esperadoMocks
should delegate to service with contactId, ticketId, tenantId, filter and langMensajes de un ticket de un contactoDelegación con los cinco argumentosmockService.findAllByContactTicket
should return standard envelope with findAll message keyResultado normalSobre con la clave message.findAllmockService.findAllByContactTicket
should propagate NotFoundException when ticket does not belong to the contactEl ticket no pertenece al contactoLa excepción se propagamockService.findAllByContactTicket

message.controller.permissions.spec.ts​

Suite raíz: MessageController — read/write split permission guard integration. Levanta una app Nest con el guard de permisos real y usa supertest.

TestEscenarioResultado esperadoMocks
a role with only MESSAGE_READ can read but cannot writeRol con solo MESSAGE_READGET /v1/messages/get-all responde 200; PATCH /v1/messages/update/:id responde 403 y no llega al servicioMessageService simulado, guard con permisos del rol
a role with only MESSAGE_UPDATE can write but cannot readRol con solo MESSAGE_UPDATEPATCH responde 200; GET /v1/messages/get-all responde 403 y no llega al servicioMessageService simulado, guard con permisos del rol

message.repository.spec.ts​

Suite raíz: MessageRepository. Usa un modelo Mongoose simulado y valida las etapas de los pipelines de agregación.

create​

TestEscenarioResultado esperadoMocks
should cast tenantId and ticketId strings to ObjectIdSe crea con ids como textotenantId y ticketId se convierten a ObjectIdmockModel.create
should cast optional sendUserId, parentId and MediaId to ObjectId when presentSe envían los ids opcionalessendUserId, parentId y MediaId se convierten a ObjectIdmockModel.create
should omit sendUserId, parentId and MediaId when not providedNo se envían los opcionalesEsos campos quedan undefinedmockModel.create
should return the created documentCreación exitosaDevuelve el documento creadomockModel.create

findAllByTicket​

TestEscenarioResultado esperadoMocks
should query using ObjectId for ticketIdConsulta por ticketLa etapa $match usa ObjectIdmockModel.aggregate
should apply sort by createdAt desc, skip and limit for paginationPágina 2, límite 5$sort por createdAt descendente, $skip 5 y $limit 5mockModel.aggregate
should return { results, total }Facet con un resultadoDevuelve { results: [msg], total: 1 }mockModel.aggregate
should return empty results and total 0 when no messages foundFacet vacíoDevuelve { results: [], total: 0 }mockModel.aggregate
should NOT add a messageType exclusion match when excludeNotes option is omittedSin opción excludeNotesNo hay etapa que excluya notasmockModel.aggregate
should NOT add a messageType exclusion match when excludeNotes is explicitly falseexcludeNotes: falseNo hay etapa que excluya notasmockModel.aggregate
should add a $match excluding messageType note when excludeNotes is trueexcludeNotes: trueAgrega $match con messageType: { $ne: 'note' }mockModel.aggregate

markAsRead​

TestEscenarioResultado esperadoMocks
should only advance messages still in sent or deliveredMarcar como leídoEl filtro exige ack en sent o delivered con _id como ObjectIdmockModel.findOneAndUpdate
should set ack read, readAt and backfill deliveredAt when it was never setActualización con pipelineFija ack en read, readAt y rellena deliveredAt si faltaba; usa la opción requerida por Mongoose para pipelinesmockModel.findOneAndUpdate
should return the updated messageHay mensaje avanzadoDevuelve el mensaje actualizadomockModel.findOneAndUpdate
should return null when nothing advancedEl mensaje ya estaba leídoDevuelve nullmockModel.findOneAndUpdate

advanceTicketAck​

TestEscenarioResultado esperadoMocks
should scope the agent audience to contact messages, excluding notes and internalsAudiencia agentFiltro por ticket, ack en sent/delivered, sendType contacto, sin internos ni notasmockModel.find, updateMany
should scope the contact audience to every backoffice-authored sendTypeAudiencia contactsendType en user, agent, system y ack solo sentmockModel.find, updateMany
should return the ids it actually advancedHay mensajes pendientesDevuelve los ids avanzados y actualiza solo los estados previosmockModel.find, updateMany
should skip the update entirely when nothing is pendingNo hay pendientesDevuelve [] y no llama a updateManymockModel.find, updateMany
should be a no-op for `sent`, which has no preceding stateSe pide avanzar a sentDevuelve [] y no consulta con findmockModel.find

advanceMessagesAck​

TestEscenarioResultado esperadoMocks
should scope to the ids the client acknowledged, cast to ObjectIdIds acusados por el clienteEl filtro usa los ids como ObjectId y el ticketmockModel.find, updateMany
should ignore ids that no longer sit in a preceding stateLos ids ya no están en un estado previoDevuelve [] y no llama a updateManymockModel.find, updateMany
should be a no-op on an empty id listLista de ids vacíaDevuelve [] y no consulta findmockModel.find

findAllByContact​

TestEscenarioResultado esperadoMocks
should call aggregate and return results and total from facetConsulta por contactoUn solo aggregate; devuelve resultados y totalmockModel.aggregate
should use tenantId as ObjectId in the initial $match stagePrimera etapatenantId como ObjectIdmockModel.aggregate
should include a $lookup on the tickets collectionPipeline de contactoIncluye $lookup a tickets por _idmockModel.aggregate
should filter by ContactId as ObjectId after the ticket lookupTras el lookupFiltra ticket.ContactId como ObjectIdmockModel.aggregate
should include a $sort stage with createdAt -1Orden$sort por createdAt descendentemockModel.aggregate
should end the pipeline with a $facet stageEstructura finalLa última etapa es $facet con data y totalmockModel.aggregate
should re-sort ascending inside the facet data branch for correct chat display orderRama de datos del facetReordena ascendente por createdAt para mostrar el chatmockModel.aggregate
should use $or with $toString in the contact sender lookup for robust ObjectId matchingLookup del remitente contactoCondición con $or de dos alternativas (ObjectId y $toString)mockModel.aggregate
should apply messageType filter to the initial $matchFiltro messageType: chatSe aplica en el primer $matchmockModel.aggregate
should apply sendType filter to the initial $matchFiltro sendType: contactSe aplica en el primer $matchmockModel.aggregate
should apply createdFrom as $gte date filterFiltro createdFromcreatedAt.$gte es un Date con el valor dadomockModel.aggregate
should apply createdTo as $lte date filterFiltro createdTocreatedAt.$lte es un DatemockModel.aggregate
should return { results: [], total: 0 } when aggregate yields no facet resultaggregate sin documento de facetDevuelve { results: [], total: 0 }mockModel.aggregate

findAllByContactTicket​

TestEscenarioResultado esperadoMocks
should call aggregate and return results and total from facetConsulta por contacto y ticketUn solo aggregate; devuelve resultados y totalmockModel.aggregate
should use ticketId and tenantId as ObjectIds in the initial $matchPrimera etapaticketId y tenantId como ObjectIdmockModel.aggregate
should include a $sort stage with createdAt -1Orden$sort descendente por createdAtmockModel.aggregate
should end the pipeline with a $facet stageEstructura finalLa última etapa es $facetmockModel.aggregate
should re-sort ascending inside the facet data branch for correct chat display orderRama de datosReordena ascendentemockModel.aggregate
should apply messageType filter to the initial $matchFiltro messageType: mediaSe aplica en el primer $matchmockModel.aggregate
should apply createdFrom and createdTo as date rangeRango de fechas$gte y $lte son instancias de DatemockModel.aggregate
should return { results: [], total: 0 } when aggregate yields no facet resultSin documento de facet{ results: [], total: 0 }mockModel.aggregate
should apply correct skip for page 2Página 2$skip es 10mockModel.aggregate

findWindow​

TestEscenarioResultado esperadoMocks
scopes the pipeline match stage by ticketId as ObjectIdVentana por ticket$match con ticketId como ObjectIdmockModel.aggregate
ends with a $facet containing before/anchor/after branchesEstructura final$facet con ramas before, anchor y aftermockModel.aggregate
limits the before/after branches to the given radiusRadio 7$limit 7 en ambas ramasmockModel.aggregate
sorts the before branch descending (closest-to-anchor first) and the after branch ascendingOrden de ramasbefore descendente y after ascendentemockModel.aggregate
returns before (reversed) + anchor + after in ascending chronological orderFacet con anteriores, ancla y posterioresDevuelve [before2, before1, ancla, after1] (invierte before)mockModel.aggregate
returns just the anchor when there are no messages before or afterSolo hay anclaDevuelve [ancla]mockModel.aggregate
returns an empty array when the facet yields no documentFacet sin documentoDevuelve []mockModel.aggregate
resolves signed media urls for the combined window resultsMensajes con media en la ventanaReemplaza la URL por una firmada con getSignedUrlmockModel.aggregate, StorageService.getSignedUrl

media signed URL resolution​

El bucket es privado: la URL persistida no sirve, por lo que toda lectura de historial debe firmar la URL desde storageKey.

TestEscenarioResultado esperadoMocks
should include storageKey in the file lookup projection so it can be signed laterPipeline de historialEl $project del lookup de archivos incluye storageKey: 1mockModel.aggregate
findAllByTicket should replace the persisted media url with a freshly signed oneMensaje con mediaLa URL se reemplaza por la firmada y storageKey no se exponeaggregate, getSignedUrl
findAllByContact should replace the persisted media url with a freshly signed oneIgual, por contactoURL firmada en el resultadoaggregate, getSignedUrl
findAllByContactTicket should replace the persisted media url with a freshly signed oneIgual, por contacto y ticketURL firmada en el resultadoaggregate, getSignedUrl
should not call getSignedUrl for messages without mediaMensajes sin mediaNo se llama a getSignedUrlaggregate, getSignedUrl

message.repository.ack.integration.spec.ts​

Suite raíz: MessageRepository — ack transitions (integration). Usa mongodb-memory-server con MongoDB real en memoria para validar transiciones de acuse (sent a delivered a read, y failed).

advanceTicketAck → read​

TestEscenarioResultado esperadoMocks
moves the backoffice side to read and stamps readAt (contact audience)Mensaje de usuario en delivered, audiencia contactoDevuelve su id; queda ack read con readAtMongoDB en memoria
backfills deliveredAt when the read lands before any delivered ackLectura sin delivered previoRellena deliveredAt y fija readAtMongoDB en memoria
preserves the original deliveredAt instead of overwriting itYa existe deliveredAtSe conserva el valor originalMongoDB en memoria
never advances the ack of the audience own messagesMensajes de la propia audienciaDevuelve [] y el ack sigue en sentMongoDB en memoria
is monotonic: a delivered arriving late cannot degrade a read messageLlega delivered sobre un mensaje readDevuelve [] y el ack sigue en readMongoDB en memoria

advanceMessagesAck → read​

TestEscenarioResultado esperadoMocks
advances only the acked idsDos mensajes, se acusa solo unoSolo avanza el acusado; el otro conserva su ackMongoDB en memoria

advanceAckByExternalMessageId​

TestEscenarioResultado esperadoMocks
advances delivered and stamps deliveredAtAcuse delivered por id externoAck delivered con deliveredAt y devuelve el resultadoMongoDB en memoria
rellena deliveredAt al saltar directo a readSalta directo a readAck read con readAt y deliveredAt rellenadoMongoDB en memoria
does not downgrade a message already readMensaje ya leído recibe deliveredDevuelve null y sigue en readMongoDB en memoria
is a no-op for sent — there is nothing before itSe pide sentNo modifica nadaMongoDB en memoria
never crosses tenantsId externo de otro tenantNo modifica el mensajeMongoDB en memoria

advanceAckUpTo (Instagram read receipts)​

TestEscenarioResultado esperadoMocks
marks the read message and every outbound one before itRecibo de lectura sobre un mensaje intermedioMarca ese y los salientes anteriores; el más nuevo sigue en sentMongoDB en memoria
never touches what the contact wroteHay mensajes del contacto anterioresNo se modificanMongoDB en memoria
skips internal traffic and notesMensajes internos y notasDevuelve [] y quedan en sentMongoDB en memoria
is idempotent: a repeated receipt advances nothingMismo recibo repetidoLa segunda vez no avanza nadaMongoDB en memoria

conversation history​

TestEscenarioResultado esperadoMocks
returns the failure reason of a rejected message but never the raw provider detailMensaje failed con motivo y detalleEl historial incluye failureReason pero no failureDetailMongoDB en memoria

markAsRead​

TestEscenarioResultado esperadoMocks
returns the updated document with ack readMarcar un mensaje como leídoDevuelve el documento con ack read y readAtMongoDB en memoria

markFailed​

TestEscenarioResultado esperadoMocks
moves a sent message to failed with its reason and raw detailMensaje sent rechazado fuera de ventanaQueda failed con failureReason y failureDetailMongoDB en memoria
never stamps deliveredAt — the message was rejected, not deliveredSe marca fallodeliveredAt queda sin definirMongoDB en memoria
falls back to unknown so a failed bubble is never left unexplainedFallo sin motivofailureReason es UNKNOWNMongoDB en memoria
does not downgrade a delivered messageMensaje ya deliveredDevuelve null y se mantiene deliveredMongoDB en memoria
does not downgrade a read messageMensaje ya readDevuelve null y se mantiene readMongoDB en memoria

markFailedByExternalMessageId​

TestEscenarioResultado esperadoMocks
fails the message matching tenant + external idId externo y tenant coincidenEl mensaje queda failed con el motivoMongoDB en memoria
is scoped by tenantTenant distintoNo modifica ningún mensajeMongoDB en memoria

the generic ack advances refuse failed​

Un mensaje failed tiene precedente (sent), por lo que los avances genéricos deben rechazarlo explícitamente.

TestEscenarioResultado esperadoMocks
advanceAckByExternalMessageId returns nullAvance genérico sobre un failedDevuelve nullMongoDB en memoria
advanceTicketAck returns an empty list and touches nothingAvance por ticketDevuelve [] y el mensaje no cambiaMongoDB en memoria
advanceMessagesAck returns an empty list and touches nothingAvance por idsDevuelve [] y el mensaje no cambiaMongoDB en memoria

message.repository.media.integration.spec.ts​

Suite raíz: MessageRepository — media projection (integration). MongoDB en memoria.

TestEscenarioResultado esperadoMocks
returns duration and waveform so the reloaded history keeps the real audio shapeAudio con duración y forma de onda guardadasEl historial devuelve durationSeconds 6 y waveform de 64 elementosMongoDB en memoria, StorageService
returns an empty waveform for an audio stored before they were computedAudio antiguo sin ondaDevuelve filename y waveform vacíoMongoDB en memoria, StorageService