status-go/protocol/messenger_testing_utils.go

426 lines
10 KiB
Go
Raw Normal View History

package protocol
import (
"context"
"crypto/rand"
"errors"
"fmt"
"math/big"
"sync"
"time"
"github.com/status-im/status-go/protocol/identity"
waku2 "github.com/status-im/status-go/wakuv2"
"golang.org/x/exp/maps"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/tt"
)
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
var hexRunes = []rune("0123456789abcdef")
// WaitOnMessengerResponse Wait until the condition is true or the timeout is reached.
func WaitOnMessengerResponse(m *Messenger, condition func(*MessengerResponse) bool, errorMessage string) (*MessengerResponse, error) {
response := &MessengerResponse{}
err := tt.RetryWithBackOff(func() error {
var err error
r, err := m.RetrieveAll()
if err != nil {
panic(err)
}
if err := response.Merge(r); err != nil {
panic(err)
}
if err == nil && !condition(response) {
err = errors.New(errorMessage)
}
return err
})
return response, err
}
type MessengerSignalsHandlerMock struct {
MessengerSignalsHandler
responseChan chan *MessengerResponse
communityFoundChan chan *communities.Community
}
func (m *MessengerSignalsHandlerMock) MessengerResponse(response *MessengerResponse) {
// Non-blocking send
select {
case m.responseChan <- response:
default:
}
}
func (m *MessengerSignalsHandlerMock) MessageDelivered(chatID string, messageID string) {}
func (m *MessengerSignalsHandlerMock) CommunityInfoFound(community *communities.Community) {
select {
case m.communityFoundChan <- community:
default:
}
}
func WaitOnSignaledMessengerResponse(m *Messenger, condition func(*MessengerResponse) bool, errorMessage string) (*MessengerResponse, error) {
interval := 500 * time.Millisecond
timeoutChan := time.After(10 * time.Second)
if m.config.messengerSignalsHandler != nil {
return nil, errors.New("messengerSignalsHandler already provided/mocked")
}
responseChan := make(chan *MessengerResponse, 1)
m.config.messengerSignalsHandler = &MessengerSignalsHandlerMock{
responseChan: responseChan,
}
defer func() {
m.config.messengerSignalsHandler = nil
}()
for {
_, err := m.RetrieveAll()
if err != nil {
return nil, err
}
select {
case r := <-responseChan:
if condition(r) {
return r, nil
}
return nil, errors.New(errorMessage)
case <-timeoutChan:
return nil, errors.New("timed out: " + errorMessage)
default: // No immediate response, rest & loop back to retrieve again
time.Sleep(interval)
}
}
}
func WaitOnSignaledCommunityFound(m *Messenger, action func(), condition func(community *communities.Community) bool, timeout time.Duration, errorMessage string) error {
timeoutChan := time.After(timeout)
if m.config.messengerSignalsHandler != nil {
return errors.New("messengerSignalsHandler already provided/mocked")
}
communityFoundChan := make(chan *communities.Community, 1)
m.config.messengerSignalsHandler = &MessengerSignalsHandlerMock{
communityFoundChan: communityFoundChan,
}
defer func() {
m.config.messengerSignalsHandler = nil
}()
// Call the action after setting up the mock
action()
// Wait for condition after
for {
select {
case c := <-communityFoundChan:
if condition(c) {
return nil
}
case <-timeoutChan:
return errors.New("timed out: " + errorMessage)
}
}
}
func WaitForConnectionStatus(s *suite.Suite, waku *waku2.Waku, action func() bool) {
subscription := waku.SubscribeToConnStatusChanges()
defer subscription.Unsubscribe()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Action should return the desired online status
wantedOnline := action()
for {
select {
case status := <-subscription.C:
if status.IsOnline == wantedOnline {
return
}
case <-ctx.Done():
s.Require().Fail(fmt.Sprintf("timeout waiting for waku connection status '%t'", wantedOnline))
return
}
}
}
func WaitForPeerConnected(s *suite.Suite, waku *waku2.Waku, action func() string) {
subscription := waku.SubscribeToConnStatusChanges()
defer subscription.Unsubscribe()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Action should return the desired peer ID
peerID := action()
if _, ok := waku.Peers()[peerID]; ok {
return
}
for {
select {
case status := <-subscription.C:
if _, ok := status.Peers[peerID]; ok {
return
}
case <-ctx.Done():
s.Require().Fail(fmt.Sprintf("timeout waiting for peer connected '%s'", peerID))
return
}
}
}
func FindFirstByContentType(messages []*common.Message, contentType protobuf.ChatMessage_ContentType) *common.Message {
for _, message := range messages {
if message.ContentType == contentType {
return message
}
}
return nil
}
func PairDevices(s *suite.Suite, device1, device2 *Messenger) {
// Send pairing data
response, err := device1.SendPairInstallation(context.Background(), nil)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Len(response.Chats(), 1)
s.False(response.Chats()[0].Active)
i, ok := device1.allInstallations.Load(device1.installationID)
s.Require().True(ok)
// Wait for the message to reach its destination
response, err = WaitOnMessengerResponse(
device2,
func(r *MessengerResponse) bool {
for _, installation := range r.Installations {
if installation.ID == device1.installationID {
return installation.InstallationMetadata != nil &&
i.InstallationMetadata.Name == installation.InstallationMetadata.Name &&
i.InstallationMetadata.DeviceType == installation.InstallationMetadata.DeviceType
}
}
return false
},
"installation not received",
)
s.Require().NoError(err)
s.Require().NotNil(response)
// Ensure installation is enabled
err = device2.EnableInstallation(device1.installationID)
s.Require().NoError(err)
}
func SetSettingsAndWaitForChange(s *suite.Suite, messenger *Messenger, timeout time.Duration,
actionCallback func(), eventCallback func(*SelfContactChangeEvent) bool) {
allEventsReceived := false
channel := messenger.SubscribeToSelfContactChanges()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for !allEventsReceived {
select {
case event := <-channel:
allEventsReceived = eventCallback(event)
case <-time.After(timeout):
return
}
}
}()
actionCallback()
wg.Wait()
s.Require().True(allEventsReceived)
}
func SetIdentityImagesAndWaitForChange(s *suite.Suite, messenger *Messenger, timeout time.Duration, actionCallback func()) {
channel := messenger.SubscribeToSelfContactChanges()
ok := false
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
select {
case event := <-channel:
if event.ImagesChanged {
ok = true
}
case <-time.After(timeout):
return
}
}()
actionCallback()
wg.Wait()
s.Require().True(ok)
}
func WaitForAvailableStoreNode(s *suite.Suite, m *Messenger, timeout time.Duration) {
available := m.waitForAvailableStoreNode(timeout)
s.Require().True(available)
}
func TearDownMessenger(s *suite.Suite, m *Messenger) {
if m == nil {
return
}
s.Require().NoError(m.Shutdown())
if m.database != nil {
s.Require().NoError(m.database.Close())
}
if m.multiAccounts != nil {
s.Require().NoError(m.multiAccounts.Close())
}
}
func randomInt(length int) int {
max := big.NewInt(int64(length))
value, err := rand.Int(rand.Reader, max)
if err != nil {
panic(err)
}
return int(value.Int64())
}
func randomString(length int, runes []rune) string {
out := make([]rune, length)
for i := range out {
out[i] = runes[randomInt(len(runes))] // nolint: gosec
}
return string(out)
}
func RandomLettersString(length int) string {
return randomString(length, letterRunes)
}
func RandomColor() string {
return "#" + randomString(6, hexRunes)
}
func RandomCommunityTags(count int) []string {
all := maps.Keys(requests.TagsEmojies)
tags := make([]string, 0, count)
indexes := map[int]struct{}{}
for len(indexes) != count {
index := randomInt(len(all))
indexes[index] = struct{}{}
}
for index := range indexes {
tags = append(tags, all[index])
}
return tags
}
func RandomBytes(length int) []byte {
out := make([]byte, length)
_, err := rand.Read(out)
if err != nil {
panic(err)
}
return out
}
func DummyProfileShowcasePreferences() *identity.ProfileShowcasePreferences {
return &identity.ProfileShowcasePreferences{
Communities: []*identity.ProfileShowcaseCommunityPreference{}, // empty to avoid fetching
Accounts: []*identity.ProfileShowcaseAccountPreference{
{
Address: "0x32433445133424",
Name: "Status Account",
ColorID: "blue",
Emoji: "-_-",
ShowcaseVisibility: identity.ProfileShowcaseVisibilityEveryone,
Order: 0,
},
{
Address: "0x3845354643324",
Name: "Money Box",
ColorID: "red",
Emoji: ":o)",
ShowcaseVisibility: identity.ProfileShowcaseVisibilityContacts,
Order: 1,
},
},
Collectibles: []*identity.ProfileShowcaseCollectiblePreference{
{
ContractAddress: "0x12378534257568678487683576",
ChainID: 1,
TokenID: "0x12321389592999f903",
CommunityID: "0x01312357798976535",
AccountAddress: "0x32433445133424",
ShowcaseVisibility: identity.ProfileShowcaseVisibilityEveryone,
Order: 0,
},
},
VerifiedTokens: []*identity.ProfileShowcaseVerifiedTokenPreference{
{
Symbol: "ETH",
ShowcaseVisibility: identity.ProfileShowcaseVisibilityEveryone,
Order: 1,
},
{
Symbol: "DAI",
ShowcaseVisibility: identity.ProfileShowcaseVisibilityIDVerifiedContacts,
Order: 2,
},
{
Symbol: "SNT",
ShowcaseVisibility: identity.ProfileShowcaseVisibilityNoOne,
Order: 3,
},
},
UnverifiedTokens: []*identity.ProfileShowcaseUnverifiedTokenPreference{
{
ContractAddress: "0x454525452023452",
ChainID: 3,
ShowcaseVisibility: identity.ProfileShowcaseVisibilityEveryone,
Order: 0,
},
{
ContractAddress: "0x12312323323233",
ChainID: 6,
ShowcaseVisibility: identity.ProfileShowcaseVisibilityContacts,
Order: 1,
},
},
}
}