Skip to main content

Casos de prueba: Meta saliente y OAuth

Casos de prueba de la autorización OAuth de Instagram, el cliente Graph, el despachador de mensajes salientes, la descarga de media, la renovación de tokens y el registro del módulo.

Specs fuente (bajo src/app/modules/meta):

  • controllers/meta-oauth.controller.spec.ts
  • services/meta-oauth.service.spec.ts
  • services/meta-graph.client.spec.ts
  • services/meta-outbound.dispatcher.spec.ts
  • services/meta-media.downloader.spec.ts
  • services/meta-token-refresh.service.spec.ts
  • jobs/meta-token-refresh.job.spec.ts
  • meta.module.spec.ts

Resumen: 8 archivos spec, 18 bloques describe y 68 casos it() (cada it.each cuenta como un caso).


meta-oauth.controller.spec.ts​

Test HTTP con supertest; AuthGuard inyecta un usuario con tenant, PermissionsGuard siempre permite, y MetaOAuthService se reemplaza por un mock.

MetaOAuthController (HTTP)​

TestEscenarioResultado esperadoMocks
returns the Instagram authorization url for the logged user tenantPOST /authorize con un departmentIds válido200, data.url con la URL de autorización y buildAuthorizeUrl llamado con tenant, id de usuario y departmentIdsMetaOAuthService, guards
rejects malformed department ids before reaching the servicePOST /authorize con departmentIds: ['not-an-id']400 y el servicio no se invocaMetaOAuthService, guards
redirects the browser to wherever the service decidedGET /callback con code y state302 con Location igual a lo que devuelve el servicio; handleCallback recibe code, state y error indefinidoMetaOAuthService
passes the cancellation from Instagram to the serviceGET /callback con error=access_deniedhandleCallback recibe error: 'access_denied' sin code ni stateMetaOAuthService

meta-oauth.service.spec.ts​

Test de integración con MongoDB en memoria; Graph, configuración y departamentos son mocks, y el state se firma con un JwtService real.

buildAuthorizeUrl​

TestEscenarioResultado esperadoMocks
points to Instagram with our app, redirect, scopes and a signed stateGenerar URL con un departamento del tenantApunta a instagram.com/oauth/authorize con client_id, redirect_uri, response_type=code, scopes de Instagram y un state JWT firmado (propósito meta-oauth, tenant, usuario, departamentos)config, departments
rejects departments that do not belong to the tenantdepartmentIds de un departamento ajenoBadRequestExceptiondepartments
rejects reauthorizing a connection that is not an Instagram one of the tenantconnectionId de una conexión web o de otro tenantNotFoundException en ambos casosconfig, departments
answers 404 when Meta is not configured for this deploymentFalta meta.instagramAppIdAppException con código META_NOT_CONFIGURED y estado 404; handleCallback también lanza NotFoundExceptionconfig

handleCallback​

TestEscenarioResultado esperadoMocks
creates the connection, subscribes the webhook and sends the user backCallback válido con code y state firmadoRedirige con meta=connected y connectionId; conexión @mi_marca CONNECTED con departamentos, token de larga duración cifrado y expiración de unos 60 días; exchangeCode y subscribeToWebhooks llamados con los argumentos esperadosGraph, config, departments
sends the user back with "denied" when they cancel on Instagramerror: access_deniedRedirige con meta=error&reason=denied sin intercambiar el codeGraph
rejects %s (it.each)state ausente, basura, firmado con otro secret, expirado o de otro propósitoRedirige con reason=invalid_state y no se intercambia el codeGraph
stores nothing when the code exchange failsexchangeCode rechaza con MetaGraphErrorRedirige con reason=exchange_failed y no se crea ninguna conexiónGraph rechazado
stores nothing when the webhook subscription failssubscribeToWebhooks rechazaRedirige con reason=subscription_failed y no se crea ninguna conexiónGraph rechazado
refuses an account already connected in another tenantLa cuenta ya está en otro tenantRedirige con reason=account_in_use y el tenant actual queda sin conexionesGraph
updates the existing connection of this tenant instead of creating anotherLa cuenta ya existe en este tenantReutiliza la conexión (misma id, una sola) y actualiza el tokenGraph
reauthorizes a disconnected connection and brings it back onlinestate con connectionId de una conexión con error de authRedirige con meta=connected; la conexión vuelve a CONNECTED y authErrorAt queda en nullGraph
refuses to reauthorize a connection with a different Instagram accountLa conexión tiene otra cuenta distintaRedirige con reason=account_mismatchGraph

meta-graph.client.spec.ts​

Tests unitarios con fetch global espiado (jest.spyOn).

MetaGraphClient — OAuth / exchangeCode​

TestEscenarioResultado esperadoMocks
posts the form Instagram expectsIntercambiar el codePOST a api.instagram.com/oauth/access_token con formulario client_id, client_secret, grant_type, redirect_uri, codefetch
reads a %s response (it.each: flat, wrapped in data[])Respuesta plana o envuelta en data[]Devuelve accessToken: 'short' y userId: '555'fetch
turns the OAuth error shape into a MetaGraphErrorRespuesta 400 con error_type, code y error_messageMetaGraphError con código 400 y mensaje que incluye "already used"fetch

MetaGraphClient — OAuth​

TestEscenarioResultado esperadoMocks
exchanges for the long-lived token on graph.instagram.com without versionCanje a token de larga duraciónDevuelve accessToken y expiresInSeconds; URL graph.instagram.com/access_token con grant_type=ig_exchange_token y client_secretfetch
reads the professional account id (user_id, not id) from a %s /me (it.each: flat, wrapped in data[])/me con respuesta plana o en data[]Se usa user_id y username; ruta /v25.0/me con fields=user_id,username,namefetch
refreshes a long-lived token on graph.instagram.com without versionRenovar un tokenDevuelve el token renovado y su vigencia; URL refresh_access_token con grant_type=ig_refresh_token y el token actualfetch

subscribeToWebhooks​

TestEscenarioResultado esperadoMocks
subscribes the account to messages and seen eventsSuscribir una cuentaPOST a /v25.0/{cuenta}/subscribed_apps con subscribed_fields=messages,messaging_seenfetch
fails when Meta does not confirm the subscriptionRespuesta success: falseRechaza con MetaGraphErrorfetch

MetaGraphClient — getUserProfile​

TestEscenarioResultado esperadoMocks
asks Instagram for name, username and picturePerfil de un contacto de InstagramHost graph.instagram.com, fields=name,username,profile_pic; devuelve name, username y profilePicfetch
asks Messenger for first and last name and joins themPerfil de un PSID de FacebookHost graph.facebook.com, ruta /v25.0/psid; nombre unido "Juana Pérez" y profilePicfetch
leaves the name out when Messenger returns neither partRespuesta sin nombre ni apellidoDevuelve solo profilePicfetch

MetaGraphClient — appsecret_proof​

TestEscenarioResultado esperadoMocks
signs every Messenger call with the page token and the app secretsendMessage por facebook con app secret configuradoResuelve messageId; ruta /v25.0/page-1/messages con appsecret_proof igual al HMAC SHA-256 del tokenfetch, config
never signs Instagram calls, which belong to another appsendMessage por instagramLa URL no lleva appsecret_prooffetch, config
sends no proof when the app secret is not configuredFacebook sin app secretLa URL no lleva appsecret_prooffetch, config

meta-outbound.dispatcher.spec.ts​

Tests unitarios del despachador con repositorios, storage, sockets, Graph y registro mockeados. El helper de token se reemplaza por uno fijo.

MetaOutboundDispatcher​

TestEscenarioResultado esperadoMocks
registers one dispatcher per Meta channel, both backed by this oneonModuleInit y despacho por el segundo registroSe registran Instagram y Facebook sin supports; el de Facebook envía a la página con destinatario PSIDregistry, graph, connections

sending​

TestEscenarioResultado esperadoMocks
sends the text to the contact igsid and stores the returned message idMensaje de texto a un contacto de InstagramsendMessage con destinatario IGSID, texto y messaging_type: RESPONSE; guarda el mid devueltograph, messages
sends an attachment as a url Meta can fetchMensaje con MediaIdAdjunto del tipo del archivo con URL firmada del storagefiles, storage, graph
never sends the filename as plain text when the file is goneEl archivo ya no existeNo se llama a sendMessage y no se marca fallidofiles, graph
adds the HUMAN_AGENT tag only when it is enabledLa configuración habilita la etiquetaPayload con messaging_type: MESSAGE_TAG y tag: HUMAN_AGENTconfig, graph
sends a Messenger reply from the page to the contact psidTicket de FacebookEnvío desde la página al PSID y guarda el midgraph, messages
does nothing when the ticket has no Meta connectionNo se encuentra la conexiónNo se envía nadaconnections

rejections​

TestEscenarioResultado esperadoMocks
error %i marks the message failed as %s (it.each: 10, 551, 36000, 99999)Graph rechaza con ese códigomarkFailed con OUT_OF_WINDOW, RECIPIENT_UNAVAILABLE, UNSUPPORTED_MEDIA o UNKNOWN y detalle code - mensajegraph rechazado, messages
tells the backoffice right away, with the reasonError 10Se emite broadcastMessageAckChanged con estado failed y motivo OUT_OF_WINDOWsocket broadcast
error 190 also disconnects the connectionError 190markMetaAuthError sobre la conexión y mensaje fallido con motivo AUTH_ERRORconnections, messages
emits nothing when the message was already delivered or readmarkFailed devuelve nullNo se emite broadcastmessages, socket broadcast

transient failures​

TestEscenarioResultado esperadoMocks
retries and succeeds without marking anything failedPrimer intento con error transitorio y segundo exitoso2 llamadas, no se marca fallido y se guarda el mid del segundograph, fake timers
gives up after the retries and marks the message failedError 429 persistenteSe agotan los reintentos (DELAYS_MS.length + 1 llamadas) y se marca fallido como RATE_LIMITEDgraph, fake timers
does not retry a permanent rejectionError 10 (permanente)Una sola llamadagraph, fake timers

markRead​

TestEscenarioResultado esperadoMocks
tells Instagram the agent saw the conversationmarkRead con mensajes en InstagramsendSenderAction con mark_seengraph
tells Messenger the agent saw the conversation, from the pagemarkRead en FacebooksendSenderAction desde la página al PSID con mark_seengraph
does nothing without messages or without a Meta connectionLista vacía y conexión inexistenteNo se llama a sendSenderActiongraph, connections

meta-media.downloader.spec.ts​

MetaMediaDownloader​

TestEscenarioResultado esperadoMocks
returns the bytes with the type taken from the responseRespuesta 200 image/jpeg; charset=binaryDevuelve buffer, mimetype image/jpeg y fileType imagefetch
rejects by content-length before reading the bodycontent-length mayor al límite de imagenMetaMediaUnavailableErrorfetch
rejects while streaming when there is no content-lengthStream sin content-length que excede el límiteMetaMediaUnavailableErrorfetch
treats HTTP %i as a permanent failure (it.each 403, 404, 410)Respuesta con ese estadoMetaMediaUnavailableErrorfetch
treats HTTP %i as transient so the job retries (it.each 429, 500, 503)Respuesta con ese estadoError genérico que no es MetaMediaUnavailableErrorfetch
treats a network failure or timeout as transientfetch rechaza con TimeoutErrorError que no es MetaMediaUnavailableError y su mensaje contiene TimeoutErrorfetch

meta-token-refresh.service.spec.ts​

Test de integración con MongoDB en memoria; Graph mockeado y fecha fija inyectada.

MetaTokenRefreshService (integration)​

TestEscenarioResultado esperadoMocks
refreshes a token that expires within 10 days and stores it encryptedToken que vence en 5 díasSe renueva, se guarda cifrado con nueva expiración y la conexión sigue CONNECTEDGraph
leaves alone a token that still has more than 10 daysVence en 30 díasNo se llama a GraphGraph
skips connections already flagged and tokens that never expireConexión con error de auth y token sin expiraciónNo se llama a GraphGraph
ignores connections of other channelsConexión de Facebook por vencerNo se llama a GraphGraph
disconnects the connection when Meta says the token was revokedGraph responde error 190Conexión DISCONNECTED y authErrorAt con fechaGraph rechazado
keeps the old token on any other failure and retries on the next runFallo transitorio y luego éxitoEl token viejo se conserva y sigue CONNECTED; en la siguiente corrida se renuevaGraph (rechaza una vez)
one failing connection does not stop the othersUna conexión falla y otra noLa que falla conserva el token y la sana se renuevaGraph
disconnects a token that expired without a successful refreshToken ya vencido (-1 día)No se llama a Graph, conexión DISCONNECTED y authErrorAt con fechaGraph

meta-token-refresh.job.spec.ts​

MetaTokenRefreshJob​

TestEscenarioResultado esperadoMocks
runs the refreshhandleIntervalrefreshExpiringTokens se llama una vezrefresh service
never lets an error escape the intervalEl servicio rechaza con db downhandleInterval resuelve sin errorrefresh service rechazado
is scheduled every hourMetadata SCHEDULE_INTERVAL_OPTIONS del métodotimeout igual a 3 600 000 msninguno

meta.module.spec.ts​

MetaModule​

TestEscenarioResultado esperadoMocks
registers the queue worker and its processing serviceMetadata de providers del móduloIncluye MetaWebhookProcessor y MetaWebhookProcessorServiceninguno
registers the outbound dispatcher so tickets on this channel can replyMetadata de providersIncluye MetaOutboundDispatcherninguno
binds the inbound handler the worker depends on to MetaInboundServiceMetadata de providersExiste { provide: MetaInboundHandler, useExisting: MetaInboundService }ninguno