status-go/protocol/communities/community_test.go

1134 lines
38 KiB
Go

package communities
import (
"crypto/ecdsa"
"encoding/json"
"testing"
"time"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
func TestCommunitySuite(t *testing.T) {
suite.Run(t, new(CommunitySuite))
}
const testChatID1 = "chat-id-1"
const testCategoryID1 = "category-id-1"
const testCategoryName1 = "category-name-1"
const testChatID2 = "chat-id-2"
type TimeSourceStub struct {
}
func (t *TimeSourceStub) GetCurrentTime() uint64 {
return uint64(time.Now().Unix())
}
type CommunitySuite struct {
suite.Suite
identity *ecdsa.PrivateKey
communityID []byte
member1 *ecdsa.PrivateKey
member2 *ecdsa.PrivateKey
member3 *ecdsa.PrivateKey
member1Key string
member2Key string
member3Key string
}
func (s *CommunitySuite) SetupTest() {
identity, err := crypto.GenerateKey()
s.Require().NoError(err)
s.identity = identity
s.communityID = crypto.CompressPubkey(&identity.PublicKey)
member1, err := crypto.GenerateKey()
s.Require().NoError(err)
s.member1 = member1
member2, err := crypto.GenerateKey()
s.Require().NoError(err)
s.member2 = member2
member3, err := crypto.GenerateKey()
s.Require().NoError(err)
s.member3 = member3
s.member1Key = common.PubkeyToHex(&s.member1.PublicKey)
s.member2Key = common.PubkeyToHex(&s.member2.PublicKey)
s.member3Key = common.PubkeyToHex(&s.member3.PublicKey)
}
func (s *CommunitySuite) TestHasPermission() {
// returns false if empty public key is passed
community := &Community{}
ownerKey, err := crypto.GenerateKey()
s.Require().NoError(err)
nonMemberKey, err := crypto.GenerateKey()
s.Require().NoError(err)
memberKey, err := crypto.GenerateKey()
s.Require().NoError(err)
s.Require().False(community.hasRoles(nil, adminRole()))
// returns false if key is passed, but config is nil
s.Require().False(community.hasRoles(&nonMemberKey.PublicKey, adminRole()))
// returns true if the user is the owner
communityDescription := &protobuf.CommunityDescription{}
communityDescription.Members = make(map[string]*protobuf.CommunityMember)
communityDescription.Members[common.PubkeyToHex(&ownerKey.PublicKey)] = &protobuf.CommunityMember{Roles: []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_OWNER}}
communityDescription.Members[common.PubkeyToHex(&memberKey.PublicKey)] = &protobuf.CommunityMember{Roles: []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_ADMIN}}
community.config = &Config{ID: &ownerKey.PublicKey, CommunityDescription: communityDescription}
s.Require().True(community.hasRoles(&ownerKey.PublicKey, ownerRole()))
// return false if user is not a member
s.Require().False(community.hasRoles(&nonMemberKey.PublicKey, adminRole()))
// return true if user is a member and has permissions
s.Require().True(community.hasRoles(&memberKey.PublicKey, adminRole()))
// return false if user is a member and does not have permissions
s.Require().False(community.hasRoles(&memberKey.PublicKey, ownerRole()))
}
func (s *CommunitySuite) TestCreateChat() {
newChatID := "new-chat-id"
org := s.buildCommunity(&s.identity.PublicKey)
org.config.PrivateKey = nil
org.config.ID = nil
identity := &protobuf.ChatIdentity{
DisplayName: "new-chat-display-name",
Description: "new-chat-description",
}
permissions := &protobuf.CommunityPermissions{
Access: protobuf.CommunityPermissions_AUTO_ACCEPT,
}
_, err := org.CreateChat(newChatID, &protobuf.CommunityChat{
Identity: identity,
Permissions: permissions,
})
s.Require().Equal(ErrNotAuthorized, err)
org.config.PrivateKey = s.identity
org.config.ID = &s.identity.PublicKey
changes, err := org.CreateChat(newChatID, &protobuf.CommunityChat{
Identity: identity,
Permissions: permissions,
})
description := org.config.CommunityDescription
s.Require().NoError(err)
s.Require().NotNil(description)
s.Require().NotNil(description.Chats[newChatID])
s.Require().NotEmpty(description.Clock)
s.Require().Equal(len(description.Chats)-1, int(description.Chats[newChatID].Position))
s.Require().Equal(permissions, description.Chats[newChatID].Permissions)
s.Require().Equal(identity, description.Chats[newChatID].Identity)
s.Require().NotNil(changes)
s.Require().NotNil(changes.ChatsAdded[newChatID])
// Add a community with the same name
_, err = org.CreateChat("different-chat-id", &protobuf.CommunityChat{
Identity: identity,
Permissions: permissions,
})
s.Require().Error(err)
}
func (s *CommunitySuite) TestEditChat() {
newChatID := "new-chat-id"
org := s.buildCommunity(&s.identity.PublicKey)
identity := &protobuf.ChatIdentity{
DisplayName: "new-chat-display-name",
Description: "new-chat-description",
Emoji: "😎",
Color: "#000000",
}
permissions := &protobuf.CommunityPermissions{
Access: protobuf.CommunityPermissions_AUTO_ACCEPT,
Private: false,
}
_, err := org.CreateChat(newChatID, &protobuf.CommunityChat{
Identity: identity,
Permissions: permissions,
HideIfPermissionsNotMet: false,
})
s.Require().NoError(err)
org.config.PrivateKey = nil
org.config.ID = nil
editedIdentity := &protobuf.ChatIdentity{
DisplayName: "edited-new-chat-display-name",
Description: "edited-new-chat-description",
Emoji: "🤘",
Color: "#FFFFFF",
}
editedPermissions := &protobuf.CommunityPermissions{
Access: protobuf.CommunityPermissions_AUTO_ACCEPT,
Private: true,
}
_, err = org.EditChat(newChatID, &protobuf.CommunityChat{
Identity: editedIdentity,
Permissions: editedPermissions,
})
s.Require().Equal(ErrNotAuthorized, err)
description := org.config.CommunityDescription
org.config.PrivateKey = s.identity
org.config.ID = &s.identity.PublicKey
editChanges, err := org.EditChat(newChatID, &protobuf.CommunityChat{
Identity: editedIdentity,
Permissions: editedPermissions,
HideIfPermissionsNotMet: true,
})
s.Require().NoError(err)
s.Require().NotNil(description.Chats[newChatID])
s.Require().NotEmpty(description.Clock)
s.Require().Equal(editedPermissions, description.Chats[newChatID].Permissions)
s.Require().Equal(editedIdentity, description.Chats[newChatID].Identity)
s.Require().NotNil(editChanges)
s.Require().NotNil(editChanges.ChatsModified[newChatID])
s.Require().Equal(editChanges.ChatsModified[newChatID].ChatModified.Identity, editedIdentity)
s.Require().Equal(editChanges.ChatsModified[newChatID].ChatModified.Permissions, editedPermissions)
s.Require().Equal(editChanges.ChatsModified[newChatID].ChatModified.HideIfPermissionsNotMet, true)
}
func (s *CommunitySuite) TestDeleteChat() {
org := s.buildCommunity(&s.identity.PublicKey)
org.config.PrivateKey = nil
org.config.ID = nil
_, err := org.DeleteChat(testChatID1)
s.Require().Equal(ErrNotAuthorized, err)
change1Clock := org.Clock()
org.config.PrivateKey = s.identity
org.config.ID = &s.identity.PublicKey
changes, err := org.DeleteChat(testChatID1)
s.Require().NoError(err)
s.Require().NotNil(changes)
change2Clock := org.Clock()
s.Require().Nil(org.Chats()[testChatID1])
s.Require().Len(changes.ChatsRemoved, 1)
s.Require().Greater(change2Clock, change1Clock)
}
func (s *CommunitySuite) TestRemoveUserFromChat() {
org := s.buildCommunity(&s.identity.PublicKey)
org.config.PrivateKey = nil
org.config.ID = nil
// Not an admin
_, err := org.RemoveUserFromOrg(&s.member1.PublicKey)
s.Require().Equal(ErrNotAuthorized, err)
// Add admin to community
org.config.PrivateKey = s.identity
org.config.ID = &s.identity.PublicKey
actualCommunity, err := org.RemoveUserFromChat(&s.member1.PublicKey, testChatID1)
s.Require().Nil(err)
s.Require().NotNil(actualCommunity)
// Check member has not been removed
s.Require().True(org.HasMember(&s.member1.PublicKey))
// Check member has not been removed from org
_, ok := actualCommunity.Members[common.PubkeyToHex(&s.member1.PublicKey)]
s.Require().True(ok)
// Check member has been removed from chat
_, ok = actualCommunity.Chats[testChatID1].Members[common.PubkeyToHex(&s.member1.PublicKey)]
s.Require().False(ok)
}
func (s *CommunitySuite) TestRemoveUserFormOrg() {
org := s.buildCommunity(&s.identity.PublicKey)
org.config.PrivateKey = nil
org.config.ID = nil
// Not an admin
_, err := org.RemoveUserFromOrg(&s.member1.PublicKey)
s.Require().Equal(ErrNotAuthorized, err)
// Add admin to community
org.config.PrivateKey = s.identity
org.config.ID = &s.identity.PublicKey
actualCommunity, err := org.RemoveUserFromOrg(&s.member1.PublicKey)
s.Require().Nil(err)
s.Require().NotNil(actualCommunity)
// Check member has been removed
s.Require().False(org.HasMember(&s.member1.PublicKey))
// Check member has been removed from org
_, ok := actualCommunity.Members[common.PubkeyToHex(&s.member1.PublicKey)]
s.Require().False(ok)
// Check member has been removed from chat
_, ok = actualCommunity.Chats[testChatID1].Members[common.PubkeyToHex(&s.member1.PublicKey)]
s.Require().False(ok)
}
func (s *CommunitySuite) TestRemoveOurselvesFormOrg() {
org := s.buildCommunity(&s.identity.PublicKey)
// We don't need to be an admin to remove ourselves from community
org.config.PrivateKey = nil
org.RemoveOurselvesFromOrg(&s.member1.PublicKey)
// Check member has been removed from org
s.Require().False(org.HasMember(&s.member1.PublicKey))
// Check member has been removed from chat
_, ok := org.config.CommunityDescription.Chats[testChatID1].Members[common.PubkeyToHex(&s.member1.PublicKey)]
s.Require().False(ok)
}
func (s *CommunitySuite) TestAcceptRequestToJoin() {
// WHAT TO DO WITH ENS
// TEST CASE 1: Not an admin
// TEST CASE 2: No request to join
// TEST CASE 3: Valid
}
func (s *CommunitySuite) TestDeclineRequestToJoin() {
// TEST CASE 1: Not an admin
// TEST CASE 2: No request to join
// TEST CASE 3: Valid
}
func (s *CommunitySuite) TestValidateRequestToJoin() {
description := &protobuf.CommunityDescription{}
key, err := crypto.GenerateKey()
s.Require().NoError(err)
signer := &key.PublicKey
revealedAccounts := []*protobuf.RevealedAccount{
&protobuf.RevealedAccount{
Address: "0x0100000000000000000000000000000000000000"},
}
request := &protobuf.CommunityRequestToJoin{
EnsName: "donvanvliet.stateofus.eth",
CommunityId: s.communityID,
Clock: uint64(time.Now().Unix()),
RevealedAccounts: revealedAccounts,
}
requestWithChatID := &protobuf.CommunityRequestToJoin{
EnsName: "donvanvliet.stateofus.eth",
CommunityId: s.communityID,
ChatId: testChatID1,
Clock: uint64(time.Now().Unix()),
RevealedAccounts: revealedAccounts,
}
requestWithoutENS := &protobuf.CommunityRequestToJoin{
CommunityId: s.communityID,
Clock: uint64(time.Now().Unix()),
RevealedAccounts: revealedAccounts,
}
requestWithChatWithoutENS := &protobuf.CommunityRequestToJoin{
CommunityId: s.communityID,
ChatId: testChatID1,
Clock: uint64(time.Now().Unix()),
RevealedAccounts: revealedAccounts,
}
// MATRIX
// NO_MEMBERHSIP - NO_MEMBERSHIP -> Error -> Anyone can join org, chat is read/write for anyone
// NO_MEMBRISHIP - INVITATION_ONLY -> Error -> Anyone can join org, chat is invitation only
// NO_MEMBERSHIP - ON_REQUEST -> Success -> Anyone can join org, chat is on request and needs approval
// INVITATION_ONLY - NO_MEMBERSHIP -> TODO -> Org is invitation only, chat is read-write for members
// INVITATION_ONLY - INVITATION_ONLY -> Error -> Org is invitation only, chat is invitation only
// INVITATION_ONLY - ON_REQUEST -> TODO -> Error -> Org is invitation only, member of the org need to request access for chat
// ON_REQUEST - NO_MEMBRERSHIP -> TODO -> Error -> Org is on request, chat is read write for members
// ON_REQUEST - INVITATION_ONLY -> Error -> Org is on request, chat is invitation only for members
// ON_REQUEST - ON_REQUEST -> Fine -> Org is on request, chat is on request
testCases := []struct {
name string
config Config
request *protobuf.CommunityRequestToJoin
signer *ecdsa.PublicKey
err error
}{
{
name: "on-request access to community",
config: s.configOnRequest(),
signer: signer,
request: request,
err: nil,
},
{
name: "not admin",
config: Config{MemberIdentity: key, CommunityDescription: description},
signer: signer,
request: request,
err: ErrNotAdmin,
},
{
name: "ens-only org and missing ens",
config: s.configENSOnly(),
signer: signer,
request: requestWithoutENS,
err: ErrCantRequestAccess,
},
{
name: "ens-only chat and missing ens",
config: s.configChatENSOnly(),
signer: signer,
request: requestWithChatWithoutENS,
err: ErrCantRequestAccess,
},
{
name: "missing chat",
config: s.configOnRequest(),
signer: signer,
request: requestWithChatID,
err: ErrChatNotFound,
},
// Org-Chat combinations
// NO_MEMBERSHIP-NO_MEMBERSHIP = error as you should not be
// requesting access
{
name: "no-membership org with no-membeship chat",
config: s.configNoMembershipOrgNoMembershipChat(),
signer: signer,
request: requestWithChatID,
err: ErrCantRequestAccess,
},
// NO_MEMBERSHIP-ON_REQUEST = this is a valid case
{
name: "no-membership org with on-request chat",
config: s.configNoMembershipOrgOnRequestChat(),
signer: signer,
request: requestWithChatID,
},
// ON_REQUEST-ON_REQUEST success
{
name: "on-request org with on-request chat",
config: s.configOnRequestOrgOnRequestChat(),
signer: signer,
request: requestWithChatID,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
org, err := New(tc.config, &TimeSourceStub{}, &DescriptionEncryptorMock{}, nil)
s.Require().NoError(err)
err = org.ValidateRequestToJoin(tc.signer, tc.request)
s.Require().Equal(tc.err, err)
})
}
}
func (s *CommunitySuite) TestCanPostCanView() {
chatID := "chat-id"
memberKey := common.PubkeyToHex(&s.member1.PublicKey)
// Member has no channel role
description := &protobuf.CommunityDescription{
Members: map[string]*protobuf.CommunityMember{
memberKey: &protobuf.CommunityMember{},
},
Chats: map[string]*protobuf.CommunityChat{
chatID: &protobuf.CommunityChat{
Members: map[string]*protobuf.CommunityMember{
memberKey: &protobuf.CommunityMember{},
},
},
},
}
community := &Community{config: &Config{ID: &s.member2.PublicKey}}
community.config.CommunityDescription = description
result, err := community.CanPost(&s.member1.PublicKey, chatID, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
s.Require().NoError(err)
s.Require().True(result)
result = community.CanView(&s.member1.PublicKey, chatID)
s.Require().True(result)
// member has view channel permissions
description.Chats[chatID].Members[memberKey].ChannelRole = protobuf.CommunityMember_CHANNEL_ROLE_VIEWER
result, err = community.CanPost(&s.member1.PublicKey, chatID, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
s.Require().NoError(err)
s.Require().False(result)
result = community.CanView(&s.member1.PublicKey, chatID)
s.Require().True(result)
}
func (s *CommunitySuite) TestCanPost() {
notMember := &s.member3.PublicKey
member := &s.member1.PublicKey
testCases := []struct {
name string
config Config
member *ecdsa.PublicKey
err error
canPost bool
}{
{
name: "no-membership org with no-membership chat",
config: s.configNoMembershipOrgNoMembershipChat(),
member: notMember,
canPost: false,
},
{
name: "membership org with no-membership chat-not-a-member",
config: s.configOnRequestOrgNoMembershipChat(),
member: notMember,
canPost: false,
},
{
name: "membership org with no-membership chat",
config: s.configOnRequestOrgNoMembershipChat(),
member: member,
canPost: true,
},
{
name: "creator can always post of course",
config: s.configOnRequestOrgNoMembershipChat(),
member: &s.identity.PublicKey,
canPost: true,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
var err error
org, err := New(tc.config, &TimeSourceStub{}, &DescriptionEncryptorMock{}, nil)
s.Require().NoError(err)
canPost, err := org.CanPost(tc.member, testChatID1, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
s.Require().Equal(tc.err, err)
s.Require().Equal(tc.canPost, canPost)
})
}
}
func (s *CommunitySuite) TestHandleCommunityDescription() {
key, err := crypto.GenerateKey()
s.Require().NoError(err)
signer := &key.PublicKey
buildChanges := func(c *Community) *CommunityChanges {
return c.emptyCommunityChanges()
}
testCases := []struct {
name string
description func(*Community) *protobuf.CommunityDescription
changes func(*Community) *CommunityChanges
signer *ecdsa.PublicKey
err error
}{
{
name: "updated version but no changes",
description: s.identicalCommunityDescription,
signer: signer,
changes: buildChanges,
err: nil,
},
{
name: "updated version but lower clock",
description: s.oldCommunityDescription,
signer: signer,
changes: func(c *Community) *CommunityChanges { return nil },
err: ErrInvalidCommunityDescriptionClockOutdated,
},
{
name: "removed member from org",
description: s.removedMemberCommunityDescription,
signer: signer,
changes: func(org *Community) *CommunityChanges {
changes := org.emptyCommunityChanges()
changes.MembersRemoved[s.member1Key] = &protobuf.CommunityMember{}
changes.ChatsModified[testChatID1] = &CommunityChatChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
}
changes.ChatsModified[testChatID1].MembersRemoved[s.member1Key] = &protobuf.CommunityMember{}
return changes
},
err: nil,
},
{
name: "added member from org",
description: s.addedMemberCommunityDescription,
signer: signer,
changes: func(org *Community) *CommunityChanges {
changes := org.emptyCommunityChanges()
changes.MembersAdded[s.member3Key] = &protobuf.CommunityMember{}
changes.ChatsModified[testChatID1] = &CommunityChatChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
}
changes.ChatsModified[testChatID1].MembersAdded[s.member3Key] = &protobuf.CommunityMember{}
return changes
},
err: nil,
},
{
name: "chat added to org",
description: s.addedChatCommunityDescription,
signer: signer,
changes: func(org *Community) *CommunityChanges {
changes := org.emptyCommunityChanges()
changes.MembersAdded[s.member3Key] = &protobuf.CommunityMember{}
changes.ChatsAdded[testChatID2] = &protobuf.CommunityChat{
Identity: &protobuf.ChatIdentity{DisplayName: "added-chat", Description: "description"},
Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_MANUAL_ACCEPT},
Members: make(map[string]*protobuf.CommunityMember)}
changes.ChatsAdded[testChatID2].Members[s.member3Key] = &protobuf.CommunityMember{}
return changes
},
err: nil,
},
{
name: "chat removed from the org",
description: s.removedChatCommunityDescription,
signer: signer,
changes: func(org *Community) *CommunityChanges {
changes := org.emptyCommunityChanges()
changes.ChatsRemoved[testChatID1] = org.config.CommunityDescription.Chats[testChatID1]
return changes
},
err: nil,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
org := s.buildCommunity(signer)
org.Join()
expectedChanges := tc.changes(org)
actualChanges, err := org.UpdateCommunityDescription(tc.description(org), []byte{0x01}, nil)
s.Require().Equal(tc.err, err)
s.Require().Equal(expectedChanges, actualChanges)
})
}
}
func (s *CommunitySuite) TestHandleCommunityDescriptionWithImageChange() {
key, err := crypto.GenerateKey()
s.Require().NoError(err)
signer := &key.PublicKey
org := s.buildCommunity(signer)
org.Join()
changes, err := org.UpdateCommunityDescription(s.changedImageCommunityDescription(org), []byte{0x01}, nil)
s.Require().NoError(err)
s.Require().True(changes.ImageModified)
}
func (s *CommunitySuite) TestValidateCommunityDescription() {
testCases := []struct {
name string
description *protobuf.CommunityDescription
err error
}{
{
name: "valid",
description: s.buildCommunityDescription(),
err: nil,
},
{
name: "empty description",
err: ErrInvalidCommunityDescription,
},
{
name: "empty org permissions",
description: s.emptyPermissionsCommunityDescription(),
err: ErrInvalidCommunityDescriptionNoOrgPermissions,
},
{
name: "empty chat permissions",
description: s.emptyChatPermissionsCommunityDescription(),
err: ErrInvalidCommunityDescriptionNoChatPermissions,
},
{
name: "unknown org permissions",
description: s.unknownOrgPermissionsCommunityDescription(),
err: ErrInvalidCommunityDescriptionUnknownOrgAccess,
},
{
name: "unknown chat permissions",
description: s.unknownChatPermissionsCommunityDescription(),
err: ErrInvalidCommunityDescriptionUnknownChatAccess,
},
{
name: "member in chat but not in org",
description: s.memberInChatNotInOrgCommunityDescription(),
err: ErrInvalidCommunityDescriptionMemberInChatButNotInOrg,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
err := ValidateCommunityDescription(tc.description)
s.Require().Equal(tc.err, err)
})
}
}
func (s *CommunitySuite) TestChatIDs() {
community := s.buildCommunity(&s.identity.PublicKey)
chatIDs := community.ChatIDs()
s.Require().Len(chatIDs, 1)
}
func (s *CommunitySuite) TestChannelTokenPermissionsByType() {
org := s.buildCommunity(&s.identity.PublicKey)
viewOnlyPermissions := []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: make([]*protobuf.TokenCriteria, 0),
ChatIds: []string{"some-chat-id"},
},
}
viewAndPostPermissions := []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-other-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL,
TokenCriteria: make([]*protobuf.TokenCriteria, 0),
ChatIds: []string{"some-chat-id-2"},
},
}
for _, viewOnlyPermission := range viewOnlyPermissions {
_, err := org.UpsertTokenPermission(viewOnlyPermission)
s.Require().NoError(err)
}
for _, viewAndPostPermission := range viewAndPostPermissions {
_, err := org.UpsertTokenPermission(viewAndPostPermission)
s.Require().NoError(err)
}
result := org.ChannelTokenPermissionsByType("some-chat-id", protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
s.Require().Len(result, 1)
s.Require().Equal(result[0].Id, viewOnlyPermissions[0].Id)
s.Require().Equal(result[0].TokenCriteria, viewOnlyPermissions[0].TokenCriteria)
s.Require().Equal(result[0].ChatIds, viewOnlyPermissions[0].ChatIds)
result = org.ChannelTokenPermissionsByType("some-chat-id-2", protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
s.Require().Len(result, 1)
s.Require().Equal(result[0].Id, viewAndPostPermissions[0].Id)
s.Require().Equal(result[0].TokenCriteria, viewAndPostPermissions[0].TokenCriteria)
s.Require().Equal(result[0].ChatIds, viewAndPostPermissions[0].ChatIds)
}
func (s *CommunitySuite) TestChannelEncrypted() {
org := s.buildCommunity(&s.identity.PublicKey)
someChannelID := "some-channel-id"
someChatID := org.ChatID(someChannelID)
s.Require().False(org.ChannelEncrypted(someChannelID))
_, err := org.UpsertTokenPermission(&protobuf.CommunityTokenPermission{
Id: "A",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL,
TokenCriteria: []*protobuf.TokenCriteria{},
ChatIds: []string{someChatID},
})
s.Require().NoError(err)
s.Require().True(org.channelEncrypted(someChannelID))
_, err = org.UpsertTokenPermission(&protobuf.CommunityTokenPermission{
Id: "B",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: []*protobuf.TokenCriteria{&protobuf.TokenCriteria{}},
ChatIds: []string{someChatID},
})
s.Require().NoError(err)
s.Require().True(org.channelEncrypted(someChannelID))
// Channels with `view` permission without token requirements shouldn't be encrypted.
// See: https://github.com/status-im/status-desktop/issues/14748
_, err = org.UpsertTokenPermission(&protobuf.CommunityTokenPermission{
Id: "C",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: []*protobuf.TokenCriteria{},
ChatIds: []string{someChatID},
})
s.Require().NoError(err)
s.Require().False(org.channelEncrypted(someChannelID))
}
func (s *CommunitySuite) emptyCommunityDescription() *protobuf.CommunityDescription {
return &protobuf.CommunityDescription{
Permissions: &protobuf.CommunityPermissions{},
}
}
func (s *CommunitySuite) emptyCommunityDescriptionWithChat() *protobuf.CommunityDescription {
desc := &protobuf.CommunityDescription{
Members: make(map[string]*protobuf.CommunityMember),
Clock: 1,
Chats: make(map[string]*protobuf.CommunityChat),
Categories: make(map[string]*protobuf.CommunityCategory),
Permissions: &protobuf.CommunityPermissions{},
}
desc.Categories[testCategoryID1] = &protobuf.CommunityCategory{CategoryId: testCategoryID1, Name: testCategoryName1, Position: 0}
desc.Chats[testChatID1] = &protobuf.CommunityChat{Position: 0, Permissions: &protobuf.CommunityPermissions{}, Members: make(map[string]*protobuf.CommunityMember)}
desc.Members[common.PubkeyToHex(&s.member1.PublicKey)] = &protobuf.CommunityMember{}
desc.Chats[testChatID1].Members[common.PubkeyToHex(&s.member1.PublicKey)] = &protobuf.CommunityMember{}
return desc
}
func (s *CommunitySuite) newConfig(identity *ecdsa.PrivateKey, description *protobuf.CommunityDescription) Config {
return Config{
MemberIdentity: identity,
ID: &identity.PublicKey,
CommunityDescription: description,
PrivateKey: identity,
ControlNode: &identity.PublicKey,
ControlDevice: true,
}
}
func (s *CommunitySuite) configOnRequest() Config {
description := s.emptyCommunityDescription()
description.Permissions.Access = protobuf.CommunityPermissions_MANUAL_ACCEPT
return s.newConfig(s.identity, description)
}
func (s *CommunitySuite) configNoMembershipOrgNoMembershipChat() Config {
description := s.emptyCommunityDescriptionWithChat()
description.Permissions.Access = protobuf.CommunityPermissions_AUTO_ACCEPT
description.Chats[testChatID1].Permissions.Access = protobuf.CommunityPermissions_AUTO_ACCEPT
return s.newConfig(s.identity, description)
}
func (s *CommunitySuite) configNoMembershipOrgOnRequestChat() Config {
description := s.emptyCommunityDescriptionWithChat()
description.Permissions.Access = protobuf.CommunityPermissions_AUTO_ACCEPT
description.Chats[testChatID1].Permissions.Access = protobuf.CommunityPermissions_MANUAL_ACCEPT
return s.newConfig(s.identity, description)
}
func (s *CommunitySuite) configOnRequestOrgOnRequestChat() Config {
description := s.emptyCommunityDescriptionWithChat()
description.Permissions.Access = protobuf.CommunityPermissions_MANUAL_ACCEPT
description.Chats[testChatID1].Permissions.Access = protobuf.CommunityPermissions_MANUAL_ACCEPT
return s.newConfig(s.identity, description)
}
func (s *CommunitySuite) configOnRequestOrgNoMembershipChat() Config {
description := s.emptyCommunityDescriptionWithChat()
description.Permissions.Access = protobuf.CommunityPermissions_MANUAL_ACCEPT
description.Chats[testChatID1].Permissions.Access = protobuf.CommunityPermissions_AUTO_ACCEPT
return s.newConfig(s.identity, description)
}
func (s *CommunitySuite) configChatENSOnly() Config {
description := s.emptyCommunityDescriptionWithChat()
description.Permissions.Access = protobuf.CommunityPermissions_MANUAL_ACCEPT
description.Chats[testChatID1].Permissions.Access = protobuf.CommunityPermissions_MANUAL_ACCEPT
description.Chats[testChatID1].Permissions.EnsOnly = true
return s.newConfig(s.identity, description)
}
func (s *CommunitySuite) configENSOnly() Config {
description := s.emptyCommunityDescription()
description.Permissions.Access = protobuf.CommunityPermissions_MANUAL_ACCEPT
description.Permissions.EnsOnly = true
return s.newConfig(s.identity, description)
}
func (s *CommunitySuite) config() Config {
config := s.configOnRequestOrgOnRequestChat()
return config
}
func (s *CommunitySuite) buildCommunityDescription() *protobuf.CommunityDescription {
config := s.configOnRequestOrgOnRequestChat()
desc := config.CommunityDescription
desc.Clock = 1
desc.Identity = &protobuf.ChatIdentity{}
desc.Identity.Images = make(map[string]*protobuf.IdentityImage)
imgs, err := images.GenerateIdentityImages("../../_assets/tests/status.png", 0, 0, 0, 0)
s.Require().NoError(err)
for _, image := range imgs {
desc.Identity.Images[image.Name] = &protobuf.IdentityImage{
Payload: []byte(""),
SourceType: protobuf.IdentityImage_RAW_PAYLOAD,
ImageFormat: images.GetProtobufImageFormat(image.Payload),
}
}
desc.Members = make(map[string]*protobuf.CommunityMember)
desc.Members[s.member1Key] = &protobuf.CommunityMember{}
desc.Members[s.member2Key] = &protobuf.CommunityMember{}
desc.Chats[testChatID1].Members = make(map[string]*protobuf.CommunityMember)
desc.Chats[testChatID1].Members[s.member1Key] = &protobuf.CommunityMember{}
desc.Chats[testChatID1].Identity = &protobuf.ChatIdentity{
DisplayName: "display-name",
Description: "description",
}
return desc
}
func (s *CommunitySuite) emptyPermissionsCommunityDescription() *protobuf.CommunityDescription {
desc := s.buildCommunityDescription()
desc.Permissions = nil
return desc
}
func (s *CommunitySuite) emptyChatPermissionsCommunityDescription() *protobuf.CommunityDescription {
desc := s.buildCommunityDescription()
desc.Chats[testChatID1].Permissions = nil
return desc
}
func (s *CommunitySuite) unknownOrgPermissionsCommunityDescription() *protobuf.CommunityDescription {
desc := s.buildCommunityDescription()
desc.Permissions.Access = protobuf.CommunityPermissions_UNKNOWN_ACCESS
return desc
}
func (s *CommunitySuite) unknownChatPermissionsCommunityDescription() *protobuf.CommunityDescription {
desc := s.buildCommunityDescription()
desc.Chats[testChatID1].Permissions.Access = protobuf.CommunityPermissions_UNKNOWN_ACCESS
return desc
}
func (s *CommunitySuite) memberInChatNotInOrgCommunityDescription() *protobuf.CommunityDescription {
desc := s.buildCommunityDescription()
desc.Chats[testChatID1].Members[s.member3Key] = &protobuf.CommunityMember{}
return desc
}
func (s *CommunitySuite) buildCommunity(owner *ecdsa.PublicKey) *Community {
config := s.config()
config.ID = owner
config.CommunityDescription = s.buildCommunityDescription()
org, err := New(config, &TimeSourceStub{}, &DescriptionEncryptorMock{}, nil)
s.Require().NoError(err)
return org
}
func (s *CommunitySuite) identicalCommunityDescription(org *Community) *protobuf.CommunityDescription {
description := proto.Clone(org.config.CommunityDescription).(*protobuf.CommunityDescription)
description.Clock++
return description
}
func (s *CommunitySuite) oldCommunityDescription(org *Community) *protobuf.CommunityDescription {
description := proto.Clone(org.config.CommunityDescription).(*protobuf.CommunityDescription)
description.Clock--
delete(description.Members, s.member1Key)
delete(description.Chats[testChatID1].Members, s.member1Key)
return description
}
func (s *CommunitySuite) removedMemberCommunityDescription(org *Community) *protobuf.CommunityDescription {
description := proto.Clone(org.config.CommunityDescription).(*protobuf.CommunityDescription)
description.Clock++
delete(description.Members, s.member1Key)
delete(description.Chats[testChatID1].Members, s.member1Key)
return description
}
func (s *CommunitySuite) addedMemberCommunityDescription(org *Community) *protobuf.CommunityDescription {
description := proto.Clone(org.config.CommunityDescription).(*protobuf.CommunityDescription)
description.Clock++
description.Members[s.member3Key] = &protobuf.CommunityMember{}
description.Chats[testChatID1].Members[s.member3Key] = &protobuf.CommunityMember{}
return description
}
func (s *CommunitySuite) addedChatCommunityDescription(org *Community) *protobuf.CommunityDescription {
description := proto.Clone(org.config.CommunityDescription).(*protobuf.CommunityDescription)
description.Clock++
description.Members[s.member3Key] = &protobuf.CommunityMember{}
description.Chats[testChatID2] = &protobuf.CommunityChat{
Identity: &protobuf.ChatIdentity{DisplayName: "added-chat", Description: "description"},
Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_MANUAL_ACCEPT},
Members: make(map[string]*protobuf.CommunityMember)}
description.Chats[testChatID2].Members[s.member3Key] = &protobuf.CommunityMember{}
return description
}
func (s *CommunitySuite) removedChatCommunityDescription(org *Community) *protobuf.CommunityDescription {
description := proto.Clone(org.config.CommunityDescription).(*protobuf.CommunityDescription)
description.Clock++
delete(description.Chats, testChatID1)
return description
}
func (s *CommunitySuite) changedImageCommunityDescription(org *Community) *protobuf.CommunityDescription {
description := proto.Clone(org.config.CommunityDescription).(*protobuf.CommunityDescription)
description.Clock++
imgs, err := images.GenerateIdentityImages("../../_assets/tests/elephant.jpg", 0, 0, 5, 5)
s.Require().NoError(err)
for _, image := range imgs {
description.Identity.Images[image.Name] = &protobuf.IdentityImage{
Payload: image.Payload,
SourceType: protobuf.IdentityImage_RAW_PAYLOAD,
ImageFormat: images.GetProtobufImageFormat(image.Payload),
}
}
return description
}
func (s *CommunitySuite) TestMarshalJSON() {
community := s.buildCommunity(&s.identity.PublicKey)
channelID := community.ChatID(testChatID1)
_, err := community.UpsertTokenPermission(&protobuf.CommunityTokenPermission{
Id: "A",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL,
TokenCriteria: []*protobuf.TokenCriteria{},
ChatIds: []string{channelID},
})
s.Require().NoError(err)
s.Require().True(community.ChannelEncrypted(testChatID1))
communityDescription := community.config.CommunityDescription
ownerKey := s.identity
s.Require().NoError(err)
memberKey, err := crypto.GenerateKey()
s.Require().NoError(err)
// returns true if the user is the owner
communityDescription.Members = make(map[string]*protobuf.CommunityMember)
communityDescription.Members[common.PubkeyToHex(&ownerKey.PublicKey)] = &protobuf.CommunityMember{Roles: []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_OWNER}}
communityDescription.Members[common.PubkeyToHex(&memberKey.PublicKey)] = &protobuf.CommunityMember{Roles: []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_ADMIN}}
communityDescription.Chats[testChatID1] = &protobuf.CommunityChat{Members: make(map[string]*protobuf.CommunityMember), Identity: &protobuf.ChatIdentity{}}
communityDescription.Chats[testChatID1].Members[common.PubkeyToHex(&ownerKey.PublicKey)] = &protobuf.CommunityMember{Roles: []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_OWNER}}
// Test token gated community
s.Require().True(community.ChannelEncrypted(testChatID1))
communityJSON, err := json.Marshal(community)
s.Require().NoError(err)
var communityData map[string]interface{}
err = json.Unmarshal(communityJSON, &communityData)
s.Require().NoError(err)
s.Require().NotNil(communityData["chats"])
expectedChats := map[string]interface{}{}
expectedChat := map[string]interface{}{
"canPost": true,
"canPostReactions": true,
"categoryID": "",
"canView": true,
"color": "",
"description": "",
"emoji": "",
"hideIfPermissionsNotMet": false,
"members": map[string]interface{}{
common.PubkeyToHex(&ownerKey.PublicKey): map[string]interface{}{
"roles": []interface{}{float64(1)},
},
},
"id": testChatID1,
"name": "",
"permissions": nil,
"position": float64(0),
"tokenGated": true,
"viewersCanPostReactions": false,
"missingEncryptionKey": false,
}
expectedChats[testChatID1] = expectedChat
s.Require().Equal(expectedChats, communityData["chats"])
// Test token gated community
community.config.CommunityDescription.TokenPermissions = nil
communityJSON, err = json.Marshal(community)
s.Require().NoError(err)
err = json.Unmarshal(communityJSON, &communityData)
s.Require().NoError(err)
s.Require().NotNil(communityData["chats"])
expectedChats = map[string]interface{}{}
expectedChat = map[string]interface{}{
"canPost": true,
"canPostReactions": true,
"categoryID": "",
"canView": true,
"color": "",
"description": "",
"emoji": "",
"hideIfPermissionsNotMet": false,
"id": testChatID1,
"members": nil,
"name": "",
"permissions": nil,
"position": float64(0),
"tokenGated": false,
"viewersCanPostReactions": false,
"missingEncryptionKey": false,
}
expectedChats[testChatID1] = expectedChat
s.Require().Equal(expectedChats, communityData["chats"])
}