fix: store LID-PN mapping from contactAction sync (#2266)
* fix: store LID-PN mapping from contactAction sync * chore: improve testing of sync actions
This commit is contained in:
committed by
GitHub
parent
a89736f89d
commit
8ff01b8bb3
@@ -1185,6 +1185,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}, 20_000)
|
||||
})
|
||||
|
||||
ev.on('lid-mapping.update', async ({ lid, pn }) => {
|
||||
try {
|
||||
await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }])
|
||||
} catch (error) {
|
||||
logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping')
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...sock,
|
||||
createCallLink,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { toNumber } from './generics'
|
||||
import type { ILogger } from './logger'
|
||||
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
||||
import { downloadContentFromMessage } from './messages-media'
|
||||
import { emitSyncActionResults, processContactAction } from './sync-action-utils'
|
||||
|
||||
type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSyncKeyData | null | undefined>
|
||||
|
||||
@@ -832,14 +833,8 @@ export const processSyncAction = (
|
||||
]
|
||||
})
|
||||
} else if (action?.contactAction) {
|
||||
ev.emit('contacts.upsert', [
|
||||
{
|
||||
id: id!,
|
||||
name: action.contactAction.fullName!,
|
||||
lid: action.contactAction.lidJid || undefined,
|
||||
phoneNumber: action.contactAction.pnJid || undefined
|
||||
}
|
||||
])
|
||||
const results = processContactAction(action.contactAction, id, logger)
|
||||
emitSyncActionResults(ev, results)
|
||||
} else if (action?.pushNameSetting) {
|
||||
const name = action?.pushNameSetting?.name
|
||||
if (name && me?.name !== name) {
|
||||
@@ -935,7 +930,7 @@ export const processSyncAction = (
|
||||
ev.emit('contacts.upsert', [
|
||||
{
|
||||
id: id!,
|
||||
name: action.lidContactAction.fullName!,
|
||||
name: action.lidContactAction.fullName || undefined,
|
||||
lid: id!,
|
||||
phoneNumber: undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { proto } from '../../WAProto/index.js'
|
||||
import type { BaileysEventEmitter, BaileysEventMap, Contact } from '../Types'
|
||||
import { isLidUser, isPnUser } from '../WABinary'
|
||||
import type { ILogger } from './logger'
|
||||
|
||||
export type ContactsUpsertResult = {
|
||||
event: 'contacts.upsert'
|
||||
data: Contact[]
|
||||
}
|
||||
|
||||
export type LidMappingUpdateResult = {
|
||||
event: 'lid-mapping.update'
|
||||
data: BaileysEventMap['lid-mapping.update']
|
||||
}
|
||||
|
||||
export type SyncActionResult = ContactsUpsertResult | LidMappingUpdateResult
|
||||
|
||||
/**
|
||||
* Process contactAction and return events to emit.
|
||||
* Pure function - no side effects.
|
||||
*/
|
||||
export const processContactAction = (
|
||||
action: proto.SyncActionValue.IContactAction,
|
||||
id: string | undefined,
|
||||
logger?: ILogger
|
||||
): SyncActionResult[] => {
|
||||
const results: SyncActionResult[] = []
|
||||
|
||||
if (!id) {
|
||||
logger?.warn(
|
||||
{ hasFullName: !!action.fullName, hasLidJid: !!action.lidJid, hasPnJid: !!action.pnJid },
|
||||
'contactAction sync: missing id in index'
|
||||
)
|
||||
return results
|
||||
}
|
||||
|
||||
const lidJid = action.lidJid
|
||||
const idIsPn = isPnUser(id)
|
||||
// PN is in index[1], not in contactAction.pnJid which is usually null
|
||||
const phoneNumber = idIsPn ? id : action.pnJid || undefined
|
||||
|
||||
// Always emit contacts.upsert
|
||||
results.push({
|
||||
event: 'contacts.upsert',
|
||||
data: [
|
||||
{
|
||||
id,
|
||||
name: action.fullName || undefined,
|
||||
lid: lidJid || undefined,
|
||||
phoneNumber
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Emit lid-mapping.update if we have valid LID-PN pair
|
||||
if (lidJid && isLidUser(lidJid) && idIsPn) {
|
||||
results.push({
|
||||
event: 'lid-mapping.update',
|
||||
data: { lid: lidJid, pn: id }
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export const emitSyncActionResults = (ev: BaileysEventEmitter, results: SyncActionResult[]): void => {
|
||||
for (const result of results) {
|
||||
if (result.event === 'contacts.upsert') {
|
||||
ev.emit('contacts.upsert', result.data)
|
||||
} else {
|
||||
ev.emit('lid-mapping.update', result.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { proto } from '../../../WAProto/index.js'
|
||||
import type { BaileysEventEmitter, ChatMutation, Contact } from '../../Types'
|
||||
import { LabelAssociationType } from '../../Types/LabelAssociation'
|
||||
import { processSyncAction } from '../../Utils/chat-utils'
|
||||
import type { ILogger } from '../../Utils/logger'
|
||||
|
||||
const createMockEventEmitter = () => {
|
||||
const emittedEvents: Array<{ event: string; data: unknown }> = []
|
||||
const emit = jest.fn((event: string, data: unknown) => {
|
||||
emittedEvents.push({ event, data })
|
||||
return true
|
||||
})
|
||||
return {
|
||||
emit,
|
||||
emittedEvents,
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
} as unknown as BaileysEventEmitter & { emittedEvents: typeof emittedEvents }
|
||||
}
|
||||
|
||||
const createMockLogger = (): ILogger =>
|
||||
({
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
child: jest.fn(function (this: ILogger) {
|
||||
return this
|
||||
}),
|
||||
level: 'silent'
|
||||
}) as unknown as ILogger
|
||||
|
||||
const createSyncAction = (
|
||||
action: proto.ISyncActionValue,
|
||||
index: string[] = ['type', 'id', 'msgId', '0']
|
||||
): ChatMutation => ({
|
||||
syncAction: { value: action },
|
||||
index
|
||||
})
|
||||
|
||||
const mockMe: Contact = { id: 'me@s.whatsapp.net', name: 'Test User' }
|
||||
|
||||
describe('processSyncAction', () => {
|
||||
let ev: ReturnType<typeof createMockEventEmitter>
|
||||
let logger: ILogger
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
ev = createMockEventEmitter()
|
||||
logger = createMockLogger()
|
||||
})
|
||||
|
||||
describe('muteAction', () => {
|
||||
it('emits chats.update with muteEndTime when muted', () => {
|
||||
const syncAction = createSyncAction({ muteAction: { muted: true, muteEndTimestamp: 1700000000 } }, [
|
||||
'mute',
|
||||
'chat123@s.whatsapp.net'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith(
|
||||
'chats.update',
|
||||
expect.arrayContaining([expect.objectContaining({ id: 'chat123@s.whatsapp.net', muteEndTime: 1700000000 })])
|
||||
)
|
||||
})
|
||||
|
||||
it('emits null muteEndTime when unmuted', () => {
|
||||
const syncAction = createSyncAction({ muteAction: { muted: false, muteEndTimestamp: 0 } }, [
|
||||
'mute',
|
||||
'chat123@s.whatsapp.net'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith(
|
||||
'chats.update',
|
||||
expect.arrayContaining([expect.objectContaining({ muteEndTime: null })])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('archiveChatAction', () => {
|
||||
it('emits chats.update with archived true/false', () => {
|
||||
const archived = createSyncAction({ archiveChatAction: { archived: true } }, ['archive', 'chat@s.whatsapp.net'])
|
||||
processSyncAction(archived, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith(
|
||||
'chats.update',
|
||||
expect.arrayContaining([expect.objectContaining({ archived: true })])
|
||||
)
|
||||
})
|
||||
|
||||
it('handles type fallback without archiveChatAction', () => {
|
||||
const syncAction = createSyncAction({}, ['archive', 'chat@s.whatsapp.net'])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith(
|
||||
'chats.update',
|
||||
expect.arrayContaining([expect.objectContaining({ archived: true })])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('markChatAsReadAction', () => {
|
||||
it('emits unreadCount 0 when read is true', () => {
|
||||
const read = createSyncAction({ markChatAsReadAction: { read: true } }, ['markRead', 'chat@s.whatsapp.net'])
|
||||
processSyncAction(read, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith(
|
||||
'chats.update',
|
||||
expect.arrayContaining([expect.objectContaining({ unreadCount: 0 })])
|
||||
)
|
||||
})
|
||||
|
||||
it('emits unreadCount -1 when read is false', () => {
|
||||
const unread = createSyncAction({ markChatAsReadAction: { read: false } }, ['markRead', 'chat@s.whatsapp.net'])
|
||||
processSyncAction(unread, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith(
|
||||
'chats.update',
|
||||
expect.arrayContaining([expect.objectContaining({ unreadCount: -1 })])
|
||||
)
|
||||
})
|
||||
|
||||
it('emits null unreadCount during initial sync when already read', () => {
|
||||
const syncAction = createSyncAction({ markChatAsReadAction: { read: true } }, ['markRead', 'chat@s.whatsapp.net'])
|
||||
processSyncAction(syncAction, ev, mockMe, { accountSettings: { unarchiveChats: false } }, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith(
|
||||
'chats.update',
|
||||
expect.arrayContaining([expect.objectContaining({ unreadCount: null })])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteMessageForMeAction', () => {
|
||||
it('emits messages.delete with correct key', () => {
|
||||
const syncAction = createSyncAction({ deleteMessageForMeAction: { deleteMedia: false } }, [
|
||||
'deleteMessageForMe',
|
||||
'chat@s.whatsapp.net',
|
||||
'msg456',
|
||||
'1'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('messages.delete', {
|
||||
keys: [{ remoteJid: 'chat@s.whatsapp.net', id: 'msg456', fromMe: true }]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('contactAction', () => {
|
||||
it('emits contacts.upsert and lid-mapping.update for PN user with LID', () => {
|
||||
const syncAction = createSyncAction({ contactAction: { fullName: 'John', lidJid: '123@lid', pnJid: null } }, [
|
||||
'contact',
|
||||
'5511999@s.whatsapp.net'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('contacts.upsert', [
|
||||
{
|
||||
id: '5511999@s.whatsapp.net',
|
||||
name: 'John',
|
||||
lid: '123@lid',
|
||||
phoneNumber: '5511999@s.whatsapp.net'
|
||||
}
|
||||
])
|
||||
expect(ev.emit).toHaveBeenCalledWith('lid-mapping.update', {
|
||||
lid: '123@lid',
|
||||
pn: '5511999@s.whatsapp.net'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not emit events when id is missing', () => {
|
||||
const syncAction = createSyncAction({ contactAction: { fullName: 'John', lidJid: '123@lid', pnJid: null } }, [
|
||||
'contact',
|
||||
''
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emittedEvents.filter(e => e.event === 'contacts.upsert')).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pushNameSetting', () => {
|
||||
it('emits creds.update when name differs', () => {
|
||||
const syncAction = createSyncAction({ pushNameSetting: { name: 'New' } }, ['pushName'])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('creds.update', { me: { ...mockMe, name: 'New' } })
|
||||
})
|
||||
|
||||
it('does not emit when name is same or empty', () => {
|
||||
const same = createSyncAction({ pushNameSetting: { name: 'Test User' } }, ['pushName'])
|
||||
processSyncAction(same, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('pinAction', () => {
|
||||
it('emits chats.update with pinned timestamp or null', () => {
|
||||
const syncAction: ChatMutation = {
|
||||
syncAction: { value: { pinAction: { pinned: true }, timestamp: 1700000000 } },
|
||||
index: ['pin', 'chat@s.whatsapp.net']
|
||||
}
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith(
|
||||
'chats.update',
|
||||
expect.arrayContaining([expect.objectContaining({ pinned: 1700000000 })])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('starAction', () => {
|
||||
it('emits messages.update with starred value', () => {
|
||||
const syncAction = createSyncAction({ starAction: { starred: true } }, [
|
||||
'star',
|
||||
'chat@s.whatsapp.net',
|
||||
'msg',
|
||||
'1'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('messages.update', [
|
||||
{
|
||||
key: { remoteJid: 'chat@s.whatsapp.net', id: 'msg', fromMe: true },
|
||||
update: { starred: true }
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteChatAction', () => {
|
||||
it('emits chats.delete when not initial sync', () => {
|
||||
const syncAction = createSyncAction({ deleteChatAction: { messageRange: null } }, [
|
||||
'deleteChat',
|
||||
'chat@s.whatsapp.net'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('chats.delete', ['chat@s.whatsapp.net'])
|
||||
})
|
||||
|
||||
it('does NOT emit during initial sync', () => {
|
||||
const syncAction = createSyncAction({ deleteChatAction: { messageRange: null } }, [
|
||||
'deleteChat',
|
||||
'chat@s.whatsapp.net'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, { accountSettings: { unarchiveChats: false } }, logger)
|
||||
expect(ev.emit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('labelEditAction', () => {
|
||||
it('emits labels.edit', () => {
|
||||
const syncAction = createSyncAction(
|
||||
{ labelEditAction: { name: 'Important', color: 1, deleted: false, predefinedId: 5 } },
|
||||
['label', 'label123']
|
||||
)
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('labels.edit', {
|
||||
id: 'label123',
|
||||
name: 'Important',
|
||||
color: 1,
|
||||
deleted: false,
|
||||
predefinedId: '5'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('labelAssociationAction', () => {
|
||||
it('emits labels.association for chat label', () => {
|
||||
const syncAction = createSyncAction({ labelAssociationAction: { labeled: true } }, [
|
||||
LabelAssociationType.Chat,
|
||||
'label123',
|
||||
'chat@s.whatsapp.net'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('labels.association', {
|
||||
type: 'add',
|
||||
association: { type: LabelAssociationType.Chat, chatId: 'chat@s.whatsapp.net', labelId: 'label123' }
|
||||
})
|
||||
})
|
||||
|
||||
it('emits labels.association for message label', () => {
|
||||
const syncAction = createSyncAction({ labelAssociationAction: { labeled: true } }, [
|
||||
LabelAssociationType.Message,
|
||||
'label123',
|
||||
'chat@s.whatsapp.net',
|
||||
'msg789'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('labels.association', {
|
||||
type: 'add',
|
||||
association: {
|
||||
type: LabelAssociationType.Message,
|
||||
chatId: 'chat@s.whatsapp.net',
|
||||
messageId: 'msg789',
|
||||
labelId: 'label123'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('pnForLidChatAction', () => {
|
||||
it('emits lid-mapping.update when pnJid is present', () => {
|
||||
const syncAction = createSyncAction({ pnForLidChatAction: { pnJid: '5511999@s.whatsapp.net' } }, [
|
||||
'pnForLid',
|
||||
'123@lid'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('lid-mapping.update', { lid: '123@lid', pn: '5511999@s.whatsapp.net' })
|
||||
})
|
||||
|
||||
it('does not emit when pnJid is missing', () => {
|
||||
const syncAction = createSyncAction({ pnForLidChatAction: { pnJid: '' } }, ['pnForLid', '123@lid'])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('lockChatAction', () => {
|
||||
it('emits chats.lock', () => {
|
||||
const syncAction = createSyncAction({ lockChatAction: { locked: true } }, ['lockChat', 'chat@s.whatsapp.net'])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('chats.lock', { id: 'chat@s.whatsapp.net', locked: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe('lidContactAction', () => {
|
||||
it('emits contacts.upsert with LID contact', () => {
|
||||
const syncAction = createSyncAction({ lidContactAction: { fullName: 'LID Contact' } }, ['lidContact', '123@lid'])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('contacts.upsert', [
|
||||
{
|
||||
id: '123@lid',
|
||||
name: 'LID Contact',
|
||||
lid: '123@lid',
|
||||
phoneNumber: undefined
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('settings actions', () => {
|
||||
it('localeSetting emits settings.update', () => {
|
||||
const syncAction = createSyncAction({ localeSetting: { locale: 'en-US' } }, ['locale'])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('settings.update', { setting: 'locale', value: 'en-US' })
|
||||
})
|
||||
|
||||
it('unarchiveChatsSetting emits creds.update', () => {
|
||||
const syncAction = createSyncAction({ unarchiveChatsSetting: { unarchiveChats: true } }, ['unarchiveChats'])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(ev.emit).toHaveBeenCalledWith('creds.update', { accountSettings: { unarchiveChats: true } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('unprocessable actions', () => {
|
||||
it('logs debug for unknown action', () => {
|
||||
const syncAction = createSyncAction({ unknownAction: {} } as unknown as proto.ISyncActionValue, [
|
||||
'unknown',
|
||||
'id123'
|
||||
])
|
||||
processSyncAction(syncAction, ev, mockMe, undefined, logger)
|
||||
expect(logger.debug).toHaveBeenCalledWith({ syncAction, id: 'id123' }, 'unprocessable update')
|
||||
expect(ev.emit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,186 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import type { BaileysEventMap, Contact } from '../../Types'
|
||||
import type { ILogger } from '../../Utils/logger'
|
||||
import { processContactAction } from '../../Utils/sync-action-utils'
|
||||
|
||||
type LidMapping = BaileysEventMap['lid-mapping.update']
|
||||
|
||||
describe('processContactAction', () => {
|
||||
const mockLogger: ILogger = {
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
child: jest.fn(() => mockLogger),
|
||||
level: 'silent'
|
||||
} as unknown as ILogger
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('contacts.upsert', () => {
|
||||
it('emits with phoneNumber from id when id is PN user', () => {
|
||||
const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null }
|
||||
const id = '5511999999999@s.whatsapp.net'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
expect(results).toContainEqual({
|
||||
event: 'contacts.upsert',
|
||||
data: [
|
||||
{
|
||||
id: '5511999999999@s.whatsapp.net',
|
||||
name: 'John Doe',
|
||||
lid: '123456789@lid',
|
||||
phoneNumber: '5511999999999@s.whatsapp.net'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('uses pnJid as phoneNumber fallback when id is LID user', () => {
|
||||
const action = { fullName: 'John Doe', lidJid: null, pnJid: '5511888888888@s.whatsapp.net' }
|
||||
const id = '123456789@lid'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
expect(results).toContainEqual({
|
||||
event: 'contacts.upsert',
|
||||
data: [
|
||||
{
|
||||
id: '123456789@lid',
|
||||
name: 'John Doe',
|
||||
lid: undefined,
|
||||
phoneNumber: '5511888888888@s.whatsapp.net'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('handles undefined fullName', () => {
|
||||
const action = { fullName: undefined, lidJid: '123456789@lid', pnJid: null }
|
||||
const id = '5511999999999@s.whatsapp.net'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
const contactData = results.find(r => r.event === 'contacts.upsert')!.data
|
||||
expect(contactData[0]!.name).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('lid-mapping.update', () => {
|
||||
it('emits when LID and PN are valid', () => {
|
||||
const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null }
|
||||
const id = '5511999999999@s.whatsapp.net'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
expect(results).toContainEqual({
|
||||
event: 'lid-mapping.update',
|
||||
data: { lid: '123456789@lid', pn: '5511999999999@s.whatsapp.net' }
|
||||
})
|
||||
})
|
||||
|
||||
it('handles LID with device ID suffix', () => {
|
||||
const action = { fullName: 'Contact', lidJid: '173233882013816:99@lid', pnJid: null }
|
||||
const id = '5511999999999@s.whatsapp.net'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
expect(results).toContainEqual({
|
||||
event: 'lid-mapping.update',
|
||||
data: { lid: '173233882013816:99@lid', pn: '5511999999999@s.whatsapp.net' }
|
||||
})
|
||||
})
|
||||
|
||||
it('does NOT emit when lidJid is missing', () => {
|
||||
const action = { fullName: 'John Doe', lidJid: null, pnJid: null }
|
||||
const id = '5511999999999@s.whatsapp.net'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
expect(results.find(r => r.event === 'lid-mapping.update')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does NOT emit when id is LID user (not PN)', () => {
|
||||
const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null }
|
||||
const id = '987654321@lid'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
expect(results.find(r => r.event === 'lid-mapping.update')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does NOT emit when lidJid is invalid format', () => {
|
||||
const action = { fullName: 'John Doe', lidJid: 'invalid-lid-format', pnJid: null }
|
||||
const id = '5511999999999@s.whatsapp.net'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
expect(results.find(r => r.event === 'lid-mapping.update')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does NOT emit for group JIDs', () => {
|
||||
const action = { fullName: 'Group', lidJid: '123456789@lid', pnJid: null }
|
||||
const id = '123456789012345678@g.us'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
expect(results.find(r => r.event === 'lid-mapping.update')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('missing id', () => {
|
||||
it('returns empty array and logs warning when id is undefined', () => {
|
||||
const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null }
|
||||
|
||||
const results = processContactAction(action, undefined, mockLogger)
|
||||
|
||||
expect(results).toEqual([])
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
{ hasFullName: true, hasLidJid: true, hasPnJid: false },
|
||||
'contactAction sync: missing id in index'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns empty array when id is empty string', () => {
|
||||
const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null }
|
||||
|
||||
const results = processContactAction(action, '', mockLogger)
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('PN extraction from index[1] (pnJid is always null)', () => {
|
||||
it('extracts PN from id when pnJid is null', () => {
|
||||
// In real WhatsApp data, pnJid is ALWAYS null - PN comes from index[1]
|
||||
const action = { fullName: 'Test Contact', lidJid: '111222333@lid', pnJid: null }
|
||||
const id = '5599887766@s.whatsapp.net'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
const contactData = results.find(r => r.event === 'contacts.upsert')!.data
|
||||
expect(contactData[0]!.phoneNumber).toBe('5599887766@s.whatsapp.net')
|
||||
expect(contactData[0]!.lid).toBe('111222333@lid')
|
||||
|
||||
const mapping = results.find(r => r.event === 'lid-mapping.update')!.data
|
||||
expect(mapping).toEqual({ lid: '111222333@lid', pn: '5599887766@s.whatsapp.net' })
|
||||
})
|
||||
|
||||
it('prefers id over pnJid when id is PN user', () => {
|
||||
const action = { fullName: 'Test', lidJid: '111222333@lid', pnJid: '1111111111@s.whatsapp.net' }
|
||||
const id = '9999999999@s.whatsapp.net'
|
||||
|
||||
const results = processContactAction(action, id)
|
||||
|
||||
const contactData = results.find(r => r.event === 'contacts.upsert')!.data
|
||||
expect(contactData[0]!.phoneNumber).toBe('9999999999@s.whatsapp.net')
|
||||
|
||||
const mapping = results.find(r => r.event === 'lid-mapping.update')!.data
|
||||
expect(mapping.pn).toBe('9999999999@s.whatsapp.net')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user