From e53fa7b8e51279b18642655d86a47186f1ba27d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= <55464917+jlucaso1@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:16:24 -0300 Subject: [PATCH] Feat improve testing coverage e2e (#1799) * fix: ensure proper socket closure and await connection termination in tests * feat(tests): enhance E2E tests for image and video message handling, including downloads and group interactions --- src/Socket/Client/websocket.ts | 7 + src/Socket/socket.ts | 4 +- .../e2e/send-receive-message.test-e2e.ts | 393 +++++++++++++++++- 3 files changed, 386 insertions(+), 18 deletions(-) diff --git a/src/Socket/Client/websocket.ts b/src/Socket/Client/websocket.ts index 113938ff..7e171eaf 100644 --- a/src/Socket/Client/websocket.ts +++ b/src/Socket/Client/websocket.ts @@ -45,7 +45,14 @@ export class WebSocketClient extends AbstractSocketClient { return } + const closePromise = new Promise(resolve => { + this.socket?.once('close', resolve) + }) + this.socket.close() + + await closePromise + this.socket = null } send(str: string | Uint8Array, cb?: (err?: Error) => void): boolean { diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index f5ce6c19..8787e642 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -605,7 +605,7 @@ export const makeSocket = (config: SocketConfig) => { }) } - const end = (error: Error | undefined) => { + const end = async (error: Error | undefined) => { if (closed) { logger.trace({ trace: error?.stack }, 'connection already closed') return @@ -623,7 +623,7 @@ export const makeSocket = (config: SocketConfig) => { if (!ws.isClosed && !ws.isClosing) { try { - ws.close() + await ws.close() } catch {} } diff --git a/src/__tests__/e2e/send-receive-message.test-e2e.ts b/src/__tests__/e2e/send-receive-message.test-e2e.ts index 89c07e54..64276be2 100644 --- a/src/__tests__/e2e/send-receive-message.test-e2e.ts +++ b/src/__tests__/e2e/send-receive-message.test-e2e.ts @@ -2,15 +2,23 @@ import { Boom } from '@hapi/boom' import { jest } from '@jest/globals' import { readFileSync } from 'node:fs' import P from 'pino' -import makeWASocket, { DisconnectReason, jidNormalizedUser, proto, useMultiFileAuthState } from '../../index' +import makeWASocket, { + DisconnectReason, + downloadContentFromMessage, + downloadMediaMessage, + jidNormalizedUser, + proto, + toBuffer, + useMultiFileAuthState +} from '../../index' jest.setTimeout(30_000) describe('E2E Tests', () => { - jest.useFakeTimers() - let sock: ReturnType let meJid: string | undefined + let meLid: string | undefined + let groupJid: string | undefined beforeAll(async () => { const { state, saveCreds } = await useMultiFileAuthState('baileys_auth_info') @@ -28,7 +36,20 @@ describe('E2E Tests', () => { const { connection, lastDisconnect } = update if (connection === 'open') { meJid = jidNormalizedUser(sock.user?.id) - resolve() + meLid = sock.user?.lid + + sock + .groupFetchAllParticipating() + .then(groups => { + const group = Object.values(groups).find(g => g.subject === 'Baileys Group Test') + if (group) { + groupJid = group.id + console.log(`Found test group "${group.subject}" with JID: ${groupJid}`) + } + + resolve() + }) + .catch(reject) } else if (connection === 'close') { const reason = (lastDisconnect?.error as Boom)?.output?.statusCode if (reason === DisconnectReason.loggedOut) { @@ -45,14 +66,7 @@ describe('E2E Tests', () => { afterAll(async () => { if (sock) { - await new Promise(resolve => { - sock.ev.on('connection.update', update => { - if (update.connection === 'close') { - resolve() - } - }) - sock.end(undefined) - }) + await sock.end(undefined) } }) @@ -253,10 +267,6 @@ describe('E2E Tests', () => { console.log('Sent poll message:', sentPoll!.key.id) expect(sentPoll?.message?.pollCreationMessageV3).toBeDefined() expect(sentPoll?.message?.pollCreationMessageV3?.name).toBe('E2E Test Poll') - - // This part of the test would require a second participant to vote - // and for the test to listen for the poll update message. - // For simplicity, we are only testing the sending of the poll here. }) test('should send a contact (vCard) message', async () => { @@ -279,4 +289,355 @@ describe('E2E Tests', () => { expect(sentMessage!.message?.contactMessage).toBeDefined() expect(sentMessage!.message?.contactMessage?.vcard).toContain('FN:E2E Test Contact') }) + + test('should send and download an image message', async () => { + const image = readFileSync('./Media/cat.jpeg') + const caption = 'E2E Test Image Download Success' + + let listener: ((data: { messages: proto.IWebMessageInfo[] }) => void) | undefined + let timeoutId: NodeJS.Timeout | undefined + + try { + const receivedMsgPromise = new Promise((resolve, reject) => { + listener = ({ messages }) => { + const msg = messages.find(m => m.message?.imageMessage?.caption === caption) + if (msg) { + resolve(msg) + } + } + + timeoutId = setTimeout(() => { + reject(new Error('Timed out waiting for the image message to be received')) + }, 30_000) + + sock.ev.on('messages.upsert', listener) + }) + + await sock.sendMessage(meJid!, { + image: image, + caption: caption + }) + + const receivedMsg = await receivedMsgPromise + + clearTimeout(timeoutId) + timeoutId = undefined + + console.log('Received image message, attempting to download...') + + const buffer = await downloadMediaMessage( + receivedMsg, + 'buffer', + {}, + { + logger: sock.logger, + reuploadRequest: m => sock.updateMediaMessage(m) + } + ) + + expect(Buffer.isBuffer(buffer)).toBe(true) + expect(buffer.length).toBeGreaterThan(0) + + console.log('Successfully downloaded the image.') + } finally { + if (listener) { + sock.ev.off('messages.upsert', listener) + } + + if (timeoutId) { + clearTimeout(timeoutId) + } + } + }) + + test('should send and download an image message via LID', async () => { + console.log(`Testing with self-LID: ${meLid}`) + + const image = readFileSync('./Media/cat.jpeg') + const caption = 'E2E Test LID Image Download' + + let listener: ((data: { messages: proto.IWebMessageInfo[] }) => void) | undefined + let timeoutId: NodeJS.Timeout | undefined + + try { + const receivedMsgPromise = new Promise((resolve, reject) => { + listener = ({ messages }) => { + const msg = messages.find(m => m.message?.imageMessage?.caption === caption) + if (msg) { + resolve(msg) + } + } + + timeoutId = setTimeout(() => { + reject(new Error('Timed out waiting for the LID image message to be received')) + }, 30_000) + + sock.ev.on('messages.upsert', listener) + }) + + await sock.sendMessage(meLid!, { + image: image, + caption: caption + }) + + const receivedMsg = await receivedMsgPromise + clearTimeout(timeoutId) + timeoutId = undefined + + console.log('Received LID image message, attempting to download...') + + const buffer = await downloadMediaMessage( + receivedMsg, + 'buffer', + {}, + { + logger: sock.logger, + reuploadRequest: m => sock.updateMediaMessage(m) + } + ) + + expect(Buffer.isBuffer(buffer)).toBe(true) + expect(buffer.length).toBeGreaterThan(0) + + console.log('Successfully downloaded the image sent via LID.') + } finally { + if (listener) { + sock.ev.off('messages.upsert', listener) + } + + if (timeoutId) { + clearTimeout(timeoutId) + } + } + }) + + test('should send and download an image using the low-level downloadContentFromMessage', async () => { + const image = readFileSync('./Media/cat.jpeg') + const caption = 'E2E Test Low-Level Download' + + let listener: ((data: { messages: proto.IWebMessageInfo[] }) => void) | undefined + let timeoutId: NodeJS.Timeout | undefined + + try { + const receivedMsgPromise = new Promise((resolve, reject) => { + listener = ({ messages }) => { + const msg = messages.find(m => m.message?.imageMessage?.caption === caption) + if (msg) { + resolve(msg) + } + } + + timeoutId = setTimeout(() => { + reject(new Error('Timed out waiting for the low-level test message')) + }, 30_000) + + sock.ev.on('messages.upsert', listener) + }) + + await sock.sendMessage(meJid!, { + image: image, + caption: caption + }) + + const receivedMsg = await receivedMsgPromise + clearTimeout(timeoutId) + timeoutId = undefined + + console.log('Received message for low-level download test, preparing to download...') + + const imageMessage = receivedMsg.message?.imageMessage + expect(imageMessage).toBeDefined() + + const downloadable: proto.Message.IImageMessage = { + url: imageMessage!.url, + mediaKey: imageMessage!.mediaKey, + directPath: imageMessage!.directPath + } + + const stream = await downloadContentFromMessage(downloadable, 'image') + const buffer = await toBuffer(stream) + + expect(Buffer.isBuffer(buffer)).toBe(true) + expect(buffer.length).toBeGreaterThan(0) + + console.log('Successfully downloaded the image using downloadContentFromMessage.') + } finally { + if (listener) { + sock.ev.off('messages.upsert', listener) + } + + if (timeoutId) { + clearTimeout(timeoutId) + } + } + }) + + test('should download a quoted image message using downloadContentFromMessage', async () => { + const image = readFileSync('./Media/cat.jpeg') + const originalCaption = 'This is the original media message' + const commandText = '-download' + + let imageListener: ((data: { messages: proto.IWebMessageInfo[] }) => void) | undefined + let commandListener: ((data: { messages: proto.IWebMessageInfo[] }) => void) | undefined + let timeoutId: NodeJS.Timeout | undefined + + try { + console.log('Sending initial image message...') + const receivedImagePromise = new Promise((resolve, reject) => { + imageListener = ({ messages }) => { + const msg = messages.find(m => m.message?.imageMessage?.caption === originalCaption) + if (msg) resolve(msg) + } + + sock.ev.on('messages.upsert', imageListener) + timeoutId = setTimeout(() => reject(new Error('Timed out waiting for initial image message')), 30_000) + }) + + const sentImageMessage = await sock.sendMessage(meJid!, { + image: image, + caption: originalCaption + }) + await receivedImagePromise + clearTimeout(timeoutId) + timeoutId = undefined + + if (imageListener) { + sock.ev.off('messages.upsert', imageListener) + } + + console.log('Initial image message sent and received.') + + console.log('Sending command message as a reply...') + const receivedCommandPromise = new Promise((resolve, reject) => { + commandListener = ({ messages }) => { + const msg = messages.find(m => m.message?.extendedTextMessage?.text === commandText) + if (msg) resolve(msg) + } + + sock.ev.on('messages.upsert', commandListener) + timeoutId = setTimeout(() => reject(new Error('Timed out waiting for command message')), 30_000) + }) + + await sock.sendMessage(meJid!, { text: commandText }, { quoted: sentImageMessage }) + const receivedCommandMessage = await receivedCommandPromise + clearTimeout(timeoutId) + timeoutId = undefined + console.log('Command message received.') + + console.log('Extracting quoted message and attempting download...') + + const quotedMessage = receivedCommandMessage.message?.extendedTextMessage?.contextInfo?.quotedMessage + expect(quotedMessage).toBeDefined() + + const quotedImage = quotedMessage!.imageMessage + expect(quotedImage).toBeDefined() + + const downloadable: proto.Message.IImageMessage = { + url: quotedImage!.url, + mediaKey: quotedImage!.mediaKey, + directPath: quotedImage!.directPath + } + + const stream = await downloadContentFromMessage(downloadable, 'image') + const buffer = await toBuffer(stream) + + expect(Buffer.isBuffer(buffer)).toBe(true) + expect(buffer.length).toBeGreaterThan(0) + + console.log('Successfully downloaded quoted image using downloadContentFromMessage.') + } finally { + if (imageListener) sock.ev.off('messages.upsert', imageListener) + if (commandListener) sock.ev.off('messages.upsert', commandListener) + if (timeoutId) clearTimeout(timeoutId) + } + }) + + test('should download a quoted videos message within a group', async () => { + if (!groupJid) { + console.warn('⚠️ Skipping group test because "Baileys Group Test" was not found.') + return + } + + const video = readFileSync('./Media/ma_gif.mp4') + const originalCaption = 'This is the original media message for the group test' + const commandText = '-download group' + + let videoListener: ((data: { messages: proto.IWebMessageInfo[] }) => void) | undefined + let commandListener: ((data: { messages: proto.IWebMessageInfo[] }) => void) | undefined + let timeoutId: NodeJS.Timeout | undefined + + try { + console.log(`Sending initial video message to group ${groupJid}...`) + const receivedVideoPromise = new Promise((resolve, reject) => { + videoListener = ({ messages }) => { + const msg = messages.find( + m => m.key.remoteJid === groupJid && m.message?.videoMessage?.caption === originalCaption + ) + if (msg) resolve(msg) + } + + sock.ev.on('messages.upsert', videoListener) + timeoutId = setTimeout(() => reject(new Error('Timed out waiting for initial group image message')), 30_000) + }) + + const sentVideoMessage = await sock.sendMessage(groupJid, { + video: video, + caption: originalCaption + }) + await receivedVideoPromise + clearTimeout(timeoutId) + timeoutId = undefined + if (videoListener) sock.ev.off('messages.upsert', videoListener) + console.log('Initial group image message sent and received.') + + console.log('Sending command message as a reply in the group...') + const receivedCommandPromise = new Promise((resolve, reject) => { + commandListener = ({ messages }) => { + const msg = messages.find( + m => m.key.remoteJid === groupJid && m.message?.extendedTextMessage?.text === commandText + ) + if (msg) resolve(msg) + } + + sock.ev.on('messages.upsert', commandListener) + timeoutId = setTimeout(() => reject(new Error('Timed out waiting for group command message')), 30_000) + }) + + await sock.sendMessage(groupJid, { text: commandText }, { quoted: sentVideoMessage }) + const receivedCommandMessage = await receivedCommandPromise + clearTimeout(timeoutId) + timeoutId = undefined + console.log('Group command message received.') + + console.log('Extracting quoted message from group chat and attempting download...') + + const quotedMessage = receivedCommandMessage.message?.extendedTextMessage?.contextInfo?.quotedMessage + expect(quotedMessage).toBeDefined() + + console.log('quotedMessage', JSON.stringify(quotedMessage, null, 2)) + + const quotedVideo = quotedMessage!.videoMessage + expect(quotedVideo).toBeDefined() + + console.log('quotedVideo', JSON.stringify(quotedVideo, null, 2)) + + const downloadable: proto.Message.IVideoMessage = { + url: quotedVideo!.url, + mediaKey: quotedVideo!.mediaKey, + directPath: quotedVideo!.directPath + } + + const stream = await downloadContentFromMessage(downloadable, 'video') + const buffer = await toBuffer(stream) + + expect(Buffer.isBuffer(buffer)).toBe(true) + expect(buffer.length).toBeGreaterThan(0) + + console.log('Successfully downloaded quoted image from group message.') + } finally { + if (videoListener) sock.ev.off('messages.upsert', videoListener) + if (commandListener) sock.ev.off('messages.upsert', commandListener) + if (timeoutId) clearTimeout(timeoutId) + } + }) })