From 95c49f078ace1246d917dfcf043f3bd2c034ca11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20G=C3=B3rka?= Date: Mon, 18 Sep 2023 15:33:04 +0200 Subject: [PATCH] feat: reevaluate supported protocols on client delete (#15840) * feat: reevaluate self supported protocols on user client delete * test: deleteSelfUserClient * test: refreshSelfSupportedProtocols * refactor: self user getter in self repo --- src/__mocks__/@wireapp/core.ts | 4 ++ src/script/self/SelfRepository.test.ts | 79 ++++++++++++++++++++++- src/script/self/SelfRepository.ts | 33 ++++++++-- src/script/view_model/ActionsViewModel.ts | 9 +-- src/script/view_model/MainViewModel.ts | 2 +- 5 files changed, 113 insertions(+), 14 deletions(-) diff --git a/src/__mocks__/@wireapp/core.ts b/src/__mocks__/@wireapp/core.ts index 533f85edb78..f4d525a190e 100644 --- a/src/__mocks__/@wireapp/core.ts +++ b/src/__mocks__/@wireapp/core.ts @@ -55,5 +55,9 @@ export class Account extends EventEmitter { setConversationLevelTimer: jest.fn(), }, }, + + client: { + deleteClient: jest.fn(), + }, }; } diff --git a/src/script/self/SelfRepository.test.ts b/src/script/self/SelfRepository.test.ts index 7c62e0b22c8..7fe5c1e5311 100644 --- a/src/script/self/SelfRepository.test.ts +++ b/src/script/self/SelfRepository.test.ts @@ -21,12 +21,15 @@ import {RegisteredClient} from '@wireapp/api-client/lib/client'; import {ConversationProtocol} from '@wireapp/api-client/lib/conversation'; import {FeatureList, FeatureStatus} from '@wireapp/api-client/lib/team'; import {act} from 'react-dom/test-utils'; +import {container} from 'tsyringe'; import {TestFactory} from 'test/helper/TestFactory'; import {TIME_IN_MILLIS} from 'Util/TimeUtil'; +import {ClientEntity} from '../client'; import * as mlsSupport from '../mls/isMLSSupportedByEnvironment'; import {MLSMigrationStatus} from '../mls/MLSMigration/migrationStatus'; +import {Core} from '../service/CoreSingleton'; const testFactory = new TestFactory(); @@ -228,7 +231,7 @@ describe('SelfRepository', () => { ])('Updates the list of supported protocols', async (initialProtocols, evaluatedProtocols) => { const selfRepository = await testFactory.exposeSelfActors(); - const selfUser = selfRepository['userState'].self(); + const selfUser = selfRepository['userState'].self()!; selfUser.supportedProtocols(initialProtocols); @@ -246,7 +249,7 @@ describe('SelfRepository', () => { it("Does not update supported protocols if they didn't change", async () => { const selfRepository = await testFactory.exposeSelfActors(); - const selfUser = selfRepository['userState'].self(); + const selfUser = selfRepository['userState'].self()!; const initialProtocols = [ConversationProtocol.PROTEUS]; selfUser.supportedProtocols(initialProtocols); @@ -265,7 +268,7 @@ describe('SelfRepository', () => { it('Re-evaluates supported protocols every 24h', async () => { const selfRepository = await testFactory.exposeSelfActors(); - const selfUser = selfRepository['userState'].self(); + const selfUser = selfRepository['userState'].self()!; const initialProtocols = [ConversationProtocol.PROTEUS]; selfUser.supportedProtocols(initialProtocols); @@ -292,4 +295,74 @@ describe('SelfRepository', () => { expect(selfRepository['selfService'].putSupportedProtocols).toHaveBeenCalledWith(evaluatedProtocols2); }); }); + + describe('deleteSelfUserClient', () => { + it('deletes the self user client and refreshes self supported protocols', async () => { + const selfRepository = await testFactory.exposeSelfActors(); + + const selfUser = selfRepository['userState'].self()!; + + selfRepository['clientRepository'].init(selfUser); + + const client1 = new ClientEntity(true, null, 'client1'); + const client2 = new ClientEntity(true, null, 'client2'); + + const initialClients = [client1, client2]; + selfUser.devices(initialClients); + + const clientToDelete = initialClients[0]; + + jest.spyOn(container.resolve(Core).service?.client!, 'deleteClient'); + jest.spyOn(selfRepository, 'refreshSelfSupportedProtocols').mockImplementationOnce(jest.fn()); + + const expectedClients = [...initialClients].filter(client => client.id !== clientToDelete.id); + + await act(async () => { + await selfRepository.deleteSelfUserClient(clientToDelete.id); + }); + + expect(selfUser.devices()).toEqual(expectedClients); + expect(selfRepository.refreshSelfSupportedProtocols).toHaveBeenCalled(); + }); + }); + + describe('refreshSelfSupportedProtocols', () => { + it('refreshes self supported protocols and updates backend with the new list', async () => { + const selfRepository = await testFactory.exposeSelfActors(); + + const selfUser = selfRepository['userState'].self()!; + + const initialProtocols = [ConversationProtocol.PROTEUS]; + selfUser.supportedProtocols(initialProtocols); + + const evaluatedProtocols = [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]; + + jest.spyOn(selfRepository, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols); + jest.spyOn(selfRepository['selfService'], 'putSupportedProtocols'); + + await selfRepository.refreshSelfSupportedProtocols(); + + expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols); + expect(selfRepository['selfService'].putSupportedProtocols).toHaveBeenCalledWith(evaluatedProtocols); + }); + + it('does not update backend with supported protocols when not changed', async () => { + const selfRepository = await testFactory.exposeSelfActors(); + + const selfUser = selfRepository['userState'].self()!; + + const initialProtocols = [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]; + selfUser.supportedProtocols(initialProtocols); + + const evaluatedProtocols = [ConversationProtocol.PROTEUS, ConversationProtocol.MLS]; + + jest.spyOn(selfRepository, 'evaluateSelfSupportedProtocols').mockResolvedValueOnce(evaluatedProtocols); + jest.spyOn(selfRepository['selfService'], 'putSupportedProtocols'); + + await selfRepository.refreshSelfSupportedProtocols(); + + expect(selfUser.supportedProtocols()).toEqual(evaluatedProtocols); + expect(selfRepository['selfService'].putSupportedProtocols).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/script/self/SelfRepository.ts b/src/script/self/SelfRepository.ts index d7ae6da50a9..ff4f872cb2e 100644 --- a/src/script/self/SelfRepository.ts +++ b/src/script/self/SelfRepository.ts @@ -19,14 +19,17 @@ import {ConversationProtocol} from '@wireapp/api-client/lib/conversation'; import {registerRecurringTask} from '@wireapp/core/lib/util/RecurringTaskScheduler'; +import {amplify} from 'amplify'; import {container} from 'tsyringe'; +import {WebAppEvents} from '@wireapp/webapp-events'; + import {Logger, getLogger} from 'Util/Logger'; import {TIME_IN_MILLIS} from 'Util/TimeUtil'; import {SelfService} from './SelfService'; -import {ClientRepository} from '../client'; +import {ClientEntity, ClientRepository} from '../client'; import {isMLSSupportedByEnvironment} from '../mls/isMLSSupportedByEnvironment'; import {MLSMigrationStatus} from '../mls/MLSMigration/migrationStatus'; import {TeamRepository} from '../team/TeamRepository'; @@ -46,6 +49,18 @@ export class SelfRepository { private readonly userState = container.resolve(UserState), ) { this.logger = getLogger('SelfRepository'); + + // Every time user's client is deleted, we need to re-evaluate self supported protocols. + // It's possible that they have removed proteus client, and now all their clients are mls-capable. + amplify.subscribe(WebAppEvents.CLIENT.REMOVE, this.refreshSelfSupportedProtocols); + } + + private get selfUser() { + const selfUser = this.userState.self(); + if (!selfUser) { + throw new Error('Self user is not available'); + } + return selfUser; } /** @@ -141,7 +156,7 @@ export class SelfRepository { ): Promise { this.logger.info('Supported protocols will get updated to:', supportedProtocols); await this.selfService.putSupportedProtocols(supportedProtocols); - await this.userRepository.updateUserSupportedProtocols(this.userState.self().qualifiedId, supportedProtocols); + await this.userRepository.updateUserSupportedProtocols(this.selfUser.qualifiedId, supportedProtocols); return supportedProtocols; } @@ -150,9 +165,8 @@ export class SelfRepository { * It will send a request to the backend to change the supported protocols and then update the user in the local state. * @param supportedProtocols - an array of new supported protocols */ - public async refreshSelfSupportedProtocols(): Promise { - const selfUser = this.userState.self(); - const localSupportedProtocols = selfUser.supportedProtocols(); + public readonly refreshSelfSupportedProtocols = async (): Promise => { + const localSupportedProtocols = this.selfUser.supportedProtocols(); this.logger.info('Evaluating self supported protocols, currently supported protocols:', localSupportedProtocols); const refreshedSupportedProtocols = await this.evaluateSelfSupportedProtocols(); @@ -171,7 +185,7 @@ export class SelfRepository { } return this.updateSelfSupportedProtocols(refreshedSupportedProtocols); - } + }; /** * Will initialise the intervals for checking (and updating if necessary) self supported protocols. @@ -198,4 +212,11 @@ export class SelfRepository { key: SELF_SUPPORTED_PROTOCOLS_CHECK_KEY, }); } + + public async deleteSelfUserClient(clientId: string, password?: string): Promise { + const clients = this.clientRepository.deleteClient(clientId, password); + + await this.refreshSelfSupportedProtocols(); + return clients; + } } diff --git a/src/script/view_model/ActionsViewModel.ts b/src/script/view_model/ActionsViewModel.ts index 641c3fb8ec0..baefd7a7234 100644 --- a/src/script/view_model/ActionsViewModel.ts +++ b/src/script/view_model/ActionsViewModel.ts @@ -27,7 +27,7 @@ import {PrimaryModal, removeCurrentModal, usePrimaryModalState} from 'Components import {t} from 'Util/LocalizerUtil'; import {isBackendError} from 'Util/TypePredicateUtil'; -import type {ClientRepository, ClientEntity} from '../client'; +import type {ClientEntity} from '../client'; import type {ConnectionRepository} from '../connection/ConnectionRepository'; import type {ConversationRepository} from '../conversation/ConversationRepository'; import type {MessageRepository} from '../conversation/MessageRepository'; @@ -37,11 +37,12 @@ import type {Message} from '../entity/message/Message'; import type {User} from '../entity/User'; import type {IntegrationRepository} from '../integration/IntegrationRepository'; import type {ServiceEntity} from '../integration/ServiceEntity'; +import {SelfRepository} from '../self/SelfRepository'; import {UserState} from '../user/UserState'; export class ActionsViewModel { constructor( - private readonly clientRepository: ClientRepository, + private readonly selfRepository: SelfRepository, private readonly connectionRepository: ConnectionRepository, private readonly conversationRepository: ConversationRepository, private readonly integrationRepository: IntegrationRepository, @@ -152,7 +153,7 @@ export class ActionsViewModel { const isTemporary = clientEntity.isTemporary(); if (isSSO || isTemporary) { // Temporary clients and clients of SSO users don't require a password to be removed - return this.clientRepository.deleteClient(clientEntity.id, undefined); + return this.selfRepository.deleteSelfUserClient(clientEntity.id, undefined); } return new Promise(resolve => { @@ -171,7 +172,7 @@ export class ActionsViewModel { if (!isSending) { isSending = true; try { - await this.clientRepository.deleteClient(clientEntity.id, password); + await this.selfRepository.deleteSelfUserClient(clientEntity.id, password); removeCurrentModal(); resolve(); } catch (error) { diff --git a/src/script/view_model/MainViewModel.ts b/src/script/view_model/MainViewModel.ts index e89d1af22cf..db2eee30724 100644 --- a/src/script/view_model/MainViewModel.ts +++ b/src/script/view_model/MainViewModel.ts @@ -108,7 +108,7 @@ export class MainViewModel { }; this.actions = new ActionsViewModel( - repositories.client, + repositories.self, repositories.connection, repositories.conversation, repositories.integration,