diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 2fb65b12e..48bbd6908 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -512,7 +512,7 @@ export async function handleDataMessage( const pubKey = new PubKey(device); const allDevices = await MultiDeviceProtocol.getAllDevices(pubKey); - return allDevices.some(d => PubKey.isEqual(d, pubKey)); + return allDevices.some(d => d.isEqual(pubKey)); }; const ownDevice = await isOwnDevice(source); diff --git a/ts/session/messages/outgoing/OpenGroupMessage.ts b/ts/session/messages/outgoing/OpenGroupMessage.ts index 286f9b593..c670a5e68 100644 --- a/ts/session/messages/outgoing/OpenGroupMessage.ts +++ b/ts/session/messages/outgoing/OpenGroupMessage.ts @@ -1,11 +1,6 @@ import { Message, MessageParams } from './Message'; import { AttachmentPointer, Preview, Quote } from './content'; - -interface OpenGroup { - server: string; - channel: number; - conversationId: string; -} +import { OpenGroup } from '../../types/OpenGroup'; interface OpenGroupMessageParams extends MessageParams { group: OpenGroup; @@ -20,7 +15,7 @@ export class OpenGroupMessage extends Message { public readonly body?: string; public readonly attachments: Array; public readonly quote?: Quote; - public readonly preview: Array; + public readonly preview?: Array; constructor({ timestamp, diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index d77b31a7b..6c8bf4720 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -152,4 +152,11 @@ export class ChatMessage extends DataMessage { return dataMessage; } + + public isEqual(comparator: ChatMessage): boolean { + return ( + this.identifier === comparator.identifier && + this.timestamp === comparator.timestamp + ); + } } diff --git a/ts/session/protocols/MultiDeviceProtocol.ts b/ts/session/protocols/MultiDeviceProtocol.ts index c4c40605b..dd286e4ec 100644 --- a/ts/session/protocols/MultiDeviceProtocol.ts +++ b/ts/session/protocols/MultiDeviceProtocol.ts @@ -214,7 +214,7 @@ export class MultiDeviceProtocol { try { const ourDevices = await this.getOurDevices(); - return ourDevices.some(d => PubKey.isEqual(d, pubKey)); + return ourDevices.some(d => d.isEqual(pubKey)); } catch (e) { return false; } diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index ac0f24f81..b2c89d804 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -61,7 +61,7 @@ export class MessageQueue implements MessageQueueInterface { // Remove our devices from currentDevices currentDevices = currentDevices.filter(device => - ourDevices.some(d => PubKey.isEqual(d, device)) + ourDevices.some(d => device.isEqual(d)) ); } } @@ -74,15 +74,8 @@ export class MessageQueue implements MessageQueueInterface { } public async sendToGroup( - message: OpenGroupMessage | ContentMessage + message: OpenGroupMessage | ClosedGroupMessage ): Promise { - if ( - !(message instanceof OpenGroupMessage) && - !(message instanceof ClosedGroupMessage) - ) { - return false; - } - // Closed groups if (message instanceof ClosedGroupMessage) { // Get devices in closed group @@ -92,23 +85,27 @@ export class MessageQueue implements MessageQueueInterface { } const recipients = await GroupUtils.getGroupMembers(groupPubKey); - await this.sendMessageToDevices(recipients, message); - return true; + if (recipients.length) { + await this.sendMessageToDevices(recipients, message); + + return true; + } } // Open groups if (message instanceof OpenGroupMessage) { // No queue needed for Open Groups; send directly - try { await MessageSender.sendToOpenGroup(message); this.events.emit('success', message); + + return true; } catch (e) { this.events.emit('fail', message, e); - } - return true; + return false; + } } return false; @@ -163,7 +160,9 @@ export class MessageQueue implements MessageQueueInterface { } private async process(device: PubKey, message?: ContentMessage) { - if (!message) { + // Don't send to ourselves + const currentDevice = await UserUtil.getCurrentDevicePubKey(); + if (!message || (currentDevice && device.isEqual(currentDevice))) { return; } diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts index 074ee8174..0a4037c1c 100644 --- a/ts/session/sending/PendingMessageCache.ts +++ b/ts/session/sending/PendingMessageCache.ts @@ -93,7 +93,7 @@ export class PendingMessageCache { await this.saveToDB(); } - public async loadFromDB() { + private async loadFromDB() { const messages = await this.getFromStorage(); this.cache = messages; } diff --git a/ts/session/sending/index.ts b/ts/session/sending/index.ts index f6cf299b6..a6de06ca7 100644 --- a/ts/session/sending/index.ts +++ b/ts/session/sending/index.ts @@ -2,5 +2,6 @@ import * as MessageSender from './MessageSender'; export { MessageSender }; +export * from './PendingMessageCache'; export * from './MessageQueue'; export * from './MessageQueueInterface'; diff --git a/ts/session/types/OpenGroup.ts b/ts/session/types/OpenGroup.ts new file mode 100644 index 000000000..01c148515 --- /dev/null +++ b/ts/session/types/OpenGroup.ts @@ -0,0 +1,82 @@ +// This is the Open Group equivalent to the PubKey type. + +interface OpenGroupParams { + server: string; + channel: number; + conversationId: string; +} + +export class OpenGroup { + private static readonly serverRegex = new RegExp( + '^([\\w-]{2,}.){1,2}[\\w-]{2,}$' + ); + private static readonly groupIdRegex = new RegExp( + '^publicChat:[0-9]*@([\\w-]{2,}.){1,2}[\\w-]{2,}$' + ); + public readonly server: string; + public readonly channel: number; + public readonly groupId?: string; + public readonly conversationId: string; + + constructor(params: OpenGroupParams) { + const strippedServer = params.server.replace('https://', ''); + this.server = strippedServer; + + // Validate server format + const isValid = OpenGroup.serverRegex.test(this.server); + if (!isValid) { + throw Error('an invalid server or groupId was provided'); + } + + this.channel = params.channel; + this.conversationId = params.conversationId; + this.groupId = OpenGroup.getGroupId(this.server, this.channel); + } + + public static from( + groupId: string, + conversationId: string + ): OpenGroup | undefined { + // Returns a new instance from a groupId if it's valid + // eg. groupId = 'publicChat:1@chat.getsession.org' + + const server = this.getServer(groupId); + const channel = this.getChannel(groupId); + + // Was groupId successfully utilized? + if (!server || !channel) { + return; + } + + const openGroupParams = { + server, + channel, + groupId, + conversationId, + } as OpenGroupParams; + + if (this.serverRegex.test(server)) { + return; + } + + return new OpenGroup(openGroupParams); + } + + private static getServer(groupId: string): string | undefined { + const isValid = this.groupIdRegex.test(groupId); + + return isValid ? groupId.split('@')[1] : undefined; + } + + private static getChannel(groupId: string): number | undefined { + const isValid = this.groupIdRegex.test(groupId); + const channelMatch = groupId.match(/^.*\:([0-9]*)\@.*$/); + + return channelMatch && isValid ? Number(channelMatch[1]) : undefined; + } + + private static getGroupId(server: string, channel: number): string { + // server is already validated in constructor; no need to re-check + return `publicChat:${channel}@${server}`; + } +} diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index e4abd83b8..a6c0a2077 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -27,8 +27,10 @@ export class PubKey { return false; } - public static isEqual(key: PubKey, comparator: PubKey) { - return key.key === comparator.key; + public isEqual(comparator: PubKey | string) { + return comparator instanceof PubKey + ? this.key === comparator.key + : this.key === comparator; } } diff --git a/ts/session/types/index.ts b/ts/session/types/index.ts index c7c994c52..14168d9e6 100644 --- a/ts/session/types/index.ts +++ b/ts/session/types/index.ts @@ -1,3 +1,4 @@ export * from './EncryptionType'; export * from './RawMessage'; export * from './PubKey'; +export * from './OpenGroup'; diff --git a/ts/session/utils/Messages.ts b/ts/session/utils/Messages.ts index 04ae0f815..f58f4c694 100644 --- a/ts/session/utils/Messages.ts +++ b/ts/session/utils/Messages.ts @@ -6,8 +6,8 @@ export function toRawMessage( device: PubKey, message: ContentMessage ): RawMessage { - const ttl = message.ttl(); const timestamp = message.timestamp; + const ttl = message.ttl(); const plainTextBuffer = message.plainTextBuffer(); // tslint:disable-next-line: no-unnecessary-local-variable diff --git a/ts/session/utils/SyncMessageUtils.ts b/ts/session/utils/SyncMessageUtils.ts index 589096f91..afab775b0 100644 --- a/ts/session/utils/SyncMessageUtils.ts +++ b/ts/session/utils/SyncMessageUtils.ts @@ -1,5 +1,5 @@ import * as _ from 'lodash'; -import * as UserUtils from '../../util/user'; +import { UserUtil } from '../../util/'; import { getAllConversations } from '../../../js/modules/data'; import { ContentMessage, SyncMessage } from '../messages/outgoing'; import { MultiDeviceProtocol } from '../protocols'; @@ -11,7 +11,7 @@ export function from(message: ContentMessage): SyncMessage | undefined { return undefined; } -export async function canSync(message: ContentMessage): Promise { +export function canSync(message: ContentMessage): boolean { // This function should be agnostic to the device; it shouldn't need // to know about the recipient @@ -20,7 +20,7 @@ export async function canSync(message: ContentMessage): Promise { } export async function getSyncContacts(): Promise | undefined> { - const thisDevice = await UserUtils.getCurrentDevicePubKey(); + const thisDevice = await UserUtil.getCurrentDevicePubKey(); if (!thisDevice) { return []; diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index e6ba4e857..b5a0c5543 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -300,10 +300,7 @@ async function queryConversationsAndContacts( const primaryDevice = resultPrimaryDevices[i]; if (primaryDevice) { - if ( - isSecondaryDevice && - PubKey.isEqual(primaryDevice, ourPrimaryDevice) - ) { + if (isSecondaryDevice && primaryDevice.isEqual(ourPrimaryDevice)) { conversations.push(ourNumber); } else { conversations.push(primaryDevice.key); diff --git a/ts/test/session/messages/OpenGroupMessage_test.ts b/ts/test/session/messages/OpenGroupMessage_test.ts index 7223ca394..e68e63555 100644 --- a/ts/test/session/messages/OpenGroupMessage_test.ts +++ b/ts/test/session/messages/OpenGroupMessage_test.ts @@ -5,13 +5,14 @@ import { OpenGroupMessage, } from '../../../session/messages/outgoing'; import * as MIME from '../../../../ts/types/MIME'; +import { OpenGroup } from '../../../session/types'; describe('OpenGroupMessage', () => { - const group = { - server: 'server', + const group = new OpenGroup({ + server: 'chat.example.server', channel: 1, conversationId: '0', - }; + }); it('can create empty message with just a timestamp and group', () => { const message = new OpenGroupMessage({ diff --git a/ts/test/session/protocols/MultiDeviceProtocol_test.ts b/ts/test/session/protocols/MultiDeviceProtocol_test.ts index 3da1c298e..09230ca2b 100644 --- a/ts/test/session/protocols/MultiDeviceProtocol_test.ts +++ b/ts/test/session/protocols/MultiDeviceProtocol_test.ts @@ -43,7 +43,7 @@ describe('MultiDeviceProtocol', () => { 'getPairingAuthorisationsFor' ).resolves([]); await MultiDeviceProtocol.getPairingAuthorisations( - TestUtils.generateFakePubkey() + TestUtils.generateFakePubKey() ); expect(fetchPairingStub.called).to.equal(true, 'Pairing is not fetched.'); expect(fetchPairingStub.calledBefore(dataStub)).to.equal( @@ -53,8 +53,8 @@ describe('MultiDeviceProtocol', () => { }); it('should return the authorisations from the database', async () => { - const device1 = TestUtils.generateFakePubkey(); - const device2 = TestUtils.generateFakePubkey(); + const device1 = TestUtils.generateFakePubKey(); + const device2 = TestUtils.generateFakePubKey(); const pairing: PairingAuthorisation = { primaryDevicePubKey: device1.key, secondaryDevicePubKey: device2.key, @@ -75,7 +75,7 @@ describe('MultiDeviceProtocol', () => { TestUtils.stubWindow('lokiFileServerAPI', undefined); expect( MultiDeviceProtocol.fetchPairingAuthorisations( - TestUtils.generateFakePubkey() + TestUtils.generateFakePubKey() ) ).to.be.rejectedWith('lokiFileServerAPI is not initialised.'); }); @@ -101,7 +101,7 @@ describe('MultiDeviceProtocol', () => { }); const authorisations = await MultiDeviceProtocol.fetchPairingAuthorisations( - TestUtils.generateFakePubkey() + TestUtils.generateFakePubKey() ); expect(authorisations.length).to.equal(1); @@ -140,8 +140,8 @@ describe('MultiDeviceProtocol', () => { fetchPairingAuthorisationStub = sandbox .stub(MultiDeviceProtocol, 'fetchPairingAuthorisations') .resolves([]); - currentDevice = TestUtils.generateFakePubkey(); - device = TestUtils.generateFakePubkey(); + currentDevice = TestUtils.generateFakePubKey(); + device = TestUtils.generateFakePubKey(); sandbox .stub(UserUtil, 'getCurrentDevicePubKey') .resolves(currentDevice.key); @@ -236,7 +236,7 @@ describe('MultiDeviceProtocol', () => { describe('getAllDevices', () => { it('should return all devices', async () => { - const primary = TestUtils.generateFakePubkey(); + const primary = TestUtils.generateFakePubKey(); const otherDevices = TestUtils.generateFakePubKeys(2); const authorisations = generateFakeAuthorisations(primary, otherDevices); sandbox @@ -254,7 +254,7 @@ describe('MultiDeviceProtocol', () => { describe('getPrimaryDevice', () => { it('should return the primary device', async () => { - const primary = TestUtils.generateFakePubkey(); + const primary = TestUtils.generateFakePubKey(); const otherDevices = TestUtils.generateFakePubKeys(2); const authorisations = generateFakeAuthorisations(primary, otherDevices); sandbox @@ -271,7 +271,7 @@ describe('MultiDeviceProtocol', () => { describe('getSecondaryDevices', () => { it('should return the secondary devices', async () => { - const primary = TestUtils.generateFakePubkey(); + const primary = TestUtils.generateFakePubKey(); const otherDevices = TestUtils.generateFakePubKeys(2); const authorisations = generateFakeAuthorisations(primary, otherDevices); sandbox diff --git a/ts/test/session/sending/MessageQueue_test.ts b/ts/test/session/sending/MessageQueue_test.ts new file mode 100644 index 000000000..746eb224f --- /dev/null +++ b/ts/test/session/sending/MessageQueue_test.ts @@ -0,0 +1,386 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as _ from 'lodash'; +import { GroupUtils, SyncMessageUtils } from '../../../session/utils'; +import { Stubs, TestUtils } from '../../../test/test-utils'; +import { MessageQueue } from '../../../session/sending/MessageQueue'; +import { + ChatMessage, + ClosedGroupMessage, + ContentMessage, + OpenGroupMessage, +} from '../../../session/messages/outgoing'; +import { PubKey, RawMessage } from '../../../session/types'; +import { UserUtil } from '../../../util'; +import { MessageSender, PendingMessageCache } from '../../../session/sending'; +import { toRawMessage } from '../../../session/utils/Messages'; +import { + MultiDeviceProtocol, + SessionProtocol, +} from '../../../session/protocols'; + +// Equivalent to Data.StorageItem +interface StorageItem { + id: string; + value: any; +} + +// Helper function to force sequential on events checks +async function tick() { + return new Promise(resolve => { + // tslint:disable-next-line: no-string-based-set-timeout + setTimeout(resolve, 0); + }); +} + +describe('MessageQueue', () => { + // Initialize new stubbed cache + let data: StorageItem; + const sandbox = sinon.createSandbox(); + const ourDevice = TestUtils.generateFakePubKey(); + const ourNumber = ourDevice.key; + const pairedDevices = TestUtils.generateFakePubKeys(2); + + // Initialize new stubbed queue + let messageQueueStub: MessageQueue; + + // Spies + let sendMessageToDevicesSpy: sinon.SinonSpy< + [Array, ContentMessage], + Promise> + >; + let sendSyncMessageSpy: sinon.SinonSpy< + [ContentMessage, Array], + Promise> + >; + let sendToGroupSpy: sinon.SinonSpy< + [OpenGroupMessage | ClosedGroupMessage], + Promise + >; + + // Message Sender Stubs + let sendStub: sinon.SinonStub<[RawMessage, (number | undefined)?]>; + let sendToOpenGroupStub: sinon.SinonStub<[OpenGroupMessage]>; + // Utils Stubs + let groupMembersStub: sinon.SinonStub; + let canSyncStub: sinon.SinonStub<[ContentMessage], boolean>; + // Session Protocol Stubs + let hasSessionStub: sinon.SinonStub<[PubKey]>; + let sendSessionRequestIfNeededStub: sinon.SinonStub<[PubKey], Promise>; + + beforeEach(async () => { + // Stub out methods which touch the database + const storageID = 'pendingMessages'; + data = { + id: storageID, + value: '[]', + }; + + // Pending Message Cache Data Stubs + TestUtils.stubData('getItemById') + .withArgs('pendingMessages') + .resolves(data); + TestUtils.stubData('createOrUpdateItem').callsFake((item: StorageItem) => { + if (item.id === storageID) { + data = item; + } + }); + + // Utils Stubs + canSyncStub = sandbox.stub(SyncMessageUtils, 'canSync'); + canSyncStub.returns(false); + sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber); + sandbox.stub(MultiDeviceProtocol, 'getAllDevices').resolves(pairedDevices); + + TestUtils.stubWindow('libsignal', { + SignalProtocolAddress: sandbox.stub(), + SessionCipher: Stubs.SessionCipherStub, + } as any); + + // Message Sender Stubs + sendStub = sandbox.stub(MessageSender, 'send').resolves(); + sendToOpenGroupStub = sandbox + .stub(MessageSender, 'sendToOpenGroup') + .resolves(true); + + // Group Utils Stubs + sandbox.stub(GroupUtils, 'isMediumGroup').returns(false); + groupMembersStub = sandbox + .stub(GroupUtils, 'getGroupMembers' as any) + .resolves(TestUtils.generateFakePubKeys(10)); + + // Session Protocol Stubs + sandbox.stub(SessionProtocol, 'sendSessionRequest').resolves(); + hasSessionStub = sandbox.stub(SessionProtocol, 'hasSession').resolves(true); + sendSessionRequestIfNeededStub = sandbox + .stub(SessionProtocol, 'sendSessionRequestIfNeeded') + .resolves(); + + // Pending Mesage Cache Stubs + const chatMessages = Array.from( + { length: 10 }, + TestUtils.generateChatMessage + ); + const rawMessage = toRawMessage( + TestUtils.generateFakePubKey(), + TestUtils.generateChatMessage() + ); + + sandbox.stub(PendingMessageCache.prototype, 'add').resolves(rawMessage); + sandbox.stub(PendingMessageCache.prototype, 'remove').resolves(); + sandbox + .stub(PendingMessageCache.prototype, 'getDevices') + .returns(TestUtils.generateFakePubKeys(10)); + sandbox + .stub(PendingMessageCache.prototype, 'getForDevice') + .returns( + chatMessages.map(m => toRawMessage(TestUtils.generateFakePubKey(), m)) + ); + + // Spies + sendSyncMessageSpy = sandbox.spy(MessageQueue.prototype, 'sendSyncMessage'); + sendMessageToDevicesSpy = sandbox.spy( + MessageQueue.prototype, + 'sendMessageToDevices' + ); + sendToGroupSpy = sandbox.spy(MessageQueue.prototype, 'sendToGroup'); + + // Init Queue + messageQueueStub = new MessageQueue(); + }); + + afterEach(() => { + TestUtils.restoreStubs(); + sandbox.restore(); + }); + + describe('send', () => { + it('can send to a single device', async () => { + const device = TestUtils.generateFakePubKey(); + const message = TestUtils.generateChatMessage(); + + const promise = messageQueueStub.send(device, message); + await expect(promise).to.be.fulfilled; + }); + + it('can send sync message', async () => { + const devices = TestUtils.generateFakePubKeys(3); + const message = TestUtils.generateChatMessage(); + + const promise = messageQueueStub.sendSyncMessage(message, devices); + expect(promise).to.be.fulfilled; + }); + }); + + describe('processPending', () => { + it('will send session request message if no session', async () => { + hasSessionStub.resolves(false); + + const device = TestUtils.generateFakePubKey(); + const promise = messageQueueStub.processPending(device); + + await expect(promise).to.be.fulfilled; + expect(sendSessionRequestIfNeededStub.callCount).to.equal(1); + }); + + it('will send message if session exists', async () => { + const device = TestUtils.generateFakePubKey(); + const hasSession = await hasSessionStub(device); + + const promise = messageQueueStub.processPending(device); + await expect(promise).to.be.fulfilled; + + expect(hasSession).to.equal(true, 'session does not exist'); + expect(sendSessionRequestIfNeededStub.callCount).to.equal(0); + }); + }); + + describe('sendUsingMultiDevice', () => { + it('can send using multidevice', async () => { + const device = TestUtils.generateFakePubKey(); + const message = TestUtils.generateChatMessage(); + + const promise = messageQueueStub.sendUsingMultiDevice(device, message); + await expect(promise).to.be.fulfilled; + + // Ensure the arguments passed into sendMessageToDevices are correct + const previousArgs = sendMessageToDevicesSpy.lastCall.args as [ + Array, + ChatMessage + ]; + + // Check that instances are equal + expect(previousArgs).to.have.length(2); + + const argsPairedDevices = previousArgs[0]; + const argsChatMessage = previousArgs[1]; + + expect(argsChatMessage instanceof ChatMessage).to.equal( + true, + 'message passed into sendMessageToDevices was not a valid ChatMessage' + ); + expect(argsChatMessage.isEqual(message)).to.equal( + true, + 'message passed into sendMessageToDevices has been mutated' + ); + + argsPairedDevices.forEach((argsPaired: PubKey, index: number) => { + expect(argsPaired instanceof PubKey).to.equal( + true, + 'a device passed into sendMessageToDevices was not a PubKey' + ); + expect(argsPaired.isEqual(pairedDevices[index])).to.equal( + true, + 'a device passed into sendMessageToDevices did not match MessageDeviceProtocol.getAllDevices' + ); + }); + }); + }); + + describe('sendMessageToDevices', () => { + it('can send to many devices', async () => { + const devices = TestUtils.generateFakePubKeys(10); + const message = TestUtils.generateChatMessage(); + + const promise = messageQueueStub.sendMessageToDevices(devices, message); + await expect(promise).to.be.fulfilled; + }); + + it('can send sync message and confirm canSync is valid', async () => { + canSyncStub.returns(true); + + const devices = TestUtils.generateFakePubKeys(3); + const message = TestUtils.generateChatMessage(); + const pairedDeviceKeys = pairedDevices.map(device => device.key); + + const promise = messageQueueStub.sendMessageToDevices(devices, message); + await expect(promise).to.be.fulfilled; + + // Check sendSyncMessage parameters + const previousArgs = sendSyncMessageSpy.lastCall.args as [ + ChatMessage, + Array + ]; + expect(sendSyncMessageSpy.callCount).to.equal(1); + + // Check that instances are equal + expect(previousArgs).to.have.length(2); + + const argsChatMessage = previousArgs[0]; + const argsPairedKeys = [...previousArgs[1]].map(d => d.key); + + expect(argsChatMessage instanceof ChatMessage).to.equal( + true, + 'message passed into sendMessageToDevices was not a valid ChatMessage' + ); + expect(argsChatMessage.isEqual(message)).to.equal( + true, + 'message passed into sendMessageToDevices has been mutated' + ); + + // argsPairedKeys and pairedDeviceKeys should contain the same values + const keyArgsValid = _.isEmpty(_.xor(argsPairedKeys, pairedDeviceKeys)); + expect(keyArgsValid).to.equal( + true, + 'devices passed into sendSyncMessage were invalid' + ); + }); + }); + + describe('sendToGroup', () => { + it('can send to closed group', async () => { + const message = TestUtils.generateClosedGroupMessage(); + const success = await messageQueueStub.sendToGroup(message); + expect(success).to.equal(true, 'sending to group failed'); + }); + + it('uses correct parameters for sendToGroup with ClosedGroupMessage', async () => { + const message = TestUtils.generateClosedGroupMessage(); + const success = await messageQueueStub.sendToGroup(message); + + expect(success).to.equal(true, 'sending to group failed'); + + // Check parameters + const previousArgs = sendMessageToDevicesSpy.lastCall.args as [ + Array, + ClosedGroupMessage + ]; + expect(previousArgs).to.have.length(2); + + const argsClosedGroupMessage = previousArgs[1]; + expect(argsClosedGroupMessage instanceof ClosedGroupMessage).to.equal( + true, + 'message passed into sendMessageToDevices was not a ClosedGroupMessage' + ); + }); + + it("won't send to invalid groupId", async () => { + const message = TestUtils.generateClosedGroupMessage('invalid-group-id'); + const success = await messageQueueStub.sendToGroup(message); + + // Ensure message parameter passed into sendToGroup is as expected + expect(success).to.equal( + false, + 'an invalid groupId was treated as valid' + ); + expect(sendToGroupSpy.callCount).to.equal(1); + + const argsMessage = sendToGroupSpy.lastCall.args[0]; + expect(argsMessage instanceof ClosedGroupMessage).to.equal( + true, + 'message passed into sendToGroup was not a ClosedGroupMessage' + ); + expect(success).to.equal( + false, + 'invalid ClosedGroupMessage was propogated through sendToGroup' + ); + }); + + it('wont send message to empty closed group', async () => { + groupMembersStub.resolves(TestUtils.generateFakePubKeys(0)); + + const message = TestUtils.generateClosedGroupMessage(); + const response = await messageQueueStub.sendToGroup(message); + + expect(response).to.equal( + false, + 'sendToGroup send a message to an empty group' + ); + }); + + it('can send to open group', async () => { + const message = TestUtils.generateOpenGroupMessage(); + const success = await messageQueueStub.sendToGroup(message); + + expect(success).to.equal(true, 'sending to group failed'); + }); + }); + + describe('events', () => { + it('can send events on message sending success', async () => { + const successSpy = sandbox.spy(); + messageQueueStub.events.on('success', successSpy); + + const device = TestUtils.generateFakePubKey(); + const promise = messageQueueStub.processPending(device); + await expect(promise).to.be.fulfilled; + + await tick(); + expect(successSpy.callCount).to.equal(1); + }); + + it('can send events on message sending failure', async () => { + sendStub.throws(new Error('Failed to send message.')); + + const failureSpy = sandbox.spy(); + messageQueueStub.events.on('fail', failureSpy); + + const device = TestUtils.generateFakePubKey(); + const promise = messageQueueStub.processPending(device); + await expect(promise).to.be.fulfilled; + + await tick(); + expect(failureSpy.callCount).to.equal(1); + }); + }); +}); diff --git a/ts/test/session/sending/PendingMessageCache_test.ts b/ts/test/session/sending/PendingMessageCache_test.ts index 05b2d7b87..5ed1e211e 100644 --- a/ts/test/session/sending/PendingMessageCache_test.ts +++ b/ts/test/session/sending/PendingMessageCache_test.ts @@ -52,7 +52,7 @@ describe('PendingMessageCache', () => { }); it('can add to cache', async () => { - const device = TestUtils.generateFakePubkey(); + const device = TestUtils.generateFakePubKey(); const message = TestUtils.generateChatMessage(); const rawMessage = MessageUtils.toRawMessage(device, message); @@ -69,7 +69,7 @@ describe('PendingMessageCache', () => { }); it('can remove from cache', async () => { - const device = TestUtils.generateFakePubkey(); + const device = TestUtils.generateFakePubKey(); const message = TestUtils.generateChatMessage(); const rawMessage = MessageUtils.toRawMessage(device, message); @@ -90,15 +90,15 @@ describe('PendingMessageCache', () => { it('can get devices', async () => { const cacheItems = [ { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, ]; @@ -122,11 +122,11 @@ describe('PendingMessageCache', () => { it('can get pending for device', async () => { const cacheItems = [ { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, ]; @@ -149,7 +149,7 @@ describe('PendingMessageCache', () => { }); it('can find nothing when empty', async () => { - const device = TestUtils.generateFakePubkey(); + const device = TestUtils.generateFakePubKey(); const message = TestUtils.generateChatMessage(); const rawMessage = MessageUtils.toRawMessage(device, message); @@ -158,7 +158,7 @@ describe('PendingMessageCache', () => { }); it('can find message in cache', async () => { - const device = TestUtils.generateFakePubkey(); + const device = TestUtils.generateFakePubKey(); const message = TestUtils.generateChatMessage(); const rawMessage = MessageUtils.toRawMessage(device, message); @@ -175,15 +175,15 @@ describe('PendingMessageCache', () => { it('can clear cache', async () => { const cacheItems = [ { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, ]; @@ -205,15 +205,15 @@ describe('PendingMessageCache', () => { it('can restore from db', async () => { const cacheItems = [ { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, { - device: TestUtils.generateFakePubkey(), + device: TestUtils.generateFakePubKey(), message: TestUtils.generateChatMessage(), }, ]; diff --git a/ts/test/test-utils/testUtils.ts b/ts/test/test-utils/testUtils.ts index e6ed7db56..ef2efc0fb 100644 --- a/ts/test/test-utils/testUtils.ts +++ b/ts/test/test-utils/testUtils.ts @@ -5,7 +5,12 @@ import * as DataShape from '../../../js/modules/data'; import { v4 as uuid } from 'uuid'; import { PubKey } from '../../../ts/session/types'; -import { ChatMessage } from '../../session/messages/outgoing'; +import { + ChatMessage, + ClosedGroupChatMessage, + OpenGroupMessage, +} from '../../session/messages/outgoing'; +import { OpenGroup } from '../../session/types/OpenGroup'; const globalAny: any = global; const sandbox = sinon.createSandbox(); @@ -64,7 +69,7 @@ export function restoreStubs() { sandbox.restore(); } -export function generateFakePubkey(): PubKey { +export function generateFakePubKey(): PubKey { // Generates a mock pubkey for testing const numBytes = PubKey.PUBKEY_LEN / 2 - 1; const hexBuffer = crypto.randomBytes(numBytes).toString('hex'); @@ -74,8 +79,10 @@ export function generateFakePubkey(): PubKey { } export function generateFakePubKeys(amount: number): Array { + const numPubKeys = amount > 0 ? Math.floor(amount) : 0; + // tslint:disable-next-line: no-unnecessary-callback-wrapper - return new Array(amount).fill(0).map(() => generateFakePubkey()); + return new Array(numPubKeys).fill(0).map(() => generateFakePubKey()); } export function generateChatMessage(): ChatMessage { @@ -90,3 +97,30 @@ export function generateChatMessage(): ChatMessage { preview: undefined, }); } + +export function generateOpenGroupMessage(): OpenGroupMessage { + const group = new OpenGroup({ + server: 'chat.example.server', + channel: 0, + conversationId: '0', + }); + + return new OpenGroupMessage({ + timestamp: Date.now(), + group, + attachments: undefined, + preview: undefined, + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + quote: undefined, + }); +} + +export function generateClosedGroupMessage( + groupId?: string +): ClosedGroupChatMessage { + return new ClosedGroupChatMessage({ + identifier: uuid(), + groupId: groupId ?? generateFakePubKey().key, + chatMessage: generateChatMessage(), + }); +}