From 2a8d764bfbbca070a9515666d893e12aeea05a4c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 8 Nov 2022 13:57:12 +1100 Subject: [PATCH] fix tests --- ts/components/conversation/TypingBubble.tsx | 4 +- .../message/message-content/MessageAvatar.tsx | 8 +- ts/models/conversation.ts | 30 +- ts/receiver/closedGroups.ts | 21 + ts/receiver/dataMessage.ts | 11 +- ts/session/apis/snode_api/swarmPolling.ts | 9 +- .../conversations/ConversationController.ts | 6 + ts/session/conversations/createClosedGroup.ts | 2 +- ts/session/group/closed-group.ts | 3 +- ts/state/ducks/search.ts | 2 - .../models/formatRowOfConversation_test.ts | 15 + .../unit/sogsv3/knownBlindedKeys_test.ts | 5 +- .../unit/swarm_polling/SwarmPolling_test.ts | 696 +++++++++++++----- 13 files changed, 584 insertions(+), 228 deletions(-) diff --git a/ts/components/conversation/TypingBubble.tsx b/ts/components/conversation/TypingBubble.tsx index 9f9dbe999..18e2e2059 100644 --- a/ts/components/conversation/TypingBubble.tsx +++ b/ts/components/conversation/TypingBubble.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { TypingAnimation } from './TypingAnimation'; import styled from 'styled-components'; -import { ConversationTypeEnum } from '../../models/conversationAttributes'; +import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes'; interface TypingBubbleProps { conversationType: ConversationTypeEnum; @@ -22,7 +22,7 @@ const TypingBubbleContainer = styled.div` `; export const TypingBubble = (props: TypingBubbleProps) => { - if (props.conversationType === ConversationTypeEnum.GROUP) { + if (isOpenOrClosedGroup(props.conversationType)) { return null; } diff --git a/ts/components/conversation/message/message-content/MessageAvatar.tsx b/ts/components/conversation/message/message-content/MessageAvatar.tsx index 50dea668e..501e8b5e9 100644 --- a/ts/components/conversation/message/message-content/MessageAvatar.tsx +++ b/ts/components/conversation/message/message-content/MessageAvatar.tsx @@ -1,7 +1,6 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { OpenGroupData } from '../../../../data/opengroups'; -import { ConversationTypeEnum } from '../../../../models/conversationAttributes'; import { MessageRenderingProps } from '../../../../models/messageType'; import { findCachedBlindedMatchOrLookItUp } from '../../../../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { getConversationController } from '../../../../session/conversations'; @@ -12,6 +11,7 @@ import { updateUserDetailsModal } from '../../../../state/ducks/modalDialog'; import { getIsTypingEnabled, getMessageAvatarProps, + getSelectedConversationIsGroup, getSelectedConversationKey, } from '../../../../state/selectors/conversations'; import { Avatar, AvatarSize, CrownIcon } from '../../../avatar/Avatar'; @@ -38,6 +38,7 @@ export const MessageAvatar = (props: Props) => { const dispatch = useDispatch(); const avatarProps = useSelector(state => getMessageAvatarProps(state as any, messageId)); const selectedConvoKey = useSelector(getSelectedConversationKey); + const isSelectedGroup = useSelector(getSelectedConversationIsGroup); const isTypingEnabled = useSelector(getIsTypingEnabled); @@ -49,15 +50,14 @@ export const MessageAvatar = (props: Props) => { authorName, sender, authorProfileName, - conversationType, direction, isSenderAdmin, lastMessageOfSeries, isPublic, } = avatarProps; - // no avatar when this is not a private conversation - if (conversationType === ConversationTypeEnum.PRIVATE || direction === 'outgoing') { + // no avatar when this if this is a private conversation + if (!isSelectedGroup || direction === 'outgoing') { return null; } const userName = authorName || authorProfileName || sender; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 9b41bca29..ca16d6c73 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -78,6 +78,8 @@ import { ConversationNotificationSetting, ConversationTypeEnum, fillConvoAttributesWithDefaults, + isDirectConversation, + isOpenOrClosedGroup, } from './conversationAttributes'; import { SogsBlinding } from '../session/apis/open_group_api/sogsv3/sogsBlinding'; import { from_hex } from 'libsodium-wrappers-sumo'; @@ -212,7 +214,18 @@ export class ConversationModel extends Backbone.Model { return OpenGroupUtils.isOpenGroupV2(this.id); } public isClosedGroup() { - return this.get('type') === ConversationTypeEnum.GROUP && !this.isPublic(); + return ( + (this.get('type') === ConversationTypeEnum.GROUP && !this.isPublic()) || + this.get('type') === ConversationTypeEnum.GROUPV3 + ); + } + public isPrivate() { + return isDirectConversation(this.get('type')); + } + + // returns true if this is a closed/medium or open group + public isGroup() { + return isOpenOrClosedGroup(this.get('type')); } public isBlocked() { @@ -274,8 +287,8 @@ export class ConversationModel extends Backbone.Model { // tslint:disable-next-line: cyclomatic-complexity const isPublic = this.isPublic(); - const members = this.isGroup() && !isPublic ? this.get('members') : []; - const zombies = this.isGroup() && !isPublic ? this.get('zombies') : []; + const members = this.isClosedGroup() ? this.get('members') : []; + const zombies = this.isClosedGroup() ? this.get('zombies') : []; const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const avatarPath = this.getAvatarPath(); const isPrivate = this.isPrivate(); @@ -304,7 +317,7 @@ export class ConversationModel extends Backbone.Model { const toRet: ReduxConversationType = { id: this.id as string, activeAt: this.get('active_at'), - type: isPrivate ? ConversationTypeEnum.PRIVATE : ConversationTypeEnum.GROUP, + type: this.get('type'), }; if (isPrivate) { @@ -1662,11 +1675,6 @@ export class ConversationModel extends Backbone.Model { } } - // returns true if this is a closed/medium or open group - public isGroup() { - return this.get('type') === ConversationTypeEnum.GROUP; - } - public async removeMessage(messageId: string) { await Data.removeMessage(messageId); this.updateLastMessage(); @@ -1720,10 +1728,6 @@ export class ConversationModel extends Backbone.Model { return profileName || PubKey.shorten(pubkey); } - public isPrivate() { - return this.get('type') === ConversationTypeEnum.PRIVATE; - } - public getAvatarPath(): string | null { const avatar = this.get('avatarInProfile'); if (isString(avatar)) { diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 754d60092..a1c7d9c1a 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -80,6 +80,14 @@ export async function handleClosedGroupControlMessage( }` ); + if (PubKey.isClosedGroupV3(envelope.source)) { + window?.log?.warn( + 'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage ' + ); + await removeFromCache(envelope); + return; + } + if (BlockedNumberController.isGroupBlocked(PubKey.cast(envelope.source))) { window?.log?.warn('Message ignored; destined for blocked group'); await removeFromCache(envelope); @@ -153,6 +161,11 @@ function sanityCheckNewGroup( return false; } + if (PubKey.isClosedGroupV3(hexGroupPublicKey)) { + window?.log?.warn('sanityCheckNewGroup: got a v3 new group as a ClosedGroupControlMessage. '); + return false; + } + if (!members?.length) { window?.log?.warn('groupUpdate: members is empty'); return false; @@ -475,6 +488,14 @@ async function performIfValid( const groupPublicKey = envelope.source; const sender = envelope.senderIdentity; + if (PubKey.isClosedGroupV3(groupPublicKey)) { + window?.log?.warn( + 'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage ' + ); + await removeFromCache(envelope); + return; + } + const convo = getConversationController().get(groupPublicKey); if (!convo) { window?.log?.warn('dropping message for nonexistent group'); diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 2d4e725eb..402a9b6b0 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -190,9 +190,18 @@ export async function handleSwarmDataMessage( isSyncedMessage ? cleanDataMessage.syncTarget : envelope.source ); + const isGroupMessage = !!envelope.senderIdentity; + const isGroupV3Message = isGroupMessage && PubKey.isClosedGroupV3(envelope.source); + let typeOfConvo = ConversationTypeEnum.PRIVATE; + if (isGroupV3Message) { + typeOfConvo = ConversationTypeEnum.GROUPV3; + } else if (isGroupMessage) { + typeOfConvo = ConversationTypeEnum.GROUP; + } + const convoToAddMessageTo = await getConversationController().getOrCreateAndWait( convoIdToAddTheMessageTo, - envelope.senderIdentity ? ConversationTypeEnum.GROUP : ConversationTypeEnum.PRIVATE + typeOfConvo ); window?.log?.info( diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index db00e9113..19c8d1957 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -355,12 +355,15 @@ export class SwarmPolling { private loadGroupIds() { const convos = getConversationController().getConversations(); - const mediumGroupsOnly = convos.filter( + const closedGroupsOnly = convos.filter( (c: ConversationModel) => - c.isMediumGroup() && !c.isBlocked() && !c.get('isKickedFromGroup') && !c.get('left') + (c.isMediumGroup() || PubKey.isClosedGroupV3(c.id)) && + !c.isBlocked() && + !c.get('isKickedFromGroup') && + !c.get('left') ); - mediumGroupsOnly.forEach((c: any) => { + closedGroupsOnly.forEach((c: any) => { this.addGroupId(new PubKey(c.id)); }); } diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 96354418d..279922655 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -73,6 +73,12 @@ export class ConversationController { throw new TypeError(`'type' must be 'private' or 'group' or 'groupv3' but got: '${type}'`); } + if (type === ConversationTypeEnum.GROUPV3 && !PubKey.isClosedGroupV3(id)) { + throw new Error( + 'required v3 closed group` ` but the pubkey does not match the 03 prefix for them' + ); + } + if (!this._initialFetchComplete) { throw new Error('getConversationController().get() needs complete initial fetch'); } diff --git a/ts/session/conversations/createClosedGroup.ts b/ts/session/conversations/createClosedGroup.ts index 7851c3dbb..8973cd24a 100644 --- a/ts/session/conversations/createClosedGroup.ts +++ b/ts/session/conversations/createClosedGroup.ts @@ -43,7 +43,7 @@ export async function createClosedGroup(groupName: string, members: Array ) { + const isV3 = PubKey.isClosedGroupV3(groupId); const convo = await getConversationController().getOrCreateAndWait( groupId, - ConversationTypeEnum.GROUP + isV3 ? ConversationTypeEnum.GROUPV3 : ConversationTypeEnum.GROUP ); if (!convo.isMediumGroup()) { diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 89dfe08c1..9b85b8389 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -230,8 +230,6 @@ async function queryConversationsAndContacts(providedQuery: string, options: Sea } } else if (conversation.type === ConversationTypeEnum.PRIVATE) { contacts.push(conversation.id); - } else if (conversation.type !== ConversationTypeEnum.GROUP) { - contacts.push(conversation.id); } else { conversations.push(conversation.id); } diff --git a/ts/test/session/unit/models/formatRowOfConversation_test.ts b/ts/test/session/unit/models/formatRowOfConversation_test.ts index 73162688b..5468b1a81 100644 --- a/ts/test/session/unit/models/formatRowOfConversation_test.ts +++ b/ts/test/session/unit/models/formatRowOfConversation_test.ts @@ -294,5 +294,20 @@ describe('formatRowOfConversation', () => { } as ConversationAttributes) ) ).have.deep.property('displayNameInProfile', 'displayNameInProfile'); + + expect( + formatRowOfConversation( + fillConvoAttributesWithDefaults({ + id: '1234565', + type: ConversationTypeEnum.GROUPV3, + nickname: 'nickname', + displayNameInProfile: 'displayNameInProfile', + profileKey: '', + avatarPointer: 'avatarPointer', + avatarInProfile: 'avatarInProfile', + avatarImageId: 1234, + } as ConversationAttributes) + ) + ).have.deep.property('displayNameInProfile', 'displayNameInProfile'); }); }); diff --git a/ts/test/session/unit/sogsv3/knownBlindedKeys_test.ts b/ts/test/session/unit/sogsv3/knownBlindedKeys_test.ts index 925591553..c12782545 100644 --- a/ts/test/session/unit/sogsv3/knownBlindedKeys_test.ts +++ b/ts/test/session/unit/sogsv3/knownBlindedKeys_test.ts @@ -562,7 +562,7 @@ describe('knownBlindedKeys', () => { expect(real).to.eq(undefined); }); - it('does iterate over all the conversations but is not private so must fail', async () => { + it('does iterate over all the conversations but is not private so must fail: group', async () => { getItemById.resolves(); await loadKnownBlindedKeys(); // adding a private conversation with a known match of the blinded pubkey we have @@ -580,6 +580,9 @@ describe('knownBlindedKeys', () => { expect(real).to.eq(undefined); }); + it('does iterate over all the conversations but is not private so must fail: groupv3', async () => { + // we actually cannot test this one as we would need to create a conversation with groupv3 as type but 05 as prefix, and the conversation controller denies it, as expected + }); }); }); }); diff --git a/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts b/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts index b6d6c3003..374a477f7 100644 --- a/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts +++ b/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts @@ -81,53 +81,106 @@ describe('SwarmPolling', () => { expect(swarmPolling.getPollingTimeout(fakeConvo)).to.eq(SWARM_POLLING_TIMEOUT.INACTIVE); }); - 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 - ); - }); + 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 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 - ); + 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 - ); + 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 + ); + }); }); - 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 + ); + }); }); }); @@ -174,210 +227,453 @@ describe('SwarmPolling', () => { expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]); }); - it('does run for group pubkey on start no matter the recent timestamp ', async () => { - const convo = getConversationController().getOrCreate( - TestUtils.generateFakePubKeyStr(), - ConversationTypeEnum.GROUP - ); - convo.set('active_at', Date.now()); - const groupConvoPubkey = PubKey.cast(convo.id as string); - swarmPolling.addGroupId(groupConvoPubkey); - await swarmPolling.start(true); + 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 + ); + 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]]); - expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); - }); + // 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]]); + 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 - ); - 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); + it('does run for groupv3 pubkey on start no matter the recent timestamp ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + convo.set('active_at', Date.now()); + const groupConvoPubkey = PubKey.cast(convo.id as string); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); - 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + }); - // 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]]); - expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); - getItemByIdStub.restore(); - getItemByIdStub = TestUtils.stubData('getItemById'); + it('does only poll from -10 for closed groups if HF >= 19.1 ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + 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); - getItemByIdStub.resolves(); - }); + await swarmPolling.start(true); - 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 - ); + // 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + getItemByIdStub.restore(); + getItemByIdStub = TestUtils.stubData('getItemById'); - convo.set('active_at', 1); // really old - const groupConvoPubkey = PubKey.cast(convo.id as string); - swarmPolling.addGroupId(groupConvoPubkey); + getItemByIdStub.resolves(); + }); - // this calls the stub 2 times, one for our direct pubkey and one for the group - await swarmPolling.start(true); + 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 + ); - // this should only call the stub one more time: for our direct pubkey but not for the group pubkey - await swarmPolling.pollForAllKeys(); + convo.set('active_at', 1); // really old + const groupConvoPubkey = PubKey.cast(convo.id as string); + swarmPolling.addGroupId(groupConvoPubkey); - expect(pollOnceForKeySpy.callCount).to.eq(3); - expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]); - expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); - expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); - }); + // this calls the stub 2 times, one for our direct pubkey and one for the group + await swarmPolling.start(true); - it('does run twice if activeAt less than one hour ', async () => { - const convo = getConversationController().getOrCreate( - TestUtils.generateFakePubKeyStr(), - ConversationTypeEnum.GROUP - ); + // this should only call the stub one more time: for our direct pubkey but not for the group pubkey + await swarmPolling.pollForAllKeys(); - convo.set('active_at', Date.now()); - const groupConvoPubkey = PubKey.cast(convo.id as string); - swarmPolling.addGroupId(groupConvoPubkey); - await swarmPolling.start(true); - 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 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 - */ - clock.restore(); - await sleepFor(10); - - expect(pollOnceForKeySpy.callCount).to.eq(4); - expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]); - expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); - expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); - expect(pollOnceForKeySpy.getCall(3).args).to.deep.eq([groupConvoPubkey, true, [-10]]); - }); + expect(pollOnceForKeySpy.callCount).to.eq(3); + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + }); - it('does run twice if activeAt is inactive and we tick longer than 2 minutes', async () => { - const convo = getConversationController().getOrCreate( - TestUtils.generateFakePubKeyStr(), - ConversationTypeEnum.GROUP - ); + it('does run twice if activeAt less than one hour ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); - 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 - */ - clock.restore(); - 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]]); - expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); - expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); - expect(pollOnceForKeySpy.getCalls()[3].args).to.deep.eq([groupConvoPubkey, true, [-10]]); - }); + convo.set('active_at', Date.now()); + const groupConvoPubkey = PubKey.cast(convo.id as string); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + 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 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 + */ + clock.restore(); + await sleepFor(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(); + expect(pollOnceForKeySpy.callCount).to.eq(4); + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + expect(pollOnceForKeySpy.getCall(3).args).to.deep.eq([groupConvoPubkey, true, [-10]]); + }); - convo.set('active_at', Date.now()); - const groupConvoPubkey = PubKey.cast(convo.id as string); - swarmPolling.addGroupId(groupConvoPubkey); - await swarmPolling.start(true); + it('does run twice if activeAt is inactive and we tick longer than 2 minutes', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + + 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 + */ + clock.restore(); + 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + 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(); + + 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); + // 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); + clock.tick(1 * 60 * 1000); - // 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]]); - }); + // 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]]); + }); - describe('multiple runs', () => { - let convo: ConversationModel; - let groupConvoPubkey: PubKey; + describe('multiple runs', () => { + let convo: ConversationModel; + let groupConvoPubkey: PubKey; + + beforeEach(async () => { + convo = getConversationController().getOrCreate( + TestUtils.generateFakePubKeyStr(), + ConversationTypeEnum.GROUP + ); + + convo.set('active_at', Date.now()); + groupConvoPubkey = PubKey.cast(convo.id as string); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + }); + + 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + 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(); + convo.set('active_at', Date.now() - 2 * 25 * 3600 * 1000); // medium active + + const timeToTick = 65 * 1000; + swarmPolling.forcePolledTimestamp(convo.id, timeToTick); + + clock.tick(timeToTick); // should tick twice more (one more our direct pubkey and one for the group) + + 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + expect(pollOnceForKeySpy.getCalls()[3].args).to.deep.eq([groupConvoPubkey, true, [-10]]); + }); + }); + }); - beforeEach(async () => { - convo = getConversationController().getOrCreate( - TestUtils.generateFakePubKeyStr(), - ConversationTypeEnum.GROUP + describe('group v3', () => { + it('does run for group pubkey on start no matter the recent timestamp ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakeClosedGroupV3PkStr(), + ConversationTypeEnum.GROUPV3 ); + 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + }); + it('does run for groupv3 pubkey on start no matter the recent timestamp ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakeClosedGroupV3PkStr(), + ConversationTypeEnum.GROUPV3 + ); convo.set('active_at', Date.now()); - groupConvoPubkey = PubKey.cast(convo.id as string); + 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); }); - 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); + it('does only poll from -10 for closed groups if HF >= 19.1 ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakeClosedGroupV3PkStr(), + ConversationTypeEnum.GROUPV3 + ); + 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); - const timeToTick = 6 * 1000; + await swarmPolling.start(true); - swarmPolling.forcePolledTimestamp(convo.id, timeToTick); - // we tick more than 5 sec - clock.tick(timeToTick); + // 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]]); + 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.generateFakeClosedGroupV3PkStr(), + ConversationTypeEnum.GROUPV3 + ); + + convo.set('active_at', 1); // really old + 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(); - // 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(3); + expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + }); + + it('does run twice if activeAt less than one hour ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakeClosedGroupV3PkStr(), + ConversationTypeEnum.GROUPV3 + ); + + convo.set('active_at', Date.now()); + const groupConvoPubkey = PubKey.cast(convo.id as string); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + 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 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 + */ + clock.restore(); + await sleepFor(10); + expect(pollOnceForKeySpy.callCount).to.eq(4); - // first two calls are our pubkey expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]); expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + expect(pollOnceForKeySpy.getCall(3).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.generateFakeClosedGroupV3PkStr(), + ConversationTypeEnum.GROUPV3 + ); + + 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 + */ + clock.restore(); + 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); 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 () => { + it('does run once only if group is inactive and we tick less than 2 minutes ', async () => { + const convo = getConversationController().getOrCreate( + TestUtils.generateFakeClosedGroupV3PkStr(), + ConversationTypeEnum.GROUPV3 + ); pollOnceForKeySpy.resetHistory(); - convo.set('active_at', Date.now() - 2 * 25 * 3600 * 1000); // medium active - const timeToTick = 65 * 1000; - swarmPolling.forcePolledTimestamp(convo.id, timeToTick); + convo.set('active_at', Date.now()); + const groupConvoPubkey = PubKey.cast(convo.id as string); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); - clock.tick(timeToTick); // should tick twice more (one more our direct pubkey and one for the group) + // 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); - await swarmPolling.pollForAllKeys(); - expect(pollOnceForKeySpy.callCount).to.eq(4); + clock.tick(1 * 60 * 1000); - // first two calls are our pubkey - expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]); - expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-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]]); - expect(pollOnceForKeySpy.getCalls()[3].args).to.deep.eq([groupConvoPubkey, true, [-10]]); + }); + + describe('multiple runs', () => { + let convo: ConversationModel; + let groupConvoPubkey: PubKey; + + beforeEach(async () => { + convo = getConversationController().getOrCreate( + TestUtils.generateFakeClosedGroupV3PkStr(), + ConversationTypeEnum.GROUPV3 + ); + + convo.set('active_at', Date.now()); + groupConvoPubkey = PubKey.cast(convo.id as string); + swarmPolling.addGroupId(groupConvoPubkey); + await swarmPolling.start(true); + }); + + 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + 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(); + convo.set('active_at', Date.now() - 2 * 25 * 3600 * 1000); // medium active + + const timeToTick = 65 * 1000; + swarmPolling.forcePolledTimestamp(convo.id, timeToTick); + + clock.tick(timeToTick); // should tick twice more (one more our direct pubkey and one for the group) + + 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]]); + expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]); + expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false, [0]]); + expect(pollOnceForKeySpy.getCalls()[3].args).to.deep.eq([groupConvoPubkey, true, [-10]]); + }); }); }); });