Skip to main content

Casos de prueba — Chat interno

Casos de prueba unitarios del módulo src/app/modules/internal-chat (conversaciones directas y grupales entre agentes). Fuentes:

  • controllers/internal-conversation.controller.spec.ts
  • repositories/internal-message.repository.spec.ts
  • services/conversation.service.spec.ts
  • services/internal-message.service.spec.ts

Resumen: 4 archivos spec, 22 bloques describe (incluidos los anidados), 66 tests (it): 4 en el controlador, 9 en el repositorio, 31 en ConversationService y 22 en InternalMessageService. Todos usan mocks de repositorios y de InternalChatBroadcastService; ninguno toca una base de datos real.

internal-conversation.controller.spec.ts​

Los guards AuthGuard y PermissionsGuard se sobrescriben para dejar pasar siempre.

getMessageWindow​

TestEscenarioResultado esperadoMocks
should delegate to the service with tenantId, userId, conversationId, messageId, radius and langSe invoca con radius: 20Llama getMessageWindow(tenantId, userId, conversationId, messageId, 20, lang)InternalMessageService.getMessageWindow
should default radius to 15 when not providedQuery sin radiusEl servicio recibe 15InternalMessageService.getMessageWindow
should return standard envelope with the window resultsEl servicio devuelve { results: [], anchorId }Responde { message: 'internalChat.findAll', data: windowResult }InternalMessageService, TranslateService
should propagate NotFoundException when the anchor message or membership is not foundEl servicio rechaza con NotFoundExceptionEl controlador propaga NotFoundExceptionInternalMessageService rechazado

internal-message.repository.spec.ts​

countUnread​

TestEscenarioResultado esperadoMocks
excludes system messages from the count query for every participant, including the actorcountUnread(conversationId, tenantId, since, userId)El filtro incluye contentType: { $ne: SYSTEM }model.countDocuments
still excludes the excludeSenderId from the count query (existing behavior preserved)Misma llamadaEl filtro mantiene senderId: { $ne: userId }, conversationId, tenantId y createdAt: { $gt: since }model.countDocuments

findById​

TestEscenarioResultado esperadoMocks
delegates to the model and returns the documentfindById('some-id') con documento existenteLlama model.findById y devuelve el documentomodel.findById
returns null when not foundEl modelo devuelve nullnullmodel.findById

findWindow​

TestEscenarioResultado esperadoMocks
queries before/after messages scoped by conversationId and returns before(reversed)+anchor+afterHay un mensaje anterior, el ancla y uno posteriorDevuelve [before, anchor, after] (los anteriores en orden cronológico)model.find, sort, limit, findById
scopes both before/after queries by conversationIdEjecución normalLas dos consultas filtran por conversationIdmodel.find
limits both before/after queries to the given radiusRadio 7limit(7)limit
returns just the anchor when there are no messages before or afterSin mensajes alrededorDevuelve solo el anclamodel.find, findById

create​

TestEscenarioResultado esperadoMocks
persists a system message with no senderId and a systemEvent subdocDatos con contentType: SYSTEM, senderId: undefined y systemEvent member_addedmodel.create recibe senderId indefinido, el tipo SYSTEM y el subdocumento systemEvent íntegromodel.create

conversation.service.spec.ts​

Mocks comunes: ConversationRepository, ConversationMemberRepository, InternalMessageRepository, UserRepository, DepartmentUserRepository, FileRepository, StorageService, InternalChatBroadcastService e InternalMessageService. Por defecto el roster de miembros es [], la validación de tenant devuelve un usuario y createSystemMessage resuelve.

createDirect​

TestEscenarioResultado esperadoMocks
returns the existing conversation when a direct conversation for the pair already existsfindDirectByKey devuelve una conversaciónDevuelve la existente sin invocar createconversationRepository
computes the same directKey regardless of argument order (idempotent pairing)Se crea con (A, B) y luego con (B, A)El directKey es idéntico en ambas creacionesconversationRepository, memberRepository.insertMany
recovers the winning conversation on a duplicate key race (Mongo error 11000)create rechaza con code: 11000Recupera y devuelve la conversación ganadora de la carreraconversationRepository.findDirectByKey (2 llamadas)
throws NotFoundException without creating anything when targetUserId does not belong to the tenantfindManyByIdsAndTenant devuelve []NotFoundException y no se crea conversaciónuserRepository, conversationRepository

createGroup​

TestEscenarioResultado esperadoMocks
throws ForbiddenException when the requesting user is not a tenant adminSolicitante con userType: 'agent'ForbiddenException sin crearconversationRepository
creates the group and adds the creator as a group admin when the requesting user is a tenant adminSolicitante admin con un memberId válidoEntre los miembros insertados el creador tiene role: 'admin'conversationRepository.create, memberRepository.insertMany
throws NotFoundException and creates nothing when a memberId does not belong to the tenantfindManyByIdsAndTenant devuelve []NotFoundException; no se llama create ni insertManyuserRepository

updateGroup (assertCanManageGroup)​

TestEscenarioResultado esperadoMocks
allows a tenant admin to manage a group even without a group-admin membership roleAdmin del tenant con rol member en el grupoLa actualización se resuelveconversationRepository.updateGroup, memberRepository.findOne
allows a group admin to manage the group even without a tenant-admin userTypeAgente con rol admin en el grupoLa actualización se resuelveIgual al anterior
throws ForbiddenException for a plain member who is neither a tenant admin nor a group adminAgente con rol memberForbiddenException y no se llama updateGroupmemberRepository.findOne

addMembers​

TestEscenarioResultado esperadoMocks
throws NotFoundException and adds nobody when a userId does not belong to the tenantSe piden 2 ids pero el tenant solo resuelve 1NotFoundException y no se llama addMemberuserRepository.findManyByIdsAndTenant
adds the members and broadcasts memberAdded once tenant membership is validatedUn usuario válidoaddMember una vez con el id correcto y broadcastMemberAdded(conversationId, miembros, usuarioAgregado)memberRepository, broadcastService
persists ONE member_added system message grouping all added userIdsSe agregan dos usuarioscreateSystemMessage una sola vez con MEMBER_ADDED, actorId y targetIds agrupados, y el idiomainternalMessageService.createSystemMessage

removeMember​

TestEscenarioResultado esperadoMocks
throws BadRequestException when a group-admin requester tries to remove themself as the only remaining adminAdmin de grupo no admin de tenant intenta removerse siendo el único adminBadRequestException y no se llama removeMembermemberRepository
allows a tenant admin to remove the group's only adminAdmin de tenant remueve al único admin del grupoDevuelve el miembro removido y emite broadcastMemberRemovedmemberRepository, broadcastService
persists a member_removed system message with the admin as actorId and the removed member as targetIdsAdmin de tenant remueve a un miembrocreateSystemMessage con MEMBER_REMOVED, actorId del solicitante y targetIds del removidointernalMessageService
allows removing a member when at least one other admin remainsQuedan otros administradoresSe ejecuta removeMembermemberRepository

leave​

TestEscenarioResultado esperadoMocks
throws BadRequestException when leaving a direct conversationConversación de tipo directBadRequestException sin removerconversationRepository, memberRepository
auto-promotes the oldest remaining member to admin when the leaving member was the group's only adminSale el único admin; quedan dos miembrosupdateRole(conversationId, miembroMásAntiguo, tenantId, 'admin')memberRepository.updateRole
does not promote anyone when another admin remains after leavingQueda otro adminNo se llama updateRolememberRepository
persists a left system message with the leaving member as both actorId and targetIds[0]Un miembro sale del grupocreateSystemMessage con LEFT, actorId y targetIds iguales al que saleinternalMessageService

system message resilience​

TestEscenarioResultado esperadoMocks
addMembers still adds the members and broadcasts even when createSystemMessage rejectscreateSystemMessage rechaza (db write error)addMembers responde y emite broadcastMemberAdded (el error del mensaje de sistema es fire-and-forget)createSystemMessage rechazado
removeMember still removes the member and broadcasts even when createSystemMessage rejectsIgual en removeMemberDevuelve el miembro removido y emite broadcastMemberRemovedcreateSystemMessage rechazado
leave still removes the member and broadcasts even when createSystemMessage rejectsIgual en leaveDevuelve el miembro que sale y emite broadcastMemberRemovedcreateSystemMessage rechazado

listAgents​

TestEscenarioResultado esperadoMocks
never leaks password (or roleId/signature/externalId/identifier) on returned agent itemsEl repositorio devuelve un agente con campos sensiblesEl resultado solo contiene campos públicos más isOnline: false; sin password, roleId, signature, externalId ni identifieruserRepository.findAgentsAndAdmins
tags isOnline true for every agent present in onlineUserIds, even when onlineOnly is not setDos agentes y uno en líneaDevuelve ambos; el conectado con isOnline: true y el otro falseuserRepository
narrows results to only online agents when onlineOnly is trueIgual con onlineOnly: trueSolo el agente en líneauserRepository
returns every matching agent (isOnline: false) when onlineUserIds is emptyLista de en línea vacíaDevuelve todos con isOnline: falseuserRepository

findMembers​

TestEscenarioResultado esperadoMocks
throws NotFoundException when the conversation does not existfindByIdAndTenant devuelve nullNotFoundExceptionconversationRepository
throws the same NotFoundException for a non-member as for a nonexistent conversationEl solicitante no es miembroNotFoundException y no se lista el rostermemberRepository.findOne
returns the enriched member list (with isOnline) for an actual memberSolicitante miembro, un miembro con datos de usuario y en líneaDevuelve la lista enriquecida (userId, username, nombre, role, lastReadAt, joinedAt, isOnline: true) sin passwordmemberRepository, userRepository, broadcastService.getOnlineAgentIds

internal-message.service.spec.ts​

El TranslateService mock reproduce las plantillas reales ({{actor}} agregó a {{targets}}, etc.) para validar el contenido interpolado.

sendMessage​

TestEscenarioResultado esperadoMocks
throws NotFoundException and persists nothing when fileId does not belong to the tenantfindByIdAndTenant devuelve nullNotFoundException, se consultó con (fileId, tenantId) y no se llama createfileRepository, messageRepository
persists the message when fileId belongs to the tenantArchivo válido del tenantSe llama createfileRepository, messageRepository.create
never checks the file repository when no fileId is providedDTO solo con contentNo se consulta findByIdAndTenantfileRepository
throws BadRequestException when neither content nor fileId is providedDTO vacíoBadRequestException sin persistirmessageRepository

mentions​

TestEscenarioResultado esperadoMocks
persists mentions as ObjectIds when the dto provides themDTO con mentions en textocreate recibe mentions como ObjectIdmessageRepository.create
persists undefined mentions when the dto omits themDTO sin mentionsmentions es undefinedmessageRepository.create
includes mentions (as string ids) in the im:new-message broadcast payloadDTO con mencionesbroadcastNewMessage recibe mentions como ids de textobroadcastService.broadcastNewMessage

createSystemMessage​

TestEscenarioResultado esperadoMocks
persists a SYSTEM message with no senderId and the systemEvent subdocEvento MEMBER_ADDED con dos destinoscreate con senderId indefinido, contentType SYSTEM y systemEvent completomessageRepository.create
resolves actor/target names BEFORE translating, and interpolates them into the contentActor y destino resueltostranslate.get con { actor: 'Ana Gomez', targets: 'Luis Perez' } y contenido Ana Gomez agregó a Luis PerezuserRepository, translate.get
interpolates real actor and target names into the persisted + broadcast content (grouped add, 2 targets)Agregado de 2 destinosContenido Ana Gomez agregó a Luis Perez y Eva Diaz en el mensaje, en updateLastMessage (lastMessagePreview) y en el broadcast a todos los miembrosuserRepository, conversationRepository, broadcastService
joins 3+ resolved target names with the localized connector (Oxford-comma style)Tres destinosAna Gomez agregó a Luis Perez, Eva Diaz y Marco RuizuserRepository
falls back to a generic actor name when the actor cannot be resolved (deleted/outside-tenant user)El actor no se resuelveUsa Alguien como actoruserRepository
falls back to a generic target name when no target can be resolvedEl destino no se resuelve (MEMBER_REMOVED)Ana Gomez removió a AlguienuserRepository
triangulates content translation for member_removed and left event typesEventos MEMBER_REMOVED y LEFTUsa la clave internalChat.systemMemberRemoved y el contenido Ana Gomez se fue del grupouserRepository, translate.get
updates the conversation lastMessage with the actor as lastMessageSenderIdEvento MEMBER_ADDEDupdateLastMessage con lastMessageSenderId igual al actorconversationRepository.updateLastMessage
broadcasts im:new-message with systemEvent + resolved actorName/targetNames, and im:conversation-updated with system:trueAgregado de 2 destinosbroadcastNewMessage con systemEvent (ids en texto), actorName y targetNames; broadcastConversationUpdated con system: truebroadcastService
resolves actor/target names exactly once — no duplicate findManyByIdsAndTenant query between content-building and broadcastEvento con broadcastfindManyByIdsAndTenant se invoca una sola vezuserRepository

getMessageWindow​

TestEscenarioResultado esperadoMocks
throws NotFoundException when the caller is not a conversation memberfindOne devuelve nullNotFoundException sin llamar findWindowmemberRepository
throws NotFoundException when the anchor message does not existfindById devuelve nullNotFoundException sin llamar findWindowmessageRepository.findById
throws NotFoundException when the anchor message belongs to a different conversationEl ancla pertenece a otra conversaciónNotFoundException sin llamar findWindowmessageRepository.findById
throws NotFoundException when the anchor message belongs to a different tenantEl ancla pertenece a otro tenantNotFoundException sin llamar findWindowmessageRepository.findById
delegates to the repository with the anchor createdAt and radius, and returns hydrated results + anchorIdAncla válida y un mensaje en la ventanafindWindow(anchorId, conversationId, createdAt, 15); devuelve anchorId y resultados con sender hidratadomessageRepository.findWindow, userRepository