/* * Wire * Copyright (C) 2019 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import {act, render} from '@testing-library/react'; import {ConnectionStatus} from '@wireapp/api-client/lib/connection/'; import {ConversationProtocol, CONVERSATION_TYPE} from '@wireapp/api-client/lib/conversation/'; import {ClientMLSError, ClientMLSErrorLabel} from '@wireapp/core/lib/messagingProtocols/mls'; import ko from 'knockout'; import {container} from 'tsyringe'; import {PrimaryModalComponent} from 'Components/Modals/PrimaryModal/PrimaryModal'; import {withTheme} from 'src/script/auth/util/test/TestUtil'; import {ConnectionEntity} from 'src/script/connection/ConnectionEntity'; import {ConversationRoleRepository} from 'src/script/conversation/ConversationRoleRepository'; import {ConversationState} from 'src/script/conversation/ConversationState'; import {Conversation} from 'src/script/entity/Conversation'; import {User} from 'src/script/entity/User'; import {TeamEntity} from 'src/script/team/TeamEntity'; import {TeamState} from 'src/script/team/TeamState'; import {UserState} from 'src/script/user/UserState'; import {ActionsViewModel} from 'src/script/view_model/ActionsViewModel'; import {noop} from 'Util/util'; import {ActionIdentifier, Actions, UserActions} from './UserActions'; const actionsViewModel = { open1to1Conversation: jest.fn(), getOrCreate1to1Conversation: jest.fn(), } as unknown as ActionsViewModel; const getAllActions = (queryFunction: (id: string) => HTMLElement | null) => Object.values(ActionIdentifier) .map(action => queryFunction(action)) .filter(action => action !== null); describe('UserActions', () => { const conversationState = container.resolve(ConversationState); const teamState = container.resolve(TeamState); const userState = container.resolve(UserState); afterEach(() => { conversationState.conversations.removeAll(); }); it('generates actions for self user profile', () => { const user = new User(''); user.isMe = true; const conversation = new Conversation(); jest.spyOn(conversation, 'isGroup').mockImplementation(ko.pureComputed(() => true)); const conversationRoleRepository: Partial = {canLeaveGroup: () => true}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser: user, user, }; const {queryByTestId} = render(); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(2); [Actions.OPEN_PROFILE, Actions.LEAVE].forEach(action => { const identifier = ActionIdentifier[action]; expect(queryByTestId(identifier)).not.toBeNull(); }); }); it('generates actions for self user profile when user is not activated', () => { const user = new User(''); user.isMe = true; const conversation = new Conversation(); const props = { actionsViewModel, conversation, conversationRoleRepository: {} as ConversationRoleRepository, isSelfActivated: false, onAction: noop, selfUser: user, user, }; const {queryByTestId} = render(); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(1); const identifier = ActionIdentifier[Actions.OPEN_PROFILE]; expect(queryByTestId(identifier)).not.toBeNull(); }); it('generates actions for another user profile to which I am connected', () => { const user = new User(''); const connection = new ConnectionEntity(); user.connection(connection); user.teamId = 'teamId'; const selfUser = new User(''); selfUser.teamId = 'teamId2'; jest.spyOn(user, 'isAvailable').mockImplementation(ko.pureComputed(() => true)); const conversation = new Conversation(); conversation.connection(connection); jest.spyOn(conversation, 'isGroup').mockImplementation(ko.pureComputed(() => true)); jest.spyOn(conversation, 'participating_user_ids').mockImplementation(ko.observableArray([new User()])); user.connection()?.status(ConnectionStatus.ACCEPTED); connection.userId = user.qualifiedId; conversationState.conversations.push(conversation); const conversationRoleRepository: Partial = {canRemoveParticipants: () => true}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser, user, }; const {queryByTestId} = render(); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(3); [Actions.OPEN_CONVERSATION, Actions.BLOCK].forEach(action => { const identifier = ActionIdentifier[action]; expect(queryByTestId(identifier)).not.toBeNull(); }); }); it('generates actions for another user profile that I have blocked', () => { const user = new User(''); const connection = new ConnectionEntity(); user.connection(connection); user.teamId = 'teamId'; const selfUser = new User(''); selfUser.teamId = 'teamId2'; jest.spyOn(user, 'isAvailable').mockImplementation(ko.pureComputed(() => true)); const conversation = new Conversation(); conversation.type(CONVERSATION_TYPE.ONE_TO_ONE); conversation.connection(connection); jest.spyOn(conversation, 'participating_user_ids').mockImplementation(ko.observableArray([new User()])); user.connection()?.status(ConnectionStatus.BLOCKED); connection.userId = user.qualifiedId; conversationState.conversations.push(conversation); const conversationRoleRepository: Partial = {canRemoveParticipants: () => false}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser, user, }; const {queryByTestId} = render(); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(1); expect(queryByTestId(ActionIdentifier[Actions.UNBLOCK])).not.toBeNull(); }); it("shows start conversation if there's no existing conversation between two users from the same team", () => { const user = new User(''); const team = new TeamEntity('teamId'); teamState.team(team); user.teamId = team.id; const selfUser = new User(''); selfUser.teamId = team.id; userState.self(selfUser); jest.spyOn(user, 'isAvailable').mockImplementation(ko.pureComputed(() => true)); const conversation = new Conversation(); conversation.participating_user_ids([user]); const conversationRoleRepository: Partial = {canRemoveParticipants: () => true}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser, user, }; const {queryByTestId} = render(); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(2); [Actions.START_CONVERSATION, Actions.REMOVE].forEach(action => { const identifier = ActionIdentifier[action]; expect(queryByTestId(identifier)).not.toBeNull(); }); }); it('opens a no available keys modal if failed when trying to establish mls 1:1', async () => { const user = new User(''); const team = new TeamEntity('teamId'); teamState.team(team); user.teamId = team.id; const selfUser = new User(''); selfUser.teamId = team.id; userState.self(selfUser); jest.spyOn(user, 'isAvailable').mockImplementation(ko.pureComputed(() => true)); const conversation = new Conversation(); conversation.participating_user_ids([user]); const conversationRoleRepository: Partial = {canRemoveParticipants: () => true}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser, user, }; jest .spyOn(actionsViewModel, 'getOrCreate1to1Conversation') .mockRejectedValueOnce(new ClientMLSError(ClientMLSErrorLabel.NO_KEY_PACKAGES_AVAILABLE)); const {getByTestId, getByText} = render( <> , ); const button = getByTestId(ActionIdentifier[Actions.START_CONVERSATION]); await act(async () => button?.click()); expect(getByText('modal1To1ConversationCreateErrorNoKeyPackagesHeadline')).toBeDefined(); expect(getByText('modal1To1ConversationCreateErrorNoKeyPackagesMessage')).toBeDefined(); }); it("shows open conversation if there's an existing conversation between two users from the same team", () => { const teamDomain = 'team-domain'; const user = new User('userid1', teamDomain); const team = new TeamEntity('teamId'); teamState.team(team); user.teamId = team.id; const selfUser = new User('userid2', teamDomain); selfUser.teamId = team.id; userState.self(selfUser); jest.spyOn(user, 'isAvailable').mockImplementation(ko.pureComputed(() => true)); const conversation = new Conversation(); jest.spyOn(conversation, 'participating_user_ids').mockImplementation(ko.observableArray([user])); const one2oneConversation = new Conversation('123', 'domain', ConversationProtocol.PROTEUS); one2oneConversation.type(CONVERSATION_TYPE.ONE_TO_ONE); one2oneConversation.participating_user_ids.push(user.qualifiedId); one2oneConversation.participating_user_ets.push(user); conversationState.conversations.push(one2oneConversation); const conversationRoleRepository: Partial = {canRemoveParticipants: () => true}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser, user, }; const {queryByTestId} = render(); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(2); [Actions.OPEN_CONVERSATION, Actions.REMOVE].forEach(action => { const identifier = ActionIdentifier[action]; expect(queryByTestId(identifier)).not.toBeNull(); }); }); it('only generates remove participant action for an unavailable user', () => { const user = new User(''); const conversation = new Conversation(); jest.spyOn(conversation, 'isGroup').mockImplementation(ko.pureComputed(() => true)); jest.spyOn(conversation, 'participating_user_ids').mockImplementation(ko.observableArray([new User()])); user.connection()?.status(ConnectionStatus.ACCEPTED); const conversationRoleRepository: Partial = {canRemoveParticipants: () => true}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser: new User(''), user, }; const {queryByTestId} = render(); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(1); }); it('displays buttons instead of a list when a single action is available in user modal', () => { const user = new User(''); const conversation = new Conversation(); const connection = new ConnectionEntity(); user.connection(connection); conversation.connection(connection); user.connection()?.status(ConnectionStatus.UNKNOWN); jest.spyOn(user, 'isAvailable').mockImplementation(ko.pureComputed(() => true)); const conversationRoleRepository: Partial = {canRemoveParticipants: () => false}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser: new User(''), user, isModal: true, }; const {queryByTestId} = render(withTheme()); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(1); const identifier = ActionIdentifier[Actions.SEND_REQUEST]; expect(queryByTestId(identifier)).not.toBeNull(); expect(queryByTestId('do-close')).not.toBeNull(); }); it('displays a list when a single action is available in sidebar', () => { const user = new User(''); const conversation = new Conversation(); const connection = new ConnectionEntity(); user.connection(connection); conversation.connection(connection); user.connection()?.status(ConnectionStatus.UNKNOWN); jest.spyOn(user, 'isAvailable').mockImplementation(ko.pureComputed(() => true)); const conversationRoleRepository: Partial = {canRemoveParticipants: () => false}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser: new User(''), user, }; const {queryByTestId} = render(withTheme()); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(1); const identifier = ActionIdentifier[Actions.SEND_REQUEST]; expect(queryByTestId(identifier)).not.toBeNull(); expect(queryByTestId('do-close')).toBeNull(); }); it('displays a list when multiple actions are available in user modal', () => { const user = new User(''); const conversation = new Conversation(); const connection = new ConnectionEntity(); user.connection(connection); conversation.connection(connection); user.connection()?.status(ConnectionStatus.SENT); jest.spyOn(user, 'isAvailable').mockImplementation(ko.pureComputed(() => true)); const conversationRoleRepository: Partial = {canRemoveParticipants: () => false}; const props = { actionsViewModel, conversation, conversationRoleRepository: conversationRoleRepository as ConversationRoleRepository, isSelfActivated: true, onAction: noop, selfUser: new User(''), user, isModal: true, }; const {queryByTestId} = render(withTheme()); const allActions = getAllActions(queryByTestId); expect(allActions).toHaveLength(2); [Actions.CANCEL_REQUEST, Actions.BLOCK].forEach(action => { const identifier = ActionIdentifier[action]; expect(queryByTestId(identifier)).not.toBeNull(); }); expect(queryByTestId('do-close')).toBeNull(); }); });