import chai 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 {
ClosedGroupMessage ,
ContentMessage ,
OpenGroupMessage ,
} from '../../../session/messages/outgoing' ;
import { PrimaryPubKey , PubKey , RawMessage } from '../../../session/types' ;
import { UserUtil } from '../../../util' ;
import { MessageSender } from '../../../session/sending' ;
import {
MultiDeviceProtocol ,
SessionProtocol ,
} from '../../../session/protocols' ;
import { PendingMessageCacheStub } from '../../test-utils/stubs' ;
import { describe } from 'mocha' ;
import { TestSyncMessage } from '../../test-utils/stubs/messages/TestSyncMessage' ;
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require ( 'chai-as-promised' ) ;
chai . use ( chaiAsPromised ) ;
const { expect } = chai ;
describe ( 'MessageQueue' , ( ) = > {
// Initialize new stubbed cache
const sandbox = sinon . createSandbox ( ) ;
const ourDevice = TestUtils . generateFakePubKey ( ) ;
const ourNumber = ourDevice . key ;
// Initialize new stubbed queue
let pendingMessageCache : PendingMessageCacheStub ;
let messageQueueStub : MessageQueue ;
// Message Sender Stubs
let sendStub : sinon.SinonStub < [ RawMessage , ( number | undefined ) ? ] > ;
// Utils Stubs
let isMediumGroupStub : sinon.SinonStub < [ string ] , boolean > ;
// Session Protocol Stubs
let hasSessionStub : sinon.SinonStub < [ PubKey ] > ;
let sendSessionRequestIfNeededStub : sinon.SinonStub < [ PubKey ] , Promise < void > > ;
beforeEach ( async ( ) = > {
// Utils Stubs
sandbox . stub ( UserUtil , 'getCurrentDevicePubKey' ) . resolves ( ourNumber ) ;
TestUtils . stubWindow ( 'libsignal' , {
SignalProtocolAddress : sandbox.stub ( ) ,
SessionCipher : Stubs.SessionCipherStub ,
} as any ) ;
// Message Sender Stubs
sendStub = sandbox . stub ( MessageSender , 'send' ) . resolves ( ) ;
// Group Utils Stubs
isMediumGroupStub = sandbox
. stub ( GroupUtils , 'isMediumGroup' )
. returns ( false ) ;
// Session Protocol Stubs
sandbox . stub ( SessionProtocol , 'sendSessionRequest' ) . resolves ( ) ;
hasSessionStub = sandbox . stub ( SessionProtocol , 'hasSession' ) . resolves ( true ) ;
sendSessionRequestIfNeededStub = sandbox
. stub ( SessionProtocol , 'sendSessionRequestIfNeeded' )
. resolves ( ) ;
// Init Queue
pendingMessageCache = new PendingMessageCacheStub ( ) ;
messageQueueStub = new MessageQueue ( pendingMessageCache ) ;
} ) ;
afterEach ( ( ) = > {
TestUtils . restoreStubs ( ) ;
sandbox . restore ( ) ;
} ) ;
describe ( 'processPending' , ( ) = > {
it ( 'will send session request message if no session' , async ( ) = > {
hasSessionStub . resolves ( false ) ;
isMediumGroupStub . resolves ( false ) ;
const device = TestUtils . generateFakePubKey ( ) ;
const stubCallPromise = TestUtils . waitUntil ( ( ) = > sendSessionRequestIfNeededStub . callCount === 1 ) ;
await messageQueueStub . processPending ( device ) ;
expect ( stubCallPromise ) . to . be . fulfilled ;
} ) ;
it ( 'will send message if session exists' , async ( ) = > {
hasSessionStub . resolves ( true ) ;
isMediumGroupStub . resolves ( false ) ;
sendStub . resolves ( ) ;
const device = TestUtils . generateFakePubKey ( ) ;
await pendingMessageCache . add ( device , TestUtils . generateChatMessage ( ) ) ;
const successPromise = TestUtils . waitForTask ( done = > {
messageQueueStub . events . once ( 'success' , done ) ;
} ) ;
await messageQueueStub . processPending ( device ) ;
await expect ( successPromise ) . to . be . fulfilled ;
expect ( sendSessionRequestIfNeededStub . called ) . to . equal (
false ,
'Session request triggered when we have a session.'
) ;
} ) ;
it ( 'will send message if sending to medium group' , async ( ) = > {
isMediumGroupStub . resolves ( true ) ;
sendStub . resolves ( ) ;
const device = TestUtils . generateFakePubKey ( ) ;
await pendingMessageCache . add ( device , TestUtils . generateChatMessage ( ) ) ;
const successPromise = TestUtils . waitForTask ( done = > {
messageQueueStub . events . once ( 'success' , done ) ;
} ) ;
await messageQueueStub . processPending ( device ) ;
await expect ( successPromise ) . to . be . fulfilled ;
expect ( sendSessionRequestIfNeededStub . called ) . to . equal (
false ,
'Session request triggered on medium group'
) ;
} ) ;
it ( 'should remove message from cache' , async ( ) = > {
hasSessionStub . resolves ( true ) ;
isMediumGroupStub . resolves ( false ) ;
const events = [ 'success' , 'fail' ] ;
for ( const event of events ) {
if ( event === 'success' ) {
sendStub . resolves ( ) ;
} else {
sendStub . throws ( new Error ( 'fail' ) ) ;
}
const device = TestUtils . generateFakePubKey ( ) ;
await pendingMessageCache . add ( device , TestUtils . generateChatMessage ( ) ) ;
const initialMessages = await pendingMessageCache . getForDevice ( device ) ;
expect ( initialMessages ) . to . have . length ( 1 ) ;
await messageQueueStub . processPending ( device ) ;
const promise = TestUtils . waitUntil ( async ( ) = > {
const messages = await pendingMessageCache . getForDevice ( device ) ;
return messages . length === 0 ;
} ) ;
expect ( promise ) . to . be . fulfilled ;
}
} ) ;
describe ( 'events' , ( ) = > {
it ( 'should send a success event if message was sent' , async ( ) = > {
hasSessionStub . resolves ( true ) ;
isMediumGroupStub . resolves ( false ) ;
sendStub . resolves ( ) ;
const device = TestUtils . generateFakePubKey ( ) ;
const message = TestUtils . generateChatMessage ( ) ;
await pendingMessageCache . add ( device , message ) ;
const eventPromise = TestUtils . waitForTask < RawMessage | OpenGroupMessage > ( complete = > {
messageQueueStub . events . once ( 'success' , complete ) ;
} ) ;
await messageQueueStub . processPending ( device ) ;
await expect ( eventPromise ) . to . be . fulfilled ;
const rawMessage = await eventPromise ;
expect ( rawMessage . identifier ) . to . equal ( message . identifier ) ;
} ) ;
it ( 'should send a fail event if something went wrong while sending' , async ( ) = > {
hasSessionStub . resolves ( true ) ;
isMediumGroupStub . resolves ( false ) ;
sendStub . throws ( new Error ( 'failure' ) ) ;
const spy = sandbox . spy ( ) ;
messageQueueStub . events . on ( 'fail' , spy ) ;
const device = TestUtils . generateFakePubKey ( ) ;
const message = TestUtils . generateChatMessage ( ) ;
await pendingMessageCache . add ( device , message ) ;
const eventPromise = TestUtils . waitForTask < [ RawMessage | OpenGroupMessage , Error ] > ( complete = > {
messageQueueStub . events . once ( 'fail' , ( . . . args ) = > {
complete ( args ) ;
} ) ;
} ) ;
await messageQueueStub . processPending ( device ) ;
await expect ( eventPromise ) . to . be . fulfilled ;
const [ rawMessage , error ] = await eventPromise ;
expect ( rawMessage . identifier ) . to . equal ( message . identifier ) ;
expect ( error . message ) . to . equal ( 'failure' ) ;
} ) ;
} ) ;
} ) ;
describe ( 'sendUsingMultiDevice' , ( ) = > {
it ( 'should send the message to all the devices' , async ( ) = > {
const devices = TestUtils . generateFakePubKeys ( 3 ) ;
sandbox . stub ( MultiDeviceProtocol , 'getAllDevices' ) . resolves ( devices ) ;
const stub = sandbox
. stub ( messageQueueStub , 'sendMessageToDevices' )
. resolves ( ) ;
const message = TestUtils . generateChatMessage ( ) ;
await messageQueueStub . sendUsingMultiDevice ( devices [ 0 ] , message ) ;
const args = stub . lastCall . args as [ Array < PubKey > , ContentMessage ] ;
expect ( args [ 0 ] ) . to . have . same . members ( devices ) ;
expect ( args [ 1 ] ) . to . equal ( message ) ;
} ) ;
} ) ;
describe ( 'sendMessageToDevices' , ( ) = > {
it ( 'can send to many devices' , async ( ) = > {
hasSessionStub . resolves ( false ) ;
const devices = TestUtils . generateFakePubKeys ( 5 ) ;
const message = TestUtils . generateChatMessage ( ) ;
await messageQueueStub . sendMessageToDevices ( devices , message ) ;
const promise = TestUtils . waitUntil ( ( ) = > pendingMessageCache . getCache ( ) . length === devices . length ) ;
await expect ( promise ) . to . be . fulfilled ;
} ) ;
it ( 'should send sync message if possible' , async ( ) = > {
hasSessionStub . returns ( false ) ;
sandbox . stub ( SyncMessageUtils , 'canSync' ) . returns ( true ) ;
sandbox
. stub ( SyncMessageUtils , 'from' )
. returns ( new TestSyncMessage ( { timestamp : Date.now ( ) } ) ) ;
// This stub ensures that the message won't process
const sendSyncMessageStub = sandbox
. stub ( messageQueueStub , 'sendSyncMessage' )
. resolves ( ) ;
const ourDevices = [ ourDevice , . . . TestUtils . generateFakePubKeys ( 2 ) ] ;
sandbox
. stub ( MultiDeviceProtocol , 'getAllDevices' )
. callsFake ( async user = > {
if ( ourDevice . isEqual ( user ) ) {
return ourDevices ;
}
return [ ] ;
} ) ;
const devices = [ . . . ourDevices , . . . TestUtils . generateFakePubKeys ( 3 ) ] ;
const message = TestUtils . generateChatMessage ( ) ;
await messageQueueStub . sendMessageToDevices ( devices , message ) ;
expect ( sendSyncMessageStub . called ) . to . equal (
true ,
'sendSyncMessage was not called.'
) ;
expect ( pendingMessageCache . getCache ( ) . map ( c = > c . device ) ) . to . not . have . members ( ourDevices . map ( d = > d . key ) , 'Sending regular messages to our own device is not allowed.' ) ;
expect ( pendingMessageCache . getCache ( ) ) . to . have . length (
devices . length - ourDevices . length ,
'Messages should not be sent to our devices.'
) ;
} ) ;
} ) ;
describe ( 'sendSyncMessage' , ( ) = > {
it ( 'should send a message to all our devices' , async ( ) = > {
hasSessionStub . resolves ( false ) ;
const ourOtherDevices = TestUtils . generateFakePubKeys ( 2 ) ;
const ourDevices = [ ourDevice , . . . ourOtherDevices ] ;
sandbox . stub ( MultiDeviceProtocol , 'getAllDevices' ) . resolves ( ourDevices ) ;
await messageQueueStub . sendSyncMessage (
new TestSyncMessage ( { timestamp : Date.now ( ) } )
) ;
expect ( pendingMessageCache . getCache ( ) ) . to . have . length ( ourOtherDevices . length ) ;
expect ( pendingMessageCache . getCache ( ) . map ( c = > c . device ) ) . to . have . members ( ourOtherDevices . map ( d = > d . key ) ) ;
} ) ;
} ) ;
describe ( 'sendToGroup' , ( ) = > {
describe ( 'closed groups' , async ( ) = > {
it ( 'can send to closed group' , async ( ) = > {
const members = TestUtils . generateFakePubKeys ( 4 ) . map (
p = > new PrimaryPubKey ( p . key )
) ;
sandbox . stub ( GroupUtils , 'getGroupMembers' ) . resolves ( members ) ;
const sendUsingMultiDeviceStub = sandbox
. stub ( messageQueueStub , 'sendUsingMultiDevice' )
. resolves ( ) ;
const message = TestUtils . generateClosedGroupMessage ( ) ;
const success = await messageQueueStub . sendToGroup ( message ) ;
expect ( success ) . to . equal ( true , 'sending to group failed' ) ;
expect ( sendUsingMultiDeviceStub . callCount ) . to . equal ( members . length ) ;
const arg = sendUsingMultiDeviceStub . getCall ( 0 ) . args ;
expect ( arg [ 1 ] instanceof ClosedGroupMessage ) . to . equal (
true ,
'message sent to group member was not a ClosedGroupMessage'
) ;
} ) ;
it ( 'wont send message to empty closed group' , async ( ) = > {
sandbox . stub ( GroupUtils , 'getGroupMembers' ) . resolves ( [ ] ) ;
const sendUsingMultiDeviceStub = sandbox
. stub ( messageQueueStub , 'sendUsingMultiDevice' )
. resolves ( ) ;
const message = TestUtils . generateClosedGroupMessage ( ) ;
const response = await messageQueueStub . sendToGroup ( message ) ;
expect ( response ) . to . equal (
false ,
'sendToGroup sent a message to an empty group'
) ;
expect ( sendUsingMultiDeviceStub . callCount ) . to . equal ( 0 ) ;
} ) ;
} ) ;
describe ( 'open groups' , async ( ) = > {
let sendToOpenGroupStub : sinon.SinonStub <
[ OpenGroupMessage ] ,
Promise < boolean >
> ;
beforeEach ( ( ) = > {
sendToOpenGroupStub = sandbox
. stub ( MessageSender , 'sendToOpenGroup' )
. resolves ( true ) ;
} ) ;
it ( 'can send to open group' , async ( ) = > {
const message = TestUtils . generateOpenGroupMessage ( ) ;
const success = await messageQueueStub . sendToGroup ( message ) ;
expect ( sendToOpenGroupStub . callCount ) . to . equal ( 1 ) ;
expect ( success ) . to . equal ( true , 'Sending to open group failed' ) ;
} ) ;
it ( 'should emit a success event when send was successful' , async ( ) = > {
const message = TestUtils . generateOpenGroupMessage ( ) ;
const eventPromise = TestUtils . waitForTask ( complete = > {
messageQueueStub . events . once ( 'success' , complete ) ;
} , 2000 ) ;
await messageQueueStub . sendToGroup ( message ) ;
await expect ( eventPromise ) . to . be . fulfilled ;
} ) ;
it ( 'should emit a fail event if something went wrong' , async ( ) = > {
sendToOpenGroupStub . resolves ( false ) ;
const message = TestUtils . generateOpenGroupMessage ( ) ;
const eventPromise = TestUtils . waitForTask ( complete = > {
messageQueueStub . events . once ( 'fail' , complete ) ;
} , 2000 ) ;
await messageQueueStub . sendToGroup ( message ) ;
await expect ( eventPromise ) . to . be . fulfilled ;
} ) ;
} ) ;
} ) ;
} ) ;