import { buildIncomingMessageBase, mapContactFields, mapWwebjsMessageType, RawContactFields, RawMessageFields, } from './message-mapper'; describe('buildIncomingMessageBase', () => { const base: RawMessageFields = { id: { _serialized: 'MSG1' }, from: '123@c.us', to: 'me@c.us', body: 'hi', type: 'chat', timestamp: 1700000000, fromMe: false, }; it('maps the core fields, neutralizes the type (chat -> text), and flags 1:1 chats as non-group', () => { const r = buildIncomingMessageBase(base); expect(r.id).toBe('MSG1'); expect(r.chatId).toBe('123@c.us'); expect(r.type).toBe('text'); // wwebjs 'chat' is neutralized to 'text' expect(r.isGroup).toBe(false); expect(r.isStatusBroadcast).toBe(false); expect(r.author).toBeUndefined(); expect(r.contact).toBeUndefined(); expect(r.isLidSender).toBeUndefined(); // a normal @c.us sender is not flagged }); it('reads a renamed `$1` id when the dependency has not normalized it (#747)', () => { // The live inbound path. Without this, every arriving message on a renamed build carries // `id: undefined` — no dedup key, no reply target, nothing to match an ack against. const r = buildIncomingMessageBase({ ...base, id: { $1: 'MSG_RENAMED' } }); expect(r.id).toBe('MSG_RENAMED'); }); it('reports an unreadable id as the empty sentinel rather than undefined', () => { // `''` means "received, id unreadable" and is normalized to NULL at the persist chokepoint. // `undefined` would type-lie about `IncomingMessage.id: string`. const r = buildIncomingMessageBase({ ...base, id: {} }); expect(r.id).toBe(''); }); it('flags a 1:1 sender identified by an @lid privacy id (#263)', () => { const r = buildIncomingMessageBase({ ...base, from: '111@lid' }); expect(r.isLidSender).toBe(true); }); it('flags an @lid group participant via author, not the group JID (#263)', () => { const r = buildIncomingMessageBase({ ...base, from: 'group-1@g.us', author: '222@lid' }); expect(r.isLidSender).toBe(true); }); it('does not flag a group whose participant is a normal number', () => { const r = buildIncomingMessageBase({ ...base, from: 'group-1@g.us', author: '456@c.us' }); expect(r.isLidSender).toBeUndefined(); }); it('flags a status/story broadcast via isStatusBroadcast (engine pseudo-JID stays in the adapter)', () => { const r = buildIncomingMessageBase({ ...base, fromMe: true, from: 'me@c.us', to: 'status@broadcast' }); expect(r.isStatusBroadcast).toBe(true); }); it('includes author and pushName for a group message', () => { const r = buildIncomingMessageBase({ ...base, from: 'group-1@g.us', author: '456@c.us', _data: { notifyName: 'Alice' }, }); expect(r.isGroup).toBe(true); expect(r.author).toBe('456@c.us'); expect(r.contact).toEqual({ pushName: 'Alice' }); }); it('omits contact when no push name is present', () => { const r = buildIncomingMessageBase({ ...base, author: '789@c.us' }); expect(r.author).toBe('789@c.us'); expect(r.contact).toBeUndefined(); }); it('maps ephemeralDuration when present on _data', () => { const r = buildIncomingMessageBase({ ...base, _data: { ephemeralDuration: 86400 }, }); expect(r.ephemeralDuration).toBe(86400); }); it('omits ephemeralDuration when absent from _data', () => { expect(buildIncomingMessageBase(base).ephemeralDuration).toBeUndefined(); }); it('omits ephemeralDuration when ephemeralDuration is 0', () => { const r = buildIncomingMessageBase({ ...base, _data: { ephemeralDuration: 0 }, }); expect(r.ephemeralDuration).toBeUndefined(); }); it('uses `to` as the chat for an outgoing (fromMe) message, not the account JID in `from`', () => { const r = buildIncomingMessageBase({ ...base, fromMe: true, from: 'me@c.us', to: 'peer@c.us' }); expect(r.chatId).toBe('peer@c.us'); expect(r.isGroup).toBe(false); }); it('flags an outgoing group send (fromMe) as a group via `to`', () => { const r = buildIncomingMessageBase({ ...base, fromMe: true, from: 'me@c.us', to: 'group-1@g.us' }); expect(r.chatId).toBe('group-1@g.us'); expect(r.isGroup).toBe(true); }); it('maps mentionedIds when present', () => { const r = buildIncomingMessageBase({ ...base, mentionedIds: ['222@lid', '333@lid'] }); expect(r.mentionedIds).toEqual(['222@lid', '333@lid']); }); it('omits mentionedIds when absent or empty', () => { expect(buildIncomingMessageBase(base).mentionedIds).toBeUndefined(); expect(buildIncomingMessageBase({ ...base, mentionedIds: [] }).mentionedIds).toBeUndefined(); }); }); describe('mapWwebjsMessageType (engine type-token -> neutral MessageType boundary, #265)', () => { it.each([ ['chat', 'text'], ['ptt', 'voice'], ['image', 'image'], ['video', 'video'], ['audio', 'audio'], ['document', 'document'], ['sticker', 'sticker'], ['location', 'location'], ['vcard', 'contact'], ['multi_vcard', 'contact'], ['call_log', 'call'], ['revoked', 'revoked'], ['poll_creation', 'poll'], ['e2e_notification', 'unknown'], // any unmapped wwebjs type ])('maps wwebjs type %s -> %s', (raw, expected) => { expect(mapWwebjsMessageType(raw)).toBe(expected); }); }); describe('mapContactFields', () => { const businessRaw: RawContactFields = { id: { _serialized: '254571402260600@lid' }, number: '254571402260600', name: 'Leo hermano', pushname: 'leo', shortName: 'Leo', type: 'in', isMyContact: true, isWAContact: true, isBusiness: true, isEnterprise: false, verifiedName: 'Leo Store', verifiedLevel: 2, isBlocked: false, labels: ['L1', 'L2'], }; describe('full mode (WEBHOOK_CONTACT_DETAILS opt-in)', () => { it('copies the sync fields for a business contact', () => { expect(mapContactFields(businessRaw, true)).toEqual({ id: '254571402260600@lid', number: '254571402260600', name: 'Leo hermano', pushName: 'leo', shortName: 'Leo', type: 'in', isMyContact: true, isWAContact: true, isBusiness: true, isEnterprise: false, verifiedName: 'Leo Store', verifiedLevel: 2, isBlocked: false, labels: ['L1', 'L2'], }); }); it('omits business fields and empty values for a plain personal contact', () => { const r = mapContactFields( { id: { _serialized: '584145200715@c.us' }, number: '584145200715', pushname: 'Ana', isMyContact: false, isWAContact: true, isBusiness: false, }, true, ); expect(r).toEqual({ id: '584145200715@c.us', number: '584145200715', pushName: 'Ana', isMyContact: false, isWAContact: true, isBusiness: false, }); expect('verifiedName' in r).toBe(false); expect('name' in r).toBe(false); expect('labels' in r).toBe(false); }); it('returns an empty object when nothing is set', () => { expect(mapContactFields({}, true)).toEqual({}); }); it('drops an empty labels array', () => { expect(mapContactFields({ labels: [] }, true)).toEqual({}); }); }); describe('minimal mode (default)', () => { it('returns only name and pushName even when the contact has full data', () => { expect(mapContactFields(businessRaw)).toEqual({ name: 'Leo hermano', pushName: 'leo' }); }); it('omits name/pushName when absent and never includes extended fields', () => { expect(mapContactFields({ number: '584145200715', isBusiness: true })).toEqual({}); }); }); });