Casos de prueba: Conexiones
Casos de prueba del módulo de conexiones: servicio, repositorio (incluyendo whatsappConfig y metaConfig cifrado), DTO de configuración web y helper de teléfonos.
Specs fuente (bajo src/app/modules/connection):
services/connection.service.spec.ts
repositories/connection.repository.spec.ts
repositories/connection.repository.meta.integration.spec.ts
dtos/connection-web-config.dto.spec.ts
helpers/phone.helper.spec.ts
Resumen: 5 archivos spec, 18 bloques describe y 51 casos it() (cada it.each cuenta como un caso).
connection.service.spec.ts
Tests unitarios con ConnectionRepository y TranslateService mockeados.
ConnectionService / create
| Test | Escenario | Resultado esperado | Mocks |
|---|
creates a web connection scoped to the tenant | create con DTO web | Llama a repository.create con tenant y DTO y devuelve lo creado | repository |
defaults the name when the minimal create omits it | DTO web sin nombre | Se crea con el nombre Web Chat Connection | repository |
rejects channelType "%s" with 400 channel not available (it.each: whatsapp-cloud, facebook, instagram) | Crear con un canal no disponible por esta vía | BadRequestException, traducción de CHANNEL_NOT_AVAILABLE y no se llama a repository.create | repository, translate |
creates a whatsapp-qr connection with status=pending and emits "created" | Crear canal whatsapp-qr | create con status: PENDING, devuelve lo creado y emite el evento created | repository, listener |
defaults the name to the WhatsApp-specific constant when a whatsapp-qr create omits it | whatsapp-qr sin nombre | Usa DEFAULT_WHATSAPP_CONNECTION_NAME | repository |
does NOT force a status override or emit "created" for the web channel | Crear conexión web | create recibe el DTO tal cual (sin forzar estado) y no se emite created | repository, listener |
findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns all connections when no pagination is given | Sin página ni límite | Devuelve results y no usa findAllPaginated | repository |
delegates to findAllPaginated when page and limit are given | Página 2, límite 10 y filtro por nombre | Llama a findAllPaginated con tenant, página, límite y filtro | repository |
findOne / update / remove
| Test | Escenario | Resultado esperado | Mocks |
|---|
findOne throws NotFoundException when missing | findById devuelve null | NotFoundException | repository |
update returns the updated document | update devuelve el documento | Devuelve el documento actualizado | repository |
update throws NotFoundException when missing | update devuelve null | NotFoundException | repository |
remove throws NotFoundException when missing | delete devuelve null | NotFoundException | repository |
regenerateToken returns the document with the fresh token | Regenerar token | Llama al repositorio con ObjectId y tenant y devuelve el documento con el token nuevo | repository |
regenerateToken throws NotFoundException when missing | regenerateToken devuelve null | NotFoundException | repository |
connection.repository.spec.ts
Test de integración con MongoDB en memoria (mongodb-memory-server) y modelo real.
ConnectionRepository — integration (mongodb-memory-server) / create
| Test | Escenario | Resultado esperado | Mocks |
|---|
stores the document with tenantId and returns it | Crear conexión web | Documento con _id, tenant, nombre y dominios permitidos | ninguno (Mongo en memoria) |
generates a unique UUID token and defaults status to connected | Crear dos conexiones | Tokens con formato UUID y distintos; estado por defecto CONNECTED | ninguno |
applies business defaults (messages, attention tab) | Crear conexión | Saludo y despedida vacíos, sin departamentos, reopenToleranceMinutes 15 y returnToLastAgent true | ninguno |
updateWhatsappConfig
| Test | Escenario | Resultado esperado | Mocks |
|---|
creates whatsappConfig from null on the first write | whatsappConfig nace en null y se escribe sessionState y phoneJid | Se crea el subobjeto sin error y se guardan ambos campos | ninguno |
keeps previously stored fields on a partial write | Escritura parcial tras sessionState: qr con lastQrAt | lastQrAt se conserva y sessionState pasa a connected (merge, no reemplazo) | ninguno |
mirrors the fine-grained state onto Connection.status | connected y luego logged_out | status pasa a CONNECTED y luego a DISCONNECTED | ninguno |
maps qr and connecting to pending | sessionState: qr | status es PENDING | ninguno |
regenerateToken
| Test | Escenario | Resultado esperado | Mocks |
|---|
issues a fresh UUID and the old token stops resolving | Regenerar el token | Nuevo UUID distinto; findByToken con el viejo devuelve null y con el nuevo devuelve la conexión | ninguno |
never crosses tenants | Regenerar con otro tenant | Devuelve null | ninguno |
findByToken
| Test | Escenario | Resultado esperado | Mocks |
|---|
resolves a connection by its public token regardless of tenant | Buscar por token público | Devuelve la conexión | ninguno |
returns null for an unknown token | Token nope | Devuelve null | ninguno |
tenant scoping
| Test | Escenario | Resultado esperado | Mocks |
|---|
findAll / findById / update / delete never cross tenants | Conexiones de dos tenants y operaciones con el tenant equivocado | findAll solo devuelve las propias; findById, update y delete con otro tenant devuelven null; el update del dueño sí funciona | ninguno |
findAllPaginated
| Test | Escenario | Resultado esperado | Mocks |
|---|
paginates and filters by name and channelType | Tres conexiones; filtro por nombre sitio (página 1, límite 2) y por canal web | total 2 con 2 resultados por nombre y total 3 por canal | ninguno |
Test de integración con MongoDB en memoria y clave de cifrado de tokens Meta definida en el entorno.
| Test | Escenario | Resultado esperado | Mocks |
|---|
still allows several web connections in the same tenant | Dos conexiones web y una whatsapp-qr en el mismo tenant | Las 3 se crean (el índice único no afecta a canales sin cuenta Meta) | ninguno |
rejects the same Instagram account connected twice, even across tenants | Misma cuenta Instagram en dos tenants | La segunda falla con error de clave duplicada (código 11000) | ninguno |
allows the same account id on instagram and facebook | Mismo accountId en Instagram y Facebook | Ambas conexiones se crean | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
persists the token as an encrypted envelope, never as plaintext | Guardar metaConfig y leer el documento crudo | El token es un objeto con iv, tag y ciphertext; el texto plano no aparece en el documento | ninguno |
clears a previous authErrorAt on re-authorization | Conexión con authErrorAt y nueva autorización | authErrorAt queda en null | ninguno |
is scoped by tenant | setMetaConfig con otro tenant | Devuelve null y la conexión conserva metaConfig en null | ninguno |
the access token never leaves through regular reads
| Test | Escenario | Resultado esperado | Mocks |
|---|
findById | Lectura normal | El JSON no contiene accessToken ni ciphertext | ninguno |
findAll | Listado | El JSON no contiene accessToken ni ciphertext | ninguno |
findAllPaginated | Listado paginado | El JSON no contiene accessToken ni ciphertext | ninguno |
setMetaConfig own return value | Valor de retorno de setMetaConfig | El JSON no contiene accessToken ni ciphertext | ninguno |
update and regenerateToken | Retorno de update y de regenerateToken | Ninguno expone el token | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
loads the token and decrypts it to the original value | Buscar por canal y cuenta Meta | Devuelve la conexión del tenant y readMetaAccessToken descifra al valor original | ninguno |
does not match another channel type | Buscar la cuenta de Instagram como Facebook | Devuelve null | ninguno |
fails loudly when the token was not selected | readMetaAccessToken sobre una lectura sin el token | Lanza error con Meta access token not loaded | ninguno |
connection-web-config.dto.spec.ts
Validación con class-transformer y class-validator.
ConnectionWebConfigDto — requiresAuth
| Test | Escenario | Resultado esperado | Mocks |
|---|
accepts requiresAuth %s (it.each: true, false) | Payload con un dominio permitido y requiresAuth booleano | Sin errores de validación | ninguno |
accepts the payload without requiresAuth | Payload sin requiresAuth (se usa el default del modelo, true) | Sin errores de validación | ninguno |
rejects a non-boolean requiresAuth | requiresAuth: 'yes' | Error en la propiedad requiresAuth | ninguno |
still enforces at least one allowed domain | allowedDomains vacío | Error en la propiedad allowedDomains | ninguno |
phone.helper.spec.ts
jidToPhoneDigits
| Test | Escenario | Resultado esperado | Mocks |
|---|
extracts the number from a phone JID | 5511999999999@s.whatsapp.net | 5511999999999 | ninguno |
strips the device suffix | 5511999999999:12@s.whatsapp.net | 5511999999999 | ninguno |
rejects a linked-id JID | JID @lid (identificador opaco, no un teléfono) | undefined | ninguno |
rejects a group JID | JID de grupo @g.us | undefined | ninguno |
rejects the status broadcast JID | status@broadcast | undefined | ninguno |
handles absent input | undefined, null y cadena vacía | undefined en los tres casos | ninguno |