Skip to main content

Casos de prueba — Core

Specs unitarios bajo src/app/core/**: cifrado de secretos, filtro global de excepciones, guards, interceptor de respuesta, procesado de media, resolución de permisos, storage S3 y utilidades.

Resumen: 14 archivos spec, 14 suites de primer nivel (describe), 94 bloques it()/it.each() declarados. Los it.each se ejecutan una vez por fila (ver tablas).

src/app/core/crypto/secret-encryption.spec.ts​

Cifrado AES-GCM de secretos con clave de 32 bytes en base64. No usa mocks de módulos; manipula process.env (se limpia en afterEach).

decodeEncryptionKey​

TestEscenarioResultado esperadoMocks
decodes a valid base64-encoded 32-byte keySe decodifica una clave base64 válida de 32 bytesDevuelve un Buffer de longitud 32Ninguno
names the env var when the key is missingValor undefinedLanza error con TEST_SECRET_ENCRYPTION_KEY is not setNinguno
names the env var when the key is an empty stringValor ''Lanza error ... is not setNinguno
rejects a key shorter than 32 bytesClave de 16 bytesError must decode to exactly 32 bytes (got 16)Ninguno
rejects a key longer than 32 bytesClave de 48 bytesError must decode to exactly 32 bytes (got 48)Ninguno

readEncryptionKey​

TestEscenarioResultado esperadoMocks
reads and decodes the named env varLa variable de entorno contiene una clave válidaDevuelve el Buffer igual a la claveprocess.env
fails at the point of use when the env var is missingVariable no definidaLanza ... is not set al leerlaprocess.env

isValidEncryptionKey​

TestEscenarioResultado esperadoMocks
accepts a 32-byte key and rejects anything elseClave válida, clave de 16 bytes, '' y undefinedtrue para la válida; false para el restoNinguno

encryptSecret / decryptSecret​

TestEscenarioResultado esperadoMocks
round-trips a plaintext string exactlySe cifra y descifra un JSON serializadoEl sobre es válido (isEncryptedEnvelope) y el descifrado coincide con el originalNinguno
uses a fresh IV on every callSe cifra dos veces el mismo textoiv y ciphertext difieren entre llamadasNinguno
never stores the plaintext verbatim inside the envelopeSe cifra un tokenEl JSON del sobre no contiene el texto planoNinguno
throws when the ciphertext is tampered withSe reemplaza el ciphertext por otroLanza authentication tag verification failedNinguno
throws when the auth tag is tampered withSe altera el primer byte del tagLanza authentication tag verification failedNinguno
throws when decrypted with a different keySe descifra con otra clave válidaLanza authentication tag verification failedNinguno

isEncryptedEnvelope​

TestEscenarioResultado esperadoMocks
returns true for a real envelopeSobre generado por encryptSecrettrueNinguno
returns false for legacy plaintext-shaped dataObjeto { type: 'Buffer', data: [1, 2, 3] }falseNinguno
returns false for null, undefined, and primitivesnull, undefined, string y númerofalse en todosNinguno
returns false for an object missing one of the three fieldsSobre sin tagfalseNinguno

src/app/core/filters/all-exceptions.filter.spec.ts​

Filtro global de excepciones: oculta errores internos y conserva los controlados.

AllExceptionsFilter — unhandled exceptions​

TestEscenarioResultado esperadoMocks
responds 500 with a generic message and never leaks the internal errorSe lanza un Error con detalle de MongoDB (E11000 ...)Status 500 y body { statusCode: 500, status: 'error', message: 'Internal server error', data: {} }; el JSON no contiene E11000ArgumentsHost falso (status/json), Logger.prototype.error
keeps the internal message and stack in the server logSe lanza Error('connection refused 10.0.0.5:27017')Logger.error recibe [UnhandledException] <mensaje> y el stackLogger.prototype.error
responds 500 with the generic message when a non-Error value is thrownSe lanza un objeto plano que no es ErrorStatus 500 con el mismo body genéricoArgumentsHost, Logger

AllExceptionsFilter — controlled exceptions​

TestEscenarioResultado esperadoMocks
preserves the HttpException message and fail status for 4xxNotFoundException('Usuario no encontrado')Status 404 y body con status: 'fail', el mensaje original y data: {}ArgumentsHost, Logger
preserves the AppException body as-isAppException('Ticket bloqueado', 409, { ticketId: 't-1' })Status 409 y body con mensaje, status: 'fail' y data: { ticketId: 't-1' }ArgumentsHost, Logger

src/app/core/guards/api-key.guard.spec.ts​

Guard de API key en el body: valida hash, estado, expiración, tenant y whitelist de dominios.

ApiKeyGuard — missing apiKey in body​

TestEscenarioResultado esperadoMocks
should throw UnauthorizedException when body has no apiKey fieldBody sin campo apiKeyRechaza con UnauthorizedExceptionApiKeyService, TenantService
should throw UnauthorizedException when body is emptyBody {}Rechaza con UnauthorizedExceptionApiKeyService, TenantService

ApiKeyGuard — invalid key​

TestEscenarioResultado esperadoMocks
should throw UnauthorizedException when no record matches the hashfindByCode devuelve nullUnauthorizedExceptionApiKeyService.findByCode

ApiKeyGuard — inactive key​

TestEscenarioResultado esperadoMocks
should throw UnauthorizedException when isActive is falseRegistro con isActive: falseUnauthorizedExceptionApiKeyService.findByCode

ApiKeyGuard — expired key​

TestEscenarioResultado esperadoMocks
should throw UnauthorizedException when dateExpiration is in the pastdateExpiration anterior a ahoraUnauthorizedExceptionApiKeyService.findByCode

ApiKeyGuard — tenant validation​

TestEscenarioResultado esperadoMocks
should throw UnauthorizedException when tenant does not existfindByMongoId devuelve nullUnauthorizedExceptionApiKeyService, TenantService.findByMongoId
should throw UnauthorizedException when tenant isActive is falseTenant con isActive: falseUnauthorizedExceptionApiKeyService, TenantService

ApiKeyGuard — domain whitelist​

El tenant de estos casos tiene domainWhitelist: ['acme.com'].

TestEscenarioResultado esperadoMocks
should throw ForbiddenException when origin is not in whitelistHeader origin: https://evil.comForbiddenExceptionApiKeyService, TenantService
should throw ForbiddenException when no origin/referer and whitelist is setSin origin ni refererForbiddenExceptionApiKeyService, TenantService
should allow when origin hostname ends with a whitelisted domainorigin: https://app.acme.comResuelve trueApiKeyService, TenantService
should allow when referer matches (no origin header)Solo referer: https://acme.com/pageResuelve trueApiKeyService, TenantService

ApiKeyGuard — body stripping​

TestEscenarioResultado esperadoMocks
should remove apiKey from body before passing to the handlerBody { apiKey, name, age }req.body queda { name: 'John', age: 30 } sin apiKeyApiKeyService, TenantService
should leave body empty object when apiKey was the only fieldBody solo con apiKeyreq.body es {}ApiKeyService, TenantService

ApiKeyGuard — successful validation​

TestEscenarioResultado esperadoMocks
should return true and attach tenant and apiKey record to requestKey, tenant y body válidosDevuelve true; req.tenant y req.apiKey quedan adjuntosApiKeyService, TenantService
should call findByCode with SHA-256 hash of the raw keyKey válida en texto planofindByCode se invoca con el hash y nunca con la key crudaApiKeyService, TenantService

src/app/core/guards/auth.guard.spec.ts​

Guard JWT (header x-api-key): verifica token, usuario activo y tenant operativo.

AuthGuard​

TestEscenarioResultado esperadoMocks
attaches the user when token, user and tenant are validToken válido, usuario activo, tenant operativoResuelve true, request.user es el usuario y se llama hasOperationalTenant(tenantId)JwtService.verify, UserService.findByEmail/hasOperationalTenant
rejects a user whose tenant is not operationalhasOperationalTenant devuelve falseUnauthorizedException y request.user sin definirJwtService, UserService
rejects a request without token before querying usersSin headersUnauthorizedException; no se consulta findByEmailJwtService, UserService
rejects an invalid or expired tokenverify lanza jwt expiredUnauthorizedException; no se consulta findByEmailJwtService, UserService
rejects an unknown userfindByEmail devuelve nullUnauthorizedException; no se valida el tenantJwtService, UserService
rejects an inactive user without checking the tenantUsuario con isActive: falseUnauthorizedException; no se valida el tenantJwtService, UserService

src/app/core/guards/permissions.guard.spec.ts​

Guard basado en @RequirePermission; resuelve permisos del rol vía PermissionResolverService.

PermissionsGuard​

TestEscenarioResultado esperadoMocks
allows access when the handler has no @RequirePermission metadataHandler sin decoradorResuelve true; el resolver no se invocaPermissionResolverService.resolve, Reflector real
rejects when the request has no user attachedRequest sin user en handler protegidoForbiddenExceptionPermissionResolverService
rejects when the resolved acronym set does not include the required permissionEl set resuelto es CONTACT_READ, se exige CONTACT_CREATEForbiddenException; resolve llamado con (tenantId, roleId)PermissionResolverService.resolve
allows access when the resolved acronym set includes the required permissionEl set resuelto incluye CONTACT_CREATEResuelve truePermissionResolverService.resolve

src/app/core/guards/platform-admin.guard.spec.ts​

PlatformAdminGuard​

TestEscenarioResultado esperadoMocks
allows a user flagged as platform adminuser.isPlatformAdmin === trueDevuelve trueContexto HTTP falso
rejects %s (it.each, 4 filas)Filas: tenant admin sin el flag (userType: 'admin'); flag en false; flag truthy no booleano ('true'); request sin userLanza ForbiddenException en cada filaContexto HTTP falso

src/app/core/guards/roles.guard.spec.ts​

Guard basado en @Roles, compara con user.userType.

RolesGuard​

TestEscenarioResultado esperadoMocks
allows access when the handler has no @Roles metadataHandler sin decorador, usuario agentDevuelve trueReflector real, contexto falso
rejects a user whose userType is not in the required rolesHandler @Roles('admin'), usuario agentForbiddenExceptionContexto falso
rejects when the request has no user attachedHandler protegido sin userForbiddenExceptionContexto falso
allows a user whose userType matches the required roleHandler @Roles('admin'), usuario adminDevuelve trueContexto falso

src/app/core/interceptors/response.interceptor.spec.ts​

Normaliza las respuestas; en GET garantiza { results, meta } sin perder claves propias del endpoint (p. ej. ids, unreadCount).

ResponseInterceptor — @RawResponse routes​

TestEscenarioResultado esperadoMocks
returns the handler payload untouched, not wrappedHandler decorado con @RawResponse devuelve 'challenge-123'La respuesta es exactamente 'challenge-123'ExecutionContext, CallHandler (of(payload))
keeps wrapping handlers without the decoratorPOST con { message, data: { a: 1 } }Respuesta con status: 'success' y data: { a: 1 }ExecutionContext, CallHandler

ResponseInterceptor — GET​

TestEscenarioResultado esperadoMocks
keeps the endpoint own keys alongside results/metadata: { ids: [...] }data.ids se conserva; results es [] y meta es {}ExecutionContext, CallHandler
keeps a scalar payload such as unreadCountdata: { unreadCount: 52 }data.unreadCount es 52ExecutionContext, CallHandler
passes a normal paginated list through untoucheddata: { results, meta }data es igual a { results, meta }ExecutionContext, CallHandler
defaults results/meta when the controller returns nothingEl controlador devuelve undefineddata es { results: [], meta: {} } y message es 'OK'ExecutionContext, CallHandler
never lets an own key overwrite results or metadata: { results: [...], ids: [...] }results e ids se preservan sin sobrescribirseExecutionContext, CallHandler
wraps with the response status and a success flagdata: { results: [] } con mensaje 'listo'statusCode 200, status: 'success', message: 'listo'ExecutionContext, CallHandler

ResponseInterceptor — non-GET​

TestEscenarioResultado esperadoMocks
leaves the payload data as the controller returned itPOST con data anidadares.data igual al data originalExecutionContext, CallHandler
falls back to an empty object when there is no dataPATCH sin datares.data es {}ExecutionContext, CallHandler

src/app/core/media/media-processor.service.spec.ts​

MediaProcessorService — processImage​

TestEscenarioResultado esperadoMocks
converts a valid input buffer to a WEBP buffer/mimetype/extBuffer JPEG de 200x200 generado con sharpmimetype image/webp, ext .webp, buffer no vacío y metadata format webp (timeout 15 s)Ninguno (usa sharp real)

src/app/core/permissions/permission-resolver.service.spec.ts​

Resuelve el set de acrónimos de permisos por tenant+rol con caché en memoria (TTL de 10 s).

PermissionResolverService​

TestEscenarioResultado esperadoMocks
resolves the acronym set from the database on a cache missSin caché; el modelo devuelve dos permisosfind({ tenantId, roleId }), populate('permissions', 'acronym') y set con ambos acrónimosModelo RolePermission (find/populate/exec)
returns an empty set when the role has no permission grantsEl modelo devuelve []Set vacíoModelo RolePermission
does not query the database again on a cache hit within the TTLDos resolve consecutivosfind se llama una sola vez y el segundo resultado es igualModelo RolePermission
re-queries the database once the cache entry has expired past the TTLReloj falso: se avanza a 10001 ms y cambian los permisosfind se llama dos veces y se devuelve el set actualizadoModelo RolePermission, jest.useFakeTimers
clears the cache entry for a tenant+role when invalidate is calledSe llama invalidate entre dos resolvefind se llama dos vecesModelo RolePermission
keeps separate cache entries per tenant+role combinationSe resuelve con dos tenants distintos y el mismo rolfind se llama dos veces (cachés separadas)Modelo RolePermission

src/app/core/storage/storage.service.spec.ts​

Servicio S3/MinIO. El bucket es privado salvo el prefijo public/ (avatares), el guardián del criterio CA7.

StorageService — onModuleInit​

TestEscenarioResultado esperadoMocks
creates the bucket when it does not existHeadBucketCommand falla con NotFoundSe envían HeadBucketCommand y CreateBucketCommandS3Client.send, ConfigService
applies the public-read policy even when the bucket already existsEl bucket existeSe envía exactamente una PutBucketPolicyCommandS3Client.send, ConfigService
opens ONLY the public prefix, never the whole bucketSe inspecciona la policy enviadaEl único recurso es arn:aws:s3:::crm-files/public/*; no incluye bucket/* ni el bucket completoS3Client.send, ConfigService
grants read only — never write or delete — to the anonymous principalSe inspeccionan los statementsLa única acción es s3:GetObjectS3Client.send, ConfigService
starts anyway when the policy cannot be appliedPutBucketPolicy falla con AccessDeniedonModuleInit resuelve sin error (undefined)S3Client.send, ConfigService

StorageService — getSignedUrl​

TestEscenarioResultado esperadoMocks
delegates to the s3-request-presigner with a GetObjectCommand and a 24h expirationSe pide URL firmada para tenant/image/uuid.jpgDevuelve la URL del presigner, llamado con un GetObjectCommand y expiresIn de 86400 sgetSignedUrl (presigner), S3Client

StorageService — upload​

TestEscenarioResultado esperadoMocks
uploads the object and returns a url string (not directly usable without signing, since the bucket is private)Se sube un buffer JPEGDevuelve endpoint/bucket/keyS3Client.send, ConfigService

StorageService — uploadPublic​

TestEscenarioResultado esperadoMocks
prefixes the key so the object falls under the public policySe sube avatars/tenant/contact.jpgLa URL devuelta contiene el prefijo public/S3Client.send, ConfigService

src/app/core/utils/message-signature.utils.spec.ts​

signAgentBody​

TestEscenarioResultado esperadoMocks
firma los mensajes de agente y de operador con la firma en su propia líneaTipos AGENT y USER con nombreDevuelve *Nombre*:\nHolaNinguno
no firma al contacto ni a los mensajes de sistemaTipos CONTACT y SYSTEMEl body queda intactoNinguno
es idempotente: un body ya firmado no se firma dos vecesSe firma un body ya firmadoDevuelve el mismo body firmadoNinguno
devuelve el body intacto sin nombre utilizable o sin textoNombre en blanco, null o body vacíoBody original sin cambios ('' se mantiene)Ninguno

src/app/core/utils/tenant.utils.spec.ts​

isTenantOperational​

TestEscenarioResultado esperadoMocks
accepts an active tenant with active status{ isActive: true, status: 'active' }trueNinguno
rejects a tenant %s (it.each, 4 filas)Filas: desactivado (isActive: false); con status inactive; sin status; sin isActivefalse en cada filaNinguno
rejects %p (it.each, 2 filas)null y undefinedfalseNinguno

src/app/core/utils/transform.utils.spec.ts​

toStringArray​

TestEscenarioResultado esperadoMocks
returns undefined for undefined/nullValor undefined y nullundefinedNinguno
returns undefined for an empty string''undefinedNinguno
returns undefined for an empty array[]undefinedNinguno
returns undefined when the array only contains empty strings['']undefinedNinguno
wraps a single scalar value into a one-item arrayUn string con un ObjectIdArray de un elementoNinguno
returns the array as-is when given multiple distinct valuesDos ids distintosEl mismo arrayNinguno
dedupes repeated values while preserving first-seen orderIds con uno repetidoArray sin duplicados en orden de apariciónNinguno
trims whitespace and drops empty entries mixed with valid ones[' id ', '']['id'] recortado y sin vacíosNinguno

Casos it() documentados en esta página: 94 declaraciones (it/it.each): 18 + 5 + 15 + 6 + 4 + 2 + 4 + 10 + 1 + 6 + 8 + 4 + 3 + 8.