479 lines
15 KiB
Go
479 lines
15 KiB
Go
package protocol
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/status-im/status-go/multiaccounts/accounts"
|
|
|
|
gethbridge "github.com/status-im/status-go/eth-node/bridge/geth"
|
|
"github.com/status-im/status-go/protocol/common"
|
|
"github.com/status-im/status-go/protocol/communities"
|
|
"github.com/status-im/status-go/protocol/tt"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/status-im/status-go/appdatabase"
|
|
"github.com/status-im/status-go/eth-node/types"
|
|
"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/t/helpers"
|
|
|
|
mailserversDB "github.com/status-im/status-go/services/mailservers"
|
|
waku2 "github.com/status-im/status-go/wakuv2"
|
|
)
|
|
|
|
const (
|
|
localFleet = "local-test-fleet-1"
|
|
localMailserverID = "local-test-mailserver"
|
|
storeNodeConnectTimeout = 500 * time.Millisecond
|
|
runLocalTests = false
|
|
)
|
|
|
|
func TestMessengerStoreNodeRequestSuite(t *testing.T) {
|
|
suite.Run(t, new(MessengerStoreNodeRequestSuite))
|
|
}
|
|
|
|
type MessengerStoreNodeRequestSuite struct {
|
|
suite.Suite
|
|
|
|
cancel chan struct{}
|
|
|
|
owner *Messenger
|
|
bob *Messenger
|
|
|
|
wakuStoreNode *waku2.Waku
|
|
storeNodeAddress string
|
|
|
|
ownerWaku types.Waku
|
|
bobWaku types.Waku
|
|
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) SetupTest() {
|
|
cfg := zap.NewDevelopmentConfig()
|
|
cfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
|
|
cfg.Development = false
|
|
cfg.DisableStacktrace = true
|
|
s.logger = tt.MustCreateTestLoggerWithConfig(cfg)
|
|
|
|
s.cancel = make(chan struct{}, 10)
|
|
|
|
storeNodeLogger := s.logger.Named("store-node-waku")
|
|
s.wakuStoreNode = NewWakuV2(&s.Suite, storeNodeLogger, true, true)
|
|
|
|
storeNodeListenAddresses := s.wakuStoreNode.ListenAddresses()
|
|
s.Require().LessOrEqual(1, len(storeNodeListenAddresses))
|
|
|
|
s.storeNodeAddress = storeNodeListenAddresses[0]
|
|
s.logger.Info("store node ready", zap.String("address", s.storeNodeAddress))
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TearDown() {
|
|
close(s.cancel)
|
|
s.wakuStoreNode.Stop() // nolint: errcheck
|
|
s.owner.Shutdown() // nolint: errcheck
|
|
s.bob.Shutdown() // nolint: errcheck
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) createOwner() {
|
|
wakuLogger := s.logger.Named("owner-waku-node")
|
|
wakuV2 := NewWakuV2(&s.Suite, wakuLogger, true, false)
|
|
s.ownerWaku = gethbridge.NewGethWakuV2Wrapper(wakuV2)
|
|
|
|
messengerLogger := s.logger.Named("owner-messenger")
|
|
s.owner = s.newMessenger(s.ownerWaku, messengerLogger, s.storeNodeAddress)
|
|
|
|
// We force the owner to use the store node as relay peer
|
|
err := s.owner.DialPeer(s.storeNodeAddress)
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) createBob() {
|
|
wakuLogger := s.logger.Named("bob-waku-node")
|
|
wakuV2 := NewWakuV2(&s.Suite, wakuLogger, true, false)
|
|
s.bobWaku = gethbridge.NewGethWakuV2Wrapper(wakuV2)
|
|
|
|
messengerLogger := s.logger.Named("bob-messenger")
|
|
s.bob = s.newMessenger(s.bobWaku, messengerLogger, s.storeNodeAddress)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) newMessenger(shh types.Waku, logger *zap.Logger, mailserverAddress string) *Messenger {
|
|
privateKey, err := crypto.GenerateKey()
|
|
s.Require().NoError(err)
|
|
|
|
mailserversSQLDb, err := helpers.SetupTestMemorySQLDB(appdatabase.DbInitializer{})
|
|
s.Require().NoError(err)
|
|
|
|
mailserversDatabase := mailserversDB.NewDB(mailserversSQLDb)
|
|
err = mailserversDatabase.Add(mailserversDB.Mailserver{
|
|
ID: localMailserverID,
|
|
Name: localMailserverID,
|
|
Address: mailserverAddress,
|
|
Fleet: localFleet,
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
options := []Option{
|
|
WithMailserversDatabase(mailserversDatabase),
|
|
WithClusterConfig(params.ClusterConfig{
|
|
Fleet: localFleet,
|
|
}),
|
|
}
|
|
|
|
messenger, err := newMessengerWithKey(shh, privateKey, logger, options)
|
|
|
|
s.Require().NoError(err)
|
|
return messenger
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) createCommunity(m *Messenger) *communities.Community {
|
|
|
|
createCommunityRequest := &requests.CreateCommunity{
|
|
Name: RandomLettersString(10),
|
|
Description: RandomLettersString(20),
|
|
Color: RandomColor(),
|
|
Tags: RandomCommunityTags(3),
|
|
Membership: protobuf.CommunityPermissions_AUTO_ACCEPT,
|
|
}
|
|
|
|
response, err := s.owner.CreateCommunity(createCommunityRequest, true)
|
|
s.Require().NoError(err)
|
|
s.Require().NotNil(response)
|
|
s.Require().Len(response.Communities(), 1)
|
|
|
|
return response.Communities()[0]
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) requireCommunitiesEqual(c *communities.Community, expected *communities.Community) {
|
|
s.Require().Equal(expected.Name(), c.Name())
|
|
s.Require().Equal(expected.Identity().Description, c.Identity().Description)
|
|
s.Require().Equal(expected.Color(), c.Color())
|
|
s.Require().Equal(expected.Tags(), c.Tags())
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) requireContactsEqual(c *Contact, expected *Contact) {
|
|
s.Require().Equal(expected.DisplayName, c.DisplayName)
|
|
s.Require().Equal(expected.Bio, c.Bio)
|
|
s.Require().Equal(expected.SocialLinks, c.SocialLinks)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) fetchCommunity(m *Messenger, communityShard communities.CommunityShard, expectedCommunityInfo *communities.Community) StoreNodeRequestStats {
|
|
fetchedCommunity, stats, err := m.storeNodeRequestsManager.FetchCommunity(communityShard, true)
|
|
|
|
s.Require().NoError(err)
|
|
s.Require().NotNil(fetchedCommunity)
|
|
s.Require().Equal(communityShard.CommunityID, fetchedCommunity.IDString())
|
|
|
|
if expectedCommunityInfo != nil {
|
|
s.requireCommunitiesEqual(fetchedCommunity, expectedCommunityInfo)
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) fetchProfile(m *Messenger, contactID string, expectedContact *Contact) {
|
|
fetchedContact, err := m.FetchContact(contactID, true)
|
|
s.Require().NoError(err)
|
|
s.Require().NotNil(fetchedContact)
|
|
s.Require().Equal(contactID, fetchedContact.ID)
|
|
|
|
if expectedContact != nil {
|
|
s.requireContactsEqual(fetchedContact, expectedContact)
|
|
}
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) waitForAvailableStoreNode(messenger *Messenger) {
|
|
WaitForAvailableStoreNode(&s.Suite, messenger, storeNodeConnectTimeout)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestCommunityInfo() {
|
|
s.createOwner()
|
|
s.createBob()
|
|
|
|
s.waitForAvailableStoreNode(s.owner)
|
|
community := s.createCommunity(s.owner)
|
|
|
|
s.waitForAvailableStoreNode(s.bob)
|
|
s.fetchCommunity(s.bob, community.CommunityShard(), community)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestConsecutiveRequests() {
|
|
s.createOwner()
|
|
s.createBob()
|
|
|
|
s.waitForAvailableStoreNode(s.owner)
|
|
community := s.createCommunity(s.owner)
|
|
|
|
// Test consecutive requests to check that requests in manager are finalized
|
|
for i := 0; i < 2; i++ {
|
|
s.waitForAvailableStoreNode(s.bob)
|
|
s.fetchCommunity(s.bob, community.CommunityShard(), community)
|
|
}
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestSimultaneousCommunityInfoRequests() {
|
|
s.createOwner()
|
|
s.createBob()
|
|
|
|
s.waitForAvailableStoreNode(s.owner)
|
|
community := s.createCommunity(s.owner)
|
|
|
|
storeNodeRequestsCount := 0
|
|
s.bob.storeNodeRequestsManager.onPerformingBatch = func(batch MailserverBatch) {
|
|
storeNodeRequestsCount++
|
|
}
|
|
|
|
s.waitForAvailableStoreNode(s.bob)
|
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
// Make 2 simultaneous fetch requests, only 1 request to store node is expected
|
|
for i := 0; i < 2; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
s.fetchCommunity(s.bob, community.CommunityShard(), community)
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
s.Require().Equal(1, storeNodeRequestsCount)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestNonExistentCommunity() {
|
|
// On test start store node database is empty, so just request any valid community ID.
|
|
request := FetchCommunityRequest{
|
|
CommunityKey: "0x036dc11a0663f88e15912f0adb68c3c5f68ca0ca7a233f1a88ff923a3d39b2cf07",
|
|
Shard: nil,
|
|
TryDatabase: false,
|
|
WaitForResponse: true,
|
|
}
|
|
|
|
s.createBob()
|
|
|
|
s.waitForAvailableStoreNode(s.bob)
|
|
fetchedCommunity, err := s.bob.FetchCommunity(&request)
|
|
|
|
s.Require().NoError(err)
|
|
s.Require().Nil(fetchedCommunity)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestCommunityInfoWithStoreNodeDisconnected() {
|
|
s.createOwner()
|
|
s.createBob()
|
|
|
|
s.waitForAvailableStoreNode(s.owner)
|
|
community := s.createCommunity(s.owner)
|
|
|
|
// WaitForAvailableStoreNode is done internally
|
|
s.fetchCommunity(s.bob, community.CommunityShard(), community)
|
|
}
|
|
|
|
// This test is intended to only run locally to test how fast is a big community fetched
|
|
// Shouldn't be executed in CI, because it relies on connection to status.prod store nodes.
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestBigCommunity() {
|
|
if !runLocalTests {
|
|
return
|
|
}
|
|
|
|
// Status CC community
|
|
const communityID = "0x03073514d4c14a7d10ae9fc9b0f05abc904d84166a6ac80add58bf6a3542a4e50a"
|
|
|
|
communityShard := communities.CommunityShard{
|
|
CommunityID: communityID,
|
|
Shard: nil,
|
|
}
|
|
|
|
wakuLogger := s.logger.Named("user-waku-node")
|
|
messengerLogger := s.logger.Named("user-messenger")
|
|
|
|
wakuV2 := NewWakuV2(&s.Suite, wakuLogger, true, false)
|
|
userWaku := gethbridge.NewGethWakuV2Wrapper(wakuV2)
|
|
|
|
privateKey, err := crypto.GenerateKey()
|
|
s.Require().NoError(err)
|
|
|
|
mailserversSQLDb, err := helpers.SetupTestMemorySQLDB(appdatabase.DbInitializer{})
|
|
s.Require().NoError(err)
|
|
|
|
mailserversDatabase := mailserversDB.NewDB(mailserversSQLDb)
|
|
s.Require().NoError(err)
|
|
|
|
options := []Option{
|
|
WithMailserversDatabase(mailserversDatabase),
|
|
WithClusterConfig(params.ClusterConfig{
|
|
Fleet: params.FleetStatusProd,
|
|
}),
|
|
}
|
|
|
|
// Create bob without `createBob` without func to force status.prod fleet
|
|
s.bob, err = newMessengerWithKey(userWaku, privateKey, messengerLogger, options)
|
|
s.Require().NoError(err)
|
|
|
|
fetchedCommunity, stats, err := s.bob.storeNodeRequestsManager.FetchCommunity(communityShard, true)
|
|
|
|
s.Require().NoError(err)
|
|
s.Require().NotNil(fetchedCommunity)
|
|
s.Require().Equal(communityID, fetchedCommunity.IDString())
|
|
|
|
s.Require().Equal(initialStoreNodeRequestPageSize, stats.FetchedEnvelopesCount)
|
|
s.Require().Equal(1, stats.FetchedPagesCount)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestCommunityPagingAlgorithm() {
|
|
s.createOwner()
|
|
s.createBob()
|
|
|
|
// Create a community
|
|
s.waitForAvailableStoreNode(s.owner)
|
|
community := s.createCommunity(s.owner)
|
|
|
|
// Push spam to the same ContentTopic & PubsubTopic
|
|
// The first requested page size is 1. All subsequent pages are limited to 20.
|
|
// We want to test the algorithm, so we push 21 spam envelopes.
|
|
for i := 0; i < defaultStoreNodeRequestPageSize+initialStoreNodeRequestPageSize; i++ {
|
|
spamMessage := common.RawMessage{
|
|
Payload: RandomBytes(16),
|
|
Sender: community.PrivateKey(),
|
|
SkipEncryptionLayer: true,
|
|
MessageType: protobuf.ApplicationMetadataMessage_COMMUNITY_DESCRIPTION,
|
|
PubsubTopic: community.PubsubTopic(),
|
|
}
|
|
_, err := s.owner.sender.SendPublic(context.Background(), community.IDString(), spamMessage)
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
// Fetch the community
|
|
stats := s.fetchCommunity(s.bob, community.CommunityShard(), community)
|
|
|
|
// Expect 3 pages and 23 (24 spam + 1 community description + 1 general channel description) envelopes to be fetched.
|
|
// First we fetch a more up-to-date, but an invalid spam message, fail to decrypt it as community description,
|
|
// then we fetch another page of data and successfully decrypt a community description.
|
|
s.Require().Equal(defaultStoreNodeRequestPageSize+initialStoreNodeRequestPageSize+2, stats.FetchedEnvelopesCount)
|
|
s.Require().Equal(3, stats.FetchedPagesCount)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestCommunityWithSameContentTopic() {
|
|
s.createOwner()
|
|
s.createBob()
|
|
|
|
// Create a community
|
|
s.waitForAvailableStoreNode(s.owner)
|
|
community1 := s.createCommunity(s.owner)
|
|
community2 := s.createCommunity(s.owner)
|
|
|
|
description2, err := community2.MarshaledDescription()
|
|
s.Require().NoError(err)
|
|
|
|
// Push community2 description to the same ContentTopic & PubsubTopic as community1.
|
|
// This way we simulate 2 communities with same ContentTopic.
|
|
spamMessage := common.RawMessage{
|
|
Payload: description2,
|
|
Sender: community2.PrivateKey(),
|
|
SkipEncryptionLayer: true,
|
|
MessageType: protobuf.ApplicationMetadataMessage_COMMUNITY_DESCRIPTION,
|
|
PubsubTopic: community1.PubsubTopic(),
|
|
}
|
|
_, err = s.owner.sender.SendPublic(context.Background(), community1.IDString(), spamMessage)
|
|
s.Require().NoError(err)
|
|
|
|
// Fetch the community
|
|
s.fetchCommunity(s.bob, community1.CommunityShard(), community1)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestMultipleCommunities() {
|
|
s.createOwner()
|
|
s.createBob()
|
|
|
|
// Create 2 communities
|
|
s.waitForAvailableStoreNode(s.owner)
|
|
community1 := s.createCommunity(s.owner)
|
|
community2 := s.createCommunity(s.owner)
|
|
|
|
fetchedCommunities := map[string]*communities.Community{}
|
|
|
|
err := WaitOnSignaledCommunityFound(s.bob,
|
|
func() {
|
|
err := s.bob.fetchCommunities([]communities.CommunityShard{
|
|
community1.CommunityShard(),
|
|
community2.CommunityShard(),
|
|
})
|
|
s.Require().NoError(err)
|
|
},
|
|
func(community *communities.Community) bool {
|
|
fetchedCommunities[community.IDString()] = community
|
|
return len(fetchedCommunities) == 2
|
|
},
|
|
1*time.Second,
|
|
"communities were not signalled in time",
|
|
)
|
|
|
|
s.Require().NoError(err)
|
|
s.Require().Contains(fetchedCommunities, community1.IDString())
|
|
s.Require().Contains(fetchedCommunities, community2.IDString())
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestWithoutWaitingResponse() {
|
|
s.createOwner()
|
|
s.createBob()
|
|
|
|
// Create a community
|
|
s.waitForAvailableStoreNode(s.owner)
|
|
community := s.createCommunity(s.owner)
|
|
|
|
request := FetchCommunityRequest{
|
|
CommunityKey: community.IDString(),
|
|
Shard: nil,
|
|
TryDatabase: false,
|
|
WaitForResponse: false,
|
|
}
|
|
|
|
fetchedCommunities := map[string]*communities.Community{}
|
|
|
|
err := WaitOnSignaledCommunityFound(s.bob,
|
|
func() {
|
|
fetchedCommunity, err := s.bob.FetchCommunity(&request)
|
|
s.Require().NoError(err)
|
|
s.Require().Nil(fetchedCommunity)
|
|
},
|
|
func(community *communities.Community) bool {
|
|
fetchedCommunities[community.IDString()] = community
|
|
return len(fetchedCommunities) == 1
|
|
},
|
|
1*time.Second,
|
|
"communities weren't signalled",
|
|
)
|
|
|
|
s.Require().NoError(err)
|
|
s.Require().Len(fetchedCommunities, 1)
|
|
s.Require().Contains(fetchedCommunities, community.IDString())
|
|
|
|
s.requireCommunitiesEqual(fetchedCommunities[community.IDString()], community)
|
|
}
|
|
|
|
func (s *MessengerStoreNodeRequestSuite) TestRequestProfileInfo() {
|
|
s.createOwner()
|
|
|
|
// Set keypair (to be able to set displayName)
|
|
ownerProfileKp := accounts.GetProfileKeypairForTest(true, false, false)
|
|
ownerProfileKp.KeyUID = s.owner.account.KeyUID
|
|
ownerProfileKp.Accounts[0].KeyUID = s.owner.account.KeyUID
|
|
|
|
err := s.owner.settings.SaveOrUpdateKeypair(ownerProfileKp)
|
|
s.Require().NoError(err)
|
|
|
|
// Set display name, this will also publish contact code
|
|
err = s.owner.SetDisplayName("super-owner")
|
|
s.Require().NoError(err)
|
|
|
|
s.createBob()
|
|
s.fetchProfile(s.bob, s.owner.selfContact.ID, s.owner.selfContact)
|
|
}
|