package protocol import ( "context" "crypto/ecdsa" "errors" "fmt" "testing" "time" "github.com/cenkalti/backoff/v3" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go.uber.org/zap" gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/images" "github.com/status-im/status-go/multiaccounts" "github.com/status-im/status-go/multiaccounts/settings" "github.com/status-im/status-go/params" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/tt" "github.com/status-im/status-go/waku" ) func TestMessengerProfilePictureHandlerSuite(t *testing.T) { suite.Run(t, new(MessengerProfilePictureHandlerSuite)) } type MessengerProfilePictureHandlerSuite struct { suite.Suite alice *Messenger // client instance of Messenger bob *Messenger // server instance of Messenger // If one wants to send messages between different instances of Messenger, // a single Waku service should be shared. shh types.Waku logger *zap.Logger } func (s *MessengerProfilePictureHandlerSuite) SetupSuite() { s.logger = tt.MustCreateTestLogger() // Setup Waku things config := waku.DefaultConfig config.MinimumAcceptedPoW = 0 wakuLogger := s.logger.Named("Waku") shh := waku.New(&config, wakuLogger) s.shh = gethbridge.NewGethWakuWrapper(shh) s.Require().NoError(shh.Start()) } func (s *MessengerProfilePictureHandlerSuite) TearDownSuite() { _ = gethbridge.GetGethWakuFrom(s.shh).Stop() _ = s.logger.Sync() } func (s *MessengerProfilePictureHandlerSuite) newMessenger(name string) *Messenger { m, err := newTestMessenger(s.shh, testMessengerConfig{ logger: s.logger.Named(fmt.Sprintf("messenger-%s", name)), name: name, extraOptions: []Option{ WithAppSettings(newTestSettings(), params.NodeConfig{}), }, }) s.Require().NoError(err) _, err = m.Start() s.Require().NoError(err) return m } func (s *MessengerProfilePictureHandlerSuite) SetupTest() { // Generate Alice Messenger s.alice = s.newMessenger("Alice") s.bob = s.newMessenger("Bobby") // Setup MultiAccount for Alice Messenger s.setupMultiAccount(s.alice) } func (s *MessengerProfilePictureHandlerSuite) TearDownTest() { // Shutdown messengers TearDownMessenger(&s.Suite, s.alice) s.alice = nil TearDownMessenger(&s.Suite, s.bob) s.bob = nil _ = s.logger.Sync() } func (s *MessengerProfilePictureHandlerSuite) setupMultiAccount(m *Messenger) { name, err := m.settings.DisplayName() s.Require().NoError(err) keyUID := m.IdentityPublicKeyString() m.account = &multiaccounts.Account{ Name: name, KeyUID: keyUID, } err = m.multiAccounts.SaveAccount(*m.account) s.NoError(err) } func (s *MessengerProfilePictureHandlerSuite) generateAndStoreIdentityImages(m *Messenger) map[string]images.IdentityImage { keyUID := m.IdentityPublicKeyString() iis := images.SampleIdentityImages() err := m.multiAccounts.StoreIdentityImages(keyUID, iis, false) s.Require().NoError(err) out := make(map[string]images.IdentityImage) for _, ii := range iis { out[ii.Name] = ii } s.Require().Contains(out, images.SmallDimName) s.Require().Contains(out, images.LargeDimName) return out } func (s *MessengerProfilePictureHandlerSuite) TestChatIdentity() { iis := s.generateAndStoreIdentityImages(s.alice) ci, err := s.alice.createChatIdentity(privateChat) s.Require().NoError(err) s.Require().Exactly(len(iis), len(ci.Images)) } func (s *MessengerProfilePictureHandlerSuite) TestEncryptDecryptIdentityImagesWithContactPubKeys() { smPayload := "hello small image" lgPayload := "hello large image" ci := protobuf.ChatIdentity{ Clock: uint64(time.Now().Unix()), Images: map[string]*protobuf.IdentityImage{ "small": { Payload: []byte(smPayload), }, "large": { Payload: []byte(lgPayload), }, }, } // Make contact keys and Contacts, set the Contacts to added contactKeys := make([]*ecdsa.PrivateKey, 10) for i := range contactKeys { contactKey, err := crypto.GenerateKey() s.Require().NoError(err) contactKeys[i] = contactKey contact, err := BuildContactFromPublicKey(&contactKey.PublicKey) s.Require().NoError(err) contact.ContactRequestLocalState = ContactRequestStateSent s.alice.allContacts.Store(contact.ID, contact) } // Test EncryptIdentityImagesWithContactPubKeys err := EncryptIdentityImagesWithContactPubKeys(ci.Images, s.alice) s.Require().NoError(err) for _, ii := range ci.Images { s.Require().Equal(s.alice.allContacts.Len(), len(ii.EncryptionKeys)) } s.Require().NotEqual([]byte(smPayload), ci.Images["small"].Payload) s.Require().NotEqual([]byte(lgPayload), ci.Images["large"].Payload) s.Require().True(ci.Images["small"].Encrypted) s.Require().True(ci.Images["large"].Encrypted) // Test DecryptIdentityImagesWithIdentityPrivateKey err = DecryptIdentityImagesWithIdentityPrivateKey(ci.Images, contactKeys[2], &s.alice.identity.PublicKey) s.Require().NoError(err) s.Require().Equal(smPayload, string(ci.Images["small"].Payload)) s.Require().Equal(lgPayload, string(ci.Images["large"].Payload)) s.Require().False(ci.Images["small"].Encrypted) s.Require().False(ci.Images["large"].Encrypted) // RESET Messenger identity, Contacts and IdentityImage.EncryptionKeys s.alice.allContacts = new(contactMap) ci.Images["small"].EncryptionKeys = nil ci.Images["large"].EncryptionKeys = nil // Test EncryptIdentityImagesWithContactPubKeys with no contacts err = EncryptIdentityImagesWithContactPubKeys(ci.Images, s.alice) s.Require().NoError(err) for _, ii := range ci.Images { s.Require().Equal(0, len(ii.EncryptionKeys)) } s.Require().NotEqual([]byte(smPayload), ci.Images["small"].Payload) s.Require().NotEqual([]byte(lgPayload), ci.Images["large"].Payload) s.Require().True(ci.Images["small"].Encrypted) s.Require().True(ci.Images["large"].Encrypted) // Test DecryptIdentityImagesWithIdentityPrivateKey with no valid identity err = DecryptIdentityImagesWithIdentityPrivateKey(ci.Images, contactKeys[2], &s.alice.identity.PublicKey) s.Require().NoError(err) s.Require().NotEqual([]byte(smPayload), ci.Images["small"].Payload) s.Require().NotEqual([]byte(lgPayload), ci.Images["large"].Payload) s.Require().True(ci.Images["small"].Encrypted) s.Require().True(ci.Images["large"].Encrypted) } func (s *MessengerProfilePictureHandlerSuite) TestPictureInPrivateChatOneSided() { err := s.bob.settings.SaveSettingField(settings.ProfilePicturesVisibility, settings.ProfilePicturesShowToEveryone) s.Require().NoError(err) err = s.alice.settings.SaveSettingField(settings.ProfilePicturesVisibility, settings.ProfilePicturesShowToEveryone) s.Require().NoError(err) bChat := CreateOneToOneChat(s.alice.IdentityPublicKeyString(), s.alice.IdentityPublicKey(), s.alice.transport) err = s.bob.SaveChat(bChat) s.Require().NoError(err) _, err = s.bob.Join(bChat) s.Require().NoError(err) // Alice sends a message to the public chat message := buildTestMessage(*bChat) response, err := s.bob.SendChatMessage(context.Background(), message) s.Require().NoError(err) s.Require().NotNil(response) options := func(b *backoff.ExponentialBackOff) { b.MaxElapsedTime = 2 * time.Second } err = tt.RetryWithBackOff(func() error { response, err = s.alice.RetrieveAll() if err != nil { return err } s.Require().NotNil(response) contacts := response.Contacts s.logger.Debug("RetryWithBackOff contact data", zap.Any("contacts", contacts)) if len(contacts) > 0 && len(contacts[0].Images) > 0 { s.logger.Debug("", zap.Any("contacts", contacts)) return nil } return errors.New("no new contacts with images received") }, options) } func (s *MessengerProfilePictureHandlerSuite) TestE2eSendingReceivingProfilePicture() { profilePicShowSettings := []settings.ProfilePicturesShowToType{ settings.ProfilePicturesShowToContactsOnly, settings.ProfilePicturesShowToEveryone, settings.ProfilePicturesShowToNone, } profilePicViewSettings := []settings.ProfilePicturesVisibilityType{ settings.ProfilePicturesVisibilityContactsOnly, settings.ProfilePicturesVisibilityEveryone, settings.ProfilePicturesVisibilityNone, } isContactFor := map[string][]bool{ "alice": {true, false}, "bob": {true, false}, } chatContexts := []ChatContext{ publicChat, privateChat, } // TODO see if possible to push each test scenario into a go routine for _, cc := range chatContexts { for _, ss := range profilePicShowSettings { for _, vs := range profilePicViewSettings { for _, ac := range isContactFor["alice"] { for _, bc := range isContactFor["bob"] { args := &e2eArgs{ chatContext: cc, showToType: ss, visibilityType: vs, aliceContact: ac, bobContact: bc, } s.Run(args.TestCaseName(s.T()), func() { s.testE2eSendingReceivingProfilePicture(args) }) } } } } } s.SetupTest() } func (s *MessengerProfilePictureHandlerSuite) testE2eSendingReceivingProfilePicture(args *e2eArgs) { // Generate Alice Messenger alice := s.newMessenger("Alice") bob := s.newMessenger("Bobby") // Setup MultiAccount for Alice Messenger s.setupMultiAccount(alice) defer func() { TearDownMessenger(&s.Suite, alice) alice = nil TearDownMessenger(&s.Suite, bob) bob = nil _ = s.logger.Sync() }() s.logger.Info("testing with criteria:", zap.Any("args", args)) defer s.logger.Info("Completed testing with criteria:", zap.Any("args", args)) expectPicture, err := args.resultExpected() s.Require().NoError(err) s.logger.Debug("expect to receive a profile pic?", zap.Bool("result", expectPicture), zap.Error(err)) // Setting up Bob err = bob.settings.SaveSettingField(settings.ProfilePicturesVisibility, args.visibilityType) s.Require().NoError(err) if args.bobContact { _, err = bob.AddContact(context.Background(), &requests.AddContact{ID: alice.IdentityPublicKeyString()}) s.Require().NoError(err) } // Create Bob's chats switch args.chatContext { case publicChat: // Bob opens up the public chat and joins it bChat := CreatePublicChat("status", alice.transport) err = bob.SaveChat(bChat) s.Require().NoError(err) _, err = bob.Join(bChat) s.Require().NoError(err) case privateChat: bChat := CreateOneToOneChat(alice.IdentityPublicKeyString(), alice.IdentityPublicKey(), alice.transport) err = bob.SaveChat(bChat) s.Require().NoError(err) _, err = bob.Join(bChat) s.Require().NoError(err) default: s.Failf("unexpected chat context type", "%s", string(args.chatContext)) } // Setting up Alice err = alice.settings.SaveSettingField(settings.ProfilePicturesShowTo, args.showToType) s.Require().NoError(err) if args.aliceContact { _, err = alice.AddContact(context.Background(), &requests.AddContact{ID: bob.IdentityPublicKeyString()}) s.Require().NoError(err) } iis := s.generateAndStoreIdentityImages(alice) // Create chats var aChat *Chat switch args.chatContext { case publicChat: // Alice opens creates a public chat aChat = CreatePublicChat("status", alice.transport) err = alice.SaveChat(aChat) s.Require().NoError(err) // Alice sends a message to the public chat message := buildTestMessage(*aChat) response, err := alice.SendChatMessage(context.Background(), message) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.messages, 1) case privateChat: aChat = CreateOneToOneChat(bob.IdentityPublicKeyString(), bob.IdentityPublicKey(), bob.transport) err = alice.SaveChat(aChat) s.Require().NoError(err) _, err = alice.Join(aChat) s.Require().NoError(err) err = alice.publishContactCode() s.Require().NoError(err) default: s.Failf("unexpected chat context type", "%s", string(args.chatContext)) } // Poll bob to see if he got the chatIdentity // Retrieve ChatIdentity var contacts []*Contact options := func(b *backoff.ExponentialBackOff) { b.MaxElapsedTime = 2 * time.Second } err = tt.RetryWithBackOff(func() error { response, err := bob.RetrieveAll() if err != nil { return err } contacts = response.Contacts if len(contacts) > 0 && len(contacts[0].Images) > 0 { return nil } return errors.New("no new contacts with images received") }, options) if !expectPicture { s.Require().EqualError(err, "no new contacts with images received") return } s.Require().NoError(err) s.Require().NotNil(contacts) // Check if alice's contact data with profile picture is there var contact *Contact for _, c := range contacts { if c.ID == alice.IdentityPublicKeyString() { contact = c } } s.Require().NotNil(contact) // Check that Bob now has Alice's profile picture(s) switch args.chatContext { case publicChat: // In public chat context we only need the images.SmallDimName, but also may have the large s.Require().GreaterOrEqual(len(contact.Images), 1) s.Require().Contains(contact.Images, images.SmallDimName) s.Require().Equal(iis[images.SmallDimName].Payload, contact.Images[images.SmallDimName].Payload) case privateChat: s.Require().Equal(len(contact.Images), 2) s.Require().Contains(contact.Images, images.SmallDimName) s.Require().Contains(contact.Images, images.LargeDimName) s.Require().Equal(iis[images.SmallDimName].Payload, contact.Images[images.SmallDimName].Payload) s.Require().Equal(iis[images.LargeDimName].Payload, contact.Images[images.LargeDimName].Payload) } } type e2eArgs struct { chatContext ChatContext showToType settings.ProfilePicturesShowToType visibilityType settings.ProfilePicturesVisibilityType aliceContact bool bobContact bool } func (args *e2eArgs) String() string { return fmt.Sprintf("ChatContext: %s, ShowTo: %s, Visibility: %s, AliceContact: %t, BobContact: %t", string(args.chatContext), profilePicShowSettingsMap[args.showToType], profilePicViewSettingsMap[args.visibilityType], args.aliceContact, args.bobContact, ) } func (args *e2eArgs) TestCaseName(t *testing.T) string { expected, err := args.resultExpected() require.NoError(t, err) return fmt.Sprintf("%s-%s-%s-ac.%t-bc.%t-exp.%t", string(args.chatContext), profilePicShowSettingsMap[args.showToType], profilePicViewSettingsMap[args.visibilityType], args.aliceContact, args.bobContact, expected, ) } func (args *e2eArgs) resultExpected() (bool, error) { switch args.showToType { case settings.ProfilePicturesShowToContactsOnly: if args.aliceContact { return args.resultExpectedVS() } return false, nil case settings.ProfilePicturesShowToEveryone: return args.resultExpectedVS() case settings.ProfilePicturesShowToNone: return false, nil default: return false, errors.New("unknown ProfilePicturesShowToType") } } func (args *e2eArgs) resultExpectedVS() (bool, error) { switch args.visibilityType { case settings.ProfilePicturesVisibilityContactsOnly: return true, nil case settings.ProfilePicturesVisibilityEveryone: return true, nil case settings.ProfilePicturesVisibilityNone: // If we are contacts, we save the image regardless return args.bobContact, nil default: return false, errors.New("unknown ProfilePicturesVisibilityType") } } var profilePicShowSettingsMap = map[settings.ProfilePicturesShowToType]string{ settings.ProfilePicturesShowToContactsOnly: "ShowToContactsOnly", settings.ProfilePicturesShowToEveryone: "ShowToEveryone", settings.ProfilePicturesShowToNone: "ShowToNone", } var profilePicViewSettingsMap = map[settings.ProfilePicturesVisibilityType]string{ settings.ProfilePicturesVisibilityContactsOnly: "ViewFromContactsOnly", settings.ProfilePicturesVisibilityEveryone: "ViewFromEveryone", settings.ProfilePicturesVisibilityNone: "ViewFromNone", }