Casos de prueba: Tenant
Documenta los tests unitarios del módulo tenant (controlador, autorización de plataforma, servicio y cliente HTTP externo). Todos usan mocks; el spec de autorización levanta una app Nest real con supertest y los guards reales.
Specs fuente (bajo src/app/modules/tenant/):
controllers/tenant.controller.auth.spec.tscontrollers/tenant.controller.spec.tsservices/external-request.service.spec.tsservices/tenant.service.spec.ts
Resumen: 4 specs, 82 tests: autorización 35 (5 tests × 7 rutas con describe.each), controlador 12, ExternalRequestService 11 y TenantService 24.
tenant.controller.auth.spec.ts
TenantController: platform admin authorization (SEC-001)
Usa AuthGuard y PlatformAdminGuard reales; JwtService.verify devuelve { sub, email } y UserService.hasOperationalTenant resuelve true por defecto. Un describe.each repite los 5 tests siguientes para cada una de estas 7 rutas:
| Ruta | Handler |
|---|---|
post /v1/tenants/create | create |
get /v1/tenants/get-all | findAll |
get /v1/tenants/get-one/abc | findOne |
patch /v1/tenants/update/abc | update |
delete /v1/tenants/remove/abc | remove |
post /v1/tenants/abc/tokens | addToken |
delete /v1/tenants/abc/tokens/some-token | removeToken |
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
returns 401 for an anonymous request | Petición sin cabecera x-api-key | HTTP 401 y el handler del servicio no se invoca | JwtService.verify, UserService, TenantService |
returns 403 for a tenant admin without the platform flag | Usuario admin con x-api-key válida pero sin isPlatformAdmin | HTTP 403 y el handler no se invoca | UserService.findByEmail (usuario admin), JwtService.verify |
returns 403 for a user whose userType claims to be superadmin | Usuario con userType: 'superadmin' pero sin flag de plataforma | HTTP 403 (el userType no concede acceso) y el handler no se invoca | UserService.findByEmail (userType: 'superadmin'), JwtService.verify |
reaches the handler for a platform admin | Usuario con isPlatformAdmin: true | Estado HTTP menor a 300 y el handler se invoca | UserService.findByEmail (isPlatformAdmin: true), UserService.hasOperationalTenant, TenantService |
returns 401 for a platform admin whose tenant is suspended | Admin de plataforma cuyo tenant no está operativo (hasOperationalTenant devuelve false) | HTTP 401 y el handler no se invoca | UserService.findByEmail, UserService.hasOperationalTenant |
tenant.controller.spec.ts
El Logger.prototype.log se espía para verificar la auditoría; AuthGuard y PlatformAdminGuard se sustituyen por guards que permiten todo.
TenantController / create
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message, tenant and tokenValue on creation | Alta con { name: 'Acme Tenant' } | Llama create(dto) y responde { message: 'tenant.created', data: { tenant, tokenValue } } | TenantService.create, Logger.log |
audits the creation with the actor and never logs the token value | El servicio devuelve un tokenValue secreto | Registra [platform] tenant.created tenant=uuid-001 actor=platform-admin-uuid y el valor del token no aparece en ningún log | TenantService.create, Logger.log |
TenantController / findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and array of tenants | El servicio devuelve una lista | Responde { message: 'tenant.findAll', data: { results } } | TenantService.findAll |
should return message and empty array when no tenants | El servicio devuelve [] | data.results es [] | TenantService.findAll |
TenantController / findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and tenant when found | El tenant existe | Llama findOne(id, lang) y responde tenant.findOne | TenantService.findOne |
should propagate NotFoundException when tenant not found | El servicio rechaza con NotFoundException | Se propaga NotFoundException | TenantService.findOne |
TenantController / update
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and updated tenant and audit the change | DTO { name: 'Acme Updated' } | Llama update(id, dto, lang), responde tenant.updated y registra [platform] tenant.updated tenant=uuid-001 actor=platform-admin-uuid | TenantService.update, Logger.log |
should propagate NotFoundException and not audit when tenant not found | El servicio rechaza con NotFoundException | Se propaga el error y no se registra auditoría | TenantService.update, Logger.log |
TenantController / remove
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and removed tenant and audit the deletion | El tenant existe | Llama remove(id, lang), responde tenant.deleted y registra [platform] tenant.deleted tenant=uuid-001 actor=platform-admin-uuid | TenantService.remove, Logger.log |
should propagate NotFoundException when tenant not found | El servicio rechaza con NotFoundException | Se propaga NotFoundException | TenantService.remove |
TenantController / tokens
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
returns the new token once and audits without logging its value | addToken con dateExpiration | La respuesta incluye tokenValue, se audita [platform] tenant.token_added ... y el valor del token no aparece en los logs | TenantService.addToken, Logger.log |
audits the token removal without logging its value | removeToken con un token secreto | Se audita [platform] tenant.token_removed ... y el valor del token no aparece en los logs | TenantService.removeToken, Logger.log |
external-request.service.spec.ts
ExternalRequestService / searchContact
El fetch global se reemplaza por un mock. La respuesta cruda del backend externo se mapea a ExternalContactData.
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should send correct body with type and id | Llamada con URL, método, token e id | fetch recibe la URL, el método, cabecera Content-Type: application/json y body { token, type: 'contact', id } | global.fetch |
should extract data from the response wrapper | Respuesta { statusCode, status, message, data } | Devuelve el contenido de data; externalId es '31912' | global.fetch |
should map firstName → firstname and secondFirstName → secondFirstname | Respuesta con firstName y secondFirstName | Devuelve firstname y secondFirstname; no existe firstName en el resultado | global.fetch |
should convert numeric externalId to string | externalId numérico (31912) | externalId es de tipo string con valor '31912' | global.fetch |
keeps the phone as text, preserving its leading zero | phone es '0999999999' | El teléfono se conserva como texto con su cero inicial | global.fetch |
should wrap metadata object in an array | metadata viene como objeto | metadata es un arreglo de 1 elemento igual al objeto original | global.fetch |
should return null when endpoint returns non-200 status | La respuesta tiene ok: false (404) | Devuelve null | global.fetch |
should return null when fetch throws a network error | fetch rechaza con Connection refused | Devuelve null | global.fetch |
should return null when fetch times out | fetch rechaza con AbortError | Devuelve null | global.fetch |
should return empty metadata array when metadata is absent | La respuesta no trae metadata | metadata es [] | global.fetch |
should return undefined phone when phone is absent | La respuesta no trae phone | phone es undefined | global.fetch |
tenant.service.spec.ts
process.env.MIGRATION_DB_DATA se restaura antes de cada test (se elimina si estaba sin definir).
TenantService / create
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should create tenant and return it with the generated tokenValue | Alta con name y domainWhitelist | Persiste el tenant con un token (value, dateExpedition, dateExpiration); devuelve tenant y un tokenValue de 64 caracteres | TenantRepository.create |
should use provided tokenExpiration when given | DTO con tokenExpiration explícito | El token persistido usa esa fecha de expiración | TenantRepository.create |
should default tokenExpiration to ~1 year from now when not provided | DTO sin tokenExpiration | La expiración queda a aproximadamente 1 año desde ahora (tolerancia de 1 s) | TenantRepository.create |
should not include tokenExpiration field in the persisted tenant data | DTO con tokenExpiration | El objeto persistido no contiene el campo tokenExpiration | TenantRepository.create |
seeds the global catalog and this tenant default role grants when MIGRATION_DB_DATA=true | MIGRATION_DB_DATA = 'true' | Se llama seedGlobalCatalog() y seedDefaultRoleGrantsForTenant(tenant._id) | TenantRepository.create, PermissionCatalogSeedService |
never calls the seeding service when MIGRATION_DB_DATA is not "true" | MIGRATION_DB_DATA = 'false' | No se llama a ningún método de seed | TenantRepository.create, PermissionCatalogSeedService |
never calls the seeding service when MIGRATION_DB_DATA is unset | Variable eliminada | No se llama a ningún método de seed | TenantRepository.create, PermissionCatalogSeedService |
still returns the created tenant and tokenValue when catalog seeding throws | seedGlobalCatalog rechaza con transient mongo error | La creación no falla: devuelve tenant y tokenValue | TenantRepository.create, PermissionCatalogSeedService.seedGlobalCatalog |
TenantService / findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return an array of tenants | El repositorio devuelve una lista | La devuelve | TenantRepository.findAll |
should return empty array when no tenants exist | Sin tenants | Devuelve [] | TenantRepository.findAll |
TenantService / findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return a tenant when found | findById devuelve el tenant | Llama findById(id) y lo devuelve | TenantRepository.findById |
should throw NotFoundException when tenant does not exist | findById devuelve null | NotFoundException | TenantRepository.findById |
TenantService / update
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should update and return the tenant | DTO { name: 'Acme Updated' } | Llama update(id, dto) y devuelve el tenant con el nombre nuevo | TenantRepository.update |
should throw NotFoundException when tenant does not exist | update devuelve null | NotFoundException | TenantRepository.update |
TenantService / remove
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should remove and return the tenant | remove devuelve el tenant | Llama remove(id) y lo devuelve | TenantRepository.remove |
should throw NotFoundException when tenant does not exist | remove devuelve null | NotFoundException | TenantRepository.remove |
TenantService / addToken
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should generate a token and add it to the tenant | El tenant existe | Llama addToken(id, { value, dateExpedition, dateExpiration }); devuelve tokenValue de 64 caracteres (32 bytes en hex) | TenantRepository.findByIdentifier, TenantRepository.addToken |
should throw NotFoundException when tenant does not exist | findByIdentifier devuelve null | NotFoundException y no se agrega ningún token | TenantRepository.findByIdentifier, TenantRepository.addToken |
should set dateExpedition to now at call time | Se llama addToken | dateExpedition queda entre el instante previo y posterior a la llamada | TenantRepository.findByIdentifier, TenantRepository.addToken |
should return a unique token each call | Se llama addToken dos veces | Los dos tokenValue son distintos | TenantRepository.findByIdentifier, TenantRepository.addToken |
TenantService / removeToken
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should remove token and return updated tenant | El tenant existe | Llama removeToken(id, tokenValue) y devuelve el tenant actualizado | TenantRepository.findByIdentifier, TenantRepository.removeToken |
should throw NotFoundException when tenant does not exist | findByIdentifier devuelve null | NotFoundException y no se elimina nada | TenantRepository.findByIdentifier, TenantRepository.removeToken |
TenantService / findByTokenValue
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should delegate to repository.findByTokenValue | Se busca por valor de token | Delega con el valor y devuelve el tenant | TenantRepository.findByTokenValue |
should return null when token does not exist | El repositorio devuelve null | Devuelve null | TenantRepository.findByTokenValue |