status-im/status-go

View on GitHub
protocol/messenger_group_chat_test.go

Summary

Maintainability
A
0 mins
Test Coverage
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())
}