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"]) }