github.com/cosmos/cosmos-sdk@v0.50.10/x/group/module/abci_test.go (about)

     1  package module_test
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
     9  	cmttime "github.com/cometbft/cometbft/types/time"
    10  	"github.com/stretchr/testify/suite"
    11  
    12  	"cosmossdk.io/core/address"
    13  	"cosmossdk.io/depinject"
    14  	"cosmossdk.io/log"
    15  	"cosmossdk.io/math"
    16  
    17  	codecaddress "github.com/cosmos/cosmos-sdk/codec/address"
    18  	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
    19  	"github.com/cosmos/cosmos-sdk/runtime"
    20  	simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
    21  	sdk "github.com/cosmos/cosmos-sdk/types"
    22  	bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
    23  	"github.com/cosmos/cosmos-sdk/x/bank/testutil"
    24  	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
    25  	"github.com/cosmos/cosmos-sdk/x/group"
    26  	"github.com/cosmos/cosmos-sdk/x/group/keeper"
    27  	"github.com/cosmos/cosmos-sdk/x/group/module"
    28  	grouptestutil "github.com/cosmos/cosmos-sdk/x/group/testutil"
    29  	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    30  )
    31  
    32  type IntegrationTestSuite struct {
    33  	suite.Suite
    34  
    35  	app               *runtime.App
    36  	ctx               sdk.Context
    37  	addrs             []sdk.AccAddress
    38  	groupKeeper       keeper.Keeper
    39  	bankKeeper        bankkeeper.Keeper
    40  	stakingKeeper     *stakingkeeper.Keeper
    41  	interfaceRegistry codectypes.InterfaceRegistry
    42  
    43  	addressCodec address.Codec
    44  }
    45  
    46  func TestIntegrationTestSuite(t *testing.T) {
    47  	suite.Run(t, new(IntegrationTestSuite))
    48  }
    49  
    50  func (s *IntegrationTestSuite) SetupTest() {
    51  	app, err := simtestutil.Setup(
    52  		depinject.Configs(
    53  			grouptestutil.AppConfig,
    54  			depinject.Supply(log.NewNopLogger()),
    55  		),
    56  		&s.interfaceRegistry,
    57  		&s.bankKeeper,
    58  		&s.stakingKeeper,
    59  		&s.groupKeeper,
    60  	)
    61  	s.Require().NoError(err)
    62  
    63  	ctx := app.BaseApp.NewContext(false)
    64  
    65  	ctx = ctx.WithBlockHeader(cmtproto.Header{Time: cmttime.Now()})
    66  
    67  	s.ctx = ctx
    68  
    69  	s.addrs = simtestutil.AddTestAddrsIncremental(s.bankKeeper, s.stakingKeeper, ctx, 4, math.NewInt(30000000))
    70  
    71  	s.addressCodec = codecaddress.NewBech32Codec("cosmos")
    72  }
    73  
    74  func (s *IntegrationTestSuite) TestEndBlockerPruning() {
    75  	ctx := s.ctx
    76  	addr1 := s.addrs[0]
    77  	addr2 := s.addrs[1]
    78  	addr3 := s.addrs[2]
    79  
    80  	addr1st, err := s.addressCodec.BytesToString(addr1)
    81  	s.Require().NoError(err)
    82  
    83  	// Initial group, group policy and balance setup
    84  	members := []group.MemberRequest{
    85  		{Address: addr1st, Weight: "1"}, {Address: addr2.String(), Weight: "2"},
    86  	}
    87  
    88  	groupRes, err := s.groupKeeper.CreateGroup(ctx, &group.MsgCreateGroup{
    89  		Admin:   addr1st,
    90  		Members: members,
    91  	})
    92  	s.Require().NoError(err)
    93  
    94  	groupRes2, err := s.groupKeeper.CreateGroup(ctx, &group.MsgCreateGroup{
    95  		Admin:   addr2.String(),
    96  		Members: members,
    97  	})
    98  	s.Require().NoError(err)
    99  
   100  	groupID := groupRes.GroupId
   101  	groupID2 := groupRes2.GroupId
   102  
   103  	policy := group.NewThresholdDecisionPolicy(
   104  		"2",
   105  		time.Second,
   106  		0,
   107  	)
   108  
   109  	policyReq := &group.MsgCreateGroupPolicy{
   110  		Admin:   addr1.String(),
   111  		GroupId: groupID,
   112  	}
   113  
   114  	err = policyReq.SetDecisionPolicy(policy)
   115  	s.Require().NoError(err)
   116  	policyRes, err := s.groupKeeper.CreateGroupPolicy(ctx, policyReq)
   117  	s.Require().NoError(err)
   118  
   119  	policy2 := group.NewThresholdDecisionPolicy(
   120  		"1",
   121  		time.Second,
   122  		0,
   123  	)
   124  
   125  	policyReq2 := &group.MsgCreateGroupPolicy{
   126  		Admin:   addr2.String(),
   127  		GroupId: groupID2,
   128  	}
   129  
   130  	err = policyReq2.SetDecisionPolicy(policy2)
   131  	s.Require().NoError(err)
   132  	policyRes2, err := s.groupKeeper.CreateGroupPolicy(ctx, policyReq2)
   133  	s.Require().NoError(err)
   134  
   135  	groupPolicyAddr, err := s.addressCodec.StringToBytes(policyRes.Address)
   136  	s.Require().NoError(err)
   137  	s.Require().NoError(testutil.FundAccount(ctx, s.bankKeeper, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)}))
   138  
   139  	groupPolicyAddr2, err := s.addressCodec.StringToBytes(policyRes2.Address)
   140  	s.Require().NoError(err)
   141  	s.Require().NoError(testutil.FundAccount(ctx, s.bankKeeper, groupPolicyAddr2, sdk.Coins{sdk.NewInt64Coin("test", 10000)}))
   142  
   143  	votingPeriod := policy.GetVotingPeriod()
   144  
   145  	msgSend1 := &banktypes.MsgSend{
   146  		FromAddress: policyRes.Address,
   147  		ToAddress:   addr2.String(),
   148  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
   149  	}
   150  	msgSend2 := &banktypes.MsgSend{
   151  		FromAddress: policyRes2.Address,
   152  		ToAddress:   addr2.String(),
   153  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
   154  	}
   155  	proposers := []string{addr2.String()}
   156  
   157  	specs := map[string]struct {
   158  		setupProposal     func(ctx sdk.Context) uint64
   159  		expErr            bool
   160  		expErrMsg         string
   161  		newCtx            sdk.Context
   162  		expExecutorResult group.ProposalExecutorResult
   163  		expStatus         group.ProposalStatus
   164  	}{
   165  		"proposal pruned after executor result success": {
   166  			setupProposal: func(ctx sdk.Context) uint64 {
   167  				msgs := []sdk.Msg{msgSend1}
   168  				pID, err := submitProposalAndVote(s, s.app, ctx, msgs, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
   169  				s.Require().NoError(err)
   170  				_, err = s.groupKeeper.Exec(ctx, &group.MsgExec{Executor: addr3.String(), ProposalId: pID})
   171  				s.Require().NoError(err)
   172  				sdkCtx := sdk.UnwrapSDKContext(ctx)
   173  				s.Require().NoError(testutil.FundAccount(sdkCtx, s.bankKeeper, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
   174  
   175  				return pID
   176  			},
   177  			expErrMsg:         "load proposal: not found",
   178  			newCtx:            ctx,
   179  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
   180  		},
   181  		"proposal with multiple messages pruned when executed with result success": {
   182  			setupProposal: func(ctx sdk.Context) uint64 {
   183  				msgs := []sdk.Msg{msgSend1, msgSend1}
   184  				pID, err := submitProposalAndVote(s, s.app, ctx, msgs, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
   185  				s.Require().NoError(err)
   186  				_, err = s.groupKeeper.Exec(ctx, &group.MsgExec{Executor: addr3.String(), ProposalId: pID})
   187  				s.Require().NoError(err)
   188  				sdkCtx := sdk.UnwrapSDKContext(ctx)
   189  				s.Require().NoError(testutil.FundAccount(sdkCtx, s.bankKeeper, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
   190  
   191  				return pID
   192  			},
   193  			expErrMsg:         "load proposal: not found",
   194  			newCtx:            ctx,
   195  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
   196  		},
   197  		"proposal not pruned when not executed and rejected": {
   198  			setupProposal: func(ctx sdk.Context) uint64 {
   199  				msgs := []sdk.Msg{msgSend1}
   200  				pID, err := submitProposalAndVote(s, s.app, ctx, msgs, proposers, groupPolicyAddr, group.VOTE_OPTION_NO)
   201  				s.Require().NoError(err)
   202  				_, err = s.groupKeeper.Exec(ctx, &group.MsgExec{Executor: addr3.String(), ProposalId: pID})
   203  				s.Require().NoError(err)
   204  				sdkCtx := sdk.UnwrapSDKContext(ctx)
   205  				s.Require().NoError(testutil.FundAccount(sdkCtx, s.bankKeeper, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
   206  
   207  				return pID
   208  			},
   209  			newCtx:            ctx,
   210  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
   211  			expStatus:         group.PROPOSAL_STATUS_REJECTED,
   212  		},
   213  		"open proposal is not pruned which must not fail ": {
   214  			setupProposal: func(ctx sdk.Context) uint64 {
   215  				pID, err := submitProposal(s, s.app, ctx, []sdk.Msg{msgSend1}, proposers, groupPolicyAddr)
   216  				s.Require().NoError(err)
   217  				_, err = s.groupKeeper.Exec(ctx, &group.MsgExec{Executor: addr3.String(), ProposalId: pID})
   218  				s.Require().NoError(err)
   219  				sdkCtx := sdk.UnwrapSDKContext(ctx)
   220  				s.Require().NoError(testutil.FundAccount(sdkCtx, s.bankKeeper, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
   221  
   222  				return pID
   223  			},
   224  			newCtx:            ctx,
   225  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
   226  			expStatus:         group.PROPOSAL_STATUS_SUBMITTED,
   227  		},
   228  		"proposal not pruned with group policy modified before tally": {
   229  			setupProposal: func(ctx sdk.Context) uint64 {
   230  				pID, err := submitProposal(s, s.app, ctx, []sdk.Msg{msgSend1}, proposers, groupPolicyAddr)
   231  				s.Require().NoError(err)
   232  				_, err = s.groupKeeper.UpdateGroupPolicyMetadata(ctx, &group.MsgUpdateGroupPolicyMetadata{
   233  					Admin:              addr1.String(),
   234  					GroupPolicyAddress: policyRes.Address,
   235  				})
   236  				s.Require().NoError(err)
   237  				_, err = s.groupKeeper.Exec(ctx, &group.MsgExec{Executor: addr3.String(), ProposalId: pID})
   238  				s.Require().Error(err) // since proposal with status Aborted cannot be executed
   239  				sdkCtx := sdk.UnwrapSDKContext(ctx)
   240  				s.Require().NoError(testutil.FundAccount(sdkCtx, s.bankKeeper, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
   241  
   242  				return pID
   243  			},
   244  			newCtx:            ctx,
   245  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
   246  			expStatus:         group.PROPOSAL_STATUS_ABORTED,
   247  		},
   248  		"pruned when proposal is executable when failed before": {
   249  			setupProposal: func(ctx sdk.Context) uint64 {
   250  				msgs := []sdk.Msg{msgSend1}
   251  				pID, err := submitProposalAndVote(s, s.app, ctx, msgs, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
   252  				s.Require().NoError(err)
   253  				_, err = s.groupKeeper.Exec(ctx, &group.MsgExec{Executor: s.addrs[2].String(), ProposalId: pID})
   254  				s.Require().NoError(err)
   255  				return pID
   256  			},
   257  			newCtx:            ctx,
   258  			expErrMsg:         "load proposal: not found",
   259  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
   260  		},
   261  		"proposal with status withdrawn is pruned after voting period end": {
   262  			setupProposal: func(sdkCtx sdk.Context) uint64 {
   263  				pID, err := submitProposal(s, s.app, sdkCtx, []sdk.Msg{msgSend1}, proposers, groupPolicyAddr)
   264  				s.Require().NoError(err)
   265  				_, err = s.groupKeeper.WithdrawProposal(ctx, &group.MsgWithdrawProposal{
   266  					ProposalId: pID,
   267  					Address:    proposers[0],
   268  				})
   269  				s.Require().NoError(err)
   270  				return pID
   271  			},
   272  			newCtx:    ctx.WithBlockTime(ctx.BlockTime().Add(votingPeriod).Add(time.Hour)),
   273  			expErrMsg: "load proposal: not found",
   274  			expStatus: group.PROPOSAL_STATUS_WITHDRAWN,
   275  		},
   276  		"proposal with status withdrawn is not pruned (before voting period)": {
   277  			setupProposal: func(sdkCtx sdk.Context) uint64 {
   278  				pID, err := submitProposal(s, s.app, sdkCtx, []sdk.Msg{msgSend1}, proposers, groupPolicyAddr)
   279  				s.Require().NoError(err)
   280  				_, err = s.groupKeeper.WithdrawProposal(ctx, &group.MsgWithdrawProposal{
   281  					ProposalId: pID,
   282  					Address:    proposers[0],
   283  				})
   284  				s.Require().NoError(err)
   285  				return pID
   286  			},
   287  			newCtx:            ctx,
   288  			expErrMsg:         "",
   289  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
   290  			expStatus:         group.PROPOSAL_STATUS_WITHDRAWN,
   291  		},
   292  		"proposal with status aborted is pruned after voting period end (due to updated group policy decision policy)": {
   293  			setupProposal: func(sdkCtx sdk.Context) uint64 {
   294  				pID, err := submitProposal(s, s.app, sdkCtx, []sdk.Msg{msgSend2}, proposers, groupPolicyAddr2)
   295  				s.Require().NoError(err)
   296  
   297  				policy := group.NewThresholdDecisionPolicy("3", time.Second, 0)
   298  				msg := &group.MsgUpdateGroupPolicyDecisionPolicy{
   299  					Admin:              s.addrs[1].String(),
   300  					GroupPolicyAddress: policyRes2.Address,
   301  				}
   302  				err = msg.SetDecisionPolicy(policy)
   303  				s.Require().NoError(err)
   304  				_, err = s.groupKeeper.UpdateGroupPolicyDecisionPolicy(ctx, msg)
   305  				s.Require().NoError(err)
   306  
   307  				return pID
   308  			},
   309  			newCtx:            ctx.WithBlockTime(ctx.BlockTime().Add(votingPeriod).Add(time.Hour)),
   310  			expErrMsg:         "load proposal: not found",
   311  			expStatus:         group.PROPOSAL_STATUS_ABORTED,
   312  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
   313  		},
   314  		"proposal with status aborted is not pruned before voting period end (due to updated group policy)": {
   315  			setupProposal: func(sdkCtx sdk.Context) uint64 {
   316  				pID, err := submitProposal(s, s.app, sdkCtx, []sdk.Msg{msgSend2}, proposers, groupPolicyAddr2)
   317  				s.Require().NoError(err)
   318  
   319  				policy := group.NewThresholdDecisionPolicy("3", time.Second, 0)
   320  				msg := &group.MsgUpdateGroupPolicyDecisionPolicy{
   321  					Admin:              s.addrs[1].String(),
   322  					GroupPolicyAddress: policyRes2.Address,
   323  				}
   324  				err = msg.SetDecisionPolicy(policy)
   325  				s.Require().NoError(err)
   326  				_, err = s.groupKeeper.UpdateGroupPolicyDecisionPolicy(ctx, msg)
   327  				s.Require().NoError(err)
   328  
   329  				return pID
   330  			},
   331  			newCtx:            ctx,
   332  			expErrMsg:         "",
   333  			expStatus:         group.PROPOSAL_STATUS_ABORTED,
   334  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
   335  		},
   336  	}
   337  	for msg, spec := range specs {
   338  		spec := spec
   339  		s.Run(msg, func() {
   340  			proposalID := spec.setupProposal(ctx)
   341  
   342  			module.EndBlocker(spec.newCtx, s.groupKeeper)
   343  
   344  			if spec.expErrMsg != "" && spec.expExecutorResult != group.PROPOSAL_EXECUTOR_RESULT_SUCCESS {
   345  				_, err = s.groupKeeper.Proposal(spec.newCtx, &group.QueryProposalRequest{ProposalId: proposalID})
   346  				s.Require().Error(err)
   347  				s.Require().Contains(err.Error(), spec.expErrMsg)
   348  				return
   349  			}
   350  			if spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS {
   351  				// Make sure proposal is deleted from state
   352  				_, err = s.groupKeeper.Proposal(spec.newCtx, &group.QueryProposalRequest{ProposalId: proposalID})
   353  				s.Require().Contains(err.Error(), spec.expErrMsg)
   354  				res, err := s.groupKeeper.VotesByProposal(ctx, &group.QueryVotesByProposalRequest{ProposalId: proposalID})
   355  				s.Require().NoError(err)
   356  				s.Require().Empty(res.GetVotes())
   357  			} else {
   358  				// Check that proposal and votes exists
   359  				res, err := s.groupKeeper.Proposal(spec.newCtx, &group.QueryProposalRequest{ProposalId: proposalID})
   360  				s.Require().NoError(err)
   361  				_, err = s.groupKeeper.VotesByProposal(ctx, &group.QueryVotesByProposalRequest{ProposalId: res.Proposal.Id})
   362  				s.Require().NoError(err)
   363  				s.Require().Equal("", spec.expErrMsg)
   364  
   365  				exp := group.ProposalExecutorResult_name[int32(spec.expExecutorResult)]
   366  				got := group.ProposalExecutorResult_name[int32(res.Proposal.ExecutorResult)]
   367  				s.Assert().Equal(exp, got)
   368  
   369  				s.Require().Equal(res.GetProposal().Status, spec.expStatus)
   370  			}
   371  		})
   372  	}
   373  }
   374  
   375  func (s *IntegrationTestSuite) TestEndBlockerTallying() {
   376  	app := s.app
   377  	ctx := s.ctx
   378  
   379  	addrs := s.addrs
   380  
   381  	// Initial group, group policy and balance setup
   382  	members := []group.MemberRequest{
   383  		{Address: addrs[1].String(), Weight: "1"}, {Address: addrs[2].String(), Weight: "2"},
   384  	}
   385  
   386  	groupRes, err := s.groupKeeper.CreateGroup(ctx, &group.MsgCreateGroup{
   387  		Admin:   addrs[0].String(),
   388  		Members: members,
   389  	})
   390  	s.Require().NoError(err)
   391  
   392  	groupID := groupRes.GroupId
   393  
   394  	policy := group.NewThresholdDecisionPolicy(
   395  		"2",
   396  		time.Second,
   397  		0,
   398  	)
   399  
   400  	policyReq := &group.MsgCreateGroupPolicy{
   401  		Admin:   addrs[0].String(),
   402  		GroupId: groupID,
   403  	}
   404  
   405  	err = policyReq.SetDecisionPolicy(policy)
   406  	s.Require().NoError(err)
   407  	policyRes, err := s.groupKeeper.CreateGroupPolicy(ctx, policyReq)
   408  	s.Require().NoError(err)
   409  
   410  	groupPolicyAddr, err := s.addressCodec.StringToBytes(policyRes.Address)
   411  	s.Require().NoError(err)
   412  
   413  	votingPeriod := policy.GetVotingPeriod()
   414  
   415  	msgSend := &banktypes.MsgSend{
   416  		FromAddress: policyRes.Address,
   417  		ToAddress:   addrs[3].String(),
   418  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
   419  	}
   420  
   421  	proposers := []string{addrs[2].String()}
   422  
   423  	specs := map[string]struct {
   424  		preRun    func(sdkCtx sdk.Context) uint64
   425  		admin     string
   426  		expErrMsg string
   427  		newCtx    sdk.Context
   428  		tallyRes  group.TallyResult
   429  		expStatus group.ProposalStatus
   430  	}{
   431  		"tally updated after voting period end": {
   432  			preRun: func(sdkCtx sdk.Context) uint64 {
   433  				pID, err := submitProposal(s, app, sdkCtx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr)
   434  				s.Require().NoError(err)
   435  				return pID
   436  			},
   437  			admin:     proposers[0],
   438  			newCtx:    ctx.WithBlockTime(ctx.BlockTime().Add(votingPeriod).Add(time.Hour)),
   439  			tallyRes:  group.DefaultTallyResult(),
   440  			expStatus: group.PROPOSAL_STATUS_REJECTED,
   441  		},
   442  		"tally within voting period": {
   443  			preRun: func(sdkCtx sdk.Context) uint64 {
   444  				pID, err := submitProposal(s, app, sdkCtx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr)
   445  				s.Require().NoError(err)
   446  
   447  				return pID
   448  			},
   449  			admin:     proposers[0],
   450  			newCtx:    ctx,
   451  			tallyRes:  group.DefaultTallyResult(),
   452  			expStatus: group.PROPOSAL_STATUS_SUBMITTED,
   453  		},
   454  		"tally within voting period(with votes)": {
   455  			preRun: func(sdkCtx sdk.Context) uint64 {
   456  				pID, err := submitProposalAndVote(s, app, ctx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
   457  				s.Require().NoError(err)
   458  
   459  				return pID
   460  			},
   461  			admin:     proposers[0],
   462  			newCtx:    ctx,
   463  			tallyRes:  group.DefaultTallyResult(),
   464  			expStatus: group.PROPOSAL_STATUS_SUBMITTED,
   465  		},
   466  		"tally after voting period (not passing)": {
   467  			preRun: func(sdkCtx sdk.Context) uint64 {
   468  				// `addrs[1]` has weight 1
   469  				pID, err := submitProposalAndVote(s, app, ctx, []sdk.Msg{msgSend}, []string{addrs[1].String()}, groupPolicyAddr, group.VOTE_OPTION_YES)
   470  				s.Require().NoError(err)
   471  
   472  				return pID
   473  			},
   474  			admin:  proposers[0],
   475  			newCtx: ctx.WithBlockTime(ctx.BlockTime().Add(votingPeriod).Add(time.Hour)),
   476  			tallyRes: group.TallyResult{
   477  				YesCount:        "1",
   478  				NoCount:         "0",
   479  				NoWithVetoCount: "0",
   480  				AbstainCount:    "0",
   481  			},
   482  			expStatus: group.PROPOSAL_STATUS_REJECTED,
   483  		},
   484  		"tally after voting period(with votes)": {
   485  			preRun: func(sdkCtx sdk.Context) uint64 {
   486  				pID, err := submitProposalAndVote(s, app, ctx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
   487  				s.Require().NoError(err)
   488  
   489  				return pID
   490  			},
   491  			admin:  proposers[0],
   492  			newCtx: ctx.WithBlockTime(ctx.BlockTime().Add(votingPeriod).Add(time.Hour)),
   493  			tallyRes: group.TallyResult{
   494  				YesCount:        "2",
   495  				NoCount:         "0",
   496  				NoWithVetoCount: "0",
   497  				AbstainCount:    "0",
   498  			},
   499  			expStatus: group.PROPOSAL_STATUS_ACCEPTED,
   500  		},
   501  		"tally of withdrawn proposal": {
   502  			preRun: func(sdkCtx sdk.Context) uint64 {
   503  				pID, err := submitProposal(s, app, sdkCtx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr)
   504  				s.Require().NoError(err)
   505  
   506  				_, err = s.groupKeeper.WithdrawProposal(ctx, &group.MsgWithdrawProposal{
   507  					ProposalId: pID,
   508  					Address:    proposers[0],
   509  				})
   510  
   511  				s.Require().NoError(err)
   512  				return pID
   513  			},
   514  			admin:     proposers[0],
   515  			newCtx:    ctx,
   516  			tallyRes:  group.DefaultTallyResult(),
   517  			expStatus: group.PROPOSAL_STATUS_WITHDRAWN,
   518  		},
   519  		"tally of withdrawn proposal (with votes)": {
   520  			preRun: func(sdkCtx sdk.Context) uint64 {
   521  				pID, err := submitProposalAndVote(s, app, ctx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
   522  				s.Require().NoError(err)
   523  
   524  				_, err = s.groupKeeper.WithdrawProposal(ctx, &group.MsgWithdrawProposal{
   525  					ProposalId: pID,
   526  					Address:    proposers[0],
   527  				})
   528  
   529  				s.Require().NoError(err)
   530  				return pID
   531  			},
   532  			admin:     proposers[0],
   533  			newCtx:    ctx,
   534  			tallyRes:  group.DefaultTallyResult(),
   535  			expStatus: group.PROPOSAL_STATUS_WITHDRAWN,
   536  		},
   537  	}
   538  
   539  	for msg, spec := range specs {
   540  		s.Run(msg, func() {
   541  			spec := spec
   542  			pID := spec.preRun(ctx)
   543  
   544  			module.EndBlocker(spec.newCtx, s.groupKeeper)
   545  			resp, err := s.groupKeeper.Proposal(spec.newCtx, &group.QueryProposalRequest{
   546  				ProposalId: pID,
   547  			})
   548  
   549  			if spec.expErrMsg != "" {
   550  				s.Require().NoError(err)
   551  				s.Require().Contains(err.Error(), spec.expErrMsg)
   552  				return
   553  			}
   554  
   555  			s.Require().NoError(err)
   556  			s.Require().Equal(resp.GetProposal().FinalTallyResult, spec.tallyRes)
   557  			s.Require().Equal(resp.GetProposal().Status, spec.expStatus)
   558  		})
   559  	}
   560  }
   561  
   562  func submitProposal(s *IntegrationTestSuite, app *runtime.App, ctx context.Context, msgs []sdk.Msg, proposers []string, groupPolicyAddr sdk.AccAddress) (uint64, error) { //nolint:revive // context-as-argument: context.Context should be the first parameter of a function
   563  	proposalReq := &group.MsgSubmitProposal{
   564  		GroupPolicyAddress: groupPolicyAddr.String(),
   565  		Proposers:          proposers,
   566  	}
   567  	err := proposalReq.SetMsgs(msgs)
   568  	if err != nil {
   569  		return 0, err
   570  	}
   571  
   572  	proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq)
   573  	if err != nil {
   574  		return 0, err
   575  	}
   576  
   577  	return proposalRes.ProposalId, nil
   578  }
   579  
   580  func submitProposalAndVote(
   581  	s *IntegrationTestSuite, app *runtime.App, ctx context.Context, msgs []sdk.Msg, //nolint:revive // context-as-argument: context.Context should be the first parameter of a function
   582  	proposers []string, groupPolicyAddr sdk.AccAddress, voteOption group.VoteOption,
   583  ) (uint64, error) {
   584  	myProposalID, err := submitProposal(s, app, ctx, msgs, proposers, groupPolicyAddr)
   585  	if err != nil {
   586  		return 0, err
   587  	}
   588  	_, err = s.groupKeeper.Vote(ctx, &group.MsgVote{
   589  		ProposalId: myProposalID,
   590  		Voter:      proposers[0],
   591  		Option:     voteOption,
   592  	})
   593  	if err != nil {
   594  		return 0, err
   595  	}
   596  	return myProposalID, nil
   597  }