Buckets:
| /** | |
| * Unit tests for WhatsApp-native bridge payload helpers. | |
| * | |
| * These tests avoid importing bridge.js because that file starts an HTTP | |
| * server and Baileys socket at module load. Keep the helper module pure. | |
| */ | |
| import { strict as assert } from 'node:assert'; | |
| import { createHash } from 'node:crypto'; | |
| import { mkdtempSync } from 'node:fs'; | |
| import { tmpdir } from 'node:os'; | |
| import path from 'node:path'; | |
| import { getAggregateVotesInPollMessage } from '@whiskeysockets/baileys'; | |
| import { | |
| buildPollPayload, | |
| buildTextSendPayload, | |
| createBoundedMessageStore, | |
| appendMediaFailureNote, | |
| extractBridgeEvent, | |
| mediaPayloadForFile, | |
| pollCreationMessageFromPayload, | |
| pollUpdateForAggregation, | |
| } from './bridge_helpers.js'; | |
| // -- quoted outbound text ------------------------------------------------- | |
| { | |
| const store = createBoundedMessageStore(2); | |
| store.remember({ | |
| key: { | |
| id: 'inbound-1', | |
| remoteJid: '15551234567@s.whatsapp.net', | |
| participant: '15550001111@s.whatsapp.net', | |
| fromMe: false, | |
| }, | |
| message: { conversation: 'original text' }, | |
| }); | |
| const { content, options } = buildTextSendPayload('reply text', { | |
| chatId: '15551234567@s.whatsapp.net', | |
| replyTo: 'inbound-1', | |
| messageStore: store, | |
| }); | |
| assert.deepEqual(content, { text: 'reply text' }); | |
| assert.equal(options.quoted.key.id, 'inbound-1'); | |
| assert.equal(options.quoted.message.conversation, 'original text'); | |
| console.log(' ✓ text replies include Baileys quoted message when resolvable'); | |
| } | |
| { | |
| const store = createBoundedMessageStore(2); | |
| const { content, options } = buildTextSendPayload('plain text', { | |
| chatId: '15551234567@s.whatsapp.net', | |
| replyTo: 'missing-id', | |
| messageStore: store, | |
| }); | |
| assert.deepEqual(content, { text: 'plain text' }); | |
| assert.deepEqual(options, {}); | |
| console.log(' ✓ unresolved replyTo falls back to plain text'); | |
| } | |
| // -- inbound quote/media/native metadata -------------------------------- | |
| { | |
| const event = await extractBridgeEvent({ | |
| msg: { | |
| key: { | |
| id: 'incoming-1', | |
| remoteJid: '15551234567@s.whatsapp.net', | |
| participant: '15550001111@s.whatsapp.net', | |
| fromMe: false, | |
| }, | |
| pushName: 'Tester', | |
| messageTimestamp: 123, | |
| message: { | |
| extendedTextMessage: { | |
| text: 'approved', | |
| contextInfo: { | |
| stanzaId: 'outbound-1', | |
| participant: '15559998888@s.whatsapp.net', | |
| remoteJid: '15551234567@s.whatsapp.net', | |
| quotedMessage: { conversation: 'approve deploy?' }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| chatId: '15551234567@s.whatsapp.net', | |
| senderId: '15550001111@s.whatsapp.net', | |
| senderNumber: '15550001111', | |
| botIds: ['15559998888@s.whatsapp.net'], | |
| downloadMedia: async () => Buffer.from(''), | |
| }); | |
| assert.equal(event.quotedMessageId, 'outbound-1'); | |
| assert.equal(event.quotedParticipant, '15559998888@s.whatsapp.net'); | |
| assert.equal(event.quotedRemoteJid, '15551234567@s.whatsapp.net'); | |
| assert.equal(event.quotedText, 'approve deploy?'); | |
| assert.equal(event.hasQuotedMessage, true); | |
| assert.equal(event.body, 'approved'); | |
| console.log(' ✓ inbound quoted metadata includes quoted text'); | |
| } | |
| { | |
| const event = await extractBridgeEvent({ | |
| msg: { | |
| key: { id: 'doc-1', remoteJid: '15551234567@s.whatsapp.net', fromMe: false }, | |
| messageTimestamp: 123, | |
| message: { | |
| documentMessage: { | |
| caption: 'see attached', | |
| fileName: 'report.pdf', | |
| mimetype: 'application/pdf', | |
| }, | |
| }, | |
| }, | |
| chatId: '15551234567@s.whatsapp.net', | |
| senderId: '15550001111@s.whatsapp.net', | |
| senderNumber: '15550001111', | |
| downloadMedia: async () => Buffer.from('pdf'), | |
| writeMediaFile: async () => '/tmp/report.pdf', | |
| }); | |
| assert.equal(event.hasMedia, true); | |
| assert.equal(event.mediaType, 'document'); | |
| assert.equal(event.mime, 'application/pdf'); | |
| assert.equal(event.fileName, 'report.pdf'); | |
| assert.equal(event.nativeType, 'documentMessage'); | |
| assert.deepEqual(event.mediaUrls, ['/tmp/report.pdf']); | |
| console.log(' ✓ inbound document metadata preserves MIME and filename'); | |
| } | |
| { | |
| const cacheDir = mkdtempSync(path.join(tmpdir(), 'hermes-wa-doc-')); | |
| const event = await extractBridgeEvent({ | |
| msg: { | |
| key: { id: 'doc-2', remoteJid: '15551234567@s.whatsapp.net', fromMe: false }, | |
| messageTimestamp: 123, | |
| message: { | |
| documentMessage: { | |
| caption: 'see attached', | |
| fileName: 'report', | |
| mimetype: 'application/pdf', | |
| }, | |
| }, | |
| }, | |
| chatId: '15551234567@s.whatsapp.net', | |
| senderId: '15550001111@s.whatsapp.net', | |
| senderNumber: '15550001111', | |
| downloadMedia: async () => Buffer.from('pdf'), | |
| cacheDirs: { document: cacheDir }, | |
| }); | |
| assert.equal(event.mediaUrls.length, 1); | |
| assert.ok(event.mediaUrls[0].endsWith('_report.pdf'), event.mediaUrls[0]); | |
| console.log(' ✓ MIME extension is preserved when document filename has none'); | |
| } | |
| { | |
| const event = await extractBridgeEvent({ | |
| msg: { | |
| key: { id: 'loc-1', remoteJid: '15551234567@s.whatsapp.net', fromMe: false }, | |
| messageTimestamp: 123, | |
| message: { | |
| locationMessage: { | |
| name: 'HQ', | |
| degreesLatitude: 41.015, | |
| degreesLongitude: 28.979, | |
| }, | |
| }, | |
| }, | |
| chatId: '15551234567@s.whatsapp.net', | |
| senderId: '15550001111@s.whatsapp.net', | |
| senderNumber: '15550001111', | |
| }); | |
| assert.equal(event.mediaType, 'location'); | |
| assert.equal(event.body, '[Location: HQ 41.015,28.979]'); | |
| assert.deepEqual(event.nativeMetadata.location, { | |
| name: 'HQ', | |
| address: '', | |
| latitude: 41.015, | |
| longitude: 28.979, | |
| isLive: false, | |
| }); | |
| console.log(' ✓ native location messages get text fallback and metadata'); | |
| } | |
| { | |
| const event = await extractBridgeEvent({ | |
| msg: { | |
| key: { id: 'poll-1', remoteJid: '15551234567@s.whatsapp.net', fromMe: false }, | |
| messageTimestamp: 123, | |
| message: { | |
| pollCreationMessage: { | |
| name: 'Approve deploy?', | |
| options: [{ optionName: 'Approve' }, { optionName: 'Deny' }], | |
| selectableOptionsCount: 1, | |
| }, | |
| }, | |
| }, | |
| chatId: '15551234567@s.whatsapp.net', | |
| senderId: '15550001111@s.whatsapp.net', | |
| senderNumber: '15550001111', | |
| }); | |
| assert.equal(event.mediaType, 'poll'); | |
| assert.equal(event.body, '[Poll: Approve deploy? Options: Approve, Deny]'); | |
| assert.deepEqual(event.nativeMetadata.poll.options, ['Approve', 'Deny']); | |
| console.log(' ✓ poll creation messages get text fallback and metadata'); | |
| } | |
| // -- outbound media/poll helpers ----------------------------------------- | |
| { | |
| const payload = mediaPayloadForFile({ | |
| buffer: Buffer.from('gif89a'), | |
| filePath: '/tmp/loop.gif', | |
| mediaType: 'image', | |
| caption: 'loop', | |
| }); | |
| assert.ok(payload.image, 'pure helper fallback keeps raw GIF as image bytes'); | |
| assert.equal(payload.gifPlayback, undefined); | |
| assert.equal(payload.mimetype, 'image/gif'); | |
| assert.equal(payload.caption, 'loop'); | |
| console.log(' ✓ local GIF helper fallback stays truthful; live bridge converts to gifPlayback when possible'); | |
| } | |
| { | |
| const payload = buildPollPayload({ | |
| question: 'Proceed?', | |
| options: ['Approve', 'Deny'], | |
| selectableCount: 1, | |
| }); | |
| assert.equal(payload.poll.name, 'Proceed?'); | |
| assert.deepEqual(payload.poll.values, ['Approve', 'Deny']); | |
| assert.equal(payload.poll.selectableCount, 1); | |
| assert.equal(Buffer.isBuffer(payload.poll.messageSecret), true); | |
| assert.equal(payload.poll.messageSecret.length, 32); | |
| assert.deepEqual(pollCreationMessageFromPayload(payload), { | |
| messageContextInfo: { | |
| messageSecret: payload.poll.messageSecret, | |
| }, | |
| pollCreationMessageV3: { | |
| name: 'Proceed?', | |
| options: [{ optionName: 'Approve' }, { optionName: 'Deny' }], | |
| selectableOptionsCount: 1, | |
| }, | |
| }); | |
| console.log(' ✓ poll payload primitive carries a cacheable vote secret'); | |
| } | |
| { | |
| const pollCreation = { | |
| key: { | |
| id: 'poll-creation', | |
| remoteJid: '15551234567@s.whatsapp.net', | |
| fromMe: true, | |
| }, | |
| message: { | |
| messageContextInfo: { | |
| messageSecret: Buffer.from('0123456789abcdef0123456789abcdef'), | |
| }, | |
| pollCreationMessageV3: { | |
| name: 'Proceed?', | |
| options: [{ optionName: 'Approve' }, { optionName: 'Deny' }], | |
| selectableOptionsCount: 1, | |
| }, | |
| }, | |
| }; | |
| const voteKey = { | |
| id: 'vote-message', | |
| remoteJid: '15551234567@s.whatsapp.net', | |
| participant: '15550001111@s.whatsapp.net', | |
| fromMe: false, | |
| }; | |
| const encryptedVote = { | |
| encPayload: Buffer.from('payload'), | |
| encIv: Buffer.from('iv'), | |
| }; | |
| const attempts = []; | |
| const pollUpdate = pollUpdateForAggregation({ | |
| pollUpdateMessage: { | |
| pollCreationMessageKey: pollCreation.key, | |
| vote: encryptedVote, | |
| senderTimestampMs: 123, | |
| }, | |
| pollUpdateMessageKey: voteKey, | |
| pollCreation, | |
| decryptPollVote: (vote, ctx) => { | |
| attempts.push({ pollCreatorJid: ctx.pollCreatorJid, voterJid: ctx.voterJid }); | |
| assert.equal(vote, encryptedVote); | |
| assert.equal(ctx.pollMsgId, 'poll-creation'); | |
| assert.equal(ctx.pollEncKey, pollCreation.message.messageContextInfo.messageSecret); | |
| if (ctx.pollCreatorJid !== 'creator-lid@lid') { | |
| throw new Error('wrong creator jid'); | |
| } | |
| assert.equal(ctx.voterJid, '15550001111@s.whatsapp.net'); | |
| return { | |
| selectedOptions: [createHash('sha256').update(Buffer.from('Approve')).digest()], | |
| }; | |
| }, | |
| getKeyAuthor: (key, meId = 'me') => (key?.fromMe ? meId : key?.participant || key?.remoteJid || ''), | |
| meId: 'classic-me@s.whatsapp.net', | |
| pollCreatorJids: ['classic-me@s.whatsapp.net', 'creator-lid@lid'], | |
| }); | |
| assert.deepEqual(attempts.map(item => item.pollCreatorJid), ['classic-me@s.whatsapp.net', 'creator-lid@lid']); | |
| assert.equal(pollUpdate.pollUpdateMessageKey.id, 'vote-message'); | |
| assert.equal(pollUpdate.senderTimestampMs, 123); | |
| const aggregation = getAggregateVotesInPollMessage({ | |
| message: pollCreation.message, | |
| pollUpdates: [pollUpdate], | |
| }); | |
| assert.deepEqual( | |
| aggregation.map(option => ({ name: option.name, voters: option.voters })), | |
| [ | |
| { name: 'Approve', voters: ['15550001111@s.whatsapp.net'] }, | |
| { name: 'Deny', voters: [] }, | |
| ], | |
| ); | |
| console.log(' ✓ encrypted poll upserts are wrapped into Baileys aggregation shape'); | |
| } | |
| // -- media download failure containment (port of nanoclaw#2895) ----------- | |
| { | |
| assert.equal(appendMediaFailureNote('hello', []), 'hello'); | |
| assert.equal( | |
| appendMediaFailureNote('check this out', ['image']), | |
| 'check this out\n[image could not be downloaded]', | |
| ); | |
| // Regression guard: an uncaptioned failed image must still produce a | |
| // non-empty body, or the empty-message guard drops the whole message. | |
| assert.equal(appendMediaFailureNote('', ['image']), '[image could not be downloaded]'); | |
| assert.equal( | |
| appendMediaFailureNote('', ['image', 'document']), | |
| '[image could not be downloaded] [document could not be downloaded]', | |
| ); | |
| console.log(' ✓ appendMediaFailureNote formats failure notes'); | |
| } | |
| { | |
| // A throwing downloadMedia (expired CDN URL) must not reject out of | |
| // extractBridgeEvent — before this guard the whole upsert batch died and | |
| // the message was silently dropped. | |
| const event = await extractBridgeEvent({ | |
| msg: { | |
| key: { id: 'img-fail-1', remoteJid: '15551234567@s.whatsapp.net', fromMe: false }, | |
| messageTimestamp: 123, | |
| message: { imageMessage: { caption: '', mimetype: 'image/jpeg' } }, | |
| }, | |
| chatId: '15551234567@s.whatsapp.net', | |
| senderId: '15551234567@s.whatsapp.net', | |
| senderNumber: '15551234567', | |
| downloadMedia: async () => { throw new Error('Failed to fetch stream from https://mmg.whatsapp.net/x'); }, | |
| cacheDirs: { image: mkdtempSync(path.join(tmpdir(), 'wa-media-')) }, | |
| }); | |
| assert.equal(event.hasMedia, true); | |
| assert.equal(event.mediaUrls.length, 0); | |
| assert.equal(event.body, '[image could not be downloaded]'); | |
| console.log(' ✓ failed media download is contained and surfaced in body'); | |
| } | |
| { | |
| // Captioned message keeps the caption and appends the failure note. | |
| const event = await extractBridgeEvent({ | |
| msg: { | |
| key: { id: 'doc-fail-1', remoteJid: '15551234567@s.whatsapp.net', fromMe: false }, | |
| messageTimestamp: 123, | |
| message: { documentMessage: { caption: 'see attached', fileName: 'q.pdf', mimetype: 'application/pdf' } }, | |
| }, | |
| chatId: '15551234567@s.whatsapp.net', | |
| senderId: '15551234567@s.whatsapp.net', | |
| senderNumber: '15551234567', | |
| downloadMedia: async () => { throw new Error('boom'); }, | |
| cacheDirs: { document: mkdtempSync(path.join(tmpdir(), 'wa-media-')) }, | |
| }); | |
| assert.equal(event.body, 'see attached\n[document could not be downloaded]'); | |
| assert.equal(event.mediaUrls.length, 0); | |
| console.log(' ✓ captioned failed download keeps caption and appends note'); | |
| } | |
| console.log('\n✅ All WhatsApp native bridge helper tests passed.'); | |
Xet Storage Details
- Size:
- 13 kB
- Xet hash:
- b25344ddfcbb7e3c437a5d7adf1266c97a26446e14970bb0bdf763adfff13388
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.