import { expect } from 'chai'; import * as sinon from 'sinon'; import { SessionProtocol } from '../../../../session/protocols'; import { Stubs, TestUtils } from '../../../test-utils'; import { UserUtil } from '../../../../util'; import { SessionRequestMessage } from '../../../../session/messages/outgoing'; import { TextEncoder } from 'util'; import { MessageSender } from '../../../../session/sending'; import { PubKey } from '../../../../session/types'; // tslint:disable-next-line: max-func-body-length describe('SessionProtocol', () => { const sandbox = sinon.createSandbox(); const ourNumber = TestUtils.generateFakePubKey(); const pubkey = TestUtils.generateFakePubKey(); let getItemById: sinon.SinonStub; let send: sinon.SinonStub; const resetMessage: SessionRequestMessage = new SessionRequestMessage({ timestamp: Date.now(), preKeyBundle: { identityKey: new TextEncoder().encode('identityKey'), deviceId: 1, preKeyId: 2, signedKeyId: 3, preKey: new TextEncoder().encode('preKey'), signedKey: new TextEncoder().encode('signedKey'), signature: new TextEncoder().encode('signature'), }, }); beforeEach(() => { TestUtils.stubWindow('libsignal', { SignalProtocolAddress: sandbox.stub(), SessionCipher: Stubs.SessionCipherStub, } as any); TestUtils.stubWindow('libloki', { storage: { getPreKeyBundleForContact: sandbox.stub(), }, }); TestUtils.stubWindow('textsecure', { storage: { protocol: sandbox.stub(), }, }); TestUtils.stubData('createOrUpdateItem'); getItemById = TestUtils.stubData('getItemById').resolves({ value: {} }); sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber.key); send = sandbox.stub(MessageSender, 'send' as any); SessionProtocol.reset(); }); afterEach(() => { sandbox.restore(); TestUtils.restoreStubs(); }); describe('db fetch', () => { it('protocol: should fetch from DB `sentSessionsTimestamp` and `processedSessionsTimestamp`', async () => { await SessionProtocol.hasSentSessionRequest(pubkey); expect(getItemById.calledWith('sentSessionsTimestamp')); expect(getItemById.calledWith('processedSessionsTimestamp')); expect(getItemById.callCount).to.equal(2); }); it('protocol: should fetch only once', async () => { await SessionProtocol.hasSentSessionRequest(pubkey); await SessionProtocol.hasSentSessionRequest(pubkey); await SessionProtocol.hasSentSessionRequest(pubkey); await SessionProtocol.hasSentSessionRequest(pubkey); expect(getItemById.calledWith('sentSessionsTimestamp')); expect(getItemById.calledWith('processedSessionsTimestamp')); expect(getItemById.callCount).to.equal(2); }); }); describe('sendSessionRequest', () => { beforeEach(async () => { // trigger a sessionReset await SessionProtocol.sendSessionRequest(resetMessage, pubkey); }); it('protocol: sendSessionRequest should add the deviceID to the sentMap', async () => { expect(SessionProtocol.getSentSessionsTimestamp()) .to.have.property(pubkey.key) .to.be.approximately(Date.now(), 100); }); it('protocol: sendSessionRequest should not have pendingSend set after', async () => { expect( SessionProtocol.getPendingSendSessionTimestamp() ).to.not.have.property(pubkey.key); }); }); describe('checkSessionRequestExpiry', () => { let clock: sinon.SinonFakeTimers; let now: number; let sendSessionRequestStub: sinon.SinonStub< [SessionRequestMessage, PubKey], Promise<void> >; beforeEach(() => { now = Date.now(); clock = sandbox.useFakeTimers(now); sendSessionRequestStub = sandbox .stub(SessionProtocol, 'sendSessionRequest') .resolves(); }); it('should not send a session request if none have expired', async () => { getItemById.withArgs('sentSessionsTimestamp').resolves({ id: 'sentSessionsTimestamp', value: { [pubkey.key]: now, }, }); // Set the time just before expiry clock.tick(SessionRequestMessage.defaultTTL() - 100); await SessionProtocol.checkSessionRequestExpiry(); expect(getItemById.calledWith('sentSessionsTimestamp')); expect(sendSessionRequestStub.callCount).to.equal(0); }); it('should send a session request if expired', async () => { getItemById.withArgs('sentSessionsTimestamp').resolves({ id: 'sentSessionsTimestamp', value: { [pubkey.key]: now, }, }); // Expire the request clock.tick(SessionRequestMessage.defaultTTL() + 100); await SessionProtocol.checkSessionRequestExpiry(); expect(getItemById.calledWith('sentSessionsTimestamp')); expect(sendSessionRequestStub.callCount).to.equal(1); }); it('should remove the old sent timestamp when expired', async () => { getItemById.withArgs('sentSessionsTimestamp').resolves({ id: 'sentSessionsTimestamp', value: { [pubkey.key]: now, }, }); // Remove this call from the equation sandbox.stub(SessionProtocol, 'sendSessionRequestIfNeeded').resolves(); // Expire the request clock.tick(SessionRequestMessage.defaultTTL() + 100); await SessionProtocol.checkSessionRequestExpiry(); expect(getItemById.calledWith('sentSessionsTimestamp')); expect(await SessionProtocol.hasSentSessionRequest(pubkey)).to.equal( false, 'hasSentSessionRequest should return false.' ); expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property( pubkey.key ); }); }); describe('onSessionEstablished', () => { beforeEach(async () => { // add an existing entry in the sentMap await SessionProtocol.sendSessionRequest(resetMessage, pubkey); }); it('protocol: onSessionEstablished should remove the device in sentTimestamps', async () => { expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property( pubkey.key ); await SessionProtocol.onSessionEstablished(pubkey); expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property( pubkey.key ); }); it('protocol: onSessionEstablished should remove the device in sentTimestamps and ONLY that one', async () => { // add a second item to the map const anotherPubKey = TestUtils.generateFakePubKey(); await SessionProtocol.sendSessionRequest(resetMessage, anotherPubKey); expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property( pubkey.key ); expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property( anotherPubKey.key ); await SessionProtocol.onSessionEstablished(pubkey); expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property( pubkey.key ); expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property( anotherPubKey.key ); }); }); describe('hasSentSessionRequest', () => { it('protocol: hasSentSessionRequest returns false if a message was not sent to that device', async () => { const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey); expect(hasSent).to.be.equal( false, `hasSent should be false for ${pubkey.key}` ); }); it('protocol: hasSentSessionRequest returns true if a message is already sent for that device', async () => { // add an existing entry in the sentMap await SessionProtocol.sendSessionRequest(resetMessage, pubkey); const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey); expect(hasSent).to.be.equal( true, `hasSent should be true for ${pubkey.key}` ); }); // TODO add a test to validate that pending is filled when message is triggered and not yet sent }); describe('sendSessionRequestIfNeeded', () => { it('protocol: sendSessionRequestIfNeeded should send a new sessionMessage ', async () => { // not called before, so the message reset sending should be triggered await SessionProtocol.sendSessionRequestIfNeeded(pubkey); expect(send.callCount).to.be.equal( 1, 'MessageSender.send() should have been called' ); // check that the map is updated with that ID const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey); expect(hasSent).to.be.equal( true, `hasSent should be true for ${pubkey.key}` ); }); it('protocol: sendSessionRequestIfNeeded should NOT send a new sessionMessage on second try ', async () => { await SessionProtocol.sendSessionRequestIfNeeded(pubkey); expect(send.callCount).to.be.equal( 1, 'MessageSender.send() should have been called' ); // check that the map is updated with that ID const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey); expect(hasSent).to.be.equal( true, `hasSent should be true for ${pubkey.key}` ); send.resetHistory(); // trigger a second call, Message.send().calledCount should still be 1 await SessionProtocol.sendSessionRequestIfNeeded(pubkey); expect(send.callCount).to.be.equal( 0, 'MessageSender.send() should NOT have been called a second time' ); }); }); describe('onSessionRequestProcessed', () => { it('protocol: onSessionRequestProcessed should insert a new item in the processedMap ', async () => { // trigger the requestProcessed and check the map is updated await SessionProtocol.onSessionRequestProcessed(pubkey); expect(SessionProtocol.getProcessedSessionsTimestamp()) .to.have.property(pubkey.key) .to.be.approximately(Date.now(), 5); }); it('protocol: onSessionRequestProcessed should update an existing item in the processedMap ', async () => { // trigger the requestProcessed and check the map is updated // then trigger it a second time, and expect a change in the processed timestamp await SessionProtocol.onSessionRequestProcessed(pubkey); expect(SessionProtocol.getProcessedSessionsTimestamp()) .to.have.property(pubkey.key) .to.be.approximately(Date.now(), 5); await TestUtils.timeout(5); const oldTimestamp = SessionProtocol.getProcessedSessionsTimestamp()[ pubkey.key ]; await SessionProtocol.onSessionRequestProcessed(pubkey); expect(SessionProtocol.getProcessedSessionsTimestamp()) .to.have.property(pubkey.key) .to.be.approximately(Date.now(), 5) .to.not.be.equal(oldTimestamp); }); }); describe('shouldProcessSessionRequest', () => { it('protocol: shouldProcessSessionRequest returns true if timestamp is more recent than processed timestamp', async () => { await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry expect( SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() + 1000) ).to.be.eventually.equal( true, 'shouldProcessSessionRequest should return true when existingProcessed is less recent' ); }); it('protocol: shouldProcessSessionRequest returns true if there is no processed timestamp yet for this device', async () => { expect( SessionProtocol.shouldProcessSessionRequest(pubkey, 100) ).to.be.eventually.equal( true, 'shouldProcessSessionRequest should return false when existingProcessed is empty for this device' ); }); it('protocol: shouldProcessSessionRequest returns false if timestamp is less recent than current processed timestamp', async () => { await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry expect( SessionProtocol.shouldProcessSessionRequest(pubkey, 100) ).to.be.eventually.equal( false, 'shouldProcessSessionRequest should return false when existingProcessed is more recent' ); }); it('protocol: shouldProcessSessionRequest returns false if timestamp is less recent than current sent timestamp', async () => { await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry expect( SessionProtocol.shouldProcessSessionRequest(pubkey, 100) ).to.be.eventually.equal( false, 'shouldProcessSessionRequest should return false when existingSent is more recent' ); }); it('protocol: shouldProcessSessionRequest returns true if timestamp is more recent than current sent timestamp', async () => { await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry expect( SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() + 1000) ).to.be.eventually.equal( true, 'shouldProcessSessionRequest should return true when existingSent is less recent' ); }); it('protocol: shouldProcessSessionRequest returns true if there is no sent timestamp', async () => { expect( SessionProtocol.shouldProcessSessionRequest(pubkey, 100) ).to.be.eventually.equal( true, 'shouldProcessSessionRequest should return true as there is no sent timestamp' ); }); it('protocol: shouldProcessSessionRequest returns false if there is a more recent sent but a less recent processed', async () => { await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry await TestUtils.timeout(100); await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry 100ms after expect( SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() - 50) ).to.be.eventually.equal( false, 'shouldProcessSessionRequest should return false if there is a more recent sent but a less recent processed' ); }); it('protocol: shouldProcessSessionRequest returns false if there is a more recent processed but a less recent sent', async () => { await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry await TestUtils.timeout(100); await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry 100ms after expect( SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() - 50) ).to.be.eventually.equal( false, 'shouldProcessSessionRequest should return false if there is a more recent processed but a less recent sent' ); }); it('protocol: shouldProcessSessionRequest returns true if both sent and processed timestamp are older', async () => { await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry expect( SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() + 1000) ).to.be.eventually.equal( true, 'shouldProcessSessionRequest should return true if there if both processed and sent are set but are older' ); }); }); });