status-im/status-go

View on GitHub
protocol/communities/persistence_test.go

Summary

Maintainability
A
3 hrs
Test Coverage
package communities

import (
    "crypto/ecdsa"
    "database/sql"
    "math/big"
    "reflect"
    "testing"
    "time"

    "github.com/golang/protobuf/proto"
    "github.com/stretchr/testify/suite"

    "github.com/status-im/status-go/appdatabase"
    "github.com/status-im/status-go/eth-node/crypto"
    "github.com/status-im/status-go/eth-node/types"
    "github.com/status-im/status-go/protocol/common"
    "github.com/status-im/status-go/protocol/common/shard"
    "github.com/status-im/status-go/protocol/communities/token"
    "github.com/status-im/status-go/protocol/encryption"
    "github.com/status-im/status-go/protocol/protobuf"
    "github.com/status-im/status-go/protocol/sqlite"
    "github.com/status-im/status-go/services/wallet/bigint"
    "github.com/status-im/status-go/t/helpers"
)

func TestPersistenceSuite(t *testing.T) {
    suite.Run(t, new(PersistenceSuite))
}

type PersistenceSuite struct {
    suite.Suite

    db       *Persistence
    identity *ecdsa.PrivateKey
}

func (s *PersistenceSuite) SetupTest() {
    s.db = nil

    db, err := helpers.SetupTestMemorySQLDB(appdatabase.DbInitializer{})
    s.Require().NoError(err, "creating sqlite db instance")

    err = sqlite.Migrate(db)
    s.Require().NoError(err, "protocol migrate")

    s.identity, err = crypto.GenerateKey()
    s.Require().NoError(err)

    s.db = &Persistence{db: db, recordBundleToCommunity: func(r *CommunityRecordBundle) (*Community, error) {
        return recordBundleToCommunity(r, &s.identity.PublicKey, "", nil, &TimeSourceStub{}, &DescriptionEncryptorMock{}, nil)
    }}
}

func (s *PersistenceSuite) TestSaveCommunity() {
    communities, err := s.db.AllCommunities(&s.identity.PublicKey)
    s.Require().NoError(err)
    s.Require().Len(communities, 0)

    community := Community{
        config: &Config{
            PrivateKey:           s.identity,
            ControlNode:          &s.identity.PublicKey,
            ControlDevice:        true,
            ID:                   &s.identity.PublicKey,
            Joined:               true,
            Spectated:            true,
            Verified:             true,
            Muted:                true,
            MuteTill:             time.Time{},
            CommunityDescription: &protobuf.CommunityDescription{},
        },
    }
    s.Require().NoError(s.db.SaveCommunity(&community))

    communities, err = s.db.AllCommunities(&s.identity.PublicKey)
    s.Require().NoError(err)
    s.Require().Len(communities, 1)
    s.Equal(types.HexBytes(crypto.CompressPubkey(&s.identity.PublicKey)), communities[0].ID())
    s.Equal(true, communities[0].Joined())
    s.Equal(true, communities[0].Spectated())
    s.Equal(true, communities[0].Verified())
    s.Equal(true, communities[0].Muted())
    s.Equal(time.Time{}, communities[0].MuteTill())
}

func (s *PersistenceSuite) TestShouldHandleSyncCommunity() {
    sc := &protobuf.SyncInstallationCommunity{
        Id:          []byte("0x123456"),
        Description: []byte("this is a description"),
        Joined:      true,
        Verified:    true,
        Clock:       uint64(time.Now().Unix()),
    }

    // check an empty db to see if a community should be synced
    should, err := s.db.ShouldHandleSyncCommunity(sc)
    s.Require().NoError(err, "SaveSyncCommunity")
    s.True(should)

    // add a new community to the db
    err = s.db.saveRawCommunityRow(fromSyncCommunityProtobuf(sc))
    s.Require().NoError(err, "saveRawCommunityRow")

    rcrs, err := s.db.getAllCommunitiesRaw()
    s.Require().NoError(err, "should have no error from getAllCommunitiesRaw")
    s.Len(rcrs, 1, "length of all communities raw should be 1")

    // check again to see is the community should be synced
    sc.Clock--
    should, err = s.db.ShouldHandleSyncCommunity(sc)
    s.Require().NoError(err, "SaveSyncCommunity")
    s.False(should)

    // check again to see is the community should be synced
    sc.Clock++
    sc.Clock++
    should, err = s.db.ShouldHandleSyncCommunity(sc)
    s.Require().NoError(err, "SaveSyncCommunity")
    s.True(should)
}

func (s *PersistenceSuite) TestSetSyncClock() {
    sc := &protobuf.SyncInstallationCommunity{
        Id:          []byte("0x123456"),
        Description: []byte("this is a description"),
        Joined:      true,
        Verified:    true,
    }

    // add a new community to the db
    err := s.db.saveRawCommunityRow(fromSyncCommunityProtobuf(sc))
    s.Require().NoError(err, "saveRawCommunityRow")

    // retrieve row from db synced_at must be zero
    rcr, err := s.db.getRawCommunityRow(sc.Id)
    s.Require().NoError(err, "getRawCommunityRow")
    s.Require().Zero(rcr.SyncedAt, "synced_at must be zero value")

    // Set the synced_at value
    clock := uint64(time.Now().Unix())
    err = s.db.SetSyncClock(sc.Id, clock)
    s.Require().NoError(err, "SetSyncClock")

    // Retrieve row from db and check clock matches synced_at value
    rcr, err = s.db.getRawCommunityRow(sc.Id)
    s.Require().NoError(err, "getRawCommunityRow")
    s.Require().Equal(clock, rcr.SyncedAt, "synced_at must equal the value of the clock")

    // Set Synced At with an older clock value
    olderClock := clock - uint64(256)
    err = s.db.SetSyncClock(sc.Id, olderClock)
    s.Require().NoError(err, "SetSyncClock")

    // Retrieve row from db and check olderClock matches synced_at value
    rcr, err = s.db.getRawCommunityRow(sc.Id)
    s.Require().NoError(err, "getRawCommunityRow")
    s.Require().NotEqual(olderClock, rcr.SyncedAt, "synced_at must not equal the value of the olderClock value")

    // Set Synced At with a newer clock value
    newerClock := clock + uint64(512)
    err = s.db.SetSyncClock(sc.Id, newerClock)
    s.Require().NoError(err, "SetSyncClock")

    // Retrieve row from db and check olderClock matches synced_at value
    rcr, err = s.db.getRawCommunityRow(sc.Id)
    s.Require().NoError(err, "getRawCommunityRow")
    s.Equal(newerClock, rcr.SyncedAt, "synced_at must equal the value of the newerClock value")
}

func (s *PersistenceSuite) TestSetPrivateKey() {
    sc := &protobuf.SyncInstallationCommunity{
        Id:          []byte("0x123456"),
        Description: []byte("this is a description"),
        Joined:      true,
        Verified:    true,
    }

    // add a new community to the db with no private key
    err := s.db.saveRawCommunityRow(fromSyncCommunityProtobuf(sc))
    s.Require().NoError(err, "saveRawCommunityRow")

    // retrieve row from db, private key must be zero
    rcr, err := s.db.getRawCommunityRow(sc.Id)
    s.Require().NoError(err, "getRawCommunityRow")
    s.Zero(rcr.PrivateKey, "private key must be zero value")

    // Set private key
    err = s.db.SetPrivateKey(sc.Id, s.identity)
    s.Require().NoError(err, "SetPrivateKey")

    // retrieve row from db again, private key must match the given key
    rcr, err = s.db.getRawCommunityRow(sc.Id)
    s.Require().NoError(err, "getRawCommunityRow")
    s.Equal(crypto.FromECDSA(s.identity), rcr.PrivateKey, "private key must match given key")
}

func (s *PersistenceSuite) TestJoinedAndPendingCommunitiesWithRequests() {
    clock := uint64(time.Now().Unix())

    // Add a new community that we have joined
    com := s.makeNewCommunity(s.identity)
    com.Join()
    sc, err := com.ToSyncInstallationCommunityProtobuf(clock, nil, nil)
    s.Require().NoError(err, "Community.ToSyncInstallationCommunityProtobuf shouldn't give any error")
    err = s.db.saveRawCommunityRow(fromSyncCommunityProtobuf(sc))
    s.Require().NoError(err, "saveRawCommunityRow")

    // Add a new community that we have requested to join, but not yet joined
    com2 := s.makeNewCommunity(s.identity)
    err = s.db.SaveCommunity(com2)
    s.Require().NoError(err, "SaveCommunity shouldn't give any error")

    rtj := &RequestToJoin{
        ID:          types.HexBytes{1, 2, 3, 4, 5, 6, 7, 8},
        PublicKey:   common.PubkeyToHex(&s.identity.PublicKey),
        Clock:       clock,
        CommunityID: com2.ID(),
        State:       RequestToJoinStatePending,
    }
    err = s.db.SaveRequestToJoin(rtj)
    s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")

    comms, err := s.db.JoinedAndPendingCommunitiesWithRequests(&s.identity.PublicKey)
    s.Require().NoError(err, "JoinedAndPendingCommunitiesWithRequests shouldn't give any error")
    s.Len(comms, 2, "Should have 2 communities")

    for _, comm := range comms {
        switch comm.IDString() {
        case com.IDString():
            s.Len(comm.RequestsToJoin(), 0, "Should have no RequestsToJoin")
        case com2.IDString():
            rtjs := comm.RequestsToJoin()
            s.Len(rtjs, 1, "Should have one RequestsToJoin")
            s.Equal(rtjs[0], rtj, "RequestToJoin should match the Request stored in the db")
        }
    }
}

func (s *PersistenceSuite) TestSaveRequestToLeave() {
    rtl := &RequestToLeave{
        ID:          []byte("0x123456"),
        PublicKey:   "0xffffff",
        Clock:       2,
        CommunityID: []byte("0x654321"),
    }

    err := s.db.SaveRequestToLeave(rtl)
    s.Require().NoError(err)

    // older clocks should not be saved
    rtl.Clock = 1
    err = s.db.SaveRequestToLeave(rtl)
    s.Error(err)
}

func (s *PersistenceSuite) makeNewCommunity(identity *ecdsa.PrivateKey) *Community {
    comPrivKey, err := crypto.GenerateKey()
    s.Require().NoError(err, "crypto.GenerateKey shouldn't give any error")

    com, err := New(Config{
        MemberIdentity: &identity.PublicKey,
        PrivateKey:     comPrivKey,
        ControlNode:    &comPrivKey.PublicKey,
        ControlDevice:  true,
        ID:             &comPrivKey.PublicKey,
    }, &TimeSourceStub{}, &DescriptionEncryptorMock{})
    s.NoError(err, "New shouldn't give any error")

    md, err := com.MarshaledDescription()
    s.Require().NoError(err, "Community.MarshaledDescription shouldn't give any error")
    com.config.CommunityDescriptionProtocolMessage = md

    return com
}

func (s *PersistenceSuite) TestGetSyncedRawCommunity() {
    sc := &protobuf.SyncInstallationCommunity{
        Id:          []byte("0x123456"),
        Description: []byte("this is a description"),
        Joined:      true,
        Verified:    true,
        Spectated:   true,
    }

    // add a new community to the db
    err := s.db.saveRawCommunityRowWithoutSyncedAt(fromSyncCommunityProtobuf(sc))
    s.Require().NoError(err, "saveRawCommunityRow")

    // retrieve row from db synced_at must be zero
    rcr, err := s.db.getRawCommunityRow(sc.Id)
    s.Require().NoError(err, "getRawCommunityRow")
    s.Zero(rcr.SyncedAt, "synced_at must be zero value")

    // retrieve synced row from db, should fail
    src, err := s.db.getSyncedRawCommunity(sc.Id)
    s.EqualError(err, sql.ErrNoRows.Error())
    s.Nil(src)

    // Set the synced_at value
    clock := uint64(time.Now().Unix())
    err = s.db.SetSyncClock(sc.Id, clock)
    s.Require().NoError(err, "SetSyncClock")

    // retrieve row from db synced_at must not be zero
    rcr, err = s.db.getRawCommunityRow(sc.Id)
    s.Require().NoError(err, "getRawCommunityRow")
    s.NotZero(rcr.SyncedAt, "synced_at must be zero value")

    // retrieve synced row from db, should succeed
    src, err = s.db.getSyncedRawCommunity(sc.Id)
    s.Require().NoError(err)
    s.NotNil(src)
    s.Equal(clock, src.SyncedAt)
}

func (s *PersistenceSuite) TestGetCommunitiesSettings() {
    settings := []CommunitySettings{
        {CommunityID: "0x01", HistoryArchiveSupportEnabled: false},
        {CommunityID: "0x02", HistoryArchiveSupportEnabled: true},
        {CommunityID: "0x03", HistoryArchiveSupportEnabled: false},
    }

    for i := range settings {
        stg := settings[i]
        err := s.db.SaveCommunitySettings(stg)
        s.Require().NoError(err)
    }

    rst, err := s.db.GetCommunitiesSettings()
    s.Require().NoError(err)
    s.Equal(settings, rst)
}

func (s *PersistenceSuite) TestSaveCommunitySettings() {
    settings := CommunitySettings{CommunityID: "0x01", HistoryArchiveSupportEnabled: false}
    err := s.db.SaveCommunitySettings(settings)
    s.Require().NoError(err)
    rst, err := s.db.GetCommunitiesSettings()
    s.Require().NoError(err)
    s.Equal(1, len(rst))
}

func (s *PersistenceSuite) TestDeleteCommunitySettings() {
    settings := CommunitySettings{CommunityID: "0x01", HistoryArchiveSupportEnabled: false}

    err := s.db.SaveCommunitySettings(settings)
    s.Require().NoError(err)

    rst, err := s.db.GetCommunitiesSettings()
    s.Require().NoError(err)
    s.Equal(1, len(rst))
    s.Require().NoError(s.db.DeleteCommunitySettings(types.HexBytes{0x01}))
    rst2, err := s.db.GetCommunitiesSettings()
    s.Require().NoError(err)
    s.Equal(0, len(rst2))
}

func (s *PersistenceSuite) TestUpdateCommunitySettings() {
    settings := []CommunitySettings{
        {CommunityID: "0x01", HistoryArchiveSupportEnabled: true},
        {CommunityID: "0x02", HistoryArchiveSupportEnabled: false},
    }

    s.Require().NoError(s.db.SaveCommunitySettings(settings[0]))
    s.Require().NoError(s.db.SaveCommunitySettings(settings[1]))

    settings[0].HistoryArchiveSupportEnabled = true
    settings[1].HistoryArchiveSupportEnabled = false

    s.Require().NoError(s.db.UpdateCommunitySettings(settings[0]))
    s.Require().NoError(s.db.UpdateCommunitySettings(settings[1]))

    rst, err := s.db.GetCommunitiesSettings()
    s.Require().NoError(err)
    s.Equal(settings, rst)
}

func (s *PersistenceSuite) TestGetCommunityToken() {
    tokens, err := s.db.GetCommunityTokens("123")
    s.Require().NoError(err)
    s.Require().Len(tokens, 0)

    tokenERC721 := token.CommunityToken{
        CommunityID:        "123",
        TokenType:          protobuf.CommunityTokenType_ERC721,
        Address:            "0x123",
        Name:               "StatusToken",
        Symbol:             "STT",
        Description:        "desc",
        Supply:             &bigint.BigInt{Int: big.NewInt(123)},
        InfiniteSupply:     false,
        Transferable:       true,
        RemoteSelfDestruct: true,
        ChainID:            1,
        DeployState:        token.InProgress,
        Base64Image:        "ABCD",
        TransactionHash:    "0x1234",
    }

    err = s.db.AddCommunityToken(&tokenERC721)
    s.Require().NoError(err)

    token, err := s.db.GetCommunityToken("123", 1, "0x123")
    s.Require().NoError(err)
    s.Require().Equal(&tokenERC721, token)
}

func (s *PersistenceSuite) TestGetCommunityTokens() {
    tokens, err := s.db.GetCommunityTokens("123")
    s.Require().NoError(err)
    s.Require().Len(tokens, 0)

    tokenERC721 := token.CommunityToken{
        CommunityID:        "123",
        TokenType:          protobuf.CommunityTokenType_ERC721,
        Address:            "0x123",
        Name:               "StatusToken",
        Symbol:             "STT",
        Description:        "desc",
        Supply:             &bigint.BigInt{Int: big.NewInt(123)},
        InfiniteSupply:     false,
        Transferable:       true,
        RemoteSelfDestruct: true,
        ChainID:            1,
        DeployState:        token.InProgress,
        Base64Image:        "ABCD",
        Deployer:           "0xDep1",
        PrivilegesLevel:    token.OwnerLevel,
        TransactionHash:    "0x1234",
    }

    tokenERC20 := token.CommunityToken{
        CommunityID:        "345",
        TokenType:          protobuf.CommunityTokenType_ERC20,
        Address:            "0x345",
        Name:               "StatusToken",
        Symbol:             "STT",
        Description:        "desc",
        Supply:             &bigint.BigInt{Int: big.NewInt(345)},
        InfiniteSupply:     false,
        Transferable:       true,
        RemoteSelfDestruct: true,
        ChainID:            2,
        DeployState:        token.Failed,
        Base64Image:        "QWERTY",
        Decimals:           21,
        Deployer:           "0xDep2",
        PrivilegesLevel:    token.CommunityLevel,
        TransactionHash:    "0x123456",
    }

    err = s.db.AddCommunityToken(&tokenERC721)
    s.Require().NoError(err)
    err = s.db.AddCommunityToken(&tokenERC20)
    s.Require().NoError(err)

    tokens, err = s.db.GetCommunityTokens("123")
    s.Require().NoError(err)
    s.Require().Len(tokens, 1)
    s.Require().Equal(tokenERC721, *tokens[0])

    err = s.db.UpdateCommunityTokenState(1, "0x123", token.Deployed)
    s.Require().NoError(err)
    tokens, err = s.db.GetCommunityTokens("123")
    s.Require().NoError(err)
    s.Require().Len(tokens, 1)
    s.Require().Equal(token.Deployed, tokens[0].DeployState)

    tokens, err = s.db.GetCommunityTokens("345")
    s.Require().NoError(err)
    s.Require().Len(tokens, 1)
    s.Require().Equal(tokenERC20, *tokens[0])

    err = s.db.UpdateCommunityTokenAddress(1, "0x123", "0x123-newAddr")
    s.Require().NoError(err)
    tokens, err = s.db.GetCommunityTokens("123")
    s.Require().NoError(err)
    s.Require().Len(tokens, 1)
    s.Require().Equal("0x123-newAddr", tokens[0].Address)
}

func (s *PersistenceSuite) TestSaveCheckChannelPermissionResponse() {

    viewAndPostPermissionResults := make(map[string]*PermissionTokenCriteriaResult)
    viewAndPostPermissionResults["one"] = &PermissionTokenCriteriaResult{
        Criteria: []bool{true, true, true, true},
    }
    viewAndPostPermissionResults["two"] = &PermissionTokenCriteriaResult{
        Criteria: []bool{false},
    }
    chatID := "some-chat-id"
    communityID := "some-community-id"

    checkChannelPermissionResponse := &CheckChannelPermissionsResponse{
        ViewOnlyPermissions: &CheckChannelViewOnlyPermissionsResult{
            Satisfied:   true,
            Permissions: make(map[string]*PermissionTokenCriteriaResult),
        },
        ViewAndPostPermissions: &CheckChannelViewAndPostPermissionsResult{
            Satisfied:   true,
            Permissions: viewAndPostPermissionResults,
        },
    }

    err := s.db.SaveCheckChannelPermissionResponse(communityID, chatID, checkChannelPermissionResponse)
    s.Require().NoError(err)

    responses, err := s.db.GetCheckChannelPermissionResponses(communityID)
    s.Require().NoError(err)
    s.Require().Len(responses, 1)
    s.Require().NotNil(responses[chatID])
    s.Require().True(responses[chatID].ViewOnlyPermissions.Satisfied)
    s.Require().Len(responses[chatID].ViewOnlyPermissions.Permissions, 0)
    s.Require().True(responses[chatID].ViewAndPostPermissions.Satisfied)
    s.Require().Len(responses[chatID].ViewAndPostPermissions.Permissions, 2)
    s.Require().Equal(responses[chatID].ViewAndPostPermissions.Permissions["one"].Criteria, []bool{true, true, true, true})
    s.Require().Equal(responses[chatID].ViewAndPostPermissions.Permissions["two"].Criteria, []bool{false})
}

func (s *PersistenceSuite) TestGetCommunityRequestsToJoinWithRevealedAddresses() {
    clock := uint64(time.Now().Unix())
    communityID := types.HexBytes{7, 7, 7, 7, 7, 7, 7, 7}
    revealedAddresses := []string{"address1", "address2", "address3"}
    chainIds := []uint64{1, 2}

    // No data in database
    rtjResult, err := s.db.GetCommunityRequestsToJoinWithRevealedAddresses(communityID)
    s.Require().NoError(err, "GetCommunityRequestsToJoinWithRevealedAddresses shouldn't give any error")
    s.Require().Len(rtjResult, 0)

    // RTJ with 2 revealed Addresses
    expectedRtj1 := &RequestToJoin{
        ID:          types.HexBytes{1, 2, 3, 4, 5, 6, 7, 8},
        PublicKey:   common.PubkeyToHex(&s.identity.PublicKey),
        Clock:       clock,
        CommunityID: communityID,
        State:       RequestToJoinStateAccepted,
        RevealedAccounts: []*protobuf.RevealedAccount{
            {
                Address: revealedAddresses[0],
            },
            {
                Address: revealedAddresses[1],
            },
        },
    }
    err = s.db.SaveRequestToJoin(expectedRtj1)
    s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")

    err = s.db.SaveRequestToJoinRevealedAddresses(expectedRtj1.ID, expectedRtj1.RevealedAccounts)
    s.Require().NoError(err, "SaveRequestToJoinRevealedAddresses shouldn't give any error")

    rtjResult, err = s.db.GetCommunityRequestsToJoinWithRevealedAddresses(communityID)
    s.Require().NoError(err, "GetCommunityRequestsToJoinWithRevealedAddresses shouldn't give any error")
    s.Require().Len(rtjResult, 1)
    s.Require().Equal(expectedRtj1.ID, rtjResult[0].ID)
    s.Require().Equal(expectedRtj1.PublicKey, rtjResult[0].PublicKey)
    s.Require().Equal(expectedRtj1.Clock, rtjResult[0].Clock)
    s.Require().Equal(expectedRtj1.CommunityID, rtjResult[0].CommunityID)
    s.Require().Len(rtjResult[0].RevealedAccounts, 2)

    for index, account := range rtjResult[0].RevealedAccounts {
        s.Require().Equal(revealedAddresses[index], account.Address)
    }

    // RTJ with 1 revealed Address, ChainIds, IsAirdropAddress and Signature
    signature := []byte("test")
    expectedRtj2 := &RequestToJoin{
        ID:          types.HexBytes{8, 7, 6, 5, 4, 3, 2, 1},
        PublicKey:   common.PubkeyToHex(&s.identity.PublicKey),
        Clock:       clock,
        CommunityID: communityID,
        State:       RequestToJoinStateAccepted,
        RevealedAccounts: []*protobuf.RevealedAccount{
            {
                Address:          revealedAddresses[2],
                ChainIds:         chainIds,
                IsAirdropAddress: true,
                Signature:        signature,
            },
        },
    }
    err = s.db.SaveRequestToJoin(expectedRtj2)
    s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")

    err = s.db.SaveRequestToJoinRevealedAddresses(expectedRtj2.ID, expectedRtj2.RevealedAccounts)
    s.Require().NoError(err, "SaveRequestToJoinRevealedAddresses shouldn't give any error")

    rtjResult, err = s.db.GetCommunityRequestsToJoinWithRevealedAddresses(communityID)
    s.Require().NoError(err, "GetCommunityRequestsToJoinWithRevealedAddresses shouldn't give any error")
    s.Require().Len(rtjResult, 2)

    s.Require().Len(rtjResult[1].RevealedAccounts, 1)
    s.Require().Equal(revealedAddresses[2], rtjResult[1].RevealedAccounts[0].Address)
    s.Require().Equal(chainIds, rtjResult[1].RevealedAccounts[0].ChainIds)
    s.Require().Equal(true, rtjResult[1].RevealedAccounts[0].IsAirdropAddress)
    s.Require().Equal(rtjResult[1].RevealedAccounts[0].Signature, signature)

    // RTJ without RevealedAccounts
    expectedRtjWithoutRevealedAccounts := &RequestToJoin{
        ID:          types.HexBytes{1, 6, 6, 6, 6, 6, 6, 6},
        PublicKey:   common.PubkeyToHex(&s.identity.PublicKey),
        Clock:       clock,
        CommunityID: communityID,
        State:       RequestToJoinStateAccepted,
    }
    err = s.db.SaveRequestToJoin(expectedRtjWithoutRevealedAccounts)
    s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")

    rtjResult, err = s.db.GetCommunityRequestsToJoinWithRevealedAddresses(communityID)
    s.Require().NoError(err, "GetCommunityRequestsToJoinWithRevealedAddresses shouldn't give any error")
    s.Require().Len(rtjResult, 3)

    s.Require().Len(rtjResult[2].RevealedAccounts, 0)

    // RTJ with RevealedAccount but with empty Address
    expectedRtjWithEmptyAddress := &RequestToJoin{
        ID:          types.HexBytes{2, 6, 6, 6, 6, 6, 6, 6},
        PublicKey:   common.PubkeyToHex(&s.identity.PublicKey),
        Clock:       clock,
        CommunityID: communityID,
        State:       RequestToJoinStateAccepted,
        RevealedAccounts: []*protobuf.RevealedAccount{
            {
                Address: "",
            },
        },
    }
    err = s.db.SaveRequestToJoin(expectedRtjWithEmptyAddress)
    s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")

    rtjResult, err = s.db.GetCommunityRequestsToJoinWithRevealedAddresses(communityID)
    s.Require().NoError(err, "GetCommunityRequestsToJoinWithRevealedAddresses shouldn't give any error")
    s.Require().Len(rtjResult, 4)
    s.Require().Len(rtjResult[3].RevealedAccounts, 0)
}

func (s *PersistenceSuite) TestCuratedCommunities() {
    communities, err := s.db.GetCuratedCommunities()
    s.Require().NoError(err)
    s.Require().Empty(communities.ContractCommunities)
    s.Require().Empty(communities.ContractFeaturedCommunities)

    setCommunities := &CuratedCommunities{
        ContractCommunities:         []string{"x", "d"},
        ContractFeaturedCommunities: []string{"x"},
    }

    err = s.db.SetCuratedCommunities(setCommunities)
    s.Require().NoError(err)

    communities, err = s.db.GetCuratedCommunities()
    s.Require().NoError(err)
    s.Require().True(reflect.DeepEqual(communities, setCommunities))

    setCommunities = &CuratedCommunities{
        ContractCommunities:         []string{"p", "a", "t", "r", "y", "k"},
        ContractFeaturedCommunities: []string{"p", "k"},
    }

    err = s.db.SetCuratedCommunities(setCommunities)
    s.Require().NoError(err)

    communities, err = s.db.GetCuratedCommunities()
    s.Require().NoError(err)
    s.Require().True(reflect.DeepEqual(communities, setCommunities))
}

func (s *PersistenceSuite) TestGetCommunityRequestToJoinWithRevealedAddresses() {
    clock := uint64(time.Now().Unix())
    communityID := types.HexBytes{7, 7, 7, 7, 7, 7, 7, 7}
    revealedAddresses := []string{"address1", "address2", "address3"}
    chainIds := []uint64{1, 2}
    publicKey := common.PubkeyToHex(&s.identity.PublicKey)
    signature := []byte("test")

    // No data in database
    _, err := s.db.GetCommunityRequestToJoinWithRevealedAddresses(publicKey, communityID)
    s.Require().ErrorIs(err, sql.ErrNoRows)

    // RTJ with 2 withoutRevealed Addresses
    expectedRtj := &RequestToJoin{
        ID:          types.HexBytes{1, 2, 3, 4, 5, 6, 7, 8},
        PublicKey:   publicKey,
        Clock:       clock,
        CommunityID: communityID,
        State:       RequestToJoinStateAccepted,
        RevealedAccounts: []*protobuf.RevealedAccount{
            {
                Address:          revealedAddresses[2],
                ChainIds:         chainIds,
                IsAirdropAddress: true,
                Signature:        signature,
            },
        },
    }
    err = s.db.SaveRequestToJoin(expectedRtj)
    s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")

    // check that there will be no error if revealed account is absent
    rtjResult, err := s.db.GetCommunityRequestToJoinWithRevealedAddresses(publicKey, communityID)
    s.Require().NoError(err, "RevealedAccounts empty, shouldn't give any error")

    s.Require().Len(rtjResult.RevealedAccounts, 0)

    // save revealed accounts for previous request to join
    err = s.db.SaveRequestToJoinRevealedAddresses(expectedRtj.ID, expectedRtj.RevealedAccounts)
    s.Require().NoError(err)

    rtjResult, err = s.db.GetCommunityRequestToJoinWithRevealedAddresses(publicKey, communityID)
    s.Require().NoError(err)
    s.Require().Equal(expectedRtj.ID, rtjResult.ID)
    s.Require().Equal(expectedRtj.PublicKey, rtjResult.PublicKey)
    s.Require().Equal(expectedRtj.Clock, rtjResult.Clock)
    s.Require().Equal(expectedRtj.CommunityID, rtjResult.CommunityID)
    s.Require().Len(rtjResult.RevealedAccounts, 1)
}

func (s *PersistenceSuite) TestAllNonApprovedCommunitiesRequestsToJoin() {
    // check on empty db
    result, err := s.db.AllNonApprovedCommunitiesRequestsToJoin()
    s.Require().NoError(err)
    s.Require().Len(result, 0)

    identity, err := crypto.GenerateKey()
    s.Require().NoError(err, "crypto.GenerateKey shouldn't give any error")

    clock := uint64(time.Now().Unix())

    // add a new community
    community := s.makeNewCommunity(identity)
    s.Require().NoError(err)

    // add requests to join to the community
    allStates := []RequestToJoinState{
        RequestToJoinStatePending,
        RequestToJoinStateDeclined,
        RequestToJoinStateAccepted,
        RequestToJoinStateCanceled,
        RequestToJoinStateAcceptedPending,
        RequestToJoinStateDeclinedPending,
        RequestToJoinStateAwaitingAddresses,
    }

    for i := range allStates {
        identity, err := crypto.GenerateKey()
        s.Require().NoError(err)

        rtj := &RequestToJoin{
            ID:          types.HexBytes{1, 2, 3, 4, 5, 6, 7, byte(i)},
            PublicKey:   common.PubkeyToHex(&identity.PublicKey),
            Clock:       clock,
            CommunityID: community.ID(),
            State:       allStates[i],
        }
        err = s.db.SaveRequestToJoin(rtj)
        s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")
    }

    result, err = s.db.AllNonApprovedCommunitiesRequestsToJoin()
    s.Require().NoError(err)
    s.Require().Len(result, 6) // all except RequestToJoinStateAccepted
}

func (s *PersistenceSuite) TestRemoveAllCommunityRequestsToJoinWithRevealedAddressesExceptPublicKey() {
    myIdentity, err := crypto.GenerateKey()
    s.Require().NoError(err, "crypto.GenerateKey shouldn't give any error")

    myPk := common.PubkeyToHex(&myIdentity.PublicKey)

    clock := uint64(time.Now().Unix())

    // add a new community
    community := s.makeNewCommunity(myIdentity)
    err = s.db.SaveCommunity(community)
    s.Require().NoError(err)

    // check on empty db
    err = s.db.RemoveAllCommunityRequestsToJoinWithRevealedAddressesExceptPublicKey(myPk, community.ID())
    s.Require().NoError(err)

    // add requests to join to the community
    allStates := []RequestToJoinState{
        RequestToJoinStatePending,
        RequestToJoinStateDeclined,
        RequestToJoinStateAccepted,
        RequestToJoinStateCanceled,
        RequestToJoinStateAcceptedPending,
        RequestToJoinStateDeclinedPending,
        RequestToJoinStateAwaitingAddresses,
    }

    allRequestsToJoinIDs := [][]byte{}

    for i := range allStates {
        identity, err := crypto.GenerateKey()
        s.Require().NoError(err)

        revealedAccounts := []*protobuf.RevealedAccount{}
        for j := 0; j < i; j++ {
            acc := &protobuf.RevealedAccount{
                Address:          "testAddr",
                ChainIds:         []uint64{123},
                IsAirdropAddress: true,
                Signature:        []byte{},
            }
            revealedAccounts = append(revealedAccounts, acc)
        }

        rtj := &RequestToJoin{
            ID:               types.HexBytes{1, 2, 3, 4, 5, 6, 7, byte(i)},
            PublicKey:        common.PubkeyToHex(&identity.PublicKey),
            Clock:            clock,
            CommunityID:      community.ID(),
            State:            allStates[i],
            RevealedAccounts: revealedAccounts,
        }

        allRequestsToJoinIDs = append(allRequestsToJoinIDs, rtj.ID)

        err = s.db.SaveRequestToJoin(rtj)
        s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")
        err = s.db.SaveRequestToJoinRevealedAddresses(rtj.ID, rtj.RevealedAccounts)
        s.Require().NoError(err)
    }

    err = s.db.RemoveAllCommunityRequestsToJoinWithRevealedAddressesExceptPublicKey(myPk, community.ID())
    s.Require().NoError(err)

    requests, err := s.db.GetCommunityRequestsToJoinWithRevealedAddresses(community.ID())
    s.Require().NoError(err)
    s.Require().Len(requests, 0)

    for _, rtjID := range allRequestsToJoinIDs {
        accounts, err := s.db.GetRequestToJoinRevealedAddresses(rtjID)
        s.Require().NoError(err)
        s.Require().Len(accounts, 0)
    }

    myRtj := &RequestToJoin{
        ID:          types.HexBytes{1, 2, 3, 4, 5, 6, 7, 8},
        PublicKey:   myPk,
        Clock:       clock,
        CommunityID: community.ID(),
        State:       RequestToJoinStateAccepted,
        RevealedAccounts: []*protobuf.RevealedAccount{
            {
                Address:          "testAddr",
                ChainIds:         []uint64{123},
                IsAirdropAddress: true,
                Signature:        []byte{},
            },
        },
    }

    err = s.db.SaveRequestToJoin(myRtj)
    s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")
    err = s.db.SaveRequestToJoinRevealedAddresses(myRtj.ID, myRtj.RevealedAccounts)
    s.Require().NoError(err)

    err = s.db.RemoveAllCommunityRequestsToJoinWithRevealedAddressesExceptPublicKey(myPk, community.ID())
    s.Require().NoError(err)

    requests, err = s.db.GetCommunityRequestsToJoinWithRevealedAddresses(community.ID())
    s.Require().NoError(err)
    s.Require().Len(requests, 1)
    s.Require().Len(requests[0].RevealedAccounts, 1)
}

func (s *PersistenceSuite) TestSaveShardInfo() {
    communityID := types.HexBytes{1, 2, 3, 4, 5, 6, 7, 8}
    clock := uint64(1)
    // get non existing community shard
    resultShard, err := s.db.GetCommunityShard(communityID)
    s.Require().Error(err, sql.ErrNoRows)
    s.Require().Nil(resultShard)

    // shard info is nil
    err = s.db.SaveCommunityShard(communityID, nil, clock)
    s.Require().NoError(err)

    // save shard info with the same clock
    err = s.db.SaveCommunityShard(communityID, nil, clock)
    s.Require().Error(err, ErrOldShardInfo)

    resultShard, err = s.db.GetCommunityShard(communityID)
    s.Require().NoError(err)
    s.Require().Nil(resultShard)

    // not nil shard
    expectedShard := &shard.Shard{
        Cluster: 1,
        Index:   2,
    }

    // save shard info with the same clock and check that data was not modified
    err = s.db.SaveCommunityShard(communityID, expectedShard, clock)
    s.Require().Error(err, ErrOldShardInfo)
    resultShard, err = s.db.GetCommunityShard(communityID)
    s.Require().NoError(err)
    s.Require().Nil(resultShard)

    // update the clock and save the shard info
    clock += clock
    err = s.db.SaveCommunityShard(communityID, expectedShard, clock)
    s.Require().NoError(err)
    resultShard, err = s.db.GetCommunityShard(communityID)
    s.Require().NoError(err)
    s.Require().NotNil(resultShard)
    s.Require().Equal(expectedShard, resultShard)

    // check shard deleting
    err = s.db.DeleteCommunityShard(communityID)
    s.Require().NoError(err)
    resultShard, err = s.db.GetCommunityShard(communityID)
    s.Require().Error(err, sql.ErrNoRows)
    s.Require().Nil(resultShard)
}

func (s *PersistenceSuite) TestGetCommunityToValidateByID() {
    communityID := types.HexBytes{1, 2, 3, 4, 5, 6, 7, 8}

    result, err := s.db.getCommunityToValidateByID(communityID)
    s.Require().NoError(err)
    s.Require().Len(result, 0)
}

func (s *PersistenceSuite) TestProcessedCommunityEvents() {
    community := types.HexBytes{1}
    events, err := s.db.GetAppliedCommunityEvents(community)
    s.Require().NoError(err)
    s.Require().Empty(events)

    err = s.db.UpsertAppliedCommunityEvents(community, map[string]uint64{"a": 1, "b": 10})
    s.Require().NoError(err)

    events, err = s.db.GetAppliedCommunityEvents(community)
    s.Require().NoError(err)
    s.Require().Len(events, 2)
    s.Require().True(reflect.DeepEqual(events, map[string]uint64{"a": 1, "b": 10}))

    err = s.db.UpsertAppliedCommunityEvents(community, map[string]uint64{"a": 2, "b": 8, "c": 1})
    s.Require().NoError(err)

    events, err = s.db.GetAppliedCommunityEvents(community)
    s.Require().NoError(err)
    s.Require().Len(events, 3)
    s.Require().True(reflect.DeepEqual(events, map[string]uint64{"a": 2, "b": 10, "c": 1}))
}

func (s *PersistenceSuite) TestDecryptedCommunityCache() {
    communityDescription := &protobuf.CommunityDescription{
        Clock: 1000,
    }
    keyID1 := []byte("key-id-1")
    keyID2 := []byte("key-id-2")
    missingKeys := []*CommunityPrivateDataFailedToDecrypt{
        {KeyID: keyID1},
        {KeyID: keyID2},
    }
    communityID := []byte("id")
    err := s.db.SaveDecryptedCommunityDescription(communityID, missingKeys, communityDescription)
    s.Require().NoError(err)

    // Can be retrieved
    retrievedCommunity, err := s.db.GetDecryptedCommunityDescription(communityID, 1000)
    s.Require().NoError(err)
    s.Require().True(proto.Equal(communityDescription, retrievedCommunity))

    // Retrieving a random one doesn't throw an error
    retrievedCommunity, err = s.db.GetDecryptedCommunityDescription([]byte("non-existent-id"), 1000)
    s.Require().NoError(err)
    s.Require().Nil(retrievedCommunity)

    // Retrieving a random one doesn't throw an error
    retrievedCommunity, err = s.db.GetDecryptedCommunityDescription(communityID, 999)
    s.Require().NoError(err)
    s.Require().Nil(retrievedCommunity)

    // invalidating the cache
    err = s.db.InvalidateDecryptedCommunityCacheForKeys([]*encryption.HashRatchetInfo{{KeyID: keyID1}})
    s.Require().NoError(err)

    // community cannot be retrieved anymore
    retrievedCommunity, err = s.db.GetDecryptedCommunityDescription(communityID, 1000)
    s.Require().NoError(err)
    s.Require().Nil(retrievedCommunity)

    // make sure everything is cleaned up

    qr := s.db.db.QueryRow("SELECT COUNT(*) FROM encrypted_community_description_missing_keys")

    var count int

    err = qr.Scan(&count)
    s.Require().NoError(err)
    s.Require().Equal(count, 0)

}

func (s *PersistenceSuite) TestDecryptedCommunityCacheClock() {
    communityDescription := &protobuf.CommunityDescription{
        Clock: 1000,
    }
    keyID1 := []byte("key-id-1")
    keyID2 := []byte("key-id-2")
    keyID3 := []byte("key-id-3")

    missingKeys := []*CommunityPrivateDataFailedToDecrypt{
        {KeyID: keyID1},
        {KeyID: keyID2},
    }
    communityID := []byte("id")
    err := s.db.SaveDecryptedCommunityDescription(communityID, missingKeys, communityDescription)
    s.Require().NoError(err)

    // Can be retrieved
    retrievedCommunity, err := s.db.GetDecryptedCommunityDescription(communityID, 1000)
    s.Require().NoError(err)
    s.Require().True(proto.Equal(communityDescription, retrievedCommunity))

    // Save an earlier community
    communityDescription.Clock = 999
    err = s.db.SaveDecryptedCommunityDescription(communityID, missingKeys, communityDescription)
    s.Require().NoError(err)

    // The old one should be retrieved
    retrievedCommunity, err = s.db.GetDecryptedCommunityDescription(communityID, 1000)
    s.Require().NoError(err)
    s.Require().NotNil(retrievedCommunity)
    s.Require().Equal(uint64(1000), retrievedCommunity.Clock)

    // Save a later community, with a single key
    missingKeys = []*CommunityPrivateDataFailedToDecrypt{
        {KeyID: keyID3},
    }

    communityDescription.Clock = 1001
    err = s.db.SaveDecryptedCommunityDescription(communityID, missingKeys, communityDescription)
    s.Require().NoError(err)

    // The new one should be retrieved
    retrievedCommunity, err = s.db.GetDecryptedCommunityDescription(communityID, 1001)
    s.Require().NoError(err)
    s.Require().Equal(uint64(1001), retrievedCommunity.Clock)

    // Make sure the previous two are cleaned up and there's only one left
    qr := s.db.db.QueryRow("SELECT COUNT(*) FROM encrypted_community_description_missing_keys")

    var count int

    err = qr.Scan(&count)
    s.Require().NoError(err)
    s.Require().Equal(count, 1)
}

func (s *PersistenceSuite) TestGetCommunityRequestsToJoinRevealedAddresses() {
    clock := uint64(time.Now().Unix())
    communityID := types.HexBytes{7, 7, 7, 7, 7, 7, 7, 7}
    revealedAddress := "address1"
    chainIds := []uint64{1, 2}
    publicKey := common.PubkeyToHex(&s.identity.PublicKey)
    signature := []byte("test")

    // No data in database
    accounts, err := s.db.GetCommunityRequestsToJoinRevealedAddresses(communityID)
    s.Require().NoError(err)
    _, exists := accounts[publicKey]
    s.Require().False(exists)

    expectedRtj := &RequestToJoin{
        ID:          types.HexBytes{1, 2, 3, 4, 5, 6, 7, 8},
        PublicKey:   publicKey,
        Clock:       clock,
        CommunityID: communityID,
        State:       RequestToJoinStateAccepted,
        RevealedAccounts: []*protobuf.RevealedAccount{
            {
                Address:          revealedAddress,
                ChainIds:         chainIds,
                IsAirdropAddress: true,
                Signature:        signature,
            },
        },
    }

    // Request to join was stored without revealed account
    err = s.db.SaveRequestToJoin(expectedRtj)
    s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error")

    // revealed account is absent
    accounts, err = s.db.GetCommunityRequestsToJoinRevealedAddresses(communityID)
    s.Require().NoError(err, "RevealedAccounts empty, shouldn't give any error")

    _, exists = accounts[publicKey]
    s.Require().False(exists)

    // save revealed accounts for the previous request to join
    err = s.db.SaveRequestToJoinRevealedAddresses(expectedRtj.ID, expectedRtj.RevealedAccounts)
    s.Require().NoError(err)

    accounts, err = s.db.GetCommunityRequestsToJoinRevealedAddresses(communityID)
    s.Require().NoError(err)
    memberAccounts, exists := accounts[publicKey]
    s.Require().True(exists)
    s.Require().Len(memberAccounts, 1)
}