From 3060ffd25ac52e8999a49e0217e0b6d34cb48e93 Mon Sep 17 00:00:00 2001 From: William Grant <willmgrant@gmail.com> Date: Tue, 30 Aug 2022 17:18:22 +1000 Subject: [PATCH] test: added tests for adding and updating sogs cache entries updated idForLogging for opengroups to be more verbose, updated reaction method calls to use exported Reactions object --- .../MessageContentWithStatus.tsx | 4 +- .../message-content/MessageContextMenu.tsx | 4 +- ts/components/dialog/ReactListModal.tsx | 12 +- ts/models/conversation.ts | 9 +- ts/receiver/dataMessage.ts | 4 +- .../open_group_api/sogsv3/sogsV3BatchPoll.ts | 6 +- .../sogsv3/sogsV3ClearReaction.ts | 4 +- .../sogsv3/sogsV3MutationCache.ts | 36 +++-- .../sogsv3/sogsV3SendReaction.ts | 6 +- .../unit/reactions/ReactionMessage_test.ts | 16 +- .../receiver/opengroup/deduplicate_test.ts | 50 +++--- .../session/unit/sending/MessageQueue_test.ts | 2 +- .../session/unit/sogsv3/MutationCache_test.ts | 142 ++++++++++++++++++ ts/test/test-utils/utils/message.ts | 25 ++- ts/test/test-utils/utils/stubbing.ts | 2 +- ts/util/reactions.ts | 25 ++- 16 files changed, 260 insertions(+), 87 deletions(-) create mode 100644 ts/test/session/unit/sogsv3/MutationCache_test.ts diff --git a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx index a069e5983..a7e0563a5 100644 --- a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx +++ b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx @@ -10,7 +10,7 @@ import { getMessageContentWithStatusesSelectorProps, isMessageSelectionMode, } from '../../../../state/selectors/conversations'; -import { sendMessageReaction } from '../../../../util/reactions'; +import { Reactions } from '../../../../util/reactions'; import { MessageAuthorText } from './MessageAuthorText'; import { MessageContent } from './MessageContent'; @@ -93,7 +93,7 @@ export const MessageContentWithStatuses = (props: Props) => { const [popupReaction, setPopupReaction] = useState(''); const handleMessageReaction = async (emoji: string) => { - await sendMessageReaction(messageId, emoji); + await Reactions.sendMessageReaction(messageId, emoji); }; const handlePopupClick = () => { diff --git a/ts/components/conversation/message/message-content/MessageContextMenu.tsx b/ts/components/conversation/message/message-content/MessageContextMenu.tsx index 8103da28c..cc28174d3 100644 --- a/ts/components/conversation/message/message-content/MessageContextMenu.tsx +++ b/ts/components/conversation/message/message-content/MessageContextMenu.tsx @@ -25,7 +25,7 @@ import { import { StateType } from '../../../../state/reducer'; import { getMessageContextMenuProps } from '../../../../state/selectors/conversations'; import { saveAttachmentToDisk } from '../../../../util/attachmentsUtil'; -import { sendMessageReaction } from '../../../../util/reactions'; +import { Reactions } from '../../../../util/reactions'; import { SessionEmojiPanel, StyledEmojiPanel } from '../../SessionEmojiPanel'; import { MessageReactBar } from './MessageReactBar'; @@ -241,7 +241,7 @@ export const MessageContextMenu = (props: Props) => { const onEmojiClick = async (args: any) => { const emoji = args.native ?? args; onCloseEmoji(); - await sendMessageReaction(messageId, emoji); + await Reactions.sendMessageReaction(messageId, emoji); }; const onEmojiKeyDown = (event: any) => { diff --git a/ts/components/dialog/ReactListModal.tsx b/ts/components/dialog/ReactListModal.tsx index cc2257e0a..4c5ac2375 100644 --- a/ts/components/dialog/ReactListModal.tsx +++ b/ts/components/dialog/ReactListModal.tsx @@ -13,7 +13,7 @@ import { } from '../../state/ducks/modalDialog'; import { SortedReactionList } from '../../types/Reaction'; import { nativeEmojiData } from '../../util/emoji'; -import { sendMessageReaction, SOGSReactorsFetchCount } from '../../util/reactions'; +import { Reactions } from '../../util/reactions'; import { Avatar, AvatarSize } from '../avatar/Avatar'; import { Flex } from '../basic/Flex'; import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer'; @@ -110,7 +110,7 @@ const ReactionSenders = (props: ReactionSendersProps) => { }; const handleRemoveReaction = async () => { - await sendMessageReaction(messageId, currentReact); + await Reactions.sendMessageReaction(messageId, currentReact); if (senders.length <= 1) { dispatch(updateReactListModal(null)); @@ -174,13 +174,13 @@ const CountText = ({ count, emoji }: { count: number; emoji: string }) => { <StyledCountText> <SessionHtmlRenderer html={ - count > SOGSReactorsFetchCount + 1 + count > Reactions.SOGSReactorsFetchCount + 1 ? window.i18n('reactionListCountPlural', [ - window.i18n('otherPlural', [String(count - SOGSReactorsFetchCount)]), + window.i18n('otherPlural', [String(count - Reactions.SOGSReactorsFetchCount)]), emoji, ]) : window.i18n('reactionListCountSingular', [ - window.i18n('otherSingular', [String(count - SOGSReactorsFetchCount)]), + window.i18n('otherSingular', [String(count - Reactions.SOGSReactorsFetchCount)]), emoji, ]) } @@ -362,7 +362,7 @@ export const ReactListModal = (props: Props): ReactElement => { handleClose={handleClose} /> )} - {isPublic && currentReact && count && count > SOGSReactorsFetchCount && ( + {isPublic && currentReact && count && count > Reactions.SOGSReactorsFetchCount && ( <CountText count={count} emoji={currentReact} /> )} </StyledSendersContainer> diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 698966d67..73d3be41e 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -93,7 +93,7 @@ import { } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { sogsV3FetchPreviewAndSaveIt } from '../session/apis/open_group_api/sogsv3/sogsV3FetchFile'; import { Reaction } from '../types/Reaction'; -import { handleMessageReaction } from '../util/reactions'; +import { Reactions } from '../util/reactions'; export class ConversationModel extends Backbone.Model<ConversationAttributes> { public updateLastMessage: () => any; @@ -193,7 +193,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> { } if (this.isPublic()) { - return `opengroup(${this.id})`; + const opengroup = this.toOpenGroupV2(); + return `${opengroup.serverUrl}/${opengroup.roomId}`; } return `group(${ed25519Str(this.id)})`; @@ -737,7 +738,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> { const chatMessagePrivate = new VisibleMessage(chatMessageParams); await getMessageQueue().sendToPubKey(destinationPubkey, chatMessagePrivate); - await handleMessageReaction({ + await Reactions.handleMessageReaction({ reaction, sender: UserUtils.getOurPubKeyStrFromCache(), you: true, @@ -754,7 +755,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> { }); // we need the return await so that errors are caught in the catch {} await getMessageQueue().sendToGroup(closedGroupVisibleMessage); - await handleMessageReaction({ + await Reactions.handleMessageReaction({ reaction, sender: UserUtils.getOurPubKeyStrFromCache(), you: true, diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index a207bc219..3be5c0935 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -21,7 +21,7 @@ import { isUsFromCache } from '../session/utils/User'; import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates'; import { toLogFormat } from '../types/attachments/Errors'; import { ConversationTypeEnum } from '../models/conversationAttributes'; -import { handleMessageReaction } from '../util/reactions'; +import { Reactions } from '../util/reactions'; import { Action, Reaction } from '../types/Reaction'; function cleanAttachment(attachment: any) { @@ -322,7 +322,7 @@ async function handleSwarmMessage( // this call has to be made inside the queueJob! // We handle reaction DataMessages separately if (!msgModel.get('isPublic') && rawDataMessage.reaction) { - await handleMessageReaction({ + await Reactions.handleMessageReaction({ reaction: rawDataMessage.reaction, sender: msgModel.get('source'), you: isUsFromCache(msgModel.get('source')), diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts index d3492417d..073ea5f11 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts @@ -8,7 +8,7 @@ import { import { addJsonContentTypeToHeaders } from './sogsV3SendMessage'; import { AbortSignal } from 'abort-controller'; import { roomHasBlindEnabled } from './sogsV3Capabilities'; -import { SOGSReactorsFetchCount } from '../../../../util/reactions'; +import { Reactions } from '../../../../util/reactions'; type BatchFetchRequestOptions = { method: 'POST' | 'PUT' | 'GET' | 'DELETE'; @@ -240,8 +240,8 @@ const makeBatchRequestPayload = ( return { method: 'GET', path: isNumber(options.messages.sinceSeqNo) - ? `/room/${options.messages.roomId}/messages/since/${options.messages.sinceSeqNo}?t=r&reactors=${SOGSReactorsFetchCount}` - : `/room/${options.messages.roomId}/messages/recent?reactors=${SOGSReactorsFetchCount}`, + ? `/room/${options.messages.roomId}/messages/since/${options.messages.sinceSeqNo}?t=r&reactors=${Reactions.SOGSReactorsFetchCount}` + : `/room/${options.messages.roomId}/messages/recent?reactors=${Reactions.SOGSReactorsFetchCount}`, }; } break; diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3ClearReaction.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3ClearReaction.ts index 03df53a74..eb358757e 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3ClearReaction.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3ClearReaction.ts @@ -1,6 +1,6 @@ import AbortController from 'abort-controller'; import { OpenGroupReactionResponse } from '../../../../types/Reaction'; -import { handleClearReaction } from '../../../../util/reactions'; +import { Reactions } from '../../../../util/reactions'; import { OpenGroupRequestCommonType } from '../opengroupV2/ApiUtil'; import { batchFirstSubIsSuccess, @@ -51,7 +51,7 @@ export const clearSogsReactionByServerId = async ( addToMutationCache(cacheEntry); // Since responses can take a long time we immediately update the moderators's UI and if there is a problem it is overwritten by handleOpenGroupMessageReactions later. - await handleClearReaction(serverId, reaction); + await Reactions.handleClearReaction(serverId, reaction); const options: Array<OpenGroupBatchRow> = [ { diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3MutationCache.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3MutationCache.ts index 3c90a66b9..9874830bc 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3MutationCache.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3MutationCache.ts @@ -4,7 +4,7 @@ */ import { filter, findIndex, remove } from 'lodash'; -import { handleOpenGroupMessageReactions } from '../../../../util/reactions'; +import { Reactions } from '../../../../util/reactions'; import { OpenGroupMessageV4 } from '../opengroupV2/OpenGroupServerPoller'; export enum ChangeType { @@ -32,28 +32,35 @@ const sogsMutationCache: Array<SogsV3Mutation> = []; function verifyEntry(entry: SogsV3Mutation): boolean { return Boolean( - !entry.server || - !entry.room || - entry.seqno !== null || - entry.metadata.messageId || - entry.metadata.emoji || - entry.metadata.action === 'ADD' || - entry.metadata.action === 'REMOVE' + entry.server && + entry.server !== '' && + entry.room && + entry.room !== '' && + entry.changeType === ChangeType.REACTIONS && + entry.metadata.messageId && + entry.metadata.emoji && + entry.metadata.emoji !== '' && + (entry.metadata.action === 'ADD' || + entry.metadata.action === 'REMOVE' || + entry.metadata.action === 'CLEAR') ); } -export function addToMutationCache(entry: SogsV3Mutation) { +// we return the cache for testing +export function addToMutationCache(entry: SogsV3Mutation): Array<SogsV3Mutation> { if (!verifyEntry(entry)) { window.log.error('SOGS Mutation Cache: Entry verification on add failed!', entry); } else { sogsMutationCache.push(entry); window.log.info('SOGS Mutation Cache: Entry added!', entry); } + return sogsMutationCache; } -export function updateMutationCache(entry: SogsV3Mutation, seqno: number) { +// we return the cache for testing +export function updateMutationCache(entry: SogsV3Mutation, seqno: number): Array<SogsV3Mutation> { if (!verifyEntry(entry)) { - window.log.error('SOGS Mutation Cache: Entry verification on update failed!'); + window.log.error('SOGS Mutation Cache: Entry verification on update failed!', entry); } else { const entryIndex = findIndex(sogsMutationCache, entry); if (entryIndex >= 0) { @@ -63,13 +70,15 @@ export function updateMutationCache(entry: SogsV3Mutation, seqno: number) { window.log.error('SOGS Mutation Cache: Updated failed! Cannot find entry', entry); } } + return sogsMutationCache; } +// return is for testing purposes only export async function processMessagesUsingCache( server: string, room: string, message: OpenGroupMessageV4 -) { +): Promise<OpenGroupMessageV4> { const updatedReactions = message.reactions; const roomMatches: Array<SogsV3Mutation> = filter(sogsMutationCache, { server, room }); @@ -125,5 +134,6 @@ export async function processMessagesUsingCache( window.log.info('SOGS Mutation Cache: Removed processed entries from cache!', removedMatches); message.reactions = updatedReactions; - await handleOpenGroupMessageReactions(message.reactions, message.id); + await Reactions.handleOpenGroupMessageReactions(message.reactions, message.id); + return message; } diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3SendReaction.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3SendReaction.ts index 1e0a9a3b1..445219e40 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3SendReaction.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3SendReaction.ts @@ -3,7 +3,7 @@ import { Data } from '../../../../data/data'; import { ConversationModel } from '../../../../models/conversation'; import { Action, OpenGroupReactionResponse, Reaction } from '../../../../types/Reaction'; import { getEmojiDataFromNative } from '../../../../util/emoji'; -import { handleMessageReaction, hitRateLimit } from '../../../../util/reactions'; +import { Reactions } from '../../../../util/reactions'; import { OnionSending } from '../../../onions/onionSend'; import { UserUtils } from '../../../utils'; import { OpenGroupPollingUtils } from '../opengroupV2/OpenGroupPollingUtils'; @@ -57,7 +57,7 @@ export const sendSogsReactionOnionV4 = async ( return false; } - if (hitRateLimit()) { + if (Reactions.hitRateLimit()) { return false; } @@ -89,7 +89,7 @@ export const sendSogsReactionOnionV4 = async ( // Since responses can take a long time we immediately update the sender's UI and if there is a problem it is overwritten by handleOpenGroupMessageReactions later. const me = UserUtils.getOurPubKeyStrFromCache(); - await handleMessageReaction({ + await Reactions.handleMessageReaction({ reaction, sender: blinded ? getUsBlindedInThatServer(conversation) || me : me, you: true, diff --git a/ts/test/session/unit/reactions/ReactionMessage_test.ts b/ts/test/session/unit/reactions/ReactionMessage_test.ts index d7a7bf127..8c9574b38 100644 --- a/ts/test/session/unit/reactions/ReactionMessage_test.ts +++ b/ts/test/session/unit/reactions/ReactionMessage_test.ts @@ -1,6 +1,6 @@ import chai, { expect } from 'chai'; import Sinon, { useFakeTimers } from 'sinon'; -import { handleMessageReaction, sendMessageReaction } from '../../../../util/reactions'; +import { Reactions } from '../../../../util/reactions'; import { Data } from '../../../../data/data'; import * as Storage from '../../../../util/storage'; import { generateFakeIncomingPrivateMessage, stubWindowLog } from '../../../test-utils/utils'; @@ -40,7 +40,7 @@ describe('ReactionMessage', () => { it('can react to a message', async () => { // Send reaction - const reaction = await sendMessageReaction(originalMessage.get('id'), '😄'); + const reaction = await Reactions.sendMessageReaction(originalMessage.get('id'), '😄'); expect(reaction?.id, 'id should match the original message timestamp').to.be.equal( Number(originalMessage.get('sent_at')) @@ -52,7 +52,7 @@ describe('ReactionMessage', () => { expect(reaction?.action, 'action should be 0').to.be.equal(0); // Handling reaction - const updatedMessage = await handleMessageReaction({ + const updatedMessage = await Reactions.handleMessageReaction({ reaction: reaction as SignalService.DataMessage.IReaction, sender: ourNumber, you: true, @@ -73,7 +73,7 @@ describe('ReactionMessage', () => { it('can remove a reaction from a message', async () => { // Send reaction - const reaction = await sendMessageReaction(originalMessage.get('id'), '😄'); + const reaction = await Reactions.sendMessageReaction(originalMessage.get('id'), '😄'); expect(reaction?.id, 'id should match the original message timestamp').to.be.equal( Number(originalMessage.get('sent_at')) @@ -85,7 +85,7 @@ describe('ReactionMessage', () => { expect(reaction?.action, 'action should be 1').to.be.equal(1); // Handling reaction - const updatedMessage = await handleMessageReaction({ + const updatedMessage = await Reactions.handleMessageReaction({ reaction: reaction as SignalService.DataMessage.IReaction, sender: ourNumber, you: true, @@ -100,10 +100,10 @@ describe('ReactionMessage', () => { // we have already sent 2 messages when this test runs for (let i = 0; i < 18; i++) { // Send reaction - await sendMessageReaction(originalMessage.get('id'), '👍'); + await Reactions.sendMessageReaction(originalMessage.get('id'), '👍'); } - let reaction = await sendMessageReaction(originalMessage.get('id'), '👎'); + let reaction = await Reactions.sendMessageReaction(originalMessage.get('id'), '👎'); expect(reaction, 'no reaction should be returned since we are over the rate limit').to.be .undefined; @@ -113,7 +113,7 @@ describe('ReactionMessage', () => { // Wait a miniute for the rate limit to clear clock.tick(1 * 60 * 1000); - reaction = await sendMessageReaction(originalMessage.get('id'), '👋'); + reaction = await Reactions.sendMessageReaction(originalMessage.get('id'), '👋'); expect(reaction?.id, 'id should match the original message timestamp').to.be.equal( Number(originalMessage.get('sent_at')) diff --git a/ts/test/session/unit/receiver/opengroup/deduplicate_test.ts b/ts/test/session/unit/receiver/opengroup/deduplicate_test.ts index 25c67d821..e7f675e44 100644 --- a/ts/test/session/unit/receiver/opengroup/deduplicate_test.ts +++ b/ts/test/session/unit/receiver/opengroup/deduplicate_test.ts @@ -21,9 +21,9 @@ describe('filterDuplicatesFromDbAndIncoming', () => { }); it('no duplicates', async () => { - const msg1 = TestUtils.generateOpenGroupMessageV2(); - const msg2 = TestUtils.generateOpenGroupMessageV2(); - const msg3 = TestUtils.generateOpenGroupMessageV2(); + const msg1 = TestUtils.generateOpenGroupMessageV2({ serverId: 111 }); + const msg2 = TestUtils.generateOpenGroupMessageV2({ serverId: 222 }); + const msg3 = TestUtils.generateOpenGroupMessageV2({ serverId: 333 }); const filtered = await filterDuplicatesFromDbAndIncoming([msg1, msg2, msg3]); expect(filtered.length).to.be.eq(3); expect(filtered[0]).to.be.deep.eq(msg1); @@ -32,11 +32,11 @@ describe('filterDuplicatesFromDbAndIncoming', () => { }); it('two duplicate sender but not the same timestamp', async () => { - const msg1 = TestUtils.generateOpenGroupMessageV2(); - const msg2 = TestUtils.generateOpenGroupMessageV2(); + const msg1 = TestUtils.generateOpenGroupMessageV2({ serverId: 111 }); + const msg2 = TestUtils.generateOpenGroupMessageV2({ serverId: 222 }); msg2.sender = msg1.sender; msg2.sentTimestamp = Date.now() + 2; - const msg3 = TestUtils.generateOpenGroupMessageV2(); + const msg3 = TestUtils.generateOpenGroupMessageV2({ serverId: 333 }); const filtered = await filterDuplicatesFromDbAndIncoming([msg1, msg2, msg3]); expect(filtered.length).to.be.eq(3); expect(filtered[0]).to.be.deep.eq(msg1); @@ -45,10 +45,10 @@ describe('filterDuplicatesFromDbAndIncoming', () => { }); it('two duplicate timestamp but not the same sender', async () => { - const msg1 = TestUtils.generateOpenGroupMessageV2(); - const msg2 = TestUtils.generateOpenGroupMessageV2(); + const msg1 = TestUtils.generateOpenGroupMessageV2({ serverId: 111 }); + const msg2 = TestUtils.generateOpenGroupMessageV2({ serverId: 222 }); msg2.sentTimestamp = msg1.sentTimestamp; - const msg3 = TestUtils.generateOpenGroupMessageV2(); + const msg3 = TestUtils.generateOpenGroupMessageV2({ serverId: 333 }); const filtered = await filterDuplicatesFromDbAndIncoming([msg1, msg2, msg3]); expect(filtered.length).to.be.eq(3); expect(filtered[0]).to.be.deep.eq(msg1); @@ -57,10 +57,10 @@ describe('filterDuplicatesFromDbAndIncoming', () => { }); it('two duplicate timestamp but not the same sender', async () => { - const msg1 = TestUtils.generateOpenGroupMessageV2(); - const msg2 = TestUtils.generateOpenGroupMessageV2(); + const msg1 = TestUtils.generateOpenGroupMessageV2({ serverId: 111 }); + const msg2 = TestUtils.generateOpenGroupMessageV2({ serverId: 222 }); msg2.sentTimestamp = msg1.sentTimestamp; - const msg3 = TestUtils.generateOpenGroupMessageV2(); + const msg3 = TestUtils.generateOpenGroupMessageV2({ serverId: 333 }); const filtered = await filterDuplicatesFromDbAndIncoming([msg1, msg2, msg3]); expect(filtered.length).to.be.eq(3); expect(filtered[0]).to.be.deep.eq(msg1); @@ -69,11 +69,11 @@ describe('filterDuplicatesFromDbAndIncoming', () => { }); it('two duplicates in the same poll ', async () => { - const msg1 = TestUtils.generateOpenGroupMessageV2(); - const msg2 = TestUtils.generateOpenGroupMessageV2(); + const msg1 = TestUtils.generateOpenGroupMessageV2({ serverId: 111 }); + const msg2 = TestUtils.generateOpenGroupMessageV2({ serverId: msg1.serverId! }); msg2.sentTimestamp = msg1.sentTimestamp; msg2.sender = msg1.sender; - const msg3 = TestUtils.generateOpenGroupMessageV2(); + const msg3 = TestUtils.generateOpenGroupMessageV2({ serverId: 333 }); const filtered = await filterDuplicatesFromDbAndIncoming([msg1, msg2, msg3]); expect(filtered.length).to.be.eq(2); expect(filtered[0]).to.be.deep.eq(msg1); @@ -81,24 +81,10 @@ describe('filterDuplicatesFromDbAndIncoming', () => { }); it('three duplicates in the same poll', async () => { - const msg1 = TestUtils.generateOpenGroupMessageV2(); - const msg2 = TestUtils.generateOpenGroupMessageV2(); + const msg1 = TestUtils.generateOpenGroupMessageV2({ serverId: 111 }); + const msg2 = TestUtils.generateOpenGroupMessageV2({ serverId: msg1.serverId! }); - const msg3 = TestUtils.generateOpenGroupMessageV2(); - msg2.sentTimestamp = msg1.sentTimestamp; - msg2.sender = msg1.sender; - msg3.sentTimestamp = msg1.sentTimestamp; - msg3.sender = msg1.sender; - const filtered = await filterDuplicatesFromDbAndIncoming([msg1, msg2, msg3]); - expect(filtered.length).to.be.eq(1); - expect(filtered[0]).to.be.deep.eq(msg1); - }); - - it('three duplicates in the same poll', async () => { - const msg1 = TestUtils.generateOpenGroupMessageV2(); - const msg2 = TestUtils.generateOpenGroupMessageV2(); - - const msg3 = TestUtils.generateOpenGroupMessageV2(); + const msg3 = TestUtils.generateOpenGroupMessageV2({ serverId: msg1.serverId! }); msg2.sentTimestamp = msg1.sentTimestamp; msg2.sender = msg1.sender; msg3.sentTimestamp = msg1.sentTimestamp; diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index 51892a99b..53a3d609b 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -215,7 +215,7 @@ describe('MessageQueue', () => { let sendToOpenGroupV2Stub: sinon.SinonStub; beforeEach(() => { sendToOpenGroupV2Stub = Sinon.stub(MessageSender, 'sendToOpenGroupV2').resolves( - TestUtils.generateOpenGroupMessageV2() + TestUtils.generateOpenGroupMessageV2({ serverId: 5125 }) ); }); diff --git a/ts/test/session/unit/sogsv3/MutationCache_test.ts b/ts/test/session/unit/sogsv3/MutationCache_test.ts new file mode 100644 index 000000000..9eb17250d --- /dev/null +++ b/ts/test/session/unit/sogsv3/MutationCache_test.ts @@ -0,0 +1,142 @@ +import { expect } from 'chai'; +import Sinon from 'sinon'; +import { + addToMutationCache, + ChangeType, + SogsV3Mutation, + updateMutationCache, +} from '../../../../session/apis/open_group_api/sogsv3/sogsV3MutationCache'; +import { Action, Reaction } from '../../../../types/Reaction'; +import { TestUtils } from '../../../test-utils'; +import { Reactions } from '../../../../util/reactions'; + +describe('mutationCache', () => { + TestUtils.stubWindowLog(); + + const roomInfos = TestUtils.generateOpenGroupV2RoomInfos(); + const originalMessage = TestUtils.generateOpenGroupMessageV2({ serverId: 111 }); + const reactor1 = TestUtils.generateFakePubKey().key; + const reactor2 = TestUtils.generateFakePubKey().key; + + const reaction: Reaction = { + id: originalMessage.serverId!, + author: originalMessage.sender!, + emoji: '😄', + action: Action.REACT, + }; + const validEntry: SogsV3Mutation = { + server: roomInfos.serverUrl, + room: roomInfos.roomId, + changeType: ChangeType.REACTIONS, + seqno: null, + metadata: { + messageId: originalMessage.serverId!, + emoji: reaction.emoji, + action: 'ADD', + }, + }; + const invalidEntry: SogsV3Mutation = { + server: '', + room: roomInfos.roomId, + changeType: ChangeType.REACTIONS, + seqno: 100, + metadata: { + messageId: originalMessage.serverId!, + emoji: reaction.emoji, + action: 'ADD', + }, + }; + const messageResponse = TestUtils.generateFakeIncomingOpenGroupMessageV4({ + id: originalMessage.serverId!, + seqno: 200, + reactions: { + '😄': { + index: 0, + count: 1, + you: true, + reactors: [originalMessage.sender!], + }, + '❤️': { + index: 1, + count: 2, + you: true, + reactors: [originalMessage.sender!, reactor1], + }, + '😈': { + index: 0, + count: 2, + you: false, + reactors: [reactor1, reactor2], + }, + }, + }); + + beforeEach(async () => { + // stubs + Sinon.stub(Reactions, 'handleOpenGroupMessageReactions').resolves(); + }); + + afterEach(Sinon.restore); + + describe('add entry to cache', () => { + it('add entry to cache that is valid', async () => { + const cacheState = addToMutationCache(validEntry); + expect(cacheState, 'should not empty').to.not.equal([]); + expect(cacheState.length, 'should have one entry').to.be.equal(1); + expect(cacheState[0], 'the entry should match the input').to.be.deep.equal(validEntry); + }); + it('add entry to cache that is invalid and fail', async () => { + const cacheState = addToMutationCache(invalidEntry); + expect(cacheState, 'should not empty').to.not.equal([]); + expect(cacheState.length, 'should have one entry').to.be.equal(1); + }); + }); + + describe('update entry in cache', () => { + it('update entry in cache with a valid source entry', async () => { + const cacheState = updateMutationCache(validEntry, messageResponse.seqno); + expect(cacheState, 'should not empty').to.not.equal([]); + expect(cacheState.length, 'should have one entry').to.be.equal(1); + expect( + cacheState[0].seqno, + 'should have an entry with a matching seqno to the message response' + ).to.be.equal(messageResponse.seqno); + }); + it('update entry in cache with an invalid source entry', async () => { + const cacheState = updateMutationCache(invalidEntry, messageResponse.seqno); + expect(cacheState, 'should not empty').to.not.equal([]); + expect(cacheState.length, 'should have one entry').to.be.equal(1); + expect( + cacheState[0].seqno, + 'should have an entry with a matching seqno to the message response' + ).to.be.equal(messageResponse.seqno); + }); + it('update entry in cache with a valid source entry but its not stored in the cache', async () => { + const notFoundEntry: SogsV3Mutation = { + server: roomInfos.serverUrl, + room: roomInfos.roomId, + changeType: ChangeType.REACTIONS, + seqno: 400, + metadata: { + messageId: originalMessage.serverId!, + emoji: reaction.emoji, + action: 'ADD', + }, + }; + const cacheState = updateMutationCache(notFoundEntry, messageResponse.seqno); + expect(cacheState, 'should not empty').to.not.equal([]); + expect(cacheState.length, 'should have one entry').to.be.equal(1); + expect( + cacheState[0].seqno, + 'should have an entry with a matching seqno to the message response' + ).to.be.equal(messageResponse.seqno); + }); + }); + + describe('process opengroup messages using the cache', () => { + it('processing a message with valid serverUrl, roomId and message should return an updated message', async () => {}); + it('processing a message with valid serverUrl, roomId and invalid message should return undefined', async () => {}); + it('processing a message with valid entries in the cache should remove them if the cached entry seqno number is less than the message seqo', async () => {}); + it('processing a message with valid entries in the cache should calculate the optimistic state if there is no message seqo or the cached entry seqno is larger than the message seqno', async () => {}); + }); +}); diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index 922cd7865..f0f5b6518 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -7,6 +7,8 @@ import { TestUtils } from '..'; import { OpenGroupRequestCommonType } from '../../../session/apis/open_group_api/opengroupV2/ApiUtil'; import { OpenGroupVisibleMessage } from '../../../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; import { MessageModel } from '../../../models/message'; +import { OpenGroupMessageV4 } from '../../../session/apis/open_group_api/opengroupV2/OpenGroupServerPoller'; +import { OpenGroupReaction } from '../../../types/Reaction'; export function generateVisibleMessage({ identifier, @@ -27,8 +29,9 @@ export function generateVisibleMessage({ }); } -export function generateOpenGroupMessageV2(): OpenGroupMessageV2 { +export function generateOpenGroupMessageV2({ serverId }: { serverId: number }): OpenGroupMessageV2 { return new OpenGroupMessageV2({ + serverId, sentTimestamp: Date.now(), sender: TestUtils.generateFakePubKey().key, base64EncodedData: 'whatever', @@ -62,3 +65,23 @@ export function generateFakeIncomingPrivateMessage(): MessageModel { type: 'incoming', }); } + +export function generateFakeIncomingOpenGroupMessageV4({ + id, + seqno, + reactions, +}: { + seqno: number; + id: number; + reactions?: Record<string, OpenGroupReaction>; +}): OpenGroupMessageV4 { + return { + id, // serverId + seqno, + /** base64 */ + signature: 'whatever', + /** timestamp number with decimal */ + posted: Date.now(), + reactions: reactions ?? {}, + }; +} diff --git a/ts/test/test-utils/utils/stubbing.ts b/ts/test/test-utils/utils/stubbing.ts index 7ec27485f..d465d6f9a 100644 --- a/ts/test/test-utils/utils/stubbing.ts +++ b/ts/test/test-utils/utils/stubbing.ts @@ -68,7 +68,7 @@ export function stubWindow<K extends keyof Window>(fn: K, value: WindowValue<K>) }; } -export const enableLogRedirect = false; +export const enableLogRedirect = true; export const stubWindowLog = () => { stubWindow('log', { diff --git a/ts/util/reactions.ts b/ts/util/reactions.ts index 9511c415d..e2569068d 100644 --- a/ts/util/reactions.ts +++ b/ts/util/reactions.ts @@ -11,12 +11,12 @@ import { UserUtils } from '../session/utils'; import { Action, OpenGroupReactionList, ReactionList, RecentReactions } from '../types/Reaction'; import { getRecentReactions, saveRecentReations } from '../util/storage'; -export const SOGSReactorsFetchCount = 5; +const SOGSReactorsFetchCount = 5; const rateCountLimit = 20; const rateTimeLimit = 60 * 1000; const latestReactionTimestamps: Array<number> = []; -export function hitRateLimit(): boolean { +function hitRateLimit(): boolean { const timestamp = Date.now(); latestReactionTimestamps.push(timestamp); @@ -71,7 +71,7 @@ const getMessageByReaction = async ( /** * Sends a Reaction Data Message */ -export const sendMessageReaction = async (messageId: string, emoji: string) => { +const sendMessageReaction = async (messageId: string, emoji: string) => { const found = await Data.getMessageById(messageId); if (found) { const conversationModel = found?.getConversation(); @@ -147,7 +147,7 @@ export const sendMessageReaction = async (messageId: string, emoji: string) => { * Handle reactions on the client by updating the state of the source message * Used in OpenGroups for sending reactions only, not handling responses */ -export const handleMessageReaction = async ({ +const handleMessageReaction = async ({ reaction, sender, you, @@ -239,7 +239,7 @@ export const handleMessageReaction = async ({ * Handles updating the UI when clearing all reactions for a certain emoji * Only usable by moderators in opengroups and runs on their client */ -export const handleClearReaction = async (serverId: number, emoji: string) => { +const handleClearReaction = async (serverId: number, emoji: string) => { const originalMessage = await Data.getMessageByServerId(serverId); if (!originalMessage) { window?.log?.warn(`Cannot find the original reacted message ${serverId}.`); @@ -265,7 +265,7 @@ export const handleClearReaction = async (serverId: number, emoji: string) => { /** * Handles all message reaction updates/responses for opengroups */ -export const handleOpenGroupMessageReactions = async ( +const handleOpenGroupMessageReactions = async ( reactions: OpenGroupReactionList, serverId: number ) => { @@ -334,7 +334,7 @@ export const handleOpenGroupMessageReactions = async ( return originalMessage; }; -export const updateRecentReactions = async (reactions: Array<string>, newReaction: string) => { +const updateRecentReactions = async (reactions: Array<string>, newReaction: string) => { window?.log?.info('updating recent reactions with', newReaction); const recentReactions = new RecentReactions(reactions); const foundIndex = recentReactions.items.indexOf(newReaction); @@ -348,3 +348,14 @@ export const updateRecentReactions = async (reactions: Array<string>, newReactio } await saveRecentReations(recentReactions.items); }; + +// exported for testing purposes +export const Reactions = { + SOGSReactorsFetchCount, + hitRateLimit, + sendMessageReaction, + handleMessageReaction, + handleClearReaction, + handleOpenGroupMessageReactions, + updateRecentReactions, +};