cosmos-sdk/x/group/keeper/msg_server_priv_test.go
Alexander Peters 2f09f3af69
Merge commit from fork
* Fix ABS061

(cherry picked from commit f6f56395fd4b919063e19aa94e7178793ac0fd6b)
(cherry picked from commit 34b30b0ed6b3145b94719650eb3246a91ed977ce)

* Review feedback

(cherry picked from commit c892055e7fa86db8382fb2af17bb66342d7e2eeb)

* Check for events

(cherry picked from commit 16fc26041457b01077df4d3d3d25c9eaf00780d2)

* Review feedback

* Remove unecessary code comments

* Update changelog

* Remove duplicate headline after merge
2025-03-12 10:51:25 -04:00

255 lines
7.9 KiB
Go

package keeper
import (
"bytes"
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/cosmos/gogoproto/proto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
coreaddress "cosmossdk.io/core/address"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec/address"
"github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
"github.com/cosmos/cosmos-sdk/x/group"
"github.com/cosmos/cosmos-sdk/x/group/internal/math"
)
func TestDoTallyAndUpdate(t *testing.T) {
var (
myAddr = sdk.AccAddress(bytes.Repeat([]byte{0x01}, 20))
myOtherAddr = sdk.AccAddress(bytes.Repeat([]byte{0x02}, 20))
)
encCfg := moduletestutil.MakeTestEncodingConfig()
group.RegisterInterfaces(encCfg.InterfaceRegistry)
storeKey := storetypes.NewKVStoreKey(group.StoreKey)
testCtx := testutil.DefaultContextWithDB(t, storeKey, storetypes.NewTransientStoreKey("transient_test"))
myAccountKeeper := &mockAccountKeeper{
AddressCodecFn: func() coreaddress.Codec {
return address.NewBech32Codec(sdk.GetConfig().GetBech32AccountAddrPrefix())
},
}
groupKeeper := NewKeeper(storeKey, encCfg.Codec, nil, myAccountKeeper, group.DefaultConfig())
noEventsFn := func(proposalID uint64) sdk.Events { return sdk.Events{} }
type memberVote struct {
address string
weight string
option group.VoteOption
}
specs := map[string]struct {
votes []memberVote
policy group.DecisionPolicy
expStatus group.ProposalStatus
expVotesCleared bool
expEvents func(proposalID uint64) sdk.Events
}{
"proposal accepted": {
votes: []memberVote{
{address: myAddr.String(), option: group.VOTE_OPTION_YES, weight: "2"},
{address: myOtherAddr.String(), option: group.VOTE_OPTION_NO, weight: "1"},
},
policy: mockDecisionPolicy{
AllowFn: func(tallyResult group.TallyResult, totalPower string) (group.DecisionPolicyResult, error) {
return group.DecisionPolicyResult{Allow: true, Final: true}, nil
},
},
expStatus: group.PROPOSAL_STATUS_ACCEPTED,
expVotesCleared: true,
expEvents: noEventsFn,
},
"proposal rejected": {
votes: []memberVote{
{address: myAddr.String(), option: group.VOTE_OPTION_YES, weight: "1"},
{address: myOtherAddr.String(), option: group.VOTE_OPTION_NO, weight: "2"},
},
policy: mockDecisionPolicy{
AllowFn: func(tallyResult group.TallyResult, totalPower string) (group.DecisionPolicyResult, error) {
return group.DecisionPolicyResult{Allow: false, Final: true}, nil
},
},
expStatus: group.PROPOSAL_STATUS_REJECTED,
expVotesCleared: true,
expEvents: noEventsFn,
},
"proposal in flight": {
votes: []memberVote{
{address: myAddr.String(), option: group.VOTE_OPTION_YES, weight: "1"},
{address: myOtherAddr.String(), option: group.VOTE_OPTION_NO, weight: "1"},
},
policy: mockDecisionPolicy{
AllowFn: func(tallyResult group.TallyResult, totalPower string) (group.DecisionPolicyResult, error) {
return group.DecisionPolicyResult{Allow: false, Final: false}, nil
},
},
expStatus: group.PROPOSAL_STATUS_SUBMITTED,
expVotesCleared: false,
expEvents: noEventsFn,
},
"policy errors": {
votes: []memberVote{
{address: myAddr.String(), option: group.VOTE_OPTION_YES, weight: "1"},
{address: myOtherAddr.String(), option: group.VOTE_OPTION_NO, weight: "2"},
},
policy: mockDecisionPolicy{
AllowFn: func(tallyResult group.TallyResult, totalPower string) (group.DecisionPolicyResult, error) {
return group.DecisionPolicyResult{}, errors.New("my test error")
},
},
expStatus: group.PROPOSAL_STATUS_REJECTED,
expVotesCleared: true,
expEvents: func(proposalID uint64) sdk.Events {
return sdk.Events{
sdk.NewEvent("cosmos.group.v1.EventTallyError",
sdk.Attribute{Key: "error_message", Value: `"my test error"`},
sdk.Attribute{Key: "proposal_id", Value: fmt.Sprintf(`"%d"`, proposalID)},
),
}
},
},
}
var (
groupID uint64
proposalId uint64
)
for name, spec := range specs {
groupID++
proposalId++
t.Run(name, func(t *testing.T) {
em := sdk.NewEventManager()
ctx := testCtx.Ctx.WithEventManager(em)
totalWeight, err := math.NewDecFromString("0")
require.NoError(t, err)
// given a group, policy and persisted votes
for _, v := range spec.votes {
err := groupKeeper.groupMemberTable.Create(ctx.KVStore(storeKey), &group.GroupMember{
GroupId: groupID,
Member: &group.Member{Address: v.address, Weight: v.weight},
})
require.NoError(t, err)
err = groupKeeper.voteTable.Create(ctx.KVStore(storeKey), &group.Vote{
ProposalId: proposalId,
Voter: v.address,
Option: v.option,
})
require.NoError(t, err)
}
myGroupInfo := group.GroupInfo{
TotalWeight: totalWeight.String(),
}
myPolicy := group.GroupPolicyInfo{GroupId: groupID}
err = myPolicy.SetDecisionPolicy(spec.policy)
require.NoError(t, err)
myProposal := &group.Proposal{
Id: proposalId,
Status: group.PROPOSAL_STATUS_SUBMITTED,
VotingPeriodEnd: ctx.BlockTime().Add(time.Hour),
}
// when
gotErr := groupKeeper.doTallyAndUpdate(ctx, myProposal, myGroupInfo, myPolicy)
// then
require.NoError(t, gotErr)
assert.Equal(t, spec.expStatus, myProposal.Status)
require.Equal(t, spec.expEvents(proposalId), em.Events())
// and persistent state updated
persistedVotes, err := groupKeeper.votesByProposal(ctx, groupID)
require.NoError(t, err)
if spec.expVotesCleared {
assert.Empty(t, persistedVotes)
} else {
assert.Len(t, persistedVotes, len(spec.votes))
}
})
}
}
var _ group.AccountKeeper = &mockAccountKeeper{}
// mockAccountKeeper is a mock implementation of the AccountKeeper interface for testing purposes.
type mockAccountKeeper struct {
AddressCodecFn func() coreaddress.Codec
}
func (m mockAccountKeeper) AddressCodec() coreaddress.Codec {
if m.AddressCodecFn == nil {
panic("not expected to be called")
}
return m.AddressCodecFn()
}
func (m mockAccountKeeper) NewAccount(ctx context.Context, i sdk.AccountI) sdk.AccountI {
panic("not expected to be called")
}
func (m mockAccountKeeper) GetAccount(ctx context.Context, address sdk.AccAddress) sdk.AccountI {
panic("not expected to be called")
}
func (m mockAccountKeeper) SetAccount(ctx context.Context, i sdk.AccountI) {
panic("not expected to be called")
}
func (m mockAccountKeeper) RemoveAccount(ctx context.Context, acc sdk.AccountI) {
panic("not expected to be called")
}
// mockDecisionPolicy is a mock implementation of a decision policy for testing purposes.
type mockDecisionPolicy struct {
fakeProtoType
AllowFn func(tallyResult group.TallyResult, totalPower string) (group.DecisionPolicyResult, error)
}
func (m mockDecisionPolicy) Allow(tallyResult group.TallyResult, totalPower string) (group.DecisionPolicyResult, error) {
if m.AllowFn == nil {
panic("not expected to be called")
}
return m.AllowFn(tallyResult, totalPower)
}
func (m mockDecisionPolicy) GetVotingPeriod() time.Duration {
panic("not expected to be called")
}
func (m mockDecisionPolicy) GetMinExecutionPeriod() time.Duration {
panic("not expected to be called")
}
func (m mockDecisionPolicy) ValidateBasic() error {
panic("not expected to be called")
}
func (m mockDecisionPolicy) Validate(g group.GroupInfo, config group.Config) error {
panic("not expected to be called")
}
var (
_ proto.Marshaler = (*fakeProtoType)(nil)
_ proto.Message = (*fakeProtoType)(nil)
)
// fakeProtoType is a struct used for mocking and testing purposes.
// Custom types can be converted into Any and back via internal CachedValue only.
type fakeProtoType struct{}
func (a fakeProtoType) Reset() {}
func (a fakeProtoType) String() string {
return "testing"
}
func (a fakeProtoType) Marshal() ([]byte, error) {
return nil, nil
}
func (a fakeProtoType) ProtoMessage() {}