diff --git a/src/__tests__/Signal/lid-mapping.test.ts b/src/__tests__/Signal/lid-mapping.test.ts new file mode 100644 index 00000000..68e49e9d --- /dev/null +++ b/src/__tests__/Signal/lid-mapping.test.ts @@ -0,0 +1,47 @@ +import { jest } from '@jest/globals' +import P from 'pino' +import { LIDMappingStore } from '../../Signal/lid-mapping' +import type { LIDMapping, SignalDataTypeMap, SignalKeyStoreWithTransaction } from '../../Types' + +const HOSTED_DEVICE_ID = 99 + +const mockKeys: jest.Mocked = { + get: jest.fn() as any, + set: jest.fn(), + transaction: jest.fn(async (work: () => any) => await work()) as any, + isInTransaction: jest.fn() +} +const logger = P({ level: 'silent' }) + +describe('LIDMappingStore', () => { + const mockPnToLIDFunc = jest.fn<(jids: string[]) => Promise>() + let lidMappingStore: LIDMappingStore + + beforeEach(() => { + jest.clearAllMocks() + lidMappingStore = new LIDMappingStore(mockKeys, logger, mockPnToLIDFunc) + }) + + describe('getPNForLID', () => { + it('should correctly map a standard LID with a hosted device ID back to a standard PN with a hosted device', async () => { + const lidWithHostedDevice = `12345:${HOSTED_DEVICE_ID}@lid` + const pnUser = '54321' + + // @ts-ignore + mockKeys.get.mockResolvedValue({ [`12345_reverse`]: pnUser } as SignalDataTypeMap['lid-mapping']) + + const result = await lidMappingStore.getPNForLID(lidWithHostedDevice) + expect(result).toBe(`${pnUser}:${HOSTED_DEVICE_ID}@s.whatsapp.net`) + }) + + it('should return null if no reverse mapping is found', async () => { + const lid = 'nonexistent@lid' + + // @ts-ignore + mockKeys.get.mockResolvedValue({} as SignalDataTypeMap['lid-mapping']) // Simulate not found in DB + + const result = await lidMappingStore.getPNForLID(lid) + expect(result).toBeNull() + }) + }) +}) diff --git a/src/__tests__/Utils/process-message.test.ts b/src/__tests__/Utils/process-message.test.ts new file mode 100644 index 00000000..12d5ea76 --- /dev/null +++ b/src/__tests__/Utils/process-message.test.ts @@ -0,0 +1,138 @@ +import type { WAMessage } from '../../Types' +import { cleanMessage } from '../../Utils/process-message' + +const createBaseMessage = (key: Partial, message?: Partial): WAMessage => { + return { + key: { + remoteJid: 'chat@s.whatsapp.net', + fromMe: false, + id: 'ABC', + ...key + }, + message: message || { conversation: 'hello' }, + messageTimestamp: 1675888000 + } +} + +describe('cleanMessage', () => { + const meId = 'me@s.whatsapp.net' + const meLid = 'me@lid' + const otherUserId = 'other@s.whatsapp.net' + + describe('JID Normalization', () => { + it('should correctly normalize a standard phone number JID with a device', () => { + const message = createBaseMessage({ + remoteJid: '1234567890:15@s.whatsapp.net', + participant: '9876543210:5@s.whatsapp.net' + }) + + cleanMessage(message, meId, meLid) + + expect(message.key.remoteJid).toBe('1234567890@s.whatsapp.net') + expect(message.key.participant).toBe('9876543210@s.whatsapp.net') + }) + + it('should not modify a group JID', () => { + const message = createBaseMessage({ + remoteJid: '123456-7890@g.us' + }) + + cleanMessage(message, meId, meLid) + + expect(message.key.remoteJid).toBe('123456-7890@g.us') + }) + + it('should correctly normalize a LID with a device', () => { + const message = createBaseMessage({ + participant: '1234567890:12@lid' + }) + + cleanMessage(message, meId, meLid) + + expect(message.key.participant).toBe('1234567890@lid') + }) + }) + + describe('Hosted JID Handling', () => { + it('should correctly normalize a hosted PN JID back to PN form', () => { + const hostedJid = '1234567890:99@hosted' + const message = createBaseMessage({ + remoteJid: hostedJid + }) + + cleanMessage(message, meId, meLid) + + expect(message.key.remoteJid).toBe('1234567890@s.whatsapp.net') + }) + + it('should correctly normalize a hosted LID JID back to LID form', () => { + const hostedLidJid = '9876543210:99@hosted.lid' + const message = createBaseMessage({ + participant: hostedLidJid + }) + + cleanMessage(message, meId, meLid) + + expect(message.key.participant).toBe('9876543210@lid') + }) + }) + + describe('Reaction Message Perspective', () => { + it("should correct the perspective of a reaction to another user's message", () => { + const message = createBaseMessage( + { fromMe: false, participant: otherUserId }, + { + reactionMessage: { + key: { + remoteJid: 'chat@s.whatsapp.net', + fromMe: false, + id: 'MSG_THEY_SENT', + participant: otherUserId + }, + text: '😂' + } + } + ) + + cleanMessage(message, meId, meLid) + + const reactionKey = message.message!.reactionMessage!.key! + expect(reactionKey.fromMe).toBe(false) + }) + + it('should not modify a reaction on a message I sent from another device', () => { + const message = createBaseMessage( + { fromMe: true }, + { + reactionMessage: { + key: { remoteJid: 'chat@s.whatsapp.net', fromMe: true, id: 'MSG_I_SENT' }, + text: '❤️' + } + } + ) + + const originalReactionKey = { ...message.message!.reactionMessage!.key! } + + cleanMessage(message, meId, meLid) + + const reactionKey = message.message!.reactionMessage!.key! + expect(reactionKey).toEqual(originalReactionKey) + }) + }) + + describe('Edge Cases', () => { + it('should not crash if JIDs are undefined', () => { + const message = createBaseMessage({ + remoteJid: undefined, + participant: undefined + }) + + expect(() => cleanMessage(message, meId, meLid)).not.toThrow() + }) + + it('should not crash on an empty message object', () => { + const message = createBaseMessage({}, {}) + expect(() => cleanMessage(message, meId, meLid)).not.toThrow() + }) + }) +}) diff --git a/src/__tests__/Utils/signal.test.ts b/src/__tests__/Utils/signal.test.ts new file mode 100644 index 00000000..67f24f5f --- /dev/null +++ b/src/__tests__/Utils/signal.test.ts @@ -0,0 +1,69 @@ +import { jest } from '@jest/globals' +import type { SignalRepositoryWithLIDStore } from '../../Types' +import { parseAndInjectE2ESessions } from '../../Utils/signal' +import type { BinaryNode } from '../../WABinary/types' + +describe('parseAndInjectE2ESessions', () => { + it('should process all user node', async () => { + const mockRepository = { + injectE2ESession: jest.fn() + } as jest.Mocked> + + mockRepository.injectE2ESession.mockResolvedValue(undefined) + + const createUserNode = (jid: string): BinaryNode => ({ + tag: 'user', + attrs: { jid }, + content: [ + { + tag: 'skey', + attrs: {}, + content: [ + { tag: 'id', attrs: {}, content: Buffer.from([0, 0, 1]) }, + { tag: 'value', attrs: {}, content: Buffer.alloc(33) }, + { tag: 'signature', attrs: {}, content: Buffer.alloc(64) } + ] + }, + { + tag: 'key', + attrs: {}, + content: [ + { tag: 'id', attrs: {}, content: Buffer.from([0, 0, 2]) }, + { tag: 'value', attrs: {}, content: Buffer.alloc(33) } + ] + }, + { tag: 'identity', attrs: {}, content: Buffer.alloc(32) }, + { tag: 'registration', attrs: {}, content: Buffer.alloc(4) } + ] + }) + + const mockNode: BinaryNode = { + tag: 'iq', + attrs: {}, + content: [ + { + tag: 'list', + attrs: {}, + content: [ + createUserNode('user1@s.whatsapp.net'), + createUserNode('user2@s.whatsapp.net'), + createUserNode('user3@s.whatsapp.net') + ] + } + ] + } + + await parseAndInjectE2ESessions(mockNode, mockRepository as any) + + expect(mockRepository.injectE2ESession).toHaveBeenCalledTimes(3) + expect(mockRepository.injectE2ESession).toHaveBeenCalledWith( + expect.objectContaining({ jid: 'user1@s.whatsapp.net' }) + ) + expect(mockRepository.injectE2ESession).toHaveBeenCalledWith( + expect.objectContaining({ jid: 'user2@s.whatsapp.net' }) + ) + expect(mockRepository.injectE2ESession).toHaveBeenCalledWith( + expect.objectContaining({ jid: 'user3@s.whatsapp.net' }) + ) + }) +})