package protocol import ( "context" "crypto/ecdsa" "fmt" "testing" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/suite" userimage "github.com/status-im/status-go/images" multiaccountscommon "github.com/status-im/status-go/multiaccounts/common" "github.com/status-im/status-go/multiaccounts/settings" "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/protobuf" ) func TestGroupChatSuite(t *testing.T) { suite.Run(t, new(MessengerGroupChatSuite)) } type MessengerGroupChatSuite struct { MessengerBaseTestSuite } func (s *MessengerGroupChatSuite) createGroupChat(creator *Messenger, name string, members []string) *Chat { response, err := creator.CreateGroupChatWithMembers(context.Background(), name, members) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) chat := response.Chats()[0] err = creator.SaveChat(chat) s.Require().NoError(err) return chat } func (s *MessengerGroupChatSuite) createEmptyGroupChat(creator *Messenger, name string) *Chat { return s.createGroupChat(creator, name, []string{}) } func (s *MessengerGroupChatSuite) verifyGroupChatCreated(member *Messenger, expectedChatActive bool) { response, err := WaitOnMessengerResponse( member, func(r *MessengerResponse) bool { return len(r.Chats()) == 1 && r.Chats()[0].Active == expectedChatActive }, "chat invitation not received", ) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) s.Require().True(response.Chats()[0].Active == expectedChatActive) } func makeMutualContact(origin *Messenger, contactPubkey *ecdsa.PublicKey) error { contact, err := BuildContactFromPublicKey(contactPubkey) if err != nil { return err } contact.ContactRequestLocalState = ContactRequestStateSent contact.ContactRequestRemoteState = ContactRequestStateReceived origin.allContacts.Store(contact.ID, contact) return nil } func (s *MessengerGroupChatSuite) makeContact(origin *Messenger, toAdd *Messenger) { s.Require().NoError(makeMutualContact(origin, &toAdd.identity.PublicKey)) } func (s *MessengerGroupChatSuite) makeMutualContacts(lhs *Messenger, rhs *Messenger) { s.makeContact(lhs, rhs) s.makeContact(rhs, lhs) } func (s *MessengerGroupChatSuite) TestGroupChatCreation() { testCases := []struct { name string creatorAddedMemberAsContact bool memberAddedCreatorAsContact bool expectedCreationSuccess bool expectedAddedMemberChatActive bool }{ { name: "not added - not added", creatorAddedMemberAsContact: false, memberAddedCreatorAsContact: false, expectedCreationSuccess: false, expectedAddedMemberChatActive: false, }, { name: "added - not added", creatorAddedMemberAsContact: true, memberAddedCreatorAsContact: false, expectedCreationSuccess: true, expectedAddedMemberChatActive: false, }, { name: "not added - added", creatorAddedMemberAsContact: false, memberAddedCreatorAsContact: true, expectedCreationSuccess: false, expectedAddedMemberChatActive: false, }, { name: "added - added", creatorAddedMemberAsContact: true, memberAddedCreatorAsContact: true, expectedCreationSuccess: true, expectedAddedMemberChatActive: true, }, } for i, testCase := range testCases { creator := s.newMessenger() member := s.newMessenger() members := []string{common.PubkeyToHex(&member.identity.PublicKey)} if testCase.creatorAddedMemberAsContact { s.makeContact(creator, member) } if testCase.memberAddedCreatorAsContact { s.makeContact(member, creator) } _, err := creator.CreateGroupChatWithMembers(context.Background(), fmt.Sprintf("test_group_chat_%d", i), members) if testCase.creatorAddedMemberAsContact { s.Require().NoError(err) s.verifyGroupChatCreated(member, testCase.expectedAddedMemberChatActive) } else { s.Require().EqualError(err, "group-chat: can't add members who are not mutual contacts") } defer s.NoError(creator.Shutdown()) defer s.NoError(member.Shutdown()) } } func (s *MessengerGroupChatSuite) TestGroupChatMembersAddition() { testCases := []struct { name string inviterAddedMemberAsContact bool memberAddedInviterAsContact bool expectedAdditionSuccess bool expectedAddedMemberChatActive bool }{ { name: "not added - not added", inviterAddedMemberAsContact: false, memberAddedInviterAsContact: false, expectedAdditionSuccess: false, expectedAddedMemberChatActive: false, }, { name: "added - not added", inviterAddedMemberAsContact: true, memberAddedInviterAsContact: false, expectedAdditionSuccess: true, expectedAddedMemberChatActive: false, }, { name: "not added - added", inviterAddedMemberAsContact: false, memberAddedInviterAsContact: true, expectedAdditionSuccess: false, expectedAddedMemberChatActive: false, }, { name: "added - added", inviterAddedMemberAsContact: true, memberAddedInviterAsContact: true, expectedAdditionSuccess: true, expectedAddedMemberChatActive: true, }, } for i, testCase := range testCases { admin := s.newMessenger() inviter := s.newMessenger() member := s.newMessenger() members := []string{common.PubkeyToHex(&member.identity.PublicKey)} if testCase.inviterAddedMemberAsContact { s.makeContact(inviter, member) } if testCase.memberAddedInviterAsContact { s.makeContact(member, inviter) } for j, inviterIsAlsoGroupCreator := range []bool{true, false} { var groupChat *Chat if inviterIsAlsoGroupCreator { groupChat = s.createEmptyGroupChat(inviter, fmt.Sprintf("test_group_chat_%d_%d", i, j)) } else { s.makeContact(admin, inviter) groupChat = s.createGroupChat(admin, fmt.Sprintf("test_group_chat_%d_%d", i, j), []string{common.PubkeyToHex(&inviter.identity.PublicKey)}) err := inviter.SaveChat(groupChat) s.Require().NoError(err) } _, err := inviter.AddMembersToGroupChat(context.Background(), groupChat.ID, members) if testCase.inviterAddedMemberAsContact { s.Require().NoError(err) s.verifyGroupChatCreated(member, testCase.expectedAddedMemberChatActive) } else { s.Require().EqualError(err, "group-chat: can't add members who are not mutual contacts") } } defer s.NoError(admin.Shutdown()) defer s.NoError(inviter.Shutdown()) defer s.NoError(member.Shutdown()) } } func (s *MessengerGroupChatSuite) TestGroupChatMembersRemoval() { admin := s.newMessenger() memberA := s.newMessenger() memberB := s.newMessenger() memberC := s.newMessenger() members := []string{common.PubkeyToHex(&memberA.identity.PublicKey), common.PubkeyToHex(&memberB.identity.PublicKey), common.PubkeyToHex(&memberC.identity.PublicKey)} s.makeMutualContacts(admin, memberA) s.makeMutualContacts(admin, memberB) s.makeMutualContacts(admin, memberC) groupChat := s.createGroupChat(admin, "test_group_chat", members) s.verifyGroupChatCreated(memberA, true) s.verifyGroupChatCreated(memberB, true) s.verifyGroupChatCreated(memberC, true) _, err := memberA.RemoveMembersFromGroupChat(context.Background(), groupChat.ID, []string{common.PubkeyToHex(&memberB.identity.PublicKey), common.PubkeyToHex(&memberC.identity.PublicKey)}) s.Require().Error(err) // only admin can remove members from the group _, err = admin.RemoveMembersFromGroupChat(context.Background(), groupChat.ID, []string{common.PubkeyToHex(&memberB.identity.PublicKey), common.PubkeyToHex(&memberC.identity.PublicKey)}) s.Require().NoError(err) // ensure removal is propagated to other members response, err := WaitOnMessengerResponse( memberA, func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, "chat invitation not received", ) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) s.Require().True(response.Chats()[0].Active) s.Require().Len(response.Chats()[0].Members, 2) defer s.NoError(admin.Shutdown()) defer s.NoError(memberA.Shutdown()) defer s.NoError(memberB.Shutdown()) defer s.NoError(memberC.Shutdown()) } func (s *MessengerGroupChatSuite) TestGroupChatEdit() { admin := s.newMessenger() member := s.newMessenger() s.makeMutualContacts(admin, member) groupChat := s.createGroupChat(admin, "test_group_chat", []string{common.PubkeyToHex(&member.identity.PublicKey)}) s.verifyGroupChatCreated(member, true) response, err := admin.EditGroupChat(context.Background(), groupChat.ID, "test_admin_group", "#FF00FF", userimage.CroppedImage{}) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) s.Require().Equal("test_admin_group", response.Chats()[0].Name) s.Require().Equal("#FF00FF", response.Chats()[0].Color) // TODO: handle image // ensure group edit is propagated to other members response, err = WaitOnMessengerResponse( member, func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, "chat invitation not received", ) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) s.Require().Equal("test_admin_group", response.Chats()[0].Name) s.Require().Equal("#FF00FF", response.Chats()[0].Color) response, err = member.EditGroupChat(context.Background(), groupChat.ID, "test_member_group", "#F0F0F0", userimage.CroppedImage{}) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) s.Require().Equal("test_member_group", response.Chats()[0].Name) s.Require().Equal("#F0F0F0", response.Chats()[0].Color) // ensure group edit is propagated to other members response, err = WaitOnMessengerResponse( admin, func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, "chat invitation not received", ) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) s.Require().Equal("test_member_group", response.Chats()[0].Name) s.Require().Equal("#F0F0F0", response.Chats()[0].Color) inputMessage := buildTestMessage(*groupChat) _, err = admin.SendChatMessage(context.Background(), inputMessage) s.Require().NoError(err) response, err = WaitOnMessengerResponse( member, func(r *MessengerResponse) bool { return len(r.Messages()) > 0 }, "chat invitation not received", ) s.Require().NoError(err) s.Require().Len(response.Messages(), 1) s.Require().Equal(inputMessage.Text, response.Messages()[0].Text) defer s.NoError(admin.Shutdown()) defer s.NoError(member.Shutdown()) } func (s *MessengerGroupChatSuite) TestGroupChatDeleteMemberMessage() { admin := s.newMessenger() member := s.newMessenger() s.makeMutualContacts(admin, member) groupChat := s.createGroupChat(admin, "test_group_chat", []string{common.PubkeyToHex(&member.identity.PublicKey)}) s.verifyGroupChatCreated(member, true) ctx := context.Background() inputMessage := buildTestMessage(*groupChat) _, err := member.SendChatMessage(ctx, inputMessage) s.Require().NoError(err) response, err := WaitOnMessengerResponse( admin, func(r *MessengerResponse) bool { return len(r.Messages()) > 0 }, "messages not received", ) s.Require().NoError(err) s.Require().Len(response.Messages(), 1) s.Require().Equal(inputMessage.Text, response.Messages()[0].Text) message := response.Messages()[0] deleteMessageResponse, err := admin.DeleteMessageAndSend(ctx, message.ID) s.Require().NoError(err) _, err = WaitOnMessengerResponse(member, func(response *MessengerResponse) bool { return len(response.RemovedMessages()) > 0 }, "removed messages not received") s.Require().Equal(deleteMessageResponse.RemovedMessages()[0].DeletedBy, contactIDFromPublicKey(admin.IdentityPublicKey())) s.Require().NoError(err) message, err = member.MessageByID(message.ID) s.Require().NoError(err) s.Require().True(message.Deleted) defer s.NoError(admin.Shutdown()) defer s.NoError(member.Shutdown()) } func (s *MessengerGroupChatSuite) TestGroupChatHandleDeleteMemberMessage() { admin := s.newMessenger() member := s.newMessenger() s.makeMutualContacts(admin, member) groupChat := s.createGroupChat(admin, "test_group_chat", []string{common.PubkeyToHex(&member.identity.PublicKey)}) s.verifyGroupChatCreated(member, true) ctx := context.Background() inputMessage := buildTestMessage(*groupChat) _, err := member.SendChatMessage(ctx, inputMessage) s.Require().NoError(err) response, err := WaitOnMessengerResponse( admin, func(r *MessengerResponse) bool { return len(r.Messages()) > 0 }, "messages not received", ) s.Require().NoError(err) s.Require().Len(response.Messages(), 1) s.Require().Equal(inputMessage.Text, response.Messages()[0].Text) deleteMessage := &DeleteMessage{ DeleteMessage: &protobuf.DeleteMessage{ Clock: 2, MessageType: protobuf.MessageType_PRIVATE_GROUP, MessageId: inputMessage.ID, ChatId: groupChat.ID, }, From: common.PubkeyToHex(&admin.identity.PublicKey), } state := &ReceivedMessageState{ Response: &MessengerResponse{}, } err = member.handleDeleteMessage(state, deleteMessage) s.Require().NoError(err) removedMessages := state.Response.RemovedMessages() s.Require().Len(removedMessages, 1) s.Require().Equal(removedMessages[0].MessageID, inputMessage.ID) defer s.NoError(admin.Shutdown()) defer s.NoError(member.Shutdown()) } func (s *MessengerGroupChatSuite) TestGroupChatMembersRemovalOutOfOrder() { admin := s.newMessenger() memberA := s.newMessenger() members := []string{common.PubkeyToHex(&memberA.identity.PublicKey)} s.makeMutualContacts(admin, memberA) groupChat := s.createGroupChat(admin, "test_group_chat", members) removeMembersResponse, err := admin.removeMembersFromGroupChat(context.Background(), groupChat, []string{common.PubkeyToHex(&memberA.identity.PublicKey)}) s.Require().NoError(err) encodedMessage := removeMembersResponse.encodedProtobuf message := protobuf.MembershipUpdateMessage{} err = proto.Unmarshal(encodedMessage, &message) s.Require().NoError(err) response := &MessengerResponse{} messageState := &ReceivedMessageState{ ExistingMessagesMap: make(map[string]bool), Response: response, AllChats: new(chatMap), Timesource: memberA.getTimesource(), } c, err := buildContact(admin.myHexIdentity(), &admin.identity.PublicKey) s.Require().NoError(err) messageState.CurrentMessageState = &CurrentMessageState{ Contact: c, } err = memberA.HandleMembershipUpdate(messageState, nil, &message, memberA.systemMessagesTranslations) s.Require().NoError(err) s.Require().NotNil(messageState.Response) s.Require().Len(messageState.Response.Chats(), 1) s.Require().Len(messageState.Response.Chats()[0].Members, 1) defer s.NoError(admin.Shutdown()) defer s.NoError(memberA.Shutdown()) } func (s *MessengerGroupChatSuite) TestGroupChatMembersInfoSync() { admin, memberA, memberB := s.newMessenger(), s.newMessenger(), s.newMessenger() memberB.account.CustomizationColor = multiaccountscommon.CustomizationColorBlue s.Require().NoError(admin.settings.SaveSettingField(settings.DisplayName, "admin")) s.Require().NoError(memberA.settings.SaveSettingField(settings.DisplayName, "memberA")) s.Require().NoError(memberB.settings.SaveSettingField(settings.DisplayName, "memberB")) members := []string{common.PubkeyToHex(&memberA.identity.PublicKey), common.PubkeyToHex(&memberB.identity.PublicKey)} s.makeMutualContacts(admin, memberA) s.makeMutualContacts(admin, memberB) s.createGroupChat(admin, "test_group_chat", members) s.verifyGroupChatCreated(memberA, true) s.verifyGroupChatCreated(memberB, true) response, err := WaitOnMessengerResponse( memberA, func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, "chat invitation not received", ) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) s.Require().True(response.Chats()[0].Active) s.Require().Len(response.Chats()[0].Members, 3) _, err = WaitOnMessengerResponse( memberA, func(r *MessengerResponse) bool { // we republish as we don't have store nodes in tests err := memberB.publishContactCode() if err != nil { return false } contact, ok := memberA.allContacts.Load(common.PubkeyToHex(&memberB.identity.PublicKey)) return ok && contact.DisplayName == "memberB" && contact.CustomizationColor == memberB.account.GetCustomizationColor() }, "DisplayName is not the same", ) s.Require().NoError(err) s.NoError(admin.Shutdown()) s.NoError(memberA.Shutdown()) s.NoError(memberB.Shutdown()) }