From 51cd71b9c8c07ea8df8cd3024918d46e04e975cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20G=C3=B3rka?= Date: Tue, 7 May 2024 15:52:09 +0200 Subject: [PATCH] fix: delete conversation locally if its deleted on BE already (#17080) (#17373) * runfix: delete conversation locally if its deleted on BE already * chore: fix test describe structure * test: add deleteConversation test coverage --- .../ConversationRepository.test.ts | 1868 +++++++++-------- .../conversation/ConversationRepository.ts | 34 +- 2 files changed, 985 insertions(+), 917 deletions(-) diff --git a/src/script/conversation/ConversationRepository.test.ts b/src/script/conversation/ConversationRepository.test.ts index e15a4e1a11d..dfbc2f27f3d 100644 --- a/src/script/conversation/ConversationRepository.test.ts +++ b/src/script/conversation/ConversationRepository.test.ts @@ -39,6 +39,7 @@ import { CONVERSATION_EVENT, ConversationMLSWelcomeEvent, } from '@wireapp/api-client/lib/event/'; +import {BackendError, BackendErrorLabel} from '@wireapp/api-client/lib/http'; import {QualifiedId} from '@wireapp/api-client/lib/user'; import {amplify} from 'amplify'; import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; @@ -1767,1128 +1768,1187 @@ describe('ConversationRepository', () => { expect(conversationRepo.updateParticipatingUserEntities).not.toHaveBeenCalled(); }); }); + }); - describe('conversation.message-delete', () => { - let message_et: Message; - const selfUser = generateUser(); - - beforeEach(() => { - conversation_et = _generateConversation(); - return testFactory.conversation_repository['saveConversation'](conversation_et).then(() => { - message_et = new Message(createUuid()); - message_et.from = selfUser.id; - conversation_et.addMessage(message_et); + describe('conversation.message-delete', () => { + let message_et: Message; + const selfUser = generateUser(); - spyOn(testFactory.conversation_repository, 'addDeleteMessage'); - spyOn(testFactory.conversation_repository as any, 'onMessageDeleted').and.callThrough(); - }); + beforeEach(() => { + conversation_et = _generateConversation(); + return testFactory.conversation_repository['saveConversation'](conversation_et).then(() => { + message_et = new Message(createUuid()); + message_et.from = selfUser.id; + conversation_et.addMessage(message_et); + + spyOn(testFactory.conversation_repository, 'addDeleteMessage'); + spyOn(testFactory.conversation_repository as any, 'onMessageDeleted').and.callThrough(); }); + }); - afterEach(() => conversation_et.removeMessages()); + afterEach(() => conversation_et.removeMessages()); - it('should not delete message if user is not matching', async () => { - const message_delete_event: DeleteEvent = createDeleteEvent(message_et.id, conversation_et.id); + it('should not delete message if user is not matching', async () => { + const message_delete_event: DeleteEvent = createDeleteEvent(message_et.id, conversation_et.id); - spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); + spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); - expect(conversation_et.getMessage(message_et.id)).toBeDefined(); - await expect( - testFactory.conversation_repository['handleConversationEvent'](message_delete_event), - ).rejects.toMatchObject({ - type: ConversationError.TYPE.WRONG_USER, - }); - expect(testFactory.conversation_repository['onMessageDeleted']).toHaveBeenCalled(); - expect(conversation_et.getMessage(message_et.id)).toBeDefined(); - expect(testFactory.conversation_repository.addDeleteMessage).not.toHaveBeenCalled(); + expect(conversation_et.getMessage(message_et.id)).toBeDefined(); + await expect( + testFactory.conversation_repository['handleConversationEvent'](message_delete_event), + ).rejects.toMatchObject({ + type: ConversationError.TYPE.WRONG_USER, }); + expect(testFactory.conversation_repository['onMessageDeleted']).toHaveBeenCalled(); + expect(conversation_et.getMessage(message_et.id)).toBeDefined(); + expect(testFactory.conversation_repository.addDeleteMessage).not.toHaveBeenCalled(); + }); - it('should delete message if user is self', () => { - spyOn(testFactory.event_service, 'deleteEvent'); - const message_delete_event: DeleteEvent = { - conversation: conversation_et.id, - data: { - deleted_time: 0, - message_id: message_et.id, - time: '', - }, - from: selfUser.id, - id: createUuid(), - qualified_conversation: {domain: '', id: conversation_et.id}, - time: new Date().toISOString(), - type: ClientEvent.CONVERSATION.MESSAGE_DELETE, - }; - - spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); - - expect(conversation_et.getMessage(message_et.id)).toBeDefined(); - return testFactory.conversation_repository['handleConversationEvent'](message_delete_event).then(() => { - expect(testFactory.conversation_repository['onMessageDeleted']).toHaveBeenCalled(); - expect(testFactory.event_service.deleteEvent).toHaveBeenCalledTimes(1); - expect(testFactory.conversation_repository.addDeleteMessage).not.toHaveBeenCalled(); - }); + it('should delete message if user is self', () => { + spyOn(testFactory.event_service, 'deleteEvent'); + const message_delete_event: DeleteEvent = { + conversation: conversation_et.id, + data: { + deleted_time: 0, + message_id: message_et.id, + time: '', + }, + from: selfUser.id, + id: createUuid(), + qualified_conversation: {domain: '', id: conversation_et.id}, + time: new Date().toISOString(), + type: ClientEvent.CONVERSATION.MESSAGE_DELETE, + }; + + spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); + + expect(conversation_et.getMessage(message_et.id)).toBeDefined(); + return testFactory.conversation_repository['handleConversationEvent'](message_delete_event).then(() => { + expect(testFactory.conversation_repository['onMessageDeleted']).toHaveBeenCalled(); + expect(testFactory.event_service.deleteEvent).toHaveBeenCalledTimes(1); + expect(testFactory.conversation_repository.addDeleteMessage).not.toHaveBeenCalled(); }); + }); - it('should delete message and add delete message if user is not self', () => { - spyOn(testFactory.event_service, 'deleteEvent'); + it('should delete message and add delete message if user is not self', () => { + spyOn(testFactory.event_service, 'deleteEvent'); - const message_delete_event = createDeleteEvent(message_et.id, conversation_et.id); - message_et.from = message_delete_event.from; + const message_delete_event = createDeleteEvent(message_et.id, conversation_et.id); + message_et.from = message_delete_event.from; - spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); + spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); - expect(conversation_et.getMessage(message_et.id)).toBeDefined(); - return testFactory.conversation_repository['handleConversationEvent'](message_delete_event).then(() => { - expect(testFactory.conversation_repository['onMessageDeleted']).toHaveBeenCalled(); - expect(testFactory.event_service.deleteEvent).toHaveBeenCalledTimes(1); - expect(testFactory.conversation_repository.addDeleteMessage).toHaveBeenCalled(); - }); + expect(conversation_et.getMessage(message_et.id)).toBeDefined(); + return testFactory.conversation_repository['handleConversationEvent'](message_delete_event).then(() => { + expect(testFactory.conversation_repository['onMessageDeleted']).toHaveBeenCalled(); + expect(testFactory.event_service.deleteEvent).toHaveBeenCalledTimes(1); + expect(testFactory.conversation_repository.addDeleteMessage).toHaveBeenCalled(); }); + }); - it('should delete message and skip adding delete message for ephemeral messages', () => { - spyOn(testFactory.event_service, 'deleteEvent'); - const other_user_id = createUuid(); - message_et.from = other_user_id; - message_et.ephemeral_expires(true); + it('should delete message and skip adding delete message for ephemeral messages', () => { + spyOn(testFactory.event_service, 'deleteEvent'); + const other_user_id = createUuid(); + message_et.from = other_user_id; + message_et.ephemeral_expires(true); - const message_delete_event = createDeleteEvent(message_et.id, conversation_et.id); + const message_delete_event = createDeleteEvent(message_et.id, conversation_et.id); - spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); + spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); - expect(conversation_et.getMessage(message_et.id)).toBeDefined(); - return testFactory.conversation_repository['handleConversationEvent'](message_delete_event).then(() => { - expect(testFactory.conversation_repository['onMessageDeleted']).toHaveBeenCalled(); - expect(testFactory.event_service.deleteEvent).toHaveBeenCalledTimes(1); - expect(testFactory.conversation_repository.addDeleteMessage).not.toHaveBeenCalled(); - }); + expect(conversation_et.getMessage(message_et.id)).toBeDefined(); + return testFactory.conversation_repository['handleConversationEvent'](message_delete_event).then(() => { + expect(testFactory.conversation_repository['onMessageDeleted']).toHaveBeenCalled(); + expect(testFactory.event_service.deleteEvent).toHaveBeenCalledTimes(1); + expect(testFactory.conversation_repository.addDeleteMessage).not.toHaveBeenCalled(); }); }); + }); - describe('conversation.message-hidden', () => { - let messageId: string; - const selfUser = generateUser(); - - beforeEach(() => { - conversation_et = _generateConversation(); + describe('conversation.message-hidden', () => { + let messageId: string; + const selfUser = generateUser(); - return testFactory.conversation_repository['saveConversation'](conversation_et).then(() => { - const messageToHideEt = new Message(createUuid()); - conversation_et.addMessage(messageToHideEt); + beforeEach(() => { + conversation_et = _generateConversation(); - messageId = messageToHideEt.id; - spyOn(testFactory.conversation_repository as any, 'onMessageHidden').and.callThrough(); - }); - }); + return testFactory.conversation_repository['saveConversation'](conversation_et).then(() => { + const messageToHideEt = new Message(createUuid()); + conversation_et.addMessage(messageToHideEt); - it('should not hide message if sender is not self user', async () => { - const messageHiddenEvent: MessageHiddenEvent = { - conversation: selfConversation.id, - data: { - conversation_id: conversation_et.id, - message_id: messageId, - }, - from: createUuid(), - id: createUuid(), - qualified_conversation: selfConversation.qualifiedId, - time: new Date().toISOString(), - type: ClientEvent.CONVERSATION.MESSAGE_HIDDEN, - }; - - expect(conversation_et.getMessage(messageId)).toBeDefined(); - - await expect( - testFactory.conversation_repository['handleConversationEvent'](messageHiddenEvent), - ).rejects.toMatchObject({ - type: ConversationError.TYPE.WRONG_USER, - }); - expect(testFactory.conversation_repository['onMessageHidden']).toHaveBeenCalled(); - expect(conversation_et.getMessage(messageId)).toBeDefined(); + messageId = messageToHideEt.id; + spyOn(testFactory.conversation_repository as any, 'onMessageHidden').and.callThrough(); }); + }); - it('should hide message if sender is self user', () => { - spyOn(testFactory.event_service, 'deleteEvent'); - const messageHiddenEvent: MessageHiddenEvent = { - conversation: selfConversation.id, - data: { - conversation_id: conversation_et.id, - message_id: messageId, - }, - from: selfUser.id, - id: createUuid(), - qualified_conversation: selfConversation.qualifiedId, - time: new Date().toISOString(), - type: ClientEvent.CONVERSATION.MESSAGE_HIDDEN, - }; - - expect(conversation_et.getMessage(messageId)).toBeDefined(); + it('should not hide message if sender is not self user', async () => { + const messageHiddenEvent: MessageHiddenEvent = { + conversation: selfConversation.id, + data: { + conversation_id: conversation_et.id, + message_id: messageId, + }, + from: createUuid(), + id: createUuid(), + qualified_conversation: selfConversation.qualifiedId, + time: new Date().toISOString(), + type: ClientEvent.CONVERSATION.MESSAGE_HIDDEN, + }; - spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); + expect(conversation_et.getMessage(messageId)).toBeDefined(); - return testFactory.conversation_repository['handleConversationEvent'](messageHiddenEvent).then(() => { - expect(testFactory.conversation_repository['onMessageHidden']).toHaveBeenCalled(); - expect(testFactory.event_service.deleteEvent).toHaveBeenCalledTimes(1); - }); + await expect( + testFactory.conversation_repository['handleConversationEvent'](messageHiddenEvent), + ).rejects.toMatchObject({ + type: ConversationError.TYPE.WRONG_USER, }); + expect(testFactory.conversation_repository['onMessageHidden']).toHaveBeenCalled(); + expect(conversation_et.getMessage(messageId)).toBeDefined(); + }); - it('should not hide message if not send via self conversation', async () => { - spyOn(testFactory.event_service, 'deleteEvent'); - const messageHiddenEvent: MessageHiddenEvent = { - conversation: createUuid(), - data: { - conversation_id: conversation_et.id, - message_id: messageId, - }, - from: selfUser.id, - id: createUuid(), - qualified_conversation: {domain: '', id: conversation_et.id}, - time: new Date().toISOString(), - type: ClientEvent.CONVERSATION.MESSAGE_HIDDEN, - }; - - expect(conversation_et.getMessage(messageId)).toBeDefined(); - spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); - - await expect( - testFactory.conversation_repository['handleConversationEvent'](messageHiddenEvent), - ).rejects.toMatchObject({ - type: ConversationError.TYPE.WRONG_CONVERSATION, - }); - expect(testFactory.event_service.deleteEvent).not.toHaveBeenCalled(); - }); + it('should hide message if sender is self user', () => { + spyOn(testFactory.event_service, 'deleteEvent'); + const messageHiddenEvent: MessageHiddenEvent = { + conversation: selfConversation.id, + data: { + conversation_id: conversation_et.id, + message_id: messageId, + }, + from: selfUser.id, + id: createUuid(), + qualified_conversation: selfConversation.qualifiedId, + time: new Date().toISOString(), + type: ClientEvent.CONVERSATION.MESSAGE_HIDDEN, + }; - it('syncs message deletion with the database', () => { - const deletedMessagePayload = { - conversation: createUuid(), - from: '', - id: createUuid(), - time: '', - type: CONVERSATION_EVENT.DELETE, - }; + expect(conversation_et.getMessage(messageId)).toBeDefined(); + + spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); - const conversationEntity = new Conversation(); - spyOn(conversationEntity, 'removeMessageById'); + return testFactory.conversation_repository['handleConversationEvent'](messageHiddenEvent).then(() => { + expect(testFactory.conversation_repository['onMessageHidden']).toHaveBeenCalled(); + expect(testFactory.event_service.deleteEvent).toHaveBeenCalledTimes(1); + }); + }); - const conversationRepository = testFactory.conversation_repository; - spyOn(conversationRepository['conversationState'], 'findConversation').and.returnValue(conversationEntity); + it('should not hide message if not send via self conversation', async () => { + spyOn(testFactory.event_service, 'deleteEvent'); + const messageHiddenEvent: MessageHiddenEvent = { + conversation: createUuid(), + data: { + conversation_id: conversation_et.id, + message_id: messageId, + }, + from: selfUser.id, + id: createUuid(), + qualified_conversation: {domain: '', id: conversation_et.id}, + time: new Date().toISOString(), + type: ClientEvent.CONVERSATION.MESSAGE_HIDDEN, + }; - conversationRepository['deleteLocalMessageEntity']({oldObj: deletedMessagePayload} as any); + expect(conversation_et.getMessage(messageId)).toBeDefined(); + spyOn(testFactory.conversation_repository['userState'], 'self').and.returnValue(selfUser); - expect(conversationRepository['conversationState'].findConversation).toHaveBeenCalledWith({ - domain: '', - id: deletedMessagePayload.conversation, - }); - expect(conversationEntity.removeMessageById).toHaveBeenCalledWith(deletedMessagePayload.id); + await expect( + testFactory.conversation_repository['handleConversationEvent'](messageHiddenEvent), + ).rejects.toMatchObject({ + type: ConversationError.TYPE.WRONG_CONVERSATION, }); + expect(testFactory.event_service.deleteEvent).not.toHaveBeenCalled(); }); - }); - describe('findConversation', () => { - let conversationRepository: ConversationRepository; - const conversationIds = ['40b05b5f-d906-4276-902c-3fa16af3b2bd', '3dd5a837-a7a5-40c6-b6c9-b1ab155e1e55']; - beforeEach(() => { - conversationRepository = testFactory.conversation_repository; + it('syncs message deletion with the database', () => { + const deletedMessagePayload = { + conversation: createUuid(), + from: '', + id: createUuid(), + time: '', + type: CONVERSATION_EVENT.DELETE, + }; - const conversationEntities = conversationIds.map(id => new Conversation(id)); - conversationRepository['conversationState'].conversations(conversationEntities); - }); + const conversationEntity = new Conversation(); + spyOn(conversationEntity, 'removeMessageById'); - afterEach(() => { - conversationRepository['conversationState'].conversations([]); - }); + const conversationRepository = testFactory.conversation_repository; + spyOn(conversationRepository['conversationState'], 'findConversation').and.returnValue(conversationEntity); - it('does not return any conversation if team is marked for deletion', () => { - spyOn(conversationRepository['teamState'], 'isTeamDeleted').and.returnValue(false); - conversationIds.forEach(conversationId => { - expect( - conversationRepository['conversationState'].findConversation({domain: '', id: conversationId}), - ).toBeDefined(); - }); + conversationRepository['deleteLocalMessageEntity']({oldObj: deletedMessagePayload} as any); - (conversationRepository['teamState'].isTeamDeleted as any).and.returnValue(true); - conversationIds.forEach(conversationId => { - expect( - conversationRepository['conversationState'].findConversation({domain: '', id: conversationId}), - ).not.toBeDefined(); + expect(conversationRepository['conversationState'].findConversation).toHaveBeenCalledWith({ + domain: '', + id: deletedMessagePayload.conversation, }); + expect(conversationEntity.removeMessageById).toHaveBeenCalledWith(deletedMessagePayload.id); }); + }); + }); - it('returns the conversation if present in the local conversations', () => { - conversationIds.forEach(conversationId => { - expect( - conversationRepository['conversationState'].findConversation({domain: '', id: conversationId}), - ).toBeDefined(); - }); + describe('findConversation', () => { + let conversationRepository: ConversationRepository; + const conversationIds = ['40b05b5f-d906-4276-902c-3fa16af3b2bd', '3dd5a837-a7a5-40c6-b6c9-b1ab155e1e55']; + beforeEach(() => { + conversationRepository = testFactory.conversation_repository; - const inexistentConversationIds = [ - 'f573c44f-c549-4e8f-a4d5-20fdc7adc789', - 'eece4e13-41d4-4ea8-9aa3-383a710a5137', - ]; - inexistentConversationIds.forEach(conversationId => { - expect( - conversationRepository['conversationState'].findConversation({domain: '', id: conversationId}), - ).not.toBeDefined(); - }); - }); + const conversationEntities = conversationIds.map(id => new Conversation(id)); + conversationRepository['conversationState'].conversations(conversationEntities); }); - describe('Encryption', () => { - let anne: User; - let bob: User; - let jane: User; - let john: User; - let lara: User; + afterEach(() => { + conversationRepository['conversationState'].conversations([]); + }); - beforeEach(() => { - anne = new User('', null); - anne.name('Anne'); - - bob = new User('532af01e-1e24-4366-aacf-33b67d4ee376', null); - bob.name('Bob'); - - jane = new User(entities.user.jane_roe.id, null); - jane.name('Jane'); - - john = new User(entities.user.john_doe.id, null); - john.name('John'); - - const johns_computer = new ClientEntity(false, null); - johns_computer.id = '83ad5d3c31d3c76b'; - johns_computer.class = ClientClassification.TABLET; - john.devices.push(johns_computer); - - lara = new User('', null); - lara.name('Lara'); - - const bobs_computer = new ClientEntity(false, null); - bobs_computer.id = '74606e4c02b2c7f9'; - bobs_computer.class = ClientClassification.DESKTOP; - - const bobs_phone = new ClientEntity(false, null); - bobs_phone.id = '8f63631e129ed19d'; - bobs_phone.class = ClientClassification.PHONE; - - bob.devices.push(bobs_computer); - bob.devices.push(bobs_phone); - - const dudes = _generateConversation(); - dudes.name('Web Dudes'); - dudes.participating_user_ets.push(bob); - dudes.participating_user_ets.push(john); - - const gals = _generateConversation(); - gals.name('Web Gals'); - gals.participating_user_ets.push(anne); - gals.participating_user_ets.push(jane); - gals.participating_user_ets.push(lara); - - const mixed_group = _generateConversation(); - mixed_group.name('Web Dudes & Gals'); - mixed_group.participating_user_ets.push(anne); - mixed_group.participating_user_ets.push(bob); - mixed_group.participating_user_ets.push(jane); - mixed_group.participating_user_ets.push(john); - mixed_group.participating_user_ets.push(lara); - - return Promise.all([ - testFactory.conversation_repository['saveConversation'](dudes), - testFactory.conversation_repository['saveConversation'](gals), - testFactory.conversation_repository['saveConversation'](mixed_group), - ]); + it('does not return any conversation if team is marked for deletion', () => { + spyOn(conversationRepository['teamState'], 'isTeamDeleted').and.returnValue(false); + conversationIds.forEach(conversationId => { + expect( + conversationRepository['conversationState'].findConversation({domain: '', id: conversationId}), + ).toBeDefined(); }); - it('should know all users participating in a conversation (including the self user)', () => { - const [, , users] = testFactory.conversation_repository['conversationState'].conversations(); - return testFactory.conversation_repository - .getAllUsersInConversation({domain: '', id: users.id}) - .then(user_ets => { - expect(user_ets.length).toBe(3); - expect(testFactory.conversation_repository['conversationState'].conversations().length).toBe(5); - }); + (conversationRepository['teamState'].isTeamDeleted as any).and.returnValue(true); + conversationIds.forEach(conversationId => { + expect( + conversationRepository['conversationState'].findConversation({domain: '', id: conversationId}), + ).not.toBeDefined(); }); }); - describe('addMissingMember', () => { - it('injects a member-join event if unknown user is detected', () => { - const conversationId = createUuid(); - const event = {conversation: conversationId, from: 'unknown-user-id'}; - spyOn(testFactory.conversation_repository, 'getConversationById').and.returnValue(Promise.resolve({})); - spyOn(EventBuilder, 'buildMemberJoin').and.returnValue(event); + it('returns the conversation if present in the local conversations', () => { + conversationIds.forEach(conversationId => { + expect( + conversationRepository['conversationState'].findConversation({domain: '', id: conversationId}), + ).toBeDefined(); + }); - return testFactory.conversation_repository - .addMissingMember({id: conversationId} as Conversation, [{domain: '', id: 'unknown-user-id'}], 0) - .then(() => { - expect(testFactory.event_repository.injectEvent).toHaveBeenCalledWith( - event, - EventRepository.SOURCE.INJECTED, - ); - }); + const inexistentConversationIds = [ + 'f573c44f-c549-4e8f-a4d5-20fdc7adc789', + 'eece4e13-41d4-4ea8-9aa3-383a710a5137', + ]; + inexistentConversationIds.forEach(conversationId => { + expect( + conversationRepository['conversationState'].findConversation({domain: '', id: conversationId}), + ).not.toBeDefined(); }); }); + }); + + describe('Encryption', () => { + let anne: User; + let bob: User; + let jane: User; + let john: User; + let lara: User; - describe('shouldSendReadReceipt', () => { - it('uses the account preference for 1:1 conversations', () => { - // Set a receipt mode on account-level - const preferenceMode = RECEIPT_MODE.ON; - testFactory.propertyRepository.receiptMode(preferenceMode); + beforeEach(() => { + anne = new User('', null); + anne.name('Anne'); - // Set the opposite receipt mode on conversation-level - const conversationEntity = _generateConversation({type: CONVERSATION_TYPE.ONE_TO_ONE}); - conversationEntity.receiptMode(RECEIPT_MODE.OFF); + bob = new User('532af01e-1e24-4366-aacf-33b67d4ee376', null); + bob.name('Bob'); - // Verify that the account-level preference wins - const shouldSend = testFactory.conversation_repository.expectReadReceipt(conversationEntity); + jane = new User(entities.user.jane_roe.id, null); + jane.name('Jane'); - expect(shouldSend).toBe(!!preferenceMode); - }); + john = new User(entities.user.john_doe.id, null); + john.name('John'); - it('uses the conversation setting for group conversations', () => { - // Set a receipt mode on account-level - const preferenceMode = RECEIPT_MODE.ON; - testFactory.propertyRepository.receiptMode(preferenceMode); + const johns_computer = new ClientEntity(false, null); + johns_computer.id = '83ad5d3c31d3c76b'; + johns_computer.class = ClientClassification.TABLET; + john.devices.push(johns_computer); - // Set the opposite receipt mode on conversation-level - const conversationEntity = _generateConversation(); - conversationEntity.receiptMode(RECEIPT_MODE.OFF); + lara = new User('', null); + lara.name('Lara'); - // Verify that the conversation-level preference wins - const shouldSend = testFactory.conversation_repository.expectReadReceipt(conversationEntity); + const bobs_computer = new ClientEntity(false, null); + bobs_computer.id = '74606e4c02b2c7f9'; + bobs_computer.class = ClientClassification.DESKTOP; - expect(shouldSend).toBe(!!conversationEntity.receiptMode()); - }); + const bobs_phone = new ClientEntity(false, null); + bobs_phone.id = '8f63631e129ed19d'; + bobs_phone.class = ClientClassification.PHONE; + + bob.devices.push(bobs_computer); + bob.devices.push(bobs_phone); + + const dudes = _generateConversation(); + dudes.name('Web Dudes'); + dudes.participating_user_ets.push(bob); + dudes.participating_user_ets.push(john); + + const gals = _generateConversation(); + gals.name('Web Gals'); + gals.participating_user_ets.push(anne); + gals.participating_user_ets.push(jane); + gals.participating_user_ets.push(lara); + + const mixed_group = _generateConversation(); + mixed_group.name('Web Dudes & Gals'); + mixed_group.participating_user_ets.push(anne); + mixed_group.participating_user_ets.push(bob); + mixed_group.participating_user_ets.push(jane); + mixed_group.participating_user_ets.push(john); + mixed_group.participating_user_ets.push(lara); + + return Promise.all([ + testFactory.conversation_repository['saveConversation'](dudes), + testFactory.conversation_repository['saveConversation'](gals), + testFactory.conversation_repository['saveConversation'](mixed_group), + ]); }); - describe('checkForDeletedConversations', () => { - it('removes conversations that have been deleted on the backend', async () => { - const deletedGroup = _generateConversation(); - const oldGroup = _generateConversation(); - const conversationRepository = testFactory.conversation_repository!; + it('should know all users participating in a conversation (including the self user)', () => { + const [, , users] = testFactory.conversation_repository['conversationState'].conversations(); + return testFactory.conversation_repository + .getAllUsersInConversation({domain: '', id: users.id}) + .then(user_ets => { + expect(user_ets.length).toBe(3); + expect(testFactory.conversation_repository['conversationState'].conversations().length).toBe(5); + }); + }); + }); - jest.spyOn(testFactory.conversation_service!, 'getConversationByIds').mockResolvedValue({ - not_found: [deletedGroup, oldGroup], + describe('addMissingMember', () => { + it('injects a member-join event if unknown user is detected', () => { + const conversationId = createUuid(); + const event = {conversation: conversationId, from: 'unknown-user-id'}; + spyOn(testFactory.conversation_repository, 'getConversationById').and.returnValue(Promise.resolve({})); + spyOn(EventBuilder, 'buildMemberJoin').and.returnValue(event); + + return testFactory.conversation_repository + .addMissingMember({id: conversationId} as Conversation, [{domain: '', id: 'unknown-user-id'}], 0) + .then(() => { + expect(testFactory.event_repository.injectEvent).toHaveBeenCalledWith(event, EventRepository.SOURCE.INJECTED); }); - jest - .spyOn(conversationRepository['conversationService'], 'getConversationById') - .mockImplementation(async conversationId => { - if (matchQualifiedIds(conversationId, deletedGroup.qualifiedId)) { - throw new ConversationError(ConversationError.TYPE.CONVERSATION_NOT_FOUND, 'Conversation not found'); - } - return {} as any; - }); - await conversationRepository['saveConversation'](deletedGroup); - await conversationRepository['saveConversation'](oldGroup); + }); + }); - const currentNbConversations = conversationRepository['conversationState'].conversations().length; - await testFactory.conversation_repository!.syncDeletedConversations(); + describe('shouldSendReadReceipt', () => { + it('uses the account preference for 1:1 conversations', () => { + // Set a receipt mode on account-level + const preferenceMode = RECEIPT_MODE.ON; + testFactory.propertyRepository.receiptMode(preferenceMode); - expect(conversationRepository['conversationState'].conversations()).toHaveLength(currentNbConversations - 1); - }); + // Set the opposite receipt mode on conversation-level + const conversationEntity = _generateConversation({type: CONVERSATION_TYPE.ONE_TO_ONE}); + conversationEntity.receiptMode(RECEIPT_MODE.OFF); + + // Verify that the account-level preference wins + const shouldSend = testFactory.conversation_repository.expectReadReceipt(conversationEntity); + + expect(shouldSend).toBe(!!preferenceMode); }); - function generateConversation( - id: QualifiedId, - name: string, - otherMembers: QualifiedId[] = [], - protocol = ConversationProtocol.PROTEUS, - type = CONVERSATION_TYPE.REGULAR, - ) { - return { - members: { - others: otherMembers.map(uid => ({qualified_id: uid})) || ([] as any), - self: {}, - }, - name, - protocol, - qualified_id: id, - receipt_mode: 1, - team: 'b0dcee1f-c64e-4d40-8b50-5baf932906b8', - type, - }; - } + it('uses the conversation setting for group conversations', () => { + // Set a receipt mode on account-level + const preferenceMode = RECEIPT_MODE.ON; + testFactory.propertyRepository.receiptMode(preferenceMode); - describe('loadConversations', () => { - beforeEach(() => { - testFactory.conversation_repository!['conversationState'].conversations.removeAll(); + // Set the opposite receipt mode on conversation-level + const conversationEntity = _generateConversation(); + conversationEntity.receiptMode(RECEIPT_MODE.OFF); + + // Verify that the conversation-level preference wins + const shouldSend = testFactory.conversation_repository.expectReadReceipt(conversationEntity); + + expect(shouldSend).toBe(!!conversationEntity.receiptMode()); + }); + }); + + describe('checkForDeletedConversations', () => { + it('removes conversations that have been deleted on the backend', async () => { + const deletedGroup = _generateConversation(); + const oldGroup = _generateConversation(); + const conversationRepository = testFactory.conversation_repository!; + + jest.spyOn(testFactory.conversation_service!, 'getConversationByIds').mockResolvedValue({ + not_found: [deletedGroup, oldGroup], }); + jest + .spyOn(conversationRepository['conversationService'], 'getConversationById') + .mockImplementation(async conversationId => { + if (matchQualifiedIds(conversationId, deletedGroup.qualifiedId)) { + throw new ConversationError(ConversationError.TYPE.CONVERSATION_NOT_FOUND, 'Conversation not found'); + } + return {} as any; + }); + await conversationRepository['saveConversation'](deletedGroup); + await conversationRepository['saveConversation'](oldGroup); - it('loads all conversations from backend when there is no local conversations', async () => { - const conversationRepository = testFactory.conversation_repository!; - const conversationService = conversationRepository['conversationService']; - const remoteConversations = { - found: [ - generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', - }, - 'conv1', - ), + const currentNbConversations = conversationRepository['conversationState'].conversations().length; + await testFactory.conversation_repository!.syncDeletedConversations(); - generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', - }, - 'conv2', - ), - ], - }; - const localConversations: any = []; + expect(conversationRepository['conversationState'].conversations()).toHaveLength(currentNbConversations - 1); + }); + }); - jest - .spyOn(conversationService, 'getAllConversations') - .mockResolvedValue(remoteConversations as unknown as RemoteConversations); - jest - .spyOn(conversationService, 'loadConversationStatesFromDb') - .mockResolvedValue(localConversations as unknown as ConversationDatabaseData[]); - jest.spyOn(conversationService, 'saveConversationsInDb').mockImplementation(data => Promise.resolve(data)); + function generateConversation( + id: QualifiedId, + name: string, + otherMembers: QualifiedId[] = [], + protocol = ConversationProtocol.PROTEUS, + type = CONVERSATION_TYPE.REGULAR, + ) { + return { + members: { + others: otherMembers.map(uid => ({qualified_id: uid})) || ([] as any), + self: {}, + }, + name, + protocol, + qualified_id: id, + receipt_mode: 1, + team: 'b0dcee1f-c64e-4d40-8b50-5baf932906b8', + type, + }; + } + + describe('loadConversations', () => { + beforeEach(() => { + testFactory.conversation_repository!['conversationState'].conversations.removeAll(); + }); - const conversations = await conversationRepository.loadConversations([]); + it('loads all conversations from backend when there is no local conversations', async () => { + const conversationRepository = testFactory.conversation_repository!; + const conversationService = conversationRepository['conversationService']; + const remoteConversations = { + found: [ + generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', + }, + 'conv1', + ), - expect(conversations).toHaveLength(remoteConversations.found.length); - }); + generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', + }, + 'conv2', + ), + ], + }; + const localConversations: any = []; - it("does not load proteus 1:1 conversation if there's mls 1:1 conversation with the same user", async () => { - const conversationRepository = testFactory.conversation_repository!; - const conversationService = conversationRepository['conversationService']; - const userId = {id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', domain: 'staging.zinfra.io'}; - - const remoteConversations = { - found: [ - generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', - }, - 'conv1', - [userId], - ConversationProtocol.PROTEUS, - CONVERSATION_TYPE.ONE_TO_ONE, - ), - - generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', - }, - 'conv2', - [userId], - ConversationProtocol.MLS, - CONVERSATION_TYPE.ONE_TO_ONE, - ), - ], - }; - const localConversations: any = []; + jest + .spyOn(conversationService, 'getAllConversations') + .mockResolvedValue(remoteConversations as unknown as RemoteConversations); + jest + .spyOn(conversationService, 'loadConversationStatesFromDb') + .mockResolvedValue(localConversations as unknown as ConversationDatabaseData[]); + jest.spyOn(conversationService, 'saveConversationsInDb').mockImplementation(data => Promise.resolve(data)); - jest - .spyOn(conversationService, 'getAllConversations') - .mockResolvedValue(remoteConversations as unknown as RemoteConversations); - jest - .spyOn(conversationService, 'loadConversationStatesFromDb') - .mockResolvedValue(localConversations as unknown as ConversationDatabaseData[]); - jest.spyOn(conversationService, 'saveConversationsInDb').mockImplementation(data => Promise.resolve(data)); + const conversations = await conversationRepository.loadConversations([]); - const conversations = await conversationRepository.loadConversations([]); + expect(conversations).toHaveLength(remoteConversations.found.length); + }); - expect(conversations).toHaveLength(1); - }); + it("does not load proteus 1:1 conversation if there's mls 1:1 conversation with the same user", async () => { + const conversationRepository = testFactory.conversation_repository!; + const conversationService = conversationRepository['conversationService']; + const userId = {id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', domain: 'staging.zinfra.io'}; - it("still loads proteus 1:1 conversation if there's mls 1:1 conversation with the same user but conversation exists locally", async () => { - const conversationRepository = testFactory.conversation_repository!; - const conversationService = conversationRepository['conversationService']; - const userId = {id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', domain: 'staging.zinfra.io'}; + const remoteConversations = { + found: [ + generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', + }, + 'conv1', + [userId], + ConversationProtocol.PROTEUS, + CONVERSATION_TYPE.ONE_TO_ONE, + ), - const mls1to1 = generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', - }, - 'conv2', - [userId], - ConversationProtocol.MLS, - CONVERSATION_TYPE.ONE_TO_ONE, - ); + generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', + }, + 'conv2', + [userId], + ConversationProtocol.MLS, + CONVERSATION_TYPE.ONE_TO_ONE, + ), + ], + }; + const localConversations: any = []; - const proteus1to1 = generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', - }, - 'conv1', - [userId], - ConversationProtocol.PROTEUS, - CONVERSATION_TYPE.ONE_TO_ONE, - ); + jest + .spyOn(conversationService, 'getAllConversations') + .mockResolvedValue(remoteConversations as unknown as RemoteConversations); + jest + .spyOn(conversationService, 'loadConversationStatesFromDb') + .mockResolvedValue(localConversations as unknown as ConversationDatabaseData[]); + jest.spyOn(conversationService, 'saveConversationsInDb').mockImplementation(data => Promise.resolve(data)); - const remoteConversations = { - found: [proteus1to1, mls1to1], - }; - const localConversations: any = [proteus1to1]; + const conversations = await conversationRepository.loadConversations([]); - jest - .spyOn(conversationService, 'getAllConversations') - .mockResolvedValue(remoteConversations as unknown as RemoteConversations); - jest - .spyOn(conversationService, 'loadConversationStatesFromDb') - .mockResolvedValue(localConversations as unknown as ConversationDatabaseData[]); - jest.spyOn(conversationService, 'saveConversationsInDb').mockImplementation(data => Promise.resolve(data)); + expect(conversations).toHaveLength(1); + }); - const conversations = await conversationRepository.loadConversations([]); + it("still loads proteus 1:1 conversation if there's mls 1:1 conversation with the same user but conversation exists locally", async () => { + const conversationRepository = testFactory.conversation_repository!; + const conversationService = conversationRepository['conversationService']; + const userId = {id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', domain: 'staging.zinfra.io'}; - expect(conversations).toHaveLength(remoteConversations.found.length); - }); + const mls1to1 = generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', + }, + 'conv2', + [userId], + ConversationProtocol.MLS, + CONVERSATION_TYPE.ONE_TO_ONE, + ); - it('does not load connection request (type 3) conversations if their users were deleted on backend', async () => { - const conversationRepository = testFactory.conversation_repository!; - const conversationService = conversationRepository['conversationService']; - const userId = {id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', domain: 'staging.zinfra.io'}; + const proteus1to1 = generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', + }, + 'conv1', + [userId], + ConversationProtocol.PROTEUS, + CONVERSATION_TYPE.ONE_TO_ONE, + ); - const connectionReq = generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', - }, - 'conv2', - [userId], - ConversationProtocol.PROTEUS, - CONVERSATION_TYPE.CONNECT, - ); + const remoteConversations = { + found: [proteus1to1, mls1to1], + }; + const localConversations: any = [proteus1to1]; - const remoteConversations = { - found: [connectionReq], - }; - const localConversations: any = [connectionReq]; + jest + .spyOn(conversationService, 'getAllConversations') + .mockResolvedValue(remoteConversations as unknown as RemoteConversations); + jest + .spyOn(conversationService, 'loadConversationStatesFromDb') + .mockResolvedValue(localConversations as unknown as ConversationDatabaseData[]); + jest.spyOn(conversationService, 'saveConversationsInDb').mockImplementation(data => Promise.resolve(data)); - jest.spyOn(conversationService, 'deleteConversationFromDb'); - jest.spyOn(conversationService, 'blacklistConversation'); - jest - .spyOn(conversationService, 'getAllConversations') - .mockResolvedValue(remoteConversations as unknown as RemoteConversations); - jest - .spyOn(conversationService, 'loadConversationStatesFromDb') - .mockResolvedValue(localConversations as unknown as ConversationDatabaseData[]); - jest.spyOn(conversationService, 'saveConversationsInDb').mockImplementation(data => Promise.resolve(data)); + const conversations = await conversationRepository.loadConversations([]); - const conversations = await conversationRepository.loadConversations([]); + expect(conversations).toHaveLength(remoteConversations.found.length); + }); - expect(conversations).toHaveLength(0); - expect(conversationService.deleteConversationFromDb).toHaveBeenCalledWith(connectionReq.qualified_id.id); - expect(conversationService.blacklistConversation).toHaveBeenCalledWith(connectionReq.qualified_id); - }); + it('does not load connection request (type 3) conversations if their users were deleted on backend', async () => { + const conversationRepository = testFactory.conversation_repository!; + const conversationService = conversationRepository['conversationService']; + const userId = {id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', domain: 'staging.zinfra.io'}; - it('keeps track of missing conversations', async () => { - const conversationRepository = testFactory.conversation_repository!; - const conversationService = conversationRepository['conversationService']; - const conversationState = conversationRepository['conversationState']; - const remoteConversations = { - found: [ - generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', - }, - 'conv1', - ), - ], - failed: [ - generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', - }, - 'conv2', - ), + const connectionReq = generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', + }, + 'conv2', + [userId], + ConversationProtocol.PROTEUS, + CONVERSATION_TYPE.CONNECT, + ); - generateConversation( - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-5678-b6cb-602dac89fa1b', - }, - 'conv3', - ), - ], - }; + const remoteConversations = { + found: [connectionReq], + }; + const localConversations: any = [connectionReq]; - jest - .spyOn(conversationService, 'getAllConversations') - .mockResolvedValue(remoteConversations as unknown as RemoteConversations); + jest.spyOn(conversationService, 'deleteConversationFromDb'); + jest.spyOn(conversationService, 'blacklistConversation'); + jest + .spyOn(conversationService, 'getAllConversations') + .mockResolvedValue(remoteConversations as unknown as RemoteConversations); + jest + .spyOn(conversationService, 'loadConversationStatesFromDb') + .mockResolvedValue(localConversations as unknown as ConversationDatabaseData[]); + jest.spyOn(conversationService, 'saveConversationsInDb').mockImplementation(data => Promise.resolve(data)); - await conversationRepository.loadConversations([]); + const conversations = await conversationRepository.loadConversations([]); - expect(conversationState.missingConversations).toHaveLength(remoteConversations.failed.length); - }); + expect(conversations).toHaveLength(0); + expect(conversationService.deleteConversationFromDb).toHaveBeenCalledWith(connectionReq.qualified_id.id); + expect(conversationService.blacklistConversation).toHaveBeenCalledWith(connectionReq.qualified_id); }); - describe('loadMissingConversations', () => { - beforeEach(() => { - testFactory.conversation_repository!['conversationState'].conversations.removeAll(); - }); - it('make sure missing conversations are properly updated', async () => { - const conversationRepository = testFactory.conversation_repository!; - const conversationService = conversationRepository['conversationService']; - const conversationState = conversationRepository['conversationState']; + it('keeps track of missing conversations', async () => { + const conversationRepository = testFactory.conversation_repository!; + const conversationService = conversationRepository['conversationService']; + const conversationState = conversationRepository['conversationState']; + const remoteConversations = { + found: [ + generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', + }, + 'conv1', + ), + ], + failed: [ + generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-1234-b6cb-602dac89fa1b', + }, + 'conv2', + ), - const missingConversations = [ - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', - }, - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-40d7-1234-602dac89fa1b', - }, - { - domain: 'staging.zinfra.io', - id: '05d0f240-bfe9-40d7-5678-602dac89fa1b', - }, - ]; + generateConversation( + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-5678-b6cb-602dac89fa1b', + }, + 'conv3', + ), + ], + }; - const remoteConversations = { - found: [generateConversation(missingConversations[0], 'conv1')], - failed: [ - generateConversation(missingConversations[1], 'conv2').qualified_id, - generateConversation(missingConversations[2], 'conv3').qualified_id, - ], - }; + jest + .spyOn(conversationService, 'getAllConversations') + .mockResolvedValue(remoteConversations as unknown as RemoteConversations); - jest.replaceProperty(conversationState, 'missingConversations', missingConversations as any); - jest - .spyOn(conversationService, 'getConversationByIds') - .mockResolvedValue(remoteConversations as unknown as RemoteConversations); + await conversationRepository.loadConversations([]); - expect(conversationState.missingConversations).toHaveLength(missingConversations.length); + expect(conversationState.missingConversations).toHaveLength(remoteConversations.failed.length); + }); + }); + describe('loadMissingConversations', () => { + beforeEach(() => { + testFactory.conversation_repository!['conversationState'].conversations.removeAll(); + }); - await conversationRepository.loadMissingConversations(); + it('make sure missing conversations are properly updated', async () => { + const conversationRepository = testFactory.conversation_repository!; + const conversationService = conversationRepository['conversationService']; + const conversationState = conversationRepository['conversationState']; - expect(conversationState.missingConversations).toHaveLength(remoteConversations.failed.length); - }); + const missingConversations = [ + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-40d7-b6cb-602dac89fa1b', + }, + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-40d7-1234-602dac89fa1b', + }, + { + domain: 'staging.zinfra.io', + id: '05d0f240-bfe9-40d7-5678-602dac89fa1b', + }, + ]; + + const remoteConversations = { + found: [generateConversation(missingConversations[0], 'conv1')], + failed: [ + generateConversation(missingConversations[1], 'conv2').qualified_id, + generateConversation(missingConversations[2], 'conv3').qualified_id, + ], + }; + + jest.replaceProperty(conversationState, 'missingConversations', missingConversations as any); + jest + .spyOn(conversationService, 'getConversationByIds') + .mockResolvedValue(remoteConversations as unknown as RemoteConversations); + + expect(conversationState.missingConversations).toHaveLength(missingConversations.length); + + await conversationRepository.loadMissingConversations(); + + expect(conversationState.missingConversations).toHaveLength(remoteConversations.failed.length); }); + }); - describe('refreshUnavailableParticipants', () => { - it('should refresh unavailable users', async () => { - const conversation = _generateConversation(); - const unavailableUsers = [generateUser(), generateUser(), generateUser()].map(user => { - user.id = ''; - user.name(''); - return user; - }); + describe('refreshUnavailableParticipants', () => { + it('should refresh unavailable users', async () => { + const conversation = _generateConversation(); + const unavailableUsers = [generateUser(), generateUser(), generateUser()].map(user => { + user.id = ''; + user.name(''); + return user; + }); - conversation.participating_user_ets.push(unavailableUsers[0], unavailableUsers[1], unavailableUsers[2]); + conversation.participating_user_ets.push(unavailableUsers[0], unavailableUsers[1], unavailableUsers[2]); - const conversationRepo = await testFactory.exposeConversationActors(); - spyOn(testFactory.user_repository!, 'refreshUsers').and.callFake(() => { - unavailableUsers.map(user => { - user.id = createUuid(); - user.name(faker.person.fullName()); - }); + const conversationRepo = await testFactory.exposeConversationActors(); + spyOn(testFactory.user_repository!, 'refreshUsers').and.callFake(() => { + unavailableUsers.map(user => { + user.id = createUuid(); + user.name(faker.person.fullName()); }); + }); - await conversationRepo.refreshUnavailableParticipants(conversation); + await conversationRepo.refreshUnavailableParticipants(conversation); - expect(testFactory.user_repository!.refreshUsers).toHaveBeenCalled(); - expect(unavailableUsers[0].name).toBeTruthy(); - expect(unavailableUsers[1].name).toBeTruthy(); - expect(unavailableUsers[2].name).toBeTruthy(); - }); + expect(testFactory.user_repository!.refreshUsers).toHaveBeenCalled(); + expect(unavailableUsers[0].name).toBeTruthy(); + expect(unavailableUsers[1].name).toBeTruthy(); + expect(unavailableUsers[2].name).toBeTruthy(); }); + }); - describe('refreshAllConversationsUnavailableParticipants', () => { - it('should refresh all unavailable users & conversations', async () => { - const conversation1 = _generateConversation(); - const conversation2 = _generateConversation(); - const unavailableUsers1 = [generateUser(), generateUser(), generateUser()].map(user => { - user.id = ''; - user.name(''); - return user; - }); - const unavailableUsers2 = [generateUser(), generateUser(), generateUser()].map(user => { - user.id = ''; - user.name(''); - return user; - }); + describe('refreshAllConversationsUnavailableParticipants', () => { + it('should refresh all unavailable users & conversations', async () => { + const conversation1 = _generateConversation(); + const conversation2 = _generateConversation(); + const unavailableUsers1 = [generateUser(), generateUser(), generateUser()].map(user => { + user.id = ''; + user.name(''); + return user; + }); + const unavailableUsers2 = [generateUser(), generateUser(), generateUser()].map(user => { + user.id = ''; + user.name(''); + return user; + }); - conversation1.participating_user_ets.push(unavailableUsers1[0], unavailableUsers1[1], unavailableUsers1[2]); - conversation2.participating_user_ets.push(unavailableUsers2[0], unavailableUsers2[1], unavailableUsers2[2]); + conversation1.participating_user_ets.push(unavailableUsers1[0], unavailableUsers1[1], unavailableUsers1[2]); + conversation2.participating_user_ets.push(unavailableUsers2[0], unavailableUsers2[1], unavailableUsers2[2]); - const conversationRepo = await testFactory.exposeConversationActors(); - testFactory.conversation_repository!['conversationState'].conversations([conversation1, conversation2]); + const conversationRepo = await testFactory.exposeConversationActors(); + testFactory.conversation_repository!['conversationState'].conversations([conversation1, conversation2]); - spyOn(testFactory.user_repository!, 'refreshUsers').and.callFake(() => { - unavailableUsers1.map(user => { - user.id = createUuid(); - user.name(faker.person.fullName()); - }); - unavailableUsers2.map(user => { - user.id = createUuid(); - user.name(faker.person.fullName()); - }); + spyOn(testFactory.user_repository!, 'refreshUsers').and.callFake(() => { + unavailableUsers1.map(user => { + user.id = createUuid(); + user.name(faker.person.fullName()); + }); + unavailableUsers2.map(user => { + user.id = createUuid(); + user.name(faker.person.fullName()); }); + }); - await conversationRepo['refreshAllConversationsUnavailableParticipants'](); + await conversationRepo['refreshAllConversationsUnavailableParticipants'](); - expect(testFactory.user_repository!.refreshUsers).toHaveBeenCalled(); - expect(unavailableUsers1[0].name).toBeTruthy(); - expect(unavailableUsers1[1].name).toBeTruthy(); - expect(unavailableUsers1[2].name).toBeTruthy(); - expect(unavailableUsers2[0].name).toBeTruthy(); - expect(unavailableUsers2[1].name).toBeTruthy(); - expect(unavailableUsers2[2].name).toBeTruthy(); - }); + expect(testFactory.user_repository!.refreshUsers).toHaveBeenCalled(); + expect(unavailableUsers1[0].name).toBeTruthy(); + expect(unavailableUsers1[1].name).toBeTruthy(); + expect(unavailableUsers1[2].name).toBeTruthy(); + expect(unavailableUsers2[0].name).toBeTruthy(); + expect(unavailableUsers2[1].name).toBeTruthy(); + expect(unavailableUsers2[2].name).toBeTruthy(); }); + }); - describe('scheduleMissingUsersAndConversationsMetadataRefresh', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); + describe('scheduleMissingUsersAndConversationsMetadataRefresh', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); - afterAll(() => { - jest.useRealTimers(); - }); + afterAll(() => { + jest.useRealTimers(); + }); + + it('should not call loadMissingConversations & refreshAllConversationsUnavailableParticipants for non federated envs', async () => { + const conversationRepo = await testFactory.exposeConversationActors(); - it('should not call loadMissingConversations & refreshAllConversationsUnavailableParticipants for non federated envs', async () => { - const conversationRepo = await testFactory.exposeConversationActors(); + spyOn(conversationRepo, 'loadMissingConversations').and.callThrough(); + spyOn( + conversationRepo, + 'refreshAllConversationsUnavailableParticipants' as keyof ConversationRepository, + ).and.callThrough(); - spyOn(conversationRepo, 'loadMissingConversations').and.callThrough(); - spyOn( - conversationRepo, - 'refreshAllConversationsUnavailableParticipants' as keyof ConversationRepository, - ).and.callThrough(); + expect(conversationRepo.loadMissingConversations).not.toHaveBeenCalled(); + expect(conversationRepo['refreshAllConversationsUnavailableParticipants']).not.toHaveBeenCalled(); + }); - expect(conversationRepo.loadMissingConversations).not.toHaveBeenCalled(); - expect(conversationRepo['refreshAllConversationsUnavailableParticipants']).not.toHaveBeenCalled(); + it('should call loadMissingConversations & refreshAllConversationsUnavailableParticipants every 3 hours for federated envs', async () => { + Object.defineProperty(container.resolve(Core).backendFeatures, 'isFederated', { + get: jest.fn(() => true), + configurable: true, }); + const conversationRepo = await testFactory.exposeConversationActors(); - it('should call loadMissingConversations & refreshAllConversationsUnavailableParticipants every 3 hours for federated envs', async () => { - Object.defineProperty(container.resolve(Core).backendFeatures, 'isFederated', { - get: jest.fn(() => true), - configurable: true, - }); - const conversationRepo = await testFactory.exposeConversationActors(); + spyOn(conversationRepo, 'loadMissingConversations').and.callThrough(); + spyOn( + conversationRepo, + 'refreshAllConversationsUnavailableParticipants' as keyof ConversationRepository, + ).and.callThrough(); - spyOn(conversationRepo, 'loadMissingConversations').and.callThrough(); - spyOn( - conversationRepo, - 'refreshAllConversationsUnavailableParticipants' as keyof ConversationRepository, - ).and.callThrough(); + jest.advanceTimersByTime(3600000 * 4); - jest.advanceTimersByTime(3600000 * 4); + await Promise.resolve(); - await Promise.resolve(); + expect(conversationRepo.loadMissingConversations).toHaveBeenCalled(); + expect(conversationRepo['refreshAllConversationsUnavailableParticipants']).toHaveBeenCalled(); + }); + }); - expect(conversationRepo.loadMissingConversations).toHaveBeenCalled(); - expect(conversationRepo['refreshAllConversationsUnavailableParticipants']).toHaveBeenCalled(); - }); + describe('updateConversationProtocol', () => { + afterEach(() => { + jest.clearAllMocks(); }); - describe('updateConversationProtocol', () => { - afterEach(() => { - jest.clearAllMocks(); + it('should update the protocol-related fields after protocol was updated to mixed and inject event', async () => { + const conversation = _generateConversation(); + const conversationRepository = await testFactory.exposeConversationActors(); + + const mockedProtocolUpdateEventResponse = { + data: { + protocol: ConversationProtocol.MIXED, + }, + qualified_conversation: { + domain: 'anta.wire.link', + id: 'fb1c0e0f-60a9-4a6c-9644-041260e7aac9', + }, + time: '2020-10-13T14:00:00.000Z', + type: CONVERSATION_EVENT.PROTOCOL_UPDATE, + } as ConversationProtocolUpdateEvent; + + jest + .spyOn(conversationRepository['conversationService'], 'updateConversationProtocol') + .mockResolvedValueOnce(mockedProtocolUpdateEventResponse); + + const newProtocol = ConversationProtocol.MIXED; + const newCipherSuite = 1; + const newEpoch = 2; + const mockedConversationResponse = generateAPIConversation({ + protocol: newProtocol, + overwites: {cipher_suite: newCipherSuite, epoch: newEpoch}, }); - it('should update the protocol-related fields after protocol was updated to mixed and inject event', async () => { - const conversation = _generateConversation(); - const conversationRepository = await testFactory.exposeConversationActors(); + jest + .spyOn(conversationRepository['conversationService'], 'getConversationById') + .mockResolvedValueOnce(mockedConversationResponse); - const mockedProtocolUpdateEventResponse = { - data: { - protocol: ConversationProtocol.MIXED, - }, - qualified_conversation: { - domain: 'anta.wire.link', - id: 'fb1c0e0f-60a9-4a6c-9644-041260e7aac9', - }, - time: '2020-10-13T14:00:00.000Z', - type: CONVERSATION_EVENT.PROTOCOL_UPDATE, - } as ConversationProtocolUpdateEvent; + jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockResolvedValueOnce(undefined); - jest - .spyOn(conversationRepository['conversationService'], 'updateConversationProtocol') - .mockResolvedValueOnce(mockedProtocolUpdateEventResponse); + const updatedConversation = await conversationRepository.updateConversationProtocol( + conversation, + ConversationProtocol.MIXED, + ); - const newProtocol = ConversationProtocol.MIXED; - const newCipherSuite = 1; - const newEpoch = 2; - const mockedConversationResponse = generateAPIConversation({ - protocol: newProtocol, - overwites: {cipher_suite: newCipherSuite, epoch: newEpoch}, - }); + expect(conversationRepository['eventRepository'].injectEvent).toHaveBeenCalledWith( + mockedProtocolUpdateEventResponse, + EventRepository.SOURCE.BACKEND_RESPONSE, + ); - jest - .spyOn(conversationRepository['conversationService'], 'getConversationById') - .mockResolvedValueOnce(mockedConversationResponse); + expect(updatedConversation.protocol).toEqual(ConversationProtocol.MIXED); + expect(updatedConversation.cipherSuite).toEqual(newCipherSuite); + expect(updatedConversation.epoch).toEqual(newEpoch); + }); - jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockResolvedValueOnce(undefined); + it('should inject a system message if conversation protocol changed to mls during a call', async () => { + jest.useFakeTimers(); + const conversation = _generateConversation(); + const selfUser = generateUser(); + conversation.selfUser(selfUser); + const conversationRepository = await testFactory.exposeConversationActors(); + const newProtocol = ConversationProtocol.MLS; - const updatedConversation = await conversationRepository.updateConversationProtocol( - conversation, - ConversationProtocol.MIXED, - ); + const mockedProtocolUpdateEventResponse = { + data: { + protocol: newProtocol, + }, + qualified_conversation: conversation.qualifiedId, + time: '2020-10-13T14:00:00.000Z', + type: CONVERSATION_EVENT.PROTOCOL_UPDATE, + } as ConversationProtocolUpdateEvent; - expect(conversationRepository['eventRepository'].injectEvent).toHaveBeenCalledWith( - mockedProtocolUpdateEventResponse, - EventRepository.SOURCE.BACKEND_RESPONSE, - ); + jest + .spyOn(conversationRepository['conversationService'], 'updateConversationProtocol') + .mockResolvedValueOnce(mockedProtocolUpdateEventResponse); - expect(updatedConversation.protocol).toEqual(ConversationProtocol.MIXED); - expect(updatedConversation.cipherSuite).toEqual(newCipherSuite); - expect(updatedConversation.epoch).toEqual(newEpoch); + const newCipherSuite = 1; + const newEpoch = 2; + const mockedConversationResponse = generateAPIConversation({ + protocol: newProtocol, + overwites: {cipher_suite: newCipherSuite, epoch: newEpoch}, }); + jest + .spyOn(conversationRepository['conversationService'], 'getConversationById') + .mockResolvedValueOnce(mockedConversationResponse); - it('should inject a system message if conversation protocol changed to mls during a call', async () => { - jest.useFakeTimers(); - const conversation = _generateConversation(); - const selfUser = generateUser(); - conversation.selfUser(selfUser); - const conversationRepository = await testFactory.exposeConversationActors(); - const newProtocol = ConversationProtocol.MLS; + const injectEventMock = jest + .spyOn(conversationRepository['eventRepository'], 'injectEvent') + .mockImplementation(jest.fn()); + jest + .spyOn(conversationRepository['callingRepository'], 'findCall') + .mockReturnValue({isActive: () => true} as any); - const mockedProtocolUpdateEventResponse = { - data: { - protocol: newProtocol, - }, - qualified_conversation: conversation.qualifiedId, - time: '2020-10-13T14:00:00.000Z', - type: CONVERSATION_EVENT.PROTOCOL_UPDATE, - } as ConversationProtocolUpdateEvent; + await conversationRepository.updateConversationProtocol(conversation, newProtocol); - jest - .spyOn(conversationRepository['conversationService'], 'updateConversationProtocol') - .mockResolvedValueOnce(mockedProtocolUpdateEventResponse); + expect(injectEventMock.mock.calls).toEqual([ + [mockedProtocolUpdateEventResponse, EventRepository.SOURCE.BACKEND_RESPONSE], + [expect.objectContaining({type: ClientEvent.CONVERSATION.MLS_MIGRATION_ONGOING_CALL})], + ]); + }); - const newCipherSuite = 1; - const newEpoch = 2; - const mockedConversationResponse = generateAPIConversation({ - protocol: newProtocol, - overwites: {cipher_suite: newCipherSuite, epoch: newEpoch}, - }); - jest - .spyOn(conversationRepository['conversationService'], 'getConversationById') - .mockResolvedValueOnce(mockedConversationResponse); + it("should NOT inject a system message if conversation protocol changed to mls if we're not atively participating in a call", async () => { + jest.useFakeTimers(); + const conversation = _generateConversation(); + const selfUser = generateUser(); + conversation.selfUser(selfUser); + const conversationRepository = await testFactory.exposeConversationActors(); + const newProtocol = ConversationProtocol.MLS; - const injectEventMock = jest - .spyOn(conversationRepository['eventRepository'], 'injectEvent') - .mockImplementation(jest.fn()); - jest - .spyOn(conversationRepository['callingRepository'], 'findCall') - .mockReturnValue({isActive: () => true} as any); + const mockedProtocolUpdateEventResponse = { + data: { + protocol: newProtocol, + }, + qualified_conversation: conversation.qualifiedId, + time: '2020-10-13T14:00:00.000Z', + type: CONVERSATION_EVENT.PROTOCOL_UPDATE, + } as ConversationProtocolUpdateEvent; - await conversationRepository.updateConversationProtocol(conversation, newProtocol); + jest + .spyOn(conversationRepository['conversationService'], 'updateConversationProtocol') + .mockResolvedValueOnce(mockedProtocolUpdateEventResponse); - expect(injectEventMock.mock.calls).toEqual([ - [mockedProtocolUpdateEventResponse, EventRepository.SOURCE.BACKEND_RESPONSE], - [expect.objectContaining({type: ClientEvent.CONVERSATION.MLS_MIGRATION_ONGOING_CALL})], - ]); + const newCipherSuite = 1; + const newEpoch = 2; + const mockedConversationResponse = generateAPIConversation({ + protocol: newProtocol, + overwites: {cipher_suite: newCipherSuite, epoch: newEpoch}, }); + jest + .spyOn(conversationRepository['conversationService'], 'getConversationById') + .mockResolvedValueOnce(mockedConversationResponse); - it("should NOT inject a system message if conversation protocol changed to mls if we're not atively participating in a call", async () => { - jest.useFakeTimers(); - const conversation = _generateConversation(); - const selfUser = generateUser(); - conversation.selfUser(selfUser); - const conversationRepository = await testFactory.exposeConversationActors(); - const newProtocol = ConversationProtocol.MLS; + const injectEventMock = jest + .spyOn(conversationRepository['eventRepository'], 'injectEvent') + .mockImplementation(jest.fn()); + jest + .spyOn(conversationRepository['callingRepository'], 'findCall') + .mockReturnValue({isActive: () => false} as any); - const mockedProtocolUpdateEventResponse = { - data: { - protocol: newProtocol, - }, - qualified_conversation: conversation.qualifiedId, - time: '2020-10-13T14:00:00.000Z', - type: CONVERSATION_EVENT.PROTOCOL_UPDATE, - } as ConversationProtocolUpdateEvent; + await conversationRepository.updateConversationProtocol(conversation, newProtocol); - jest - .spyOn(conversationRepository['conversationService'], 'updateConversationProtocol') - .mockResolvedValueOnce(mockedProtocolUpdateEventResponse); + expect(injectEventMock).toHaveBeenCalledWith( + mockedProtocolUpdateEventResponse, + EventRepository.SOURCE.BACKEND_RESPONSE, + ); - const newCipherSuite = 1; - const newEpoch = 2; - const mockedConversationResponse = generateAPIConversation({ - protocol: newProtocol, - overwites: {cipher_suite: newCipherSuite, epoch: newEpoch}, - }); - jest - .spyOn(conversationRepository['conversationService'], 'getConversationById') - .mockResolvedValueOnce(mockedConversationResponse); + expect(injectEventMock).toHaveBeenCalledTimes(1); + }); + }); - const injectEventMock = jest - .spyOn(conversationRepository['eventRepository'], 'injectEvent') - .mockImplementation(jest.fn()); - jest - .spyOn(conversationRepository['callingRepository'], 'findCall') - .mockReturnValue({isActive: () => false} as any); + describe('addUsers', () => { + it('should add users to proteus conversation', async () => { + const conversation = _generateConversation(); + const conversationRepository = await testFactory.exposeConversationActors(); - await conversationRepository.updateConversationProtocol(conversation, newProtocol); + const usersToAdd = [generateUser(), generateUser()]; - expect(injectEventMock).toHaveBeenCalledWith( - mockedProtocolUpdateEventResponse, - EventRepository.SOURCE.BACKEND_RESPONSE, - ); + const coreConversationService = container.resolve(Core).service!.conversation; + spyOn(coreConversationService, 'addUsersToProteusConversation'); - expect(injectEventMock).toHaveBeenCalledTimes(1); + await conversationRepository.addUsers(conversation, usersToAdd); + expect(coreConversationService.addUsersToProteusConversation).toHaveBeenCalledWith({ + conversationId: conversation.qualifiedId, + qualifiedUsers: usersToAdd.map(user => user.qualifiedId), }); }); - describe('addUsers', () => { - it('should add users to proteus conversation', async () => { - const conversation = _generateConversation(); - const conversationRepository = await testFactory.exposeConversationActors(); + it('should add users to mls group of mixed conversation', async () => { + const mockedGroupId = `mockedGroupId`; + const conversation = _generateConversation({ + protocol: ConversationProtocol.MIXED, + groupId: mockedGroupId, + }); + const conversationRepository = await testFactory.exposeConversationActors(); - const usersToAdd = [generateUser(), generateUser()]; + const usersToAdd = [generateUser(), generateUser()]; - const coreConversationService = container.resolve(Core).service!.conversation; - spyOn(coreConversationService, 'addUsersToProteusConversation'); + const coreConversationService = container.resolve(Core).service!.conversation; + jest.spyOn(coreConversationService, 'addUsersToMLSConversation'); + jest.spyOn(coreConversationService, 'addUsersToProteusConversation').mockResolvedValueOnce({}); - await conversationRepository.addUsers(conversation, usersToAdd); - expect(coreConversationService.addUsersToProteusConversation).toHaveBeenCalledWith({ - conversationId: conversation.qualifiedId, - qualifiedUsers: usersToAdd.map(user => user.qualifiedId), - }); + await conversationRepository.addUsers(conversation, usersToAdd); + expect(coreConversationService.addUsersToProteusConversation).toHaveBeenCalledWith({ + conversationId: conversation.qualifiedId, + qualifiedUsers: usersToAdd.map(user => user.qualifiedId), }); + expect(coreConversationService.addUsersToMLSConversation).toHaveBeenCalledWith({ + conversationId: conversation.qualifiedId, + qualifiedUsers: usersToAdd.map(user => user.qualifiedId), + groupId: mockedGroupId, + }); + }); - it('should add users to mls group of mixed conversation', async () => { - const mockedGroupId = `mockedGroupId`; - const conversation = _generateConversation({ - protocol: ConversationProtocol.MIXED, - groupId: mockedGroupId, - }); - const conversationRepository = await testFactory.exposeConversationActors(); + it('should add users to mls group of mls conversation', async () => { + const mockedGroupId = `mockedGroupId`; + const conversation = _generateConversation({protocol: ConversationProtocol.MLS, groupId: mockedGroupId}); + const conversationRepository = await testFactory.exposeConversationActors(); - const usersToAdd = [generateUser(), generateUser()]; + const usersToAdd = [generateUser(), generateUser()]; - const coreConversationService = container.resolve(Core).service!.conversation; - jest.spyOn(coreConversationService, 'addUsersToMLSConversation'); - jest.spyOn(coreConversationService, 'addUsersToProteusConversation').mockResolvedValueOnce({}); + const coreConversationService = container.resolve(Core).service!.conversation; + spyOn(coreConversationService, 'addUsersToMLSConversation'); - await conversationRepository.addUsers(conversation, usersToAdd); - expect(coreConversationService.addUsersToProteusConversation).toHaveBeenCalledWith({ - conversationId: conversation.qualifiedId, - qualifiedUsers: usersToAdd.map(user => user.qualifiedId), - }); - expect(coreConversationService.addUsersToMLSConversation).toHaveBeenCalledWith({ - conversationId: conversation.qualifiedId, - qualifiedUsers: usersToAdd.map(user => user.qualifiedId), - groupId: mockedGroupId, - }); + await conversationRepository.addUsers(conversation, usersToAdd); + expect(coreConversationService.addUsersToMLSConversation).toHaveBeenCalledWith({ + conversationId: conversation.qualifiedId, + qualifiedUsers: usersToAdd.map(user => user.qualifiedId), + groupId: mockedGroupId, }); + }); + }); - it('should add users to mls group of mls conversation', async () => { - const mockedGroupId = `mockedGroupId`; - const conversation = _generateConversation({protocol: ConversationProtocol.MLS, groupId: mockedGroupId}); + describe('removeMembers', () => { + it.each([ConversationProtocol.PROTEUS, ConversationProtocol.MIXED])( + 'should remove member from %s conversation', + async protocol => { const conversationRepository = await testFactory.exposeConversationActors(); - const usersToAdd = [generateUser(), generateUser()]; + const conversation = _generateConversation({protocol}); + + const selfUser = generateUser(); + conversation.selfUser(selfUser); + + const user1 = generateUser(); + const user2 = generateUser(); + + conversation.participating_user_ets([user1, user2]); const coreConversationService = container.resolve(Core).service!.conversation; - spyOn(coreConversationService, 'addUsersToMLSConversation'); - await conversationRepository.addUsers(conversation, usersToAdd); - expect(coreConversationService.addUsersToMLSConversation).toHaveBeenCalledWith({ - conversationId: conversation.qualifiedId, - qualifiedUsers: usersToAdd.map(user => user.qualifiedId), - groupId: mockedGroupId, - }); - }); - }); + jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockImplementation(jest.fn()); - describe('removeMembers', () => { - it.each([ConversationProtocol.PROTEUS, ConversationProtocol.MIXED])( - 'should remove member from %s conversation', - async protocol => { - const conversationRepository = await testFactory.exposeConversationActors(); + await conversationRepository.removeMembers(conversation, [user1.qualifiedId]); - const conversation = _generateConversation({protocol}); + expect(coreConversationService.removeUserFromConversation).toHaveBeenCalledWith( + conversation.qualifiedId, + user1.qualifiedId, + ); + }, + ); - const selfUser = generateUser(); - conversation.selfUser(selfUser); + it('should remove member from mls conversation', async () => { + const conversationRepository = await testFactory.exposeConversationActors(); - const user1 = generateUser(); - const user2 = generateUser(); + const conversation = _generateConversation({protocol: ConversationProtocol.MLS}); - conversation.participating_user_ets([user1, user2]); + const selfUser = generateUser(); + conversation.selfUser(selfUser); - const coreConversationService = container.resolve(Core).service!.conversation; + const user1 = generateUser(); + const user2 = generateUser(); - jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockImplementation(jest.fn()); + conversation.participating_user_ets([user1, user2]); - await conversationRepository.removeMembers(conversation, [user1.qualifiedId]); + const coreConversationService = container.resolve(Core).service!.conversation; - expect(coreConversationService.removeUserFromConversation).toHaveBeenCalledWith( - conversation.qualifiedId, - user1.qualifiedId, - ); - }, - ); + jest + .spyOn(coreConversationService, 'removeUsersFromMLSConversation') + .mockResolvedValueOnce({events: [], conversation: {} as BackendConversation}); + jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockImplementation(jest.fn()); + + const mockedMemberLeaveEvent: ConversationMemberLeaveEvent = { + conversation: conversation.id, + data: {qualified_user_ids: [], user_ids: []}, + from: '', + time: '', + type: CONVERSATION_EVENT.MEMBER_LEAVE, + }; + jest + .spyOn(coreConversationService, 'removeUsersFromMLSConversation') + .mockResolvedValueOnce({events: [mockedMemberLeaveEvent], conversation: {} as BackendConversation}); + await conversationRepository.removeMembers(conversation, [user1.qualifiedId]); + + expect(coreConversationService.removeUsersFromMLSConversation).toHaveBeenCalledWith({ + conversationId: conversation.qualifiedId, + qualifiedUserIds: [user1.qualifiedId], + groupId: conversation.groupId, + }); + }); + }); - it('should remove member from mls conversation', async () => { + describe('leaveConversation', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it.each([ConversationProtocol.PROTEUS, ConversationProtocol.MIXED, ConversationProtocol.MLS])( + 'should leave %s conversation', + async protocol => { const conversationRepository = await testFactory.exposeConversationActors(); - const conversation = _generateConversation({protocol: ConversationProtocol.MLS}); + const conversation = _generateConversation({protocol}); const selfUser = generateUser(); conversation.selfUser(selfUser); - const user1 = generateUser(); - const user2 = generateUser(); + spyOn(conversationRepository['userState'], 'self').and.returnValue(selfUser); - conversation.participating_user_ets([user1, user2]); + conversation.participating_user_ets([generateUser(), generateUser()]); const coreConversationService = container.resolve(Core).service!.conversation; - jest - .spyOn(coreConversationService, 'removeUsersFromMLSConversation') - .mockResolvedValueOnce({events: [], conversation: {} as BackendConversation}); jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockImplementation(jest.fn()); - const mockedMemberLeaveEvent: ConversationMemberLeaveEvent = { - conversation: conversation.id, - data: {qualified_user_ids: [], user_ids: []}, - from: '', - time: '', - type: CONVERSATION_EVENT.MEMBER_LEAVE, - }; - jest - .spyOn(coreConversationService, 'removeUsersFromMLSConversation') - .mockResolvedValueOnce({events: [mockedMemberLeaveEvent], conversation: {} as BackendConversation}); - await conversationRepository.removeMembers(conversation, [user1.qualifiedId]); + await conversationRepository.leaveConversation(conversation); - expect(coreConversationService.removeUsersFromMLSConversation).toHaveBeenCalledWith({ - conversationId: conversation.qualifiedId, - qualifiedUserIds: [user1.qualifiedId], - groupId: conversation.groupId, - }); - }); - }); + expect(coreConversationService.removeUserFromConversation).toHaveBeenCalledWith( + conversation.qualifiedId, + selfUser.qualifiedId, + ); + expect(conversationRepository['eventRepository'].injectEvent).toHaveBeenCalled(); + }, + ); + }); - describe('leaveConversation', () => { - afterEach(() => { - jest.clearAllMocks(); - }); + describe('deleteConversation', () => { + it('should delete conversation on backend and locally', async () => { + const conversationRepository = await testFactory.exposeConversationActors(); + const teamId = createUuid(); - it.each([ConversationProtocol.PROTEUS, ConversationProtocol.MIXED, ConversationProtocol.MLS])( - 'should leave %s conversation', - async protocol => { - const conversationRepository = await testFactory.exposeConversationActors(); + spyOn(conversationRepository['teamState'], 'team').and.returnValue({id: teamId} as any); - const conversation = _generateConversation({protocol}); + const conversation = _generateConversation({protocol: ConversationProtocol.MLS}); + conversationRepository['conversationState'].conversations.push(conversation); - const selfUser = generateUser(); - conversation.selfUser(selfUser); + jest.spyOn(conversationRepository['conversationService'], 'deleteConversation'); + jest.spyOn(conversationRepository['conversationService'], 'deleteConversationFromDb'); + jest.spyOn(conversationRepository['conversationService'], 'wipeMLSCapableConversation'); - spyOn(conversationRepository['userState'], 'self').and.returnValue(selfUser); + await conversationRepository.deleteConversation(conversation); - conversation.participating_user_ets([generateUser(), generateUser()]); + expect(conversationRepository['conversationService'].deleteConversation).toHaveBeenCalledWith( + teamId, + conversation.id, + ); - const coreConversationService = container.resolve(Core).service!.conversation; + expect(conversationRepository['conversationState'].conversations()).toEqual([]); + expect(conversationRepository['conversationService'].deleteConversationFromDb).toHaveBeenCalledWith( + conversation.id, + ); + expect(conversationRepository['conversationService'].wipeMLSCapableConversation).toHaveBeenCalledWith( + conversation, + ); + }); - jest.spyOn(conversationRepository['eventRepository'], 'injectEvent').mockImplementation(jest.fn()); + it('should still delete conversation locally if it is deleted on backend already', async () => { + const conversationRepository = await testFactory.exposeConversationActors(); + const teamId = createUuid(); - await conversationRepository.leaveConversation(conversation); + spyOn(conversationRepository['teamState'], 'team').and.returnValue({id: teamId} as any); - expect(coreConversationService.removeUserFromConversation).toHaveBeenCalledWith( - conversation.qualifiedId, - selfUser.qualifiedId, - ); - expect(conversationRepository['eventRepository'].injectEvent).toHaveBeenCalled(); - }, + const conversation = _generateConversation({protocol: ConversationProtocol.MLS}); + conversationRepository['conversationState'].conversations.push(conversation); + + jest + .spyOn(conversationRepository['conversationService'], 'deleteConversation') + .mockRejectedValueOnce(new BackendError('Conversation not found', BackendErrorLabel.NO_CONVERSATION)); + jest.spyOn(conversationRepository['conversationService'], 'deleteConversationFromDb'); + jest.spyOn(conversationRepository['conversationService'], 'wipeMLSCapableConversation'); + + await conversationRepository.deleteConversation(conversation); + + expect(conversationRepository['conversationService'].deleteConversation).toHaveBeenCalledWith( + teamId, + conversation.id, + ); + + expect(conversationRepository['conversationState'].conversations()).toEqual([]); + expect(conversationRepository['conversationService'].deleteConversationFromDb).toHaveBeenCalledWith( + conversation.id, + ); + expect(conversationRepository['conversationService'].wipeMLSCapableConversation).toHaveBeenCalledWith( + conversation, ); }); }); diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts index 0feea69abc0..704bb22e175 100644 --- a/src/script/conversation/ConversationRepository.ts +++ b/src/script/conversation/ConversationRepository.ts @@ -1091,20 +1091,28 @@ export class ConversationRepository { }); } - public deleteConversation(conversationEntity: Conversation) { - this.conversationService - .deleteConversation(this.teamState.team().id, conversationEntity.id) - .then(() => { - this.deleteConversationLocally(conversationEntity, true); - }) - .catch(() => { - PrimaryModal.show(PrimaryModal.type.ACKNOWLEDGE, { - text: { - message: t('modalConversationDeleteErrorMessage', conversationEntity.name()), - title: t('modalConversationDeleteErrorHeadline'), - }, - }); + public async deleteConversation(conversationEntity: Conversation) { + const teamId = this.teamState.team().id; + if (!teamId) { + throw new Error('Team ID is missing'); + } + + try { + await this.conversationService.deleteConversation(teamId, conversationEntity.id); + return this.deleteConversationLocally(conversationEntity, true); + } catch (error) { + const isAlreadyDeletedOnBackend = isBackendError(error) && error.label === BackendErrorLabel.NO_CONVERSATION; + if (isAlreadyDeletedOnBackend) { + return this.deleteConversationLocally(conversationEntity, true); + } + + PrimaryModal.show(PrimaryModal.type.ACKNOWLEDGE, { + text: { + message: t('modalConversationDeleteErrorMessage', conversationEntity.name()), + title: t('modalConversationDeleteErrorHeadline'), + }, }); + } } private readonly deleteConversationLocally = async (conversationId: QualifiedId, skipNotification: boolean) => {