import chai from 'chai'; import { describe } from 'mocha'; import Sinon, * as sinon from 'sinon'; import chaiAsPromised from 'chai-as-promised'; import { ConversationModel } from '../../../../models/conversation'; import { getSwarmPollingInstance, SnodePool } from '../../../../session/apis/snode_api'; import { resetHardForkCachedValues } from '../../../../session/apis/snode_api/hfHandling'; import { SnodeAPIRetrieve } from '../../../../session/apis/snode_api/retrieveRequest'; import { SwarmPolling } from '../../../../session/apis/snode_api/swarmPolling'; import { SWARM_POLLING_TIMEOUT } from '../../../../session/constants'; import { getConversationController } from '../../../../session/conversations'; import { PubKey } from '../../../../session/types'; import { UserUtils } from '../../../../session/utils'; import { ConfigurationSync } from '../../../../session/utils/job_runners/jobs/ConfigurationSyncJob'; import { sleepFor } from '../../../../session/utils/Promise'; import { UserGroupsWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; import { TestUtils } from '../../../test-utils'; import { generateFakeSnodes, stubData } from '../../../test-utils/utils'; import { ConversationTypeEnum } from '../../../../models/types'; chai.use(chaiAsPromised as any); chai.should(); const { expect } = chai; describe('SwarmPolling', () => { // Initialize new stubbed cache const ourPubkey = TestUtils.generateFakePubKey(); const ourNumber = ourPubkey.key; let pollOnceForKeySpy: Sinon.SinonSpy; let swarmPolling: SwarmPolling; let getItemByIdStub: Sinon.SinonStub; let clock: Sinon.SinonFakeTimers; beforeEach(async () => { getConversationController().reset(); TestUtils.stubWindowFeatureFlags(); Sinon.stub(ConfigurationSync, 'queueNewJobIfNeeded').resolves(); // Utils Stubs Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber); stubData('getAllConversations').resolves([]); getItemByIdStub = TestUtils.stubData('getItemById'); stubData('saveConversation').resolves(); stubData('getSwarmNodesForPubkey').resolves(); stubData('getLastHashBySnode').resolves(); Sinon.stub(SnodePool, 'getSwarmFor').resolves(generateFakeSnodes(5)); Sinon.stub(SnodeAPIRetrieve, 'retrieveNextMessages').resolves([]); TestUtils.stubWindow('inboxStore', undefined); TestUtils.stubWindow('getGlobalOnlineStatus', () => true); TestUtils.stubWindowLog(); const convoController = getConversationController(); await convoController.load(); getConversationController().getOrCreate(ourPubkey.key, ConversationTypeEnum.PRIVATE); swarmPolling = getSwarmPollingInstance(); swarmPolling.resetSwarmPolling(); pollOnceForKeySpy = Sinon.spy(swarmPolling, 'pollOnceForKey'); clock = sinon.useFakeTimers({ now: Date.now(), shouldAdvanceTime: true }); }); afterEach(() => { Sinon.restore(); getConversationController().reset(); clock.restore(); resetHardForkCachedValues(); }); describe('getPollingTimeout', () => { beforeEach(() => { TestUtils.stubLibSessionWorker(undefined); }); it('returns INACTIVE for non existing convo', () => { const fakeConvo = TestUtils.generateFakePubKey(); expect(swarmPolling.getPollingTimeout(fakeConvo)).to.eq(SWARM_POLLING_TIMEOUT.INACTIVE); }); describe('legacy groups', () => { it('returns ACTIVE for convo with less than two days old activeAt', () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); convo.set('active_at', Date.now() - 2 * 23 * 3600 * 1000); // 23 * 2 = 46 hours old expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.ACTIVE ); }); it('returns INACTIVE for convo with undefined activeAt', () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); convo.set('active_at', undefined); expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.INACTIVE ); }); it('returns MEDIUM_ACTIVE for convo with activeAt of more than 2 days but less than a week old', () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); convo.set('active_at', Date.now() - 1000 * 3600 * 25 * 2); // 25 hours x 2 = 50 hours old expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.MEDIUM_ACTIVE ); convo.set('active_at', Date.now() - 1000 * 3600 * 24 * 7 + 3600); // a week minus an hour old expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.MEDIUM_ACTIVE ); }); it('returns INACTIVE for convo with activeAt of more than a week', () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); convo.set('active_at', Date.now() - 1000 * 3600 * 24 * 8); // 8 days expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.INACTIVE ); }); }); describe('groupv3', () => { it('returns ACTIVE for convo with less than two days old activeAt', () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakeClosedGroupV3PkStr(), ConversationTypeEnum.GROUPV3 ); convo.set('active_at', Date.now() - 2 * 23 * 3600 * 1000); // 23 * 2 = 46 hours old expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.ACTIVE ); }); it('returns INACTIVE for convo with undefined activeAt', () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakeClosedGroupV3PkStr(), ConversationTypeEnum.GROUPV3 ); convo.set('active_at', undefined); expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.INACTIVE ); }); it('returns MEDIUM_ACTIVE for convo with activeAt of more than 2 days but less than a week old', () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakeClosedGroupV3PkStr(), ConversationTypeEnum.GROUPV3 ); convo.set('active_at', Date.now() - 1000 * 3600 * 25 * 2); // 25 hours x 2 = 50 hours old expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.MEDIUM_ACTIVE ); convo.set('active_at', Date.now() - 1000 * 3600 * 24 * 7 + 3600); // a week minus an hour old expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.MEDIUM_ACTIVE ); }); it('returns INACTIVE for convo with activeAt of more than a week', () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakeClosedGroupV3PkStr(), ConversationTypeEnum.GROUPV3 ); convo.set('active_at', Date.now() - 1000 * 3600 * 24 * 8); // 8 days expect(swarmPolling.getPollingTimeout(PubKey.cast(convo.id as string))).to.eq( SWARM_POLLING_TIMEOUT.INACTIVE ); }); }); }); describe('pollForAllKeys', () => { beforeEach(() => { stubData('createOrUpdateItem').resolves(); }); afterEach(() => { Sinon.restore(); }); it('does run for our pubkey even if activeAt is really old ', async () => { const convo = getConversationController().getOrCreate( ourNumber, ConversationTypeEnum.PRIVATE ); convo.set('active_at', Date.now() - 1000 * 3600 * 25); await swarmPolling.start(true); expect(pollOnceForKeySpy.callCount).to.eq(1); expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); }); it('does run for our pubkey even if activeAt is recent ', async () => { const convo = getConversationController().getOrCreate( ourNumber, ConversationTypeEnum.PRIVATE ); convo.set('active_at', Date.now()); await swarmPolling.start(true); expect(pollOnceForKeySpy.callCount).to.eq(1); expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); }); describe('legacy group', () => { it('does run for group pubkey on start no matter the recent timestamp', async () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); TestUtils.stubLibSessionWorker(undefined); convo.set('active_at', Date.now()); const groupConvoPubkey = PubKey.cast(convo.id as string); swarmPolling.addGroupId(groupConvoPubkey); await swarmPolling.start(true); // our pubkey will be polled for, hence the 2 expect(pollOnceForKeySpy.callCount).to.eq(2); expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); }); it('does only poll from -10 for closed groups if HF >= 19.1 ', async () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); TestUtils.stubLibSessionWorker(undefined); getItemByIdStub.restore(); getItemByIdStub = TestUtils.stubData('getItemById'); getItemByIdStub .withArgs('hasSeenHardfork190') .resolves({ id: 'hasSeenHardfork190', value: true }) .withArgs('hasSeenHardfork191') .resolves({ id: 'hasSeenHardfork191', value: true }); convo.set('active_at', 1); const groupConvoPubkey = PubKey.cast(convo.id as string); swarmPolling.addGroupId(groupConvoPubkey); await swarmPolling.start(true); // our pubkey will be polled for, hence the 2 expect(pollOnceForKeySpy.callCount).to.eq(2); expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); getItemByIdStub.restore(); getItemByIdStub = TestUtils.stubData('getItemById'); getItemByIdStub.resolves(); }); it('does run for group pubkey on start but not another time if activeAt is old ', async () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); TestUtils.stubLibSessionWorker(undefined); convo.set('active_at', 1); // really old, but active const groupConvoPubkey = PubKey.cast(convo.id as string); swarmPolling.addGroupId(groupConvoPubkey); // this calls the stub 2 times, one for our direct pubkey and one for the group await swarmPolling.start(true); // this should only call the stub one more time: for our direct pubkey but not for the group pubkey await swarmPolling.pollForAllKeys(); expect(pollOnceForKeySpy.callCount).to.eq(3); expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); }); it('does run twice if activeAt less than one hour ', async () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); // fake that the group is part of the wrapper otherwise we stop tracking it after the first polling event Sinon.stub(UserGroupsWrapperActions, 'getLegacyGroup').resolves({} as any); convo.set('active_at', Date.now()); const groupConvoPubkey = PubKey.cast(convo.id as string); swarmPolling.addGroupId(groupConvoPubkey); await swarmPolling.start(true); expect(pollOnceForKeySpy.callCount).to.eq(2); expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); pollOnceForKeySpy.resetHistory(); clock.tick(9000); // no need to do that as the tick will trigger a call in all cases after 5 secs await swarmPolling.pollForAllKeys(); /** this is not easy to explain, but * - during the swarmPolling.start, we get two calls to pollOnceForKeySpy (one for our id and one for group id) * - the clock ticks 9sec, and another call of pollOnceForKeySpy get started, but as we do not await them, this test fails. * the only fix is to restore the clock and force the a small sleep to let the thing run in bg */ await sleepFor(10); expect(pollOnceForKeySpy.callCount).to.eq(2); expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); }); it('does run twice if activeAt is inactive and we tick longer than 2 minutes', async () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); // fake that the group is part of the wrapper otherwise we stop tracking it after the first polling event Sinon.stub(UserGroupsWrapperActions, 'getLegacyGroup').resolves({} as any); pollOnceForKeySpy.resetHistory(); convo.set('active_at', Date.now()); const groupConvoPubkey = PubKey.cast(convo.id as string); swarmPolling.addGroupId(groupConvoPubkey); // this call the stub two times already, one for our direct pubkey and one for the group await swarmPolling.start(true); const timeToTick = 3 * 60 * 1000; swarmPolling.forcePolledTimestamp(groupConvoPubkey, Date.now() - timeToTick); // more than week old, so inactive group but we have to tick after more than 2 min convo.set('active_at', Date.now() - 7 * 25 * 3600 * 1000); clock.tick(timeToTick); /** this is not easy to explain, but * - during the swarmPolling.start, we get two calls to pollOnceForKeySpy (one for our id and one for group od) * - the clock ticks 9sec, and another call of pollOnceForKeySpy get started, but as we do not await them, this test fails. * the only fix is to restore the clock and force the a small sleep to let the thing run in bg */ await sleepFor(10); // we should have two more calls here, so 4 total. expect(pollOnceForKeySpy.callCount).to.eq(4); expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.getCalls()[3].args).to.deep.eq([groupConvoPubkey, true, [-10]]); }); it('does run once only if group is inactive and we tick less than 2 minutes ', async () => { const convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); pollOnceForKeySpy.resetHistory(); TestUtils.stubLibSessionWorker(undefined); convo.set('active_at', Date.now()); const groupConvoPubkey = PubKey.cast(convo.id as string); swarmPolling.addGroupId(groupConvoPubkey); await swarmPolling.start(true); // more than a week old, we should not tick after just 5 seconds convo.set('active_at', Date.now() - 7 * 24 * 3600 * 1000 - 3600 * 1000); clock.tick(1 * 60 * 1000); await sleepFor(10); // we should have only one more call here, the one for our direct pubkey fetch expect(pollOnceForKeySpy.callCount).to.eq(3); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); // this one comes from the swarmPolling.start expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); }); describe('multiple runs', () => { let convo: ConversationModel; let groupConvoPubkey: PubKey; beforeEach(async () => { convo = getConversationController().getOrCreate( TestUtils.generateFakePubKeyStr(), ConversationTypeEnum.GROUP ); TestUtils.stubLibSessionWorker({}); convo.set('active_at', Date.now()); groupConvoPubkey = PubKey.cast(convo.id as string); swarmPolling.addGroupId(groupConvoPubkey); await swarmPolling.start(true); }); afterEach(() => { Sinon.restore(); getConversationController().reset(); clock.restore(); resetHardForkCachedValues(); }); it('does run twice if activeAt is less than 2 days', async () => { pollOnceForKeySpy.resetHistory(); // less than 2 days old, this is an active group convo.set('active_at', Date.now() - 2 * 24 * 3600 * 1000 - 3600 * 1000); const timeToTick = 6 * 1000; swarmPolling.forcePolledTimestamp(convo.id, timeToTick); // we tick more than 5 sec clock.tick(timeToTick); await swarmPolling.pollForAllKeys(); // we have 4 calls total. 2 for our direct promises run each 5 seconds, and 2 for the group pubkey active (so run every 5 sec too) expect(pollOnceForKeySpy.callCount).to.eq(4); // first two calls are our pubkey expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.getCalls()[3].args).to.deep.eq([groupConvoPubkey, true, [-10]]); }); it('does run twice if activeAt is more than 2 days old and we tick more than one minute', async () => { pollOnceForKeySpy.resetHistory(); TestUtils.stubWindowLog(); convo.set('active_at', Date.now() - 2 * 25 * 3600 * 1000); // medium active // fake that the group is part of the wrapper otherwise we stop tracking it after the first polling event const timeToTick = 65 * 1000; // more than one minute swarmPolling.forcePolledTimestamp(convo.id, timeToTick); clock.tick(timeToTick); // should tick twice more (one more our direct pubkey and one for the group) // fake that the group is part of the wrapper otherwise we stop tracking it after the first polling event await swarmPolling.pollForAllKeys(); expect(pollOnceForKeySpy.callCount).to.eq(4); // first two calls are our pubkey expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0, 2, 3, 5, 4]]); expect(pollOnceForKeySpy.getCalls()[3].args).to.deep.eq([groupConvoPubkey, true, [-10]]); }); }); }); }); });