fix: shared links and link previews contain full self information (#4169)

* fix(StatusUnfurler): allow contact without icon
This commit is contained in:
Igor Sirotin 2023-10-24 11:15:32 +01:00 committed by GitHub
parent 311e463eed
commit e83be20def
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 398 additions and 50 deletions

View File

@ -95,9 +95,13 @@ type MultiAccountMarshaller interface {
ToMultiAccount() *Account
}
type IdentityImageSubscriptionChange struct {
PublishExpected bool
}
type Database struct {
db *sql.DB
identityImageSubscriptions []chan struct{}
identityImageSubscriptions []chan *IdentityImageSubscriptionChange
}
// InitializeDB creates db file at a given path and applies migrations.
@ -422,24 +426,24 @@ func (db *Database) StoreIdentityImages(keyUID string, iis []images.IdentityImag
}
}
if publish {
db.publishOnIdentityImageSubscriptions()
}
db.publishOnIdentityImageSubscriptions(&IdentityImageSubscriptionChange{
PublishExpected: publish,
})
return nil
}
func (db *Database) SubscribeToIdentityImageChanges() chan struct{} {
s := make(chan struct{}, 100)
func (db *Database) SubscribeToIdentityImageChanges() chan *IdentityImageSubscriptionChange {
s := make(chan *IdentityImageSubscriptionChange, 100)
db.identityImageSubscriptions = append(db.identityImageSubscriptions, s)
return s
}
func (db *Database) publishOnIdentityImageSubscriptions() {
func (db *Database) publishOnIdentityImageSubscriptions(change *IdentityImageSubscriptionChange) {
// Publish on channels, drop if buffer is full
for _, s := range db.identityImageSubscriptions {
select {
case s <- struct{}{}:
case s <- change:
default:
log.Warn("subscription channel full, dropping message")
}

View File

@ -8,6 +8,8 @@ import (
"sync"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/common/dbsetup"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/errors"
@ -30,6 +32,7 @@ var (
type Database struct {
db *sql.DB
SyncQueue chan SyncSettingField
changesSubscriptions []chan *SyncSettingField
notifier Notifier
}
@ -254,6 +257,9 @@ func (db *Database) parseSaveAndSyncSetting(sf SettingField, value interface{})
if sf.CanSync(FromInterface) {
db.SyncQueue <- SyncSettingField{sf, value}
}
db.postChangesToSubscribers(&SyncSettingField{sf, value})
return nil
}
@ -280,7 +286,8 @@ func (db *Database) DeleteMnemonic() error {
}
// SaveSyncSetting stores setting data from a sync protobuf source, note it does not call SettingField.ValueHandler()
// nor does this function attempt to write to the Database.SyncQueue
// nor does this function attempt to write to the Database.SyncQueue,
// yet it still writes to Database.changesSubscriptions.
func (db *Database) SaveSyncSetting(setting SettingField, value interface{}, clock uint64) error {
ls, err := db.GetSettingLastSynced(setting)
if err != nil {
@ -295,7 +302,13 @@ func (db *Database) SaveSyncSetting(setting SettingField, value interface{}, clo
return err
}
return db.saveSetting(setting, value)
err = db.saveSetting(setting, value)
if err != nil {
return err
}
db.postChangesToSubscribers(&SyncSettingField{setting, value})
return nil
}
func (db *Database) GetSettingLastSynced(setting SettingField) (result uint64, err error) {
@ -712,3 +725,20 @@ func (db *Database) URLUnfurlingMode() (result int64, err error) {
}
return result, err
}
func (db *Database) SubscribeToChanges() chan *SyncSettingField {
s := make(chan *SyncSettingField, 100)
db.changesSubscriptions = append(db.changesSubscriptions, s)
return s
}
func (db *Database) postChangesToSubscribers(change *SyncSettingField) {
// Publish on channels, drop if buffer is full
for _, s := range db.changesSubscriptions {
select {
case s <- change:
default:
log.Warn("settings changes subscription channel full, dropping message")
}
}
}

View File

@ -10,6 +10,8 @@ import (
"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/accounts"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/identity"
@ -386,6 +388,41 @@ func buildContact(publicKeyString string, publicKey *ecdsa.PublicKey) (*Contact,
return contact, nil
}
func buildSelfContact(identity *ecdsa.PrivateKey, settings *accounts.Database, multiAccounts *multiaccounts.Database, account *multiaccounts.Account) (*Contact, error) {
myPublicKeyString := types.EncodeHex(crypto.FromECDSAPub(&identity.PublicKey))
c, err := buildContact(myPublicKeyString, &identity.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to build contact: %w", err)
}
if settings != nil {
if s, err := settings.GetSettings(); err == nil {
c.DisplayName = s.DisplayName
c.Bio = s.Bio
if s.PreferredName != nil {
c.EnsName = *s.PreferredName
}
}
if socialLinks, err := settings.GetSocialLinks(); err != nil {
c.SocialLinks = socialLinks
}
}
if multiAccounts != nil && account != nil {
if identityImages, err := multiAccounts.GetIdentityImages(account.KeyUID); err != nil {
imagesMap := make(map[string]images.IdentityImage)
for _, img := range identityImages {
imagesMap[img.Name] = *img
}
c.Images = imagesMap
}
}
return c, nil
}
func contactIDFromPublicKey(key *ecdsa.PublicKey) string {
return types.EncodeHex(crypto.FromECDSAPub(key))
}

View File

@ -20,7 +20,7 @@ type StatusUnfurler struct {
func NewStatusUnfurler(URL string, messenger *Messenger, logger *zap.Logger) *StatusUnfurler {
return &StatusUnfurler{
m: messenger,
logger: logger,
logger: logger.With(zap.String("url", URL)),
url: URL,
}
}
@ -30,18 +30,20 @@ func updateThumbnail(image *images.IdentityImage, thumbnail *common.LinkPreviewT
return nil
}
var err error
thumbnail.Width, thumbnail.Height, err = images.GetImageDimensions(image.Payload)
width, height, err := images.GetImageDimensions(image.Payload)
if err != nil {
return fmt.Errorf("failed to get image dimensions: %w", err)
}
thumbnail.DataURI, err = image.GetDataURI()
dataURI, err := image.GetDataURI()
if err != nil {
return fmt.Errorf("failed to get data uri: %w", err)
}
thumbnail.Width = width
thumbnail.Height = height
thumbnail.DataURI = dataURI
return nil
}
@ -67,7 +69,7 @@ func (u *StatusUnfurler) buildContactData(publicKey string) (*common.StatusConta
if image, ok := contact.Images[images.SmallDimName]; ok {
if err = updateThumbnail(&image, &c.Icon); err != nil {
return nil, fmt.Errorf("failed to set thumbnail: %w", err)
u.logger.Warn("unfurling status link: failed to set contact thumbnail", zap.Error(err))
}
}

View File

@ -127,6 +127,7 @@ type Messenger struct {
shouldPublishContactCode bool
systemMessagesTranslations *systemMessageTranslationsMap
allChats *chatMap
selfContact *Contact
allContacts *contactMap
allInstallations *installationMap
modifiedInstallations *stringBoolMap
@ -479,10 +480,9 @@ func NewMessenger(
savedAddressesManager := wallet.NewSavedAddressesManager(c.walletDb)
myPublicKeyString := types.EncodeHex(crypto.FromECDSAPub(&identity.PublicKey))
myContact, err := buildContact(myPublicKeyString, &identity.PublicKey)
selfContact, err := buildSelfContact(identity, settings, c.multiAccount, c.account)
if err != nil {
return nil, errors.New("failed to build contact of ourself: " + err.Error())
return nil, fmt.Errorf("failed to build contact of ourself: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
@ -511,9 +511,10 @@ func NewMessenger(
featureFlags: c.featureFlags,
systemMessagesTranslations: c.systemMessagesTranslations,
allChats: new(chatMap),
selfContact: selfContact,
allContacts: &contactMap{
logger: logger,
me: myContact,
me: selfContact,
},
allInstallations: new(installationMap),
installationID: installationID,
@ -816,6 +817,7 @@ func (m *Messenger) Start() (*MessengerResponse, error) {
return nil, err
}
m.startSyncSettingsLoop()
m.startSettingsChangesLoop()
m.startCommunityRekeyLoop()
m.startCuratedCommunitiesUpdateLoop()
@ -1547,8 +1549,21 @@ func (m *Messenger) watchIdentityImageChanges() {
go func() {
for {
select {
case <-channel:
err := m.syncProfilePictures(m.dispatchMessage)
case change := <-channel:
identityImages, err := m.multiAccounts.GetIdentityImages(m.account.KeyUID)
if err != nil {
m.logger.Error("failed to get profile pictures to save self contact", zap.Error(err))
break
}
identityImagesMap := make(map[string]images.IdentityImage)
for _, img := range identityImages {
identityImagesMap[img.Name] = *img
}
m.selfContact.Images = identityImagesMap
if change.PublishExpected {
err = m.syncProfilePictures(m.dispatchMessage, identityImages)
if err != nil {
m.logger.Error("failed to sync profile pictures to paired devices", zap.Error(err))
}
@ -1556,6 +1571,7 @@ func (m *Messenger) watchIdentityImageChanges() {
if err != nil {
m.logger.Error("failed to publish identity image", zap.Error(err))
}
}
case <-m.quit:
return
}
@ -2464,23 +2480,26 @@ func (m *Messenger) ShareImageMessage(request *requests.ShareImageMessage) (*Mes
return response, nil
}
func (m *Messenger) syncProfilePictures(rawMessageHandler RawMessageHandler) error {
if !m.hasPairedDevices() {
return nil
}
func (m *Messenger) syncProfilePicturesFromDatabase(rawMessageHandler RawMessageHandler) error {
keyUID := m.account.KeyUID
images, err := m.multiAccounts.GetIdentityImages(keyUID)
identityImages, err := m.multiAccounts.GetIdentityImages(keyUID)
if err != nil {
return err
}
return m.syncProfilePictures(rawMessageHandler, identityImages)
}
func (m *Messenger) syncProfilePictures(rawMessageHandler RawMessageHandler, identityImages []*images.IdentityImage) error {
if !m.hasPairedDevices() {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
pictures := make([]*protobuf.SyncProfilePicture, len(images))
pictures := make([]*protobuf.SyncProfilePicture, len(identityImages))
clock, chat := m.getLastClockWithRelatedChat()
for i, image := range images {
for i, image := range identityImages {
p := &protobuf.SyncProfilePicture{}
p.Name = image.Name
p.Payload = image.Payload
@ -2497,7 +2516,7 @@ func (m *Messenger) syncProfilePictures(rawMessageHandler RawMessageHandler) err
}
message := &protobuf.SyncProfilePictures{}
message.KeyUid = keyUID
message.KeyUid = m.account.KeyUID
message.Pictures = pictures
encodedMessage, err := proto.Marshal(message)
@ -2606,7 +2625,7 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string,
return err
}
err = m.syncProfilePictures(rawMessageHandler)
err = m.syncProfilePicturesFromDatabase(rawMessageHandler)
if err != nil {
return err
}

View File

@ -756,8 +756,14 @@ func (m *Messenger) BlockedContacts() []*Contact {
return contacts
}
// GetContactByID assumes pubKey includes 0x prefix
// GetContactByID returns a Contact for given pubKey, if it's known.
// This function automatically checks if pubKey is self identity key and returns a Contact
// filled with self information.
// pubKey is assumed to include `0x` prefix
func (m *Messenger) GetContactByID(pubKey string) *Contact {
if pubKey == m.IdentityPublicKeyString() {
return m.selfContact
}
contact, _ := m.allContacts.Load(pubKey)
return contact
}

View File

@ -0,0 +1,86 @@
package protocol
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/protocol/identity"
)
func TestMessengerContacts(t *testing.T) {
suite.Run(t, new(MessengerContactsTestSuite))
}
type MessengerContactsTestSuite struct {
MessengerBaseTestSuite
}
func (s *MessengerContactsTestSuite) Test_SelfContact() {
const timeout = 1 * time.Second
profileKp := accounts.GetProfileKeypairForTest(true, false, false)
profileKp.KeyUID = s.m.account.KeyUID
profileKp.Accounts[0].KeyUID = s.m.account.KeyUID
err := s.m.settings.SaveOrUpdateKeypair(profileKp)
s.Require().NoError(err)
// Create values
displayName := "DisplayName_1"
bio := "Bio_1"
ensName := "EnsName_1.eth"
socialLinks := identity.SocialLinks{{Text: identity.TelegramID, URL: "dummy.telegram"}}
identityImages := images.SampleIdentityImages()
identityImagesMap := make(map[string]images.IdentityImage)
for _, img := range identityImages {
img.KeyUID = s.m.account.KeyUID
identityImagesMap[img.Name] = img
}
// Set values stored in settings
settingsList := []string{settings.DisplayName.GetReactName(), settings.PreferredName.GetReactName(), settings.Bio.GetReactName()}
setSettingsValues := func() {
err := s.m.SetDisplayName(displayName)
s.Require().NoError(err)
err = s.m.SetBio(bio)
s.Require().NoError(err)
err = s.m.settings.SaveSettingField(settings.PreferredName, ensName)
s.Require().NoError(err)
}
SetSettingsAndWaitForChange(&s.Suite, s.m, settingsList, timeout, setSettingsValues)
// Set values stored in multiaccounts
setIdentityImages := func() {
err := s.m.multiAccounts.StoreIdentityImages(s.m.account.KeyUID, identityImages, false)
s.Require().NoError(err)
}
SetIdentityImagesAndWaitForChange(&s.Suite, s.m.multiAccounts, timeout, setIdentityImages)
// Set social links. They are applied immediately, no need to wait.
err = s.m.AddOrReplaceSocialLinks(socialLinks)
s.Require().NoError(err)
// Check values
selfContact := s.m.GetContactByID(s.m.IdentityPublicKeyString())
s.Require().NotNil(selfContact)
s.Require().Equal(displayName, selfContact.DisplayName)
s.Require().Equal(bio, selfContact.Bio)
s.Require().Equal(ensName, selfContact.EnsName)
s.Require().Equal(socialLinks, selfContact.SocialLinks)
s.Require().Equal(identityImagesMap, selfContact.Images)
}

View File

@ -180,6 +180,7 @@ func (m *Messenger) AddOrReplaceSocialLinks(socialLinks identity.SocialLinks) er
if err != nil {
return err
}
m.selfContact.SocialLinks = socialLinks
err = m.syncSocialLinks(context.Background(), m.dispatchMessage)
return err

View File

@ -15,12 +15,19 @@ import (
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
)
const (
exampleIdenticonURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixA" +
"AAAiklEQVR4nOzWwQmFQAwG4ffEXmzLIizDImzLarQBhSwSGH7mO+9hh0DI9AthCI0hNIbQGEJjCI0hNIbQxITM1YfHfl69X3m2bsu/8i5mI" +
"obQGEJjCI0hNIbQlG+tUW83UtfNFjMRQ2gMofm8tUa3U9c2i5mIITSGqEnMRAyhMYTGEBpDaO4AAAD//5POEGncqtj1AAAAAElFTkSuQmCC"
)
func TestMessengerLinkPreviews(t *testing.T) {
suite.Run(t, new(MessengerLinkPreviewsTestSuite))
}
@ -394,9 +401,7 @@ func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContactAdded() {
shortKey, err := s.m.SerializePublicKey(crypto.CompressPubkey(pubkey))
s.Require().NoError(err)
payload, err := images.GetPayloadFromURI("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixA" +
"AAAiklEQVR4nOzWwQmFQAwG4ffEXmzLIizDImzLarQBhSwSGH7mO+9hh0DI9AthCI0hNIbQGEJjCI0hNIbQxITM1YfHfl69X3m2bsu/8i5mI" +
"obQGEJjCI0hNIbQlG+tUW83UtfNFjMRQ2gMofm8tUa3U9c2i5mIITSGqEnMRAyhMYTGEBpDaO4AAAD//5POEGncqtj1AAAAAElFTkSuQmCC")
payload, err := images.GetPayloadFromURI(exampleIdenticonURI)
s.Require().NoError(err)
icon := images.IdentityImage{
@ -406,7 +411,7 @@ func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContactAdded() {
}
c.Bio = "TestBio_1"
c.DisplayName = "TestDisplayName_2"
c.DisplayName = "TestDisplayName_1"
c.Images = map[string]images.IdentityImage{}
c.Images[images.SmallDimName] = icon
s.m.allContacts.Store(c.ID, c)
@ -443,6 +448,84 @@ func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContactAdded() {
s.Require().Equal(expectedDataURI, preview.Contact.Icon.DataURI)
}
func (s *MessengerLinkPreviewsTestSuite) setProfileParameters(messenger *Messenger, displayName string, bio string, identityImages []images.IdentityImage) {
const timeout = 1 * time.Second
settingsList := []string{
settings.DisplayName.GetReactName(),
settings.Bio.GetReactName(),
}
SetSettingsAndWaitForChange(&s.Suite, messenger, settingsList, timeout, func() {
err := messenger.SetDisplayName(displayName)
s.Require().NoError(err)
err = messenger.SetBio(bio)
s.Require().NoError(err)
})
SetIdentityImagesAndWaitForChange(&s.Suite, messenger.multiAccounts, timeout, func() {
err := messenger.multiAccounts.StoreIdentityImages(messenger.account.KeyUID, identityImages, false)
s.Require().NoError(err)
})
}
func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_SelfLink() {
shortKey, err := s.m.SerializePublicKey(crypto.CompressPubkey(s.m.IdentityPublicKey()))
s.Require().NoError(err)
profileKp := accounts.GetProfileKeypairForTest(true, false, false)
profileKp.KeyUID = s.m.account.KeyUID
profileKp.Accounts[0].KeyUID = s.m.account.KeyUID
err = s.m.settings.SaveOrUpdateKeypair(profileKp)
s.Require().NoError(err)
// Set initial profile parameters
identityImages := images.SampleIdentityImages()
s.setProfileParameters(s.m, "TestDisplayName_3", "TestBio_3", identityImages)
// Generate a shared URL
u, err := s.m.ShareUserURLWithData(s.m.IdentityPublicKeyString())
s.Require().NoError(err)
// Update contact info locally after creating the shared URL
// This is required to test that URL-decoded data is not used in the preview.
iconPayload, err := images.GetPayloadFromURI(exampleIdenticonURI)
s.Require().NoError(err)
icon := images.IdentityImage{
Name: images.SmallDimName,
Width: 50,
Height: 50,
Payload: iconPayload,
}
s.setProfileParameters(s.m, "TestDisplayName_4", "TestBio_4", []images.IdentityImage{icon})
r, err := s.m.UnfurlURLs(nil, []string{u})
s.Require().NoError(err)
s.Require().Len(r.StatusLinkPreviews, 1)
s.Require().Len(r.LinkPreviews, 0)
userSettings, err := s.m.getSettings()
s.Require().NoError(err)
preview := r.StatusLinkPreviews[0]
s.Require().Equal(u, preview.URL)
s.Require().Nil(preview.Community)
s.Require().Nil(preview.Channel)
s.Require().NotNil(preview.Contact)
s.Require().Equal(shortKey, preview.Contact.PublicKey)
s.Require().Equal(userSettings.DisplayName, preview.Contact.DisplayName)
s.Require().Equal(userSettings.Bio, preview.Contact.Description)
s.Require().Equal(icon.Width, preview.Contact.Icon.Width)
s.Require().Equal(icon.Height, preview.Contact.Icon.Height)
s.Require().Equal("", preview.Contact.Icon.URL)
expectedDataURI, err := images.GetPayloadDataURI(icon.Payload)
s.Require().NoError(err)
s.Require().Equal(expectedDataURI, preview.Contact.Icon.DataURI)
}
func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusCommunityJoined() {
description := &requests.CreateCommunity{

View File

@ -435,8 +435,8 @@ func (m *Messenger) parseUserURLWithChatKey(urlData string) (*URLDataResponse, e
contactID := common.PubkeyToHex(pubKey)
contact, ok := m.allContacts.Load(contactID)
if !ok {
contact := m.GetContactByID(contactID)
if contact == nil {
return nil, ErrContactNotFound
}
@ -446,8 +446,8 @@ func (m *Messenger) parseUserURLWithChatKey(urlData string) (*URLDataResponse, e
}
func (m *Messenger) ShareUserURLWithENS(contactID string) (string, error) {
contact, ok := m.allContacts.Load(contactID)
if !ok {
contact := m.GetContactByID(contactID)
if contact == nil {
return "", ErrContactNotFound
}
return fmt.Sprintf("%s/u#%s", baseShareURL, contact.EnsName), nil
@ -497,8 +497,8 @@ func (m *Messenger) prepareEncodedUserData(contact *Contact) (string, string, er
}
func (m *Messenger) ShareUserURLWithData(contactID string) (string, error) {
contact, ok := m.allContacts.Load(contactID)
if !ok {
contact := m.GetContactByID(contactID)
if contact == nil {
return "", ErrContactNotFound
}

View File

@ -159,3 +159,24 @@ func (m *Messenger) startSyncSettingsLoop() {
}
}()
}
func (m *Messenger) startSettingsChangesLoop() {
channel := m.settings.SubscribeToChanges()
go func() {
for {
select {
case s := <-channel:
switch s.GetReactName() {
case settings.DisplayName.GetReactName():
m.selfContact.DisplayName = s.Value.(string)
case settings.PreferredName.GetReactName():
m.selfContact.EnsName = s.Value.(string)
case settings.Bio.GetReactName():
m.selfContact.Bio = s.Value.(string)
}
case <-m.quit:
return
}
}
}()
}

View File

@ -3,10 +3,12 @@ package protocol
import (
"context"
"errors"
"sync"
"time"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/tt"
@ -132,3 +134,60 @@ func PairDevices(s *suite.Suite, device1, device2 *Messenger) {
err = device2.EnableInstallation(device1.installationID)
s.Require().NoError(err)
}
func SetSettingsAndWaitForChange(s *suite.Suite, messenger *Messenger, settingsReactNames []string, timeout time.Duration, actionCallback func()) {
changedSettings := map[string]struct{}{}
wg := sync.WaitGroup{}
for _, reactName := range settingsReactNames {
wg.Add(1)
settingReactName := reactName // Loop variables captured by 'func' literals in 'go' statements might have unexpected values
channel := messenger.settings.SubscribeToChanges()
go func() {
defer wg.Done()
for {
select {
case setting := <-channel:
if setting.GetReactName() == settingReactName {
changedSettings[settingReactName] = struct{}{}
return
}
case <-time.After(timeout):
return
}
}
}()
}
actionCallback()
wg.Wait()
s.Require().Len(changedSettings, len(settingsReactNames))
for _, reactName := range settingsReactNames {
_, ok := changedSettings[reactName]
s.Require().True(ok)
}
}
func SetIdentityImagesAndWaitForChange(s *suite.Suite, multiAccounts *multiaccounts.Database, timeout time.Duration, actionCallback func()) {
channel := multiAccounts.SubscribeToIdentityImageChanges()
ok := false
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-channel:
ok = true
case <-time.After(timeout):
return
}
}()
actionCallback()
wg.Wait()
s.Require().True(ok)
}