Casos de prueba — Contact
Catálogo de los tests del módulo contact. Specs fuente (relativas a src/app/modules/contact/):
controllers/contact.controller.permissions.spec.tscontrollers/contact.controller.spec.tsservices/contact.service.spec.tsrepositories/contact.repository.spec.ts
Resumen: 4 specs, 11 suites (describe) y 52 tests (it).
controllers/contact.controller.permissions.spec.ts
Integración del guard real PermissionsGuard + Reflector a través del pipeline HTTP (supertest). AuthGuard se reemplaza por un FakeAuthGuard que inyecta tenantId/roleId.
ContactController — assignLabels (custom verb -> CONTACT_UPDATE) permission guard integration
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
returns 403 Forbidden when CONTACT_UPDATE is missing | El rol solo tiene CONTACT_READ; se hace PATCH /v1/contacts/:id/assign-labels | Responde 403 y assignLabels no se invoca | ContactService, TranslateService, PermissionResolverService (resuelve el set de acrónimos), FakeAuthGuard |
returns 200 OK when CONTACT_UPDATE is granted | El rol tiene CONTACT_UPDATE; mismo PATCH | Responde 200 y assignLabels es invocado | Los mismos |
controllers/contact.controller.spec.ts
Unitario del controlador con guards (AuthGuard, PermissionsGuard) anulados.
ContactController › create
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should pass user.tenantId and return created contact | create con DTO válido y usuario autenticado | Llama service.create(dto, tenantId, lang) y devuelve { message: 'contact.created', data } | ContactService, TranslateService |
should propagate ConflictException when contact already exists | El servicio rechaza con ConflictException | El controlador propaga ConflictException | ContactService.create rechaza |
ContactController › findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return all contacts without pagination | Sin query de paginación | Llama findAll(tenantId, {}) y responde data.results con los contactos | ContactService.findAll |
should return empty array when no contacts exist | El servicio devuelve [] | data: { results: [] } | ContactService.findAll |
should return paginated contacts when page and limit are provided | Query con page: 1, limit: 10 | Usa findAllPaginated(tenantId, 1, 10, {}) y devuelve meta: { total, page, limit } | ContactService.findAllPaginated |
should pass filters to findAll when no pagination | Query con username e isActive sin paginación | findAll recibe el filtro tal cual | ContactService.findAll |
should pass filters to findAllPaginated when pagination is provided | Query con page, limit e isBlocked | findAllPaginated recibe solo { isBlocked: false } como filtro (page/limit se separan) | ContactService.findAllPaginated |
ContactController › findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return contact scoped to user tenant | Contacto existente | Llama findOne(id, tenantId, lang) y responde { message: 'contact.findOne', data: { results: [contact] } } | ContactService.findOne |
should propagate NotFoundException when contact not found in this tenant | El servicio rechaza con NotFoundException | Se propaga NotFoundException | ContactService.findOne rechaza |
ContactController › update
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should update contact scoped to user tenant | DTO con username nuevo | Llama update(id, tenantId, dto, lang) y responde contact.updated con el contacto actualizado | ContactService.update |
should propagate NotFoundException when contact not found in this tenant | Contacto inexistente en el tenant | Se propaga NotFoundException | ContactService.update rechaza |
ContactController › remove
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should remove contact scoped to user tenant | Contacto existente | Llama remove(id, tenantId, lang) y responde contact.deleted | ContactService.remove |
should propagate NotFoundException when contact not found in this tenant | Contacto inexistente | Se propaga NotFoundException | ContactService.remove rechaza |
ContactController › assignLabels
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should assign labels to contact scoped to user tenant | Body con labelIds: [labelId] | Llama assignLabels(id, tenantId, [labelId], lang) y responde contact.updated | ContactService.assignLabels |
should use empty array when labelIds is undefined | Body vacío {} | El servicio recibe [] como labels | ContactService.assignLabels |
should propagate NotFoundException when contact not found | Contacto inexistente | Se propaga NotFoundException | ContactService.assignLabels rechaza |
services/contact.service.spec.ts
Unitario del servicio con repositorio y LabelService simulados.
ContactService › create
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should throw ConflictException when contact already exists for that tenant+externalId | findByExternalIdAndTenant devuelve un contacto | Lanza ConflictException y no llama repository.create | ContactRepository |
should create and return contact when valid | findByExternalIdAndTenant devuelve null | create recibe externalId, username y tenantId; devuelve el contacto | ContactRepository |
ContactService › findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return contacts scoped to the tenant | Sin filtro | repository.findAll(tenantId, undefined) y devuelve la lista | ContactRepository.findAll |
should forward filter to repository | Con filtro username/isActive | El filtro se reenvía al repositorio | ContactRepository.findAll |
should return empty array when no contacts exist for the tenant | El repositorio devuelve [] | Devuelve [] | ContactRepository.findAll |
ContactService › findAllPaginated
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return paginated contacts with total | Página 1, límite 10, sin filtro | Delega con (tenantId, 1, 10, undefined) y devuelve { results, total } | ContactRepository.findAllPaginated |
should forward filter when paginating | Página 2, límite 5, filtro isBlocked: true | El filtro se reenvía al repositorio | ContactRepository.findAllPaginated |
ContactService › findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return contact when found within the tenant | findByIdAndTenant devuelve el contacto | Devuelve el contacto | ContactRepository.findByIdAndTenant |
should throw NotFoundException when contact does not exist in this tenant | findByIdAndTenant devuelve null | Lanza NotFoundException | ContactRepository.findByIdAndTenant |
ContactService › update
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return updated contact when found within tenant | repository.update devuelve el contacto actualizado | Devuelve el contacto y llama update(id, tenantId, dto) | ContactRepository.update |
should throw NotFoundException when contact does not belong to this tenant | repository.update devuelve null | Lanza NotFoundException | ContactRepository.update |
ContactService › remove
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return removed contact when found within tenant | repository.remove devuelve el contacto | Devuelve el contacto y llama remove(id, tenantId) | ContactRepository.remove |
should throw NotFoundException when contact does not belong to this tenant | repository.remove devuelve null | Lanza NotFoundException | ContactRepository.remove |
ContactService › findOrCreate
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return existing contact when found | El repositorio responde created: false | Reenvía el payload y devuelve created === false | ContactRepository.findOrCreate |
should return new contact when not found | El repositorio responde created: true | Devuelve created === true | ContactRepository.findOrCreate |
ContactService › findByMongoId
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return contact when found | El repositorio devuelve el contacto | Devuelve el contacto | ContactRepository.findByMongoId |
should return null when not found | El repositorio devuelve null | Devuelve null (no lanza excepción) | ContactRepository.findByMongoId |
ContactService › assignLabels
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should assign labels and return updated contact | Contacto existente y labels válidos del tenant | Valida labels, llama repository.assignLabels con ObjectId y devuelve el contacto actualizado | ContactRepository, LabelService.validateLabelsBelongToTenant |
should throw NotFoundException when contact does not exist | findByIdAndTenant devuelve null | Lanza NotFoundException y no asigna labels | ContactRepository |
should assign empty labels array when labelIds is empty | labelIds vacío | repository.assignLabels recibe [] y el contacto queda con labels vacío | ContactRepository, LabelService |
ContactService › findOrCreateFromExternal
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return created=true and the new contact when externalId does not exist | upsertFromExternal responde created: true | Devuelve created: true y el contacto | ContactRepository.upsertFromExternal |
should return created=false and existing contact when externalId already exists | upsertFromExternal responde created: false | Devuelve created: false y el contacto existente | ContactRepository.upsertFromExternal |
should forward tenantId and all mapped fields to repository | Datos externos con externalId y metadata | El repositorio recibe el tenantId, externalId '31912' y metadata intactos | ContactRepository.upsertFromExternal |
repositories/contact.repository.spec.ts
Tests contra MongoDB real en memoria (mongodb-memory-server); no hay mocks de persistencia.
ContactRepository.findOrCreate — identity persistence (mongodb-memory-server)
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
finds a contact by phone in any stored format, scoped to the tenant | Contactos con teléfono 593991234567, +573001234567, externalId WhatsApp y uno de otro tenant; se busca con distintos formatos | Cada búsqueda devuelve solo el contacto del tenant que corresponde; un teléfono inexistente da total 0 | Ninguno (MongoDB en memoria) |
creates the contact with the identity fields from the host session | findOrCreate con email en mayúsculas, teléfono y nombre | created: true, email en minúsculas (el schema lo normaliza), teléfono y nombres guardados | Ninguno |
updates identity fields on a returning contact | Contacto existente; se vuelve a llamar con email y nombre nuevos | created: false y campos actualizados | Ninguno |
keeps stored identity when the session sends nothing (no erasure) | Contacto con email y teléfono; segunda llamada sin identidad | created: false y se conservan email y teléfono | Ninguno |
preserves a leading zero in the phone | Teléfono 0999999999 | Se guarda como string con el cero inicial | Ninguno |
preserves the international prefix | Teléfono +593984701187 | Se conserva el prefijo + | Ninguno |
still finds without duplicating when no identity is sent (legacy behavior) | Dos llamadas sin identidad | Primera created: true, segunda false, mismo _id y solo 1 documento | Ninguno |
ContactRepository.setPresence / touchLastSeen (mongodb-memory-server)
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
updates isOnline/lastSeen/lastDisconnectReason when tenant matches, without populating labels | setPresence con el tenant correcto | Actualiza los tres campos y no puebla labels | Ninguno |
does nothing (returns null) when the contact belongs to a different tenant | setPresence con otro tenant | Devuelve null y el contacto no cambia | Ninguno |
bulk-updates lastSeen for every contact id given, ignoring tenant | touchLastSeen con ids de dos tenants | Ambos contactos reciben el mismo lastSeen | Ninguno |
is a no-op on an empty array (no write attempted) | touchLastSeen([]) | Resuelve sin lanzar error | Ninguno |