status-go/protocol/messenger_installations_test.go
Icaro Motta dbed69d155
perf(login)!: Set-up messenger filters outside login flow (#5229)
Fixes the slow login in mobile devices when users have joined large communities,
such as the Status one. A user would get stuck for almost 20s in some devices.

We identified that the step to set-up filters in the messenger is potentially
expensive and that it is not critical to happen before the node.login signal is
emitted. The solution presented in this PR is to set-up filters inside
messenger.Start(), which the client already calls immediately after login.

With this change, users of the mobile app can login pretty fast even when they
joined large communities. They can immediately interact with other parts of the
app even if filter initialization is running in the background, like Wallet,
Activity Center, Settings, and Profile.

Breaking changes: in the mobile repository, we had to change where the endpoint
wakuext_startMessenger was called and the order of a few events to process
chats. So essentially ordering, but no data changes.

- Root issue https://github.com/status-im/status-mobile/issues/20059
- Related mobile PR https://github.com/status-im/status-mobile/pull/20173
2024-06-10 12:02:42 -03:00

428 lines
13 KiB
Go

package protocol
import (
"context"
"errors"
"image"
"image/png"
"os"
"runtime"
"testing"
userimage "github.com/status-im/status-go/images"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/browsers"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
multiaccountscommon "github.com/status-im/status-go/multiaccounts/common"
"github.com/status-im/status-go/protocol/encryption/multidevice"
"github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/tt"
)
const statusChatID = "status"
const removedChatID = "deactivated"
func TestMessengerInstallationSuite(t *testing.T) {
suite.Run(t, new(MessengerInstallationSuite))
}
type MessengerInstallationSuite struct {
MessengerBaseTestSuite
}
func (s *MessengerInstallationSuite) TestReceiveInstallation() {
theirMessenger, err := newMessengerWithKey(s.shh, s.privateKey, s.logger, nil)
s.Require().NoError(err)
err = theirMessenger.SetInstallationMetadata(theirMessenger.installationID, &multidevice.InstallationMetadata{
Name: "their-name",
DeviceType: "their-device-type",
})
s.Require().NoError(err)
response, err := theirMessenger.SendPairInstallation(context.Background(), nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Chats(), 1)
s.Require().False(response.Chats()[0].Active)
// Wait for the message to reach its destination
response, err = WaitOnMessengerResponse(
s.m,
func(r *MessengerResponse) bool { return len(r.Installations) > 0 },
"installation not received",
)
s.Require().NoError(err)
actualInstallation := response.Installations[0]
s.Require().Equal(theirMessenger.installationID, actualInstallation.ID)
s.Require().NotNil(actualInstallation.InstallationMetadata)
s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name)
s.Require().Equal("their-device-type", actualInstallation.InstallationMetadata.DeviceType)
err = s.m.EnableInstallation(theirMessenger.installationID)
s.Require().NoError(err)
contactKey, err := crypto.GenerateKey()
s.Require().NoError(err)
contact, err := BuildContactFromPublicKey(&contactKey.PublicKey)
s.Require().NoError(err)
response, err = s.m.AddContact(context.Background(), &requests.AddContact{ID: contact.ID, CustomizationColor: string(multiaccountscommon.CustomizationColorRed)})
s.Require().NoError(err)
s.Require().Len(response.Contacts, 1)
s.Require().Equal(response.Contacts[0].ID, contact.ID)
s.Require().Equal(response.Contacts[0].CustomizationColor, multiaccountscommon.CustomizationColorRed)
// Wait for the message to reach its destination
response, err = WaitOnMessengerResponse(
theirMessenger,
func(r *MessengerResponse) bool { return len(r.Contacts) == 1 && r.Contacts[0].ID == contact.ID },
"contact not received",
)
s.Require().NoError(err)
actualContact := response.Contacts[0]
s.Require().Equal(contact.ID, actualContact.ID)
s.Require().True(actualContact.added())
// Simulate update from contact
contact.LastUpdated = 10
contact.DisplayName = "display-name"
contact.CustomizationColor = multiaccountscommon.CustomizationColorRed
s.Require().NoError(s.m.persistence.SaveContacts([]*Contact{contact}))
// Trigger syncing of contact
err = s.m.syncContact(context.Background(), contact, s.m.dispatchMessage)
s.Require().NoError(err)
// Wait for the message to reach its destination
_, err = WaitOnMessengerResponse(
theirMessenger,
func(r *MessengerResponse) bool {
return len(r.Contacts) == 1 &&
r.Contacts[0].ID == contact.ID &&
// Make sure lastupdated is **not** synced
actualContact.LastUpdated == 0 &&
r.Contacts[0].DisplayName == "display-name" &&
r.Contacts[0].CustomizationColor == multiaccountscommon.CustomizationColorRed
},
"contact not received",
)
s.Require().NoError(err)
chat := CreatePublicChat(statusChatID, s.m.transport)
err = s.m.SaveChat(chat)
s.Require().NoError(err)
response, err = WaitOnMessengerResponse(
theirMessenger,
func(r *MessengerResponse) bool { return len(r.Chats()) > 0 },
"sync chat not received",
)
s.Require().NoError(err)
actualChat := response.Chats()[0]
s.Require().Equal(statusChatID, actualChat.ID)
s.Require().True(actualChat.Active)
s.Require().NoError(theirMessenger.Shutdown())
}
func (s *MessengerInstallationSuite) TestSyncInstallation() {
// add contact
contactKey, err := crypto.GenerateKey()
s.Require().NoError(err)
contact, err := BuildContactFromPublicKey(&contactKey.PublicKey)
s.Require().NoError(err)
// mock added as mutual contact
contact.LastUpdated = 1
contact.ContactRequestReceived(1)
s.m.allContacts.Store(contact.ID, contact)
contact.LocalNickname = "Test Nickname"
_, err = s.m.AddContact(context.Background(), &requests.AddContact{ID: contact.ID, CustomizationColor: string(multiaccountscommon.CustomizationColorRed)})
s.Require().NoError(err)
_, err = s.m.SetContactLocalNickname(&requests.SetContactLocalNickname{ID: types.Hex2Bytes(contact.ID), Nickname: contact.LocalNickname})
s.Require().NoError(err)
//add bookmark
bookmark := browsers.Bookmark{
Name: "status official site",
URL: "https://status.im",
Removed: false,
}
_, err = s.m.browserDatabase.StoreBookmark(bookmark)
s.Require().NoError(err)
// add chat
chat := CreatePublicChat(statusChatID, s.m.transport)
err = s.m.SaveChat(chat)
s.Require().NoError(err)
// Create group chat
response, err := s.m.CreateGroupChatWithMembers(context.Background(), "group", []string{})
s.NoError(err)
s.Require().Len(response.Chats(), 1)
ourGroupChat := response.Chats()[0]
err = s.m.SaveChat(ourGroupChat)
s.NoError(err)
// Generate test image bigger than BannerDim
testImage := image.NewRGBA(image.Rect(0, 0, 20, 10))
tmpTestFilePath := s.T().TempDir() + "/test.png"
file, err := os.Create(tmpTestFilePath)
s.NoError(err)
defer file.Close()
err = png.Encode(file, testImage)
s.Require().NoError(err)
groupImg := userimage.CroppedImage{
ImagePath: tmpTestFilePath,
X: 1,
Y: 1,
Width: 10,
Height: 5,
}
// Add image to chat
response, err = s.m.EditGroupChat(context.Background(), ourGroupChat.ID, "test_admin_group", "#FF00FF", groupImg)
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)
ourGroupChat = response.Chats()[0]
// Create second group chat and deactivate it
response, err = s.m.CreateGroupChatWithMembers(context.Background(), "deactivated-group", []string{})
s.NoError(err)
s.Require().Len(response.Chats(), 1)
ourDeactivatedGroupChat := response.Chats()[0]
err = s.m.SaveChat(ourDeactivatedGroupChat)
s.NoError(err)
_, err = s.m.deactivateChat(ourDeactivatedGroupChat.ID, 0, true, true)
s.NoError(err)
// Create Alice for the 1-1 chat
alice := s.newMessenger()
defer TearDownMessenger(&s.Suite, alice)
// Create 1-1 chat
ourOneOneChat := CreateOneToOneChat("Our 1TO1", &alice.identity.PublicKey, alice.transport)
err = s.m.SaveChat(ourOneOneChat)
s.Require().NoError(err)
// add and deactivate chat
chat2 := CreatePublicChat(removedChatID, s.m.transport)
chat2.DeletedAtClockValue = 1
err = s.m.SaveChat(chat2)
s.Require().NoError(err)
_, err = s.m.deactivateChat(removedChatID, 0, true, true)
s.Require().NoError(err)
// pair
theirMessenger, err := newMessengerWithKey(s.shh, s.privateKey, s.logger, nil)
s.Require().NoError(err)
err = theirMessenger.SaveChat(chat2)
s.Require().NoError(err)
err = theirMessenger.SetInstallationMetadata(theirMessenger.installationID, &multidevice.InstallationMetadata{
Name: "their-name",
DeviceType: "their-device-type",
})
s.Require().NoError(err)
response, err = theirMessenger.SendPairInstallation(context.Background(), nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Chats(), 1)
s.Require().False(response.Chats()[0].Active)
// Wait for the message to reach its destination
response, err = WaitOnMessengerResponse(
s.m,
func(r *MessengerResponse) bool { return len(r.Installations) > 0 },
"installation not received",
)
s.Require().NoError(err)
actualInstallation := response.Installations[0]
s.Require().Equal(theirMessenger.installationID, actualInstallation.ID)
s.Require().NotNil(actualInstallation.InstallationMetadata)
s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name)
s.Require().Equal("their-device-type", actualInstallation.InstallationMetadata.DeviceType)
err = s.m.EnableInstallation(theirMessenger.installationID)
s.Require().NoError(err)
// sync
err = s.m.SyncDevices(context.Background(), "ens-name", "profile-image", nil)
s.Require().NoError(err)
var allChats []*Chat
var actualContact *Contact
var bookmarks []*browsers.Bookmark
// Wait for the message to reach its destination
err = tt.RetryWithBackOff(func() error {
var err error
response, err = theirMessenger.RetrieveAll()
if err != nil {
return err
}
allChats = append(allChats, response.Chats()...)
for _, c := range response.Contacts {
if c.LocalNickname == contact.LocalNickname {
actualContact = c
break
}
}
bookmarks = append(bookmarks, response.GetBookmarks()...)
if len(allChats) >= 5 && actualContact != nil && len(bookmarks) >= 1 {
return nil
}
return errors.New("not received all chats & contacts & bookmarks yet")
})
s.Require().NoError(err)
var statusChat *Chat
var groupChat *Chat
var removedGroupChat *Chat
var oneToOneChat *Chat
var removedChat *Chat
for _, c := range allChats {
if c.ID == statusChatID {
statusChat = c
}
if c.ID == ourGroupChat.ID {
groupChat = c
}
if c.ID == ourDeactivatedGroupChat.ID {
removedGroupChat = c
}
if c.ID == ourOneOneChat.ID {
oneToOneChat = c
}
if c.ID == removedChatID {
removedChat = c
}
}
s.Require().NotNil(statusChat)
s.Require().NotNil(groupChat)
s.Require().NotNil(removedGroupChat)
s.Require().NotNil(oneToOneChat)
s.Require().Equal(ourGroupChat.Name, groupChat.Name)
s.Require().True(ourGroupChat.Active)
s.Require().Equal(ourDeactivatedGroupChat.Name, removedGroupChat.Name)
s.Require().False(removedGroupChat.Active)
s.Require().Equal("", oneToOneChat.Name) // We set 1-1 chat names to "" because the name is not good
s.Require().True(oneToOneChat.Active)
s.Require().True(actualContact.added())
s.Require().Equal("Test Nickname", actualContact.LocalNickname)
s.Require().Equal(multiaccountscommon.CustomizationColorRed, actualContact.CustomizationColor)
s.Require().True(actualContact.hasAddedUs())
s.Require().True(actualContact.mutual())
bookmarks, err = theirMessenger.browserDatabase.GetBookmarks()
s.Require().NoError(err)
s.Require().Equal(1, len(bookmarks))
s.Require().NoError(theirMessenger.Shutdown())
s.Require().NotNil(removedChat)
s.Require().False(removedChat.Active)
}
func (s *MessengerInstallationSuite) TestSyncInstallationNewMessages() {
bob1 := s.m
// pair
bob2, err := newMessengerWithKey(s.shh, s.privateKey, s.logger, nil)
s.Require().NoError(err)
alice := s.newMessenger()
err = bob2.SetInstallationMetadata(bob2.installationID, &multidevice.InstallationMetadata{
Name: "their-name",
DeviceType: "their-device-type",
})
s.Require().NoError(err)
response, err := bob2.SendPairInstallation(context.Background(), nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Chats(), 1)
s.Require().False(response.Chats()[0].Active)
// Wait for the message to reach its destination
response, err = WaitOnMessengerResponse(
bob1,
func(r *MessengerResponse) bool { return len(r.Installations) > 0 },
"installation not received",
)
s.Require().NoError(err)
actualInstallation := response.Installations[0]
s.Require().Equal(bob2.installationID, actualInstallation.ID)
err = bob1.EnableInstallation(bob2.installationID)
s.Require().NoError(err)
// send a message from bob1 to alice, it should be received on both bob1 and bob2
alicePkString := types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey))
chat := CreateOneToOneChat(alicePkString, &alice.identity.PublicKey, bob1.transport)
s.Require().NoError(bob1.SaveChat(chat))
inputMessage := buildTestMessage(*chat)
_, err = s.m.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
// Wait for the message to reach its destination
_, err = WaitOnMessengerResponse(
bob2,
func(r *MessengerResponse) bool { return len(r.Messages()) > 0 },
"message not received",
)
s.Require().NoError(err)
s.Require().NoError(bob2.Shutdown())
s.Require().NoError(alice.Shutdown())
}
func (s *MessengerInstallationSuite) TestInitInstallations() {
m, err := newMessengerWithKey(s.shh, s.privateKey, s.logger, nil)
s.Require().NoError(err)
// m.InitInstallations is already called when we set-up the messenger for
// testing, thus this test has no act phase.
// err = m.InitInstallations()
// We get one installation when the messenger initializes installations
// correctly.
s.Require().Equal(1, m.allInstallations.Len())
deviceName, err := server.GetDeviceName()
s.Require().NoError(err)
installation, ok := m.allInstallations.Load(m.installationID)
s.Require().True(ok)
s.Require().Equal(deviceName+" ", installation.InstallationMetadata.Name)
s.Require().Equal(runtime.GOOS, installation.InstallationMetadata.DeviceType)
}