Casos de prueba: Auth y API Keys
Documenta los tests unitarios (Jest + @nestjs/testing) de los módulos auth y api-key. Todos usan mocks de repositorios y servicios; no tocan base de datos.
Specs fuente (bajo src/app/modules/):
auth/controllers/auth.controller.spec.tsauth/services/auth.service.spec.tsapi-key/controllers/api-key.controller.spec.tsapi-key/services/api-key.service.spec.ts
Resumen: 4 specs, 22 suites describe (incluye las raíces), 48 tests (46 bloques it(); uno es un it.each con 3 filas).
auth.controller.spec.ts
AuthController / login
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and token on successful login | AuthService.login resuelve { token } y se llama controller.login(dto, 'es') | Llama al servicio con (dto, lang) y devuelve { message: 'auth.loginSuccess', data: { token } } | AuthService.login, TranslateService.get (devuelve la clave) |
should propagate UnauthorizedException on invalid credentials | El servicio rechaza con UnauthorizedException | El controlador propaga UnauthorizedException | AuthService.login |
auth.service.spec.ts
AuthService / login
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return a token on valid credentials | Usuario existe, bcrypt.compare es true, tenant activo | Busca por email, compara la contraseña, firma JWT con { sub: identifier, email } y devuelve { token } | AuthRepository.findByEmail, bcrypt.compare, TenantService.findByMongoId, JwtService.sign |
should throw UnauthorizedException when user is not found | findByEmail devuelve null | Lanza UnauthorizedException | AuthRepository.findByEmail |
should throw UnauthorizedException when user is inactive | Usuario con isActive: false | Lanza UnauthorizedException | AuthRepository.findByEmail |
should throw UnauthorizedException when password is invalid | bcrypt.compare devuelve false | Lanza UnauthorizedException | AuthRepository.findByEmail, bcrypt.compare |
rejects valid credentials when the tenant is %s (it.each, 3 filas: deactivated, with a non active status, missing) | Credenciales válidas pero tenant con isActive: false, con status: 'inactive' o inexistente (null) | Lanza UnauthorizedException, traduce auth.tenantInactive y no firma el JWT | AuthRepository.findByEmail, bcrypt.compare, TenantService.findByMongoId, TranslateService.get, JwtService.sign |
does not reveal the tenant state when the password is wrong | Contraseña incorrecta y tenant inactivo | Lanza UnauthorizedException con auth.invalidCredentials; ni siquiera consulta el tenant (no se filtra su estado) | AuthRepository.findByEmail, bcrypt.compare, TenantService.findByMongoId, TranslateService.get |
AuthService / tenant state on frame endpoints (B4)
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
frameContact rejects a tenant whose status is not active | findByTokenValue devuelve tenant con status: 'inactive' | UnauthorizedException; no se llama a searchContact | TenantService.findByTokenValue, ExternalRequestService.searchContact |
frameLogin rejects a tenant whose status is not active | Igual, para frameLogin con type: 'user' | UnauthorizedException; no se llama a findByExternalIdAndTenant | TenantService.findByTokenValue, UserRepository.findByExternalIdAndTenant |
AuthService / frameContact
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return upserted contact when token and external search are valid | Token válido, la búsqueda externa devuelve datos, findOrCreateFromExternal devuelve { contact, created: true } | Busca el tenant por token, llama searchContact(url, method, token, id), hace upsert con (tenantId, externalData) y devuelve { contact, created: true } | TenantService.findByTokenValue, ExternalRequestService.searchContact, ContactService.findOrCreateFromExternal |
should throw UnauthorizedException when token is not found | findByTokenValue devuelve null | UnauthorizedException; no se consulta el endpoint externo | TenantService.findByTokenValue, ExternalRequestService.searchContact |
should throw UnauthorizedException when tenant is inactive | Tenant con isActive: false | UnauthorizedException; no se consulta el endpoint externo | TenantService.findByTokenValue, ExternalRequestService.searchContact |
should throw BadRequestException when tenant has no externalIntegration | Tenant con externalIntegration: null | BadRequestException; no se consulta el endpoint externo | TenantService.findByTokenValue, ExternalRequestService.searchContact |
should create a placeholder contact when external endpoint returns null | La búsqueda externa devuelve null | Hace upsert con un contacto placeholder (externalId = String(id), username que empieza por user-) y devuelve { contact, created: true } | TenantService.findByTokenValue, ExternalRequestService.searchContact, ContactService.findOrCreateFromExternal |
api-key.controller.spec.ts
ApiKeyController / create
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message, record and plainKey on creation | El servicio devuelve { message, data: { record, plainKey } } | Delega con (dto, lang), devuelve la respuesta del servicio y plainKey está definido | ApiKeyService.create |
should propagate NotFoundException when tenant does not exist | El servicio rechaza con NotFoundException (tenant inexistente) | El controlador propaga NotFoundException | ApiKeyService.create |
ApiKeyController / findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and array of api keys | El servicio devuelve una lista con una key | { message: 'apiKey.findAll', data: { results: [key] } } | ApiKeyService.findAll, TranslateService.get |
should return empty array when no keys exist | El servicio devuelve [] | { message: 'apiKey.findAll', data: { results: [] } } | ApiKeyService.findAll |
ApiKeyController / findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and api key when found | El servicio devuelve la key | Delega con (id, lang); devuelve { message: 'apiKey.findOne', data: { results: [key] } } | ApiKeyService.findOne |
should propagate NotFoundException when key not found | El servicio rechaza con NotFoundException | Propaga NotFoundException | ApiKeyService.findOne |
ApiKeyController / findByTenant
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return all keys assigned to the tenant | El servicio devuelve las keys del tenant | Delega con (tenantId, lang); devuelve { message: 'apiKey.findAll', data: { results: [key] } } | ApiKeyService.findByTenant |
should return empty array when tenant has no keys | El servicio devuelve [] | data.results es [] | ApiKeyService.findByTenant |
should propagate NotFoundException when tenant does not exist | El servicio rechaza con NotFoundException | Propaga NotFoundException | ApiKeyService.findByTenant |
ApiKeyController / update
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and updated api key | dto = { name, isActive: false }, el servicio devuelve la key actualizada | Delega con (id, dto, lang); devuelve { message: 'apiKey.updated', data: updated } | ApiKeyService.update |
should propagate NotFoundException when key not found | El servicio rechaza con NotFoundException | Propaga NotFoundException | ApiKeyService.update |
ApiKeyController / remove
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return message and removed api key | El servicio devuelve la key eliminada | Delega con (id, lang); devuelve { message: 'apiKey.deleted', data: key } | ApiKeyService.remove |
should propagate NotFoundException when key not found | El servicio rechaza con NotFoundException | Propaga NotFoundException | ApiKeyService.remove |
api-key.service.spec.ts
ApiKeyService / hashApiKey
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should produce a consistent SHA-256 hex hash | Se hashea dos veces la misma cadena | Ambos hashes son iguales y miden 64 caracteres | Ninguno |
should produce different hashes for different inputs | Se hashean key-a y key-b | Los hashes difieren | Ninguno |
ApiKeyService / create
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should throw NotFoundException when tenant does not exist | findByIdentifier devuelve null | NotFoundException; no se persiste nada | TenantService.findByIdentifier, ApiKeyRepository.create |
should generate a plain key, hash it, persist and return both | Tenant existe | Persiste name, tenantId, code (string) y dateExpiration (Date); devuelve data.plainKey y data.record | TenantService.findByIdentifier, ApiKeyRepository.create, crypto.randomBytes |
should store code as SHA-256 of the plain key | Se captura el code enviado al repositorio | code es igual a hashApiKey(plainKey) (nunca se guarda la clave en claro) | TenantService.findByIdentifier, ApiKeyRepository.create, crypto.randomBytes |
ApiKeyService / findAll
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return an array of api keys | El repositorio devuelve una key | Devuelve el arreglo del repositorio | ApiKeyRepository.findAll |
should return an empty array when no keys exist | El repositorio devuelve [] | Devuelve [] | ApiKeyRepository.findAll |
ApiKeyService / findOne
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should return the api key when found | findById devuelve la key | Llama findById(id) y devuelve la key | ApiKeyRepository.findById |
should throw NotFoundException when key does not exist | findById devuelve null | NotFoundException | ApiKeyRepository.findById |
ApiKeyService / findByTenant
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should throw NotFoundException when tenant does not exist | Tenant inexistente | NotFoundException; no se consulta findByTenantId | TenantService.findByIdentifier, ApiKeyRepository.findByTenantId |
should return all keys for an existing tenant | Tenant existe y tiene keys | Valida el tenant, llama findByTenantId(tenantId) y devuelve las keys | TenantService.findByIdentifier, ApiKeyRepository.findByTenantId |
should return empty array when tenant has no keys | Tenant existe sin keys | Devuelve [] | TenantService.findByIdentifier, ApiKeyRepository.findByTenantId |
ApiKeyService / findByCode
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should delegate to repository findByCode | Se busca por un hash | Delega a findByCode('some-hash') y devuelve su resultado | ApiKeyRepository.findByCode |
ApiKeyService / update
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should update name and isActive without touching code | dto = { name, isActive: false } | El repositorio recibe los datos sin el campo code; devuelve la key actualizada | ApiKeyRepository.update |
should convert dateExpiration string to Date on update | dto.dateExpiration es un string ISO | El repositorio recibe dateExpiration como Date | ApiKeyRepository.update |
should throw NotFoundException when key does not exist | update devuelve null | NotFoundException | ApiKeyRepository.update |
ApiKeyService / remove
| Test | Escenario | Resultado esperado | Mocks |
|---|---|---|---|
should remove and return the api key | remove devuelve la key | Llama remove(id) y devuelve la key | ApiKeyRepository.remove |
should throw NotFoundException when key does not exist | remove devuelve null | NotFoundException | ApiKeyRepository.remove |