From cac3415b0fa962821a3677ba27e53f6f77202cf0 Mon Sep 17 00:00:00 2001 From: Hanjun Kim Date: Thu, 7 Oct 2021 00:05:31 +0900 Subject: [PATCH] test: add tests for types (#157) * test: add TestPlanI * test: add TestBasePlanValidate * test: add TestIsPlanActiveAt * fix: fix proposal validation logic - fix nil pointer dereference issue * test: add TestValidateStakingCoinTotalWeights * test: add tests for proposal validation * test: change expected error messages --- x/farming/keeper/proposal_handler_test.go | 8 +- x/farming/types/plan.go | 6 +- x/farming/types/plan_test.go | 401 ++++++++++++++++++++++ x/farming/types/proposal.go | 42 ++- x/farming/types/proposal_test.go | 390 +++++++++++++++++++++ 5 files changed, 823 insertions(+), 24 deletions(-) create mode 100644 x/farming/types/proposal_test.go diff --git a/x/farming/keeper/proposal_handler_test.go b/x/farming/keeper/proposal_handler_test.go index 79fe959c..bbe0bc2d 100644 --- a/x/farming/keeper/proposal_handler_test.go +++ b/x/farming/keeper/proposal_handler_test.go @@ -125,7 +125,7 @@ func (suite *KeeperTestSuite) TestValidateAddPublicPlanProposal() { sdk.NewCoins(sdk.NewInt64Coin(denom3, 1)), sdk.NewDec(1), )}, - sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "either epoch amount or epoch ratio should be provided"), + sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only one of epoch amount or epoch ratio must be provided"), }, { "epoch amount & epoch ratio case #2", @@ -142,7 +142,7 @@ func (suite *KeeperTestSuite) TestValidateAddPublicPlanProposal() { sdk.NewCoins(), sdk.ZeroDec(), )}, - sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "either epoch amount or epoch ratio must not be zero"), + sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only one of epoch amount or epoch ratio must be provided"), }, } { suite.Run(tc.name, func() { @@ -320,7 +320,7 @@ func (suite *KeeperTestSuite) TestValidateUpdatePublicPlanProposal() { sdk.NewCoins(sdk.NewInt64Coin("stake", 100_000)), plan.(*types.RatioPlan).EpochRatio, )}, - sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "either epoch amount or epoch ratio should be provided"), + sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only one of epoch amount or epoch ratio must be provided"), }, { "epoch amount & epoch ratio case #2", @@ -336,7 +336,7 @@ func (suite *KeeperTestSuite) TestValidateUpdatePublicPlanProposal() { sdk.NewCoins(sdk.NewInt64Coin("stake", 0)), sdk.ZeroDec(), )}, - sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "either epoch amount or epoch ratio must not be zero"), + sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only one of epoch amount or epoch ratio must be provided"), }, } { suite.Run(tc.name, func() { diff --git a/x/farming/types/plan.go b/x/farming/types/plan.go index 581e049e..da70d66b 100644 --- a/x/farming/types/plan.go +++ b/x/farming/types/plan.go @@ -190,8 +190,8 @@ func (plan BasePlan) Validate() error { if !plan.EndTime.After(plan.StartTime) { return sdkerrors.Wrapf(ErrInvalidPlanEndTime, "end time %s must be greater than start time %s", plan.EndTime, plan.StartTime) } - if plan.DistributedCoins.IsAnyNegative() { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "distributed coins must not be negative") + if err := plan.DistributedCoins.Validate(); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "invalid distributed coins: %v", err) } return nil } @@ -201,7 +201,7 @@ func (plan BasePlan) String() string { return out.(string) } -// MarshalYAML returns the YAML representation of an Plan. +// MarshalYAML returns the YAML representation of a Plan. func (plan BasePlan) MarshalYAML() (interface{}, error) { bz, err := codec.MarshalYAML(codec.NewProtoCodec(codectypes.NewInterfaceRegistry()), &plan) if err != nil { diff --git a/x/farming/types/plan_test.go b/x/farming/types/plan_test.go index 33528e47..c75f5ea5 100644 --- a/x/farming/types/plan_test.go +++ b/x/farming/types/plan_test.go @@ -1,10 +1,12 @@ package types_test import ( + "strings" "testing" "time" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/address" @@ -13,6 +15,405 @@ import ( "github.com/tendermint/farming/x/farming/types" ) +func TestPlanI(t *testing.T) { + bp := types.NewBasePlan( + 1, + "sample plan", + types.PlanTypePublic, + sdk.AccAddress(crypto.AddressHash([]byte("address1"))).String(), + sdk.AccAddress(crypto.AddressHash([]byte("address2"))).String(), + sdk.NewDecCoins(sdk.NewInt64DecCoin("stake1", 1)), + types.ParseTime("0001-01-01T00:00:00Z"), + types.ParseTime("9999-12-31T00:00:00Z"), + ) + plan := types.NewFixedAmountPlan(bp, sdk.NewCoins(sdk.NewInt64Coin("reward1", 10000000))) + lastDistributionTime := types.ParseTime("2021-11-01T00:00:00Z") + + require.Equal(t, bp, plan.GetBasePlan()) + + for _, tc := range []struct { + name string + get func() interface{} + set func(types.PlanI, interface{}) error + oldVal, newVal interface{} + equal func(interface{}, interface{}) bool + }{ + { + "Id", + func() interface{} { + return plan.GetId() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetId(val.(uint64)) + }, + uint64(1), uint64(2), + nil, + }, + { + "Name", + func() interface{} { + return plan.GetName() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetName(val.(string)) + }, + "sample plan", "new plan", + nil, + }, + { + "Type", + func() interface{} { + return plan.GetType() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetType(val.(types.PlanType)) + }, + types.PlanTypePublic, types.PlanTypePrivate, + nil, + }, + { + "FarmingPoolAddress", + func() interface{} { + return plan.GetFarmingPoolAddress() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetFarmingPoolAddress(val.(sdk.AccAddress)) + }, + sdk.AccAddress(crypto.AddressHash([]byte("address1"))), + sdk.AccAddress(crypto.AddressHash([]byte("address3"))), + func(a, b interface{}) bool { + return a.(sdk.AccAddress).Equals(b.(sdk.AccAddress)) + }, + }, + { + "TerminationAddress", + func() interface{} { + return plan.GetTerminationAddress() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetTerminationAddress(val.(sdk.AccAddress)) + }, + sdk.AccAddress(crypto.AddressHash([]byte("address2"))), + sdk.AccAddress(crypto.AddressHash([]byte("address4"))), + func(a, b interface{}) bool { + return a.(sdk.AccAddress).Equals(b.(sdk.AccAddress)) + }, + }, + { + "StakingCoinWeights", + func() interface{} { + return plan.GetStakingCoinWeights() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetStakingCoinWeights(val.(sdk.DecCoins)) + }, + sdk.NewDecCoins(sdk.NewInt64DecCoin("stake1", 1)), + sdk.NewDecCoins( + sdk.NewDecCoinFromDec("stake1", sdk.NewDecWithPrec(5, 1)), + sdk.NewDecCoinFromDec("stake2", sdk.NewDecWithPrec(5, 1)), + ), + func(a, b interface{}) bool { + return a.(sdk.DecCoins).IsEqual(b.(sdk.DecCoins)) + }, + }, + { + "StartTime", + func() interface{} { + return plan.GetStartTime() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetStartTime(val.(time.Time)) + }, + types.ParseTime("0001-01-01T00:00:00Z"), + types.ParseTime("2021-10-01T00:00:00Z"), + nil, + }, + { + "EndTime", + func() interface{} { + return plan.GetEndTime() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetEndTime(val.(time.Time)) + }, + types.ParseTime("9999-12-31T00:00:00Z"), + types.ParseTime("2021-12-31T00:00:00Z"), + nil, + }, + { + "Terminated", + func() interface{} { + return plan.GetTerminated() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetTerminated(val.(bool)) + }, + false, true, + nil, + }, + { + "LastDistributionTime", + func() interface{} { + return plan.GetLastDistributionTime() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetLastDistributionTime(val.(*time.Time)) + }, + (*time.Time)(nil), &lastDistributionTime, + func(a, b interface{}) bool { + at := a.(*time.Time) + bt := b.(*time.Time) + if at == nil && bt == nil { + return true + } else if (at == nil) != (bt == nil) { + return false + } + return (*at).Equal(*bt) + }, + }, + { + "DistributedCoins", + func() interface{} { + return plan.GetDistributedCoins() + }, + func(plan types.PlanI, val interface{}) error { + return plan.SetDistributedCoins(val.(sdk.Coins)) + }, + sdk.NewCoins(), + sdk.NewCoins(sdk.NewInt64Coin("reward1", 10000000)), + func(a, b interface{}) bool { + return a.(sdk.Coins).IsEqual(b.(sdk.Coins)) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + val := tc.get() + if tc.equal != nil { + require.True(t, tc.equal(tc.oldVal, val)) + } else { + require.Equal(t, tc.oldVal, val) + } + err := tc.set(plan, tc.newVal) + require.NoError(t, err) + val = tc.get() + if tc.equal != nil { + require.True(t, tc.equal(tc.newVal, val)) + } else { + require.Equal(t, tc.newVal, val) + } + }) + } +} + +func TestBasePlanValidate(t *testing.T) { + for _, tc := range []struct { + name string + malleate func(*types.BasePlan) + expectedErr string + }{ + { + "happy case", + func(plan *types.BasePlan) {}, + "", + }, + { + "invalid plan type", + func(plan *types.BasePlan) { + plan.Type = 3 + }, + "unknown plan type: 3: invalid plan type", + }, + { + "invalid farming pool addr", + func(plan *types.BasePlan) { + plan.FarmingPoolAddress = "invalid" + }, + "invalid farming pool address \"invalid\": decoding bech32 failed: invalid bech32 string length 7: invalid address", + }, + { + "invalid termination addr", + func(plan *types.BasePlan) { + plan.TerminationAddress = "invalid" + }, + "invalid termination address \"invalid\": decoding bech32 failed: invalid bech32 string length 7: invalid address", + }, + { + "invalid plan name", + func(plan *types.BasePlan) { + plan.Name = "a|b|c" + }, + "plan name cannot contain |: invalid plan name", + }, + { + "too long plan name", + func(plan *types.BasePlan) { + plan.Name = strings.Repeat("a", 256) + }, + "plan name cannot be longer than max length of 140: invalid plan name length", + }, + { + "invalid staking coin weights - empty weights", + func(plan *types.BasePlan) { + plan.StakingCoinWeights = sdk.DecCoins{} + }, + "staking coin weights must not be empty: invalid request", + }, + { + "invalid staking coin weights - invalid denom", + func(plan *types.BasePlan) { + plan.StakingCoinWeights = sdk.DecCoins{ + sdk.DecCoin{Denom: "!", Amount: sdk.NewDec(1)}, + } + }, + "invalid staking coin weights: invalid denom: !: invalid request", + }, + { + "invalid staking coin weights - invalid amount", + func(plan *types.BasePlan) { + plan.StakingCoinWeights = sdk.DecCoins{ + sdk.DecCoin{Denom: "stake1", Amount: sdk.NewDec(-1)}, + } + }, + "invalid staking coin weights: coin -1.000000000000000000stake1 amount is not positive: invalid request", + }, + { + "invalid staking coin weights - invalid sum of weights #1", + func(plan *types.BasePlan) { + plan.StakingCoinWeights = sdk.NewDecCoins( + sdk.NewDecCoinFromDec("stake1", sdk.NewDecWithPrec(7, 1)), + ) + }, + "total weight must be 1: invalid request", + }, + { + "invalid staking coin weights - invalid sum of weights #2", + func(plan *types.BasePlan) { + plan.StakingCoinWeights = sdk.NewDecCoins( + sdk.NewDecCoinFromDec("stake1", sdk.NewDecWithPrec(7, 1)), + sdk.NewDecCoinFromDec("stake2", sdk.NewDecWithPrec(4, 1)), + ) + }, + "total weight must be 1: invalid request", + }, + { + "invalid start/end time", + func(plan *types.BasePlan) { + plan.StartTime = types.ParseTime("2021-10-01T00:00:00Z") + plan.EndTime = types.ParseTime("2021-09-30T00:00:00Z") + }, + "end time 2021-09-30 00:00:00 +0000 UTC must be greater than start time 2021-10-01 00:00:00 +0000 UTC: invalid plan end time", + }, + { + "valid distributed coins", + func(plan *types.BasePlan) { + plan.DistributedCoins = sdk.NewCoins() + }, + "", + }, + { + "invalid distributed coins - invalid amount", + func(plan *types.BasePlan) { + plan.DistributedCoins = sdk.Coins{sdk.Coin{Denom: "reward1", Amount: sdk.ZeroInt()}} + }, + "invalid distributed coins: coin 0reward1 amount is not positive: invalid coins", + }, + } { + t.Run(tc.name, func(t *testing.T) { + bp := types.NewBasePlan( + 1, + "sample plan", + types.PlanTypePublic, + sdk.AccAddress(crypto.AddressHash([]byte("address1"))).String(), + sdk.AccAddress(crypto.AddressHash([]byte("address2"))).String(), + sdk.NewDecCoins(sdk.NewInt64DecCoin("stake1", 1)), + types.ParseTime("0001-01-01T00:00:00Z"), + types.ParseTime("9999-12-31T00:00:00Z"), + ) + tc.malleate(bp) + err := bp.Validate() + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestIsPlanActiveAt(t *testing.T) { + plan := types.NewFixedAmountPlan( + types.NewBasePlan( + 1, + "sample plan", + types.PlanTypePublic, + sdk.AccAddress(crypto.AddressHash([]byte("address1"))).String(), + sdk.AccAddress(crypto.AddressHash([]byte("address2"))).String(), + sdk.NewDecCoins(sdk.NewInt64DecCoin("stake1", 1)), + types.ParseTime("2021-10-10T00:00:00Z"), + types.ParseTime("2021-10-15T00:00:00Z"), + ), + sdk.NewCoins(sdk.NewInt64Coin("reward1", 10000000)), + ) + + for _, tc := range []struct { + timeStr string + active bool + }{ + {"2021-09-01T00:00:00Z", false}, + {"2021-10-09T23:59:59Z", false}, + {"2021-10-10T00:00:00Z", true}, + {"2021-10-13T12:00:00Z", true}, + {"2021-10-14T23:59:59Z", true}, + {"2021-10-15T00:00:00Z", false}, + {"2021-11-01T00:00:00Z", false}, + } { + require.Equal(t, tc.active, types.IsPlanActiveAt(plan, types.ParseTime(tc.timeStr))) + } +} + +func TestValidateStakingCoinTotalWeights(t *testing.T) { + for _, tc := range []struct { + stakingCoinWeights sdk.DecCoins + valid bool + }{ + { + nil, + false, + }, + { + sdk.DecCoins{}, + false, + }, + { + sdk.NewDecCoins(sdk.NewInt64DecCoin("stake1", 1)), + true, + }, + { + sdk.NewDecCoins( + sdk.NewDecCoinFromDec("stake1", sdk.NewDecWithPrec(5, 1)), + sdk.NewDecCoinFromDec("stake2", sdk.NewDecWithPrec(5, 1)), + ), + true, + }, + { + sdk.NewDecCoins( + sdk.NewDecCoinFromDec("stake1", sdk.NewDecWithPrec(3, 1)), + sdk.NewDecCoinFromDec("stake2", sdk.NewDecWithPrec(6, 1)), + ), + false, + }, + { + sdk.NewDecCoins( + sdk.NewDecCoinFromDec("stake1", sdk.NewDecWithPrec(5, 1)), + sdk.NewDecCoinFromDec("stake2", sdk.NewDecWithPrec(6, 1)), + ), + false, + }, + } { + require.Equal(t, tc.valid, types.ValidateStakingCoinTotalWeights(tc.stakingCoinWeights)) + } +} + func TestTotalEpochRatio(t *testing.T) { name1 := "testPlan1" name2 := "testPlan2" diff --git a/x/farming/types/proposal.go b/x/farming/types/proposal.go index d88637fc..2ee89edd 100644 --- a/x/farming/types/proposal.go +++ b/x/farming/types/proposal.go @@ -2,7 +2,7 @@ package types import ( "fmt" - time "time" + "time" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -47,10 +47,6 @@ func (p *PublicPlanProposal) ProposalRoute() string { return RouterKey } func (p *PublicPlanProposal) ProposalType() string { return ProposalTypePublicPlan } func (p *PublicPlanProposal) ValidateBasic() error { - if p.AddRequestProposals == nil && p.UpdateRequestProposals == nil && p.DeleteRequestProposals == nil { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "proposal request must not be empty") - } - if len(p.AddRequestProposals) == 0 && len(p.UpdateRequestProposals) == 0 && len(p.DeleteRequestProposals) == 0 { return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "proposal request must not be empty") } @@ -108,6 +104,14 @@ func NewAddRequestProposal( } } +func (p *AddRequestProposal) IsForFixedAmountPlan() bool { + return !p.EpochAmount.IsZero() +} + +func (p *AddRequestProposal) IsForRatioPlan() bool { + return !p.EpochRatio.IsNil() && !p.EpochRatio.IsZero() +} + func (p *AddRequestProposal) Validate() error { if len(p.Name) > MaxNameLength { return sdkerrors.Wrapf(ErrInvalidPlanNameLength, "plan name cannot be longer than max length of %d", MaxNameLength) @@ -130,11 +134,8 @@ func (p *AddRequestProposal) Validate() error { if !p.EndTime.After(p.StartTime) { return sdkerrors.Wrapf(ErrInvalidPlanEndTime, "end time %s must be greater than start time %s", p.EndTime, p.StartTime) } - if !p.EpochAmount.IsZero() && !p.EpochRatio.IsZero() { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "either epoch amount or epoch ratio should be provided") - } - if p.EpochAmount.IsZero() && p.EpochRatio.IsZero() { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "either epoch amount or epoch ratio must not be zero") + if p.IsForFixedAmountPlan() == p.IsForRatioPlan() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only one of epoch amount or epoch ratio must be provided") } return nil } @@ -164,6 +165,14 @@ func NewUpdateRequestProposal( } } +func (p *UpdateRequestProposal) IsForFixedAmountPlan() bool { + return !p.EpochAmount.IsZero() +} + +func (p *UpdateRequestProposal) IsForRatioPlan() bool { + return !p.EpochRatio.IsNil() && !p.EpochRatio.IsZero() +} + func (p *UpdateRequestProposal) Validate() error { if p.PlanId == 0 { return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid plan id: %d", p.PlanId) @@ -186,14 +195,13 @@ func (p *UpdateRequestProposal) Validate() error { if ok := ValidateStakingCoinTotalWeights(p.StakingCoinWeights); !ok { return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "total weight must be 1") } - if !p.EndTime.After(*p.StartTime) { - return sdkerrors.Wrapf(ErrInvalidPlanEndTime, "end time %s must be greater than start time %s", p.EndTime, p.StartTime) - } - if !p.EpochAmount.IsZero() && !p.EpochRatio.IsZero() { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "either epoch amount or epoch ratio should be provided") + if p.StartTime != nil && p.EndTime != nil { + if !p.EndTime.After(*p.StartTime) { + return sdkerrors.Wrapf(ErrInvalidPlanEndTime, "end time %s must be greater than start time %s", p.EndTime, p.StartTime) + } } - if p.EpochAmount.IsZero() && p.EpochRatio.IsZero() { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "either epoch amount or epoch ratio must not be zero") + if p.IsForFixedAmountPlan() == p.IsForRatioPlan() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only one of epoch amount or epoch ratio must be provided") } return nil } diff --git a/x/farming/types/proposal_test.go b/x/farming/types/proposal_test.go new file mode 100644 index 00000000..1d5d020b --- /dev/null +++ b/x/farming/types/proposal_test.go @@ -0,0 +1,390 @@ +package types_test + +import ( + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" + + "github.com/tendermint/farming/x/farming/types" +) + +func TestPublicPlanProposal_ValidateBasic(t *testing.T) { + for _, tc := range []struct { + name string + malleate func(*types.PublicPlanProposal) + expectedErr string + }{ + { + "happy case", + func(proposal *types.PublicPlanProposal) {}, + "", + }, + { + "empty proposals", + func(proposal *types.PublicPlanProposal) { + proposal.AddRequestProposals = []*types.AddRequestProposal{} + proposal.UpdateRequestProposals = []*types.UpdateRequestProposal{} + proposal.DeleteRequestProposals = []*types.DeleteRequestProposal{} + }, + "proposal request must not be empty: invalid request", + }, + { + "invalid add request proposal", + func(proposal *types.PublicPlanProposal) { + proposal.AddRequestProposals[0].Name = strings.Repeat("a", 256) + }, + "plan name cannot be longer than max length of 140: invalid plan name length", + }, + { + "invalid update request proposal", + func(proposal *types.PublicPlanProposal) { + proposal.UpdateRequestProposals[0].Name = strings.Repeat("a", 256) + }, + "plan name cannot be longer than max length of 140: invalid plan name length", + }, + { + "invalid delete request proposal", + func(proposal *types.PublicPlanProposal) { + proposal.DeleteRequestProposals[0].PlanId = 0 + }, + "invalid plan id: 0: invalid request", + }, + } { + t.Run(tc.name, func(t *testing.T) { + proposal := types.NewPublicPlanProposal( + "title", + "description", + []*types.AddRequestProposal{ + { + Name: "", + FarmingPoolAddress: sdk.AccAddress(crypto.AddressHash([]byte("address1"))).String(), + TerminationAddress: sdk.AccAddress(crypto.AddressHash([]byte("address1"))).String(), + StakingCoinWeights: sdk.NewDecCoins(sdk.NewInt64DecCoin("stake1", 1)), + StartTime: types.ParseTime("0001-01-01T00:00:00Z"), + EndTime: types.ParseTime("9999-12-31T00:00:00Z"), + EpochAmount: sdk.NewCoins(sdk.NewInt64Coin("reward1", 10000000)), + }, + }, + []*types.UpdateRequestProposal{ + { + PlanId: 1, + Name: "new name", + FarmingPoolAddress: sdk.AccAddress(crypto.AddressHash([]byte("address2"))).String(), + TerminationAddress: sdk.AccAddress(crypto.AddressHash([]byte("address2"))).String(), + StakingCoinWeights: sdk.NewDecCoins(sdk.NewInt64DecCoin("stake2", 1)), + EpochAmount: sdk.NewCoins(sdk.NewInt64Coin("reward2", 10000000)), + }, + }, + []*types.DeleteRequestProposal{ + { + PlanId: 1, + }, + }, + ) + tc.malleate(proposal) + err := proposal.ValidateBasic() + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestAddRequestProposal_Validate(t *testing.T) { + for _, tc := range []struct { + name string + malleate func(*types.AddRequestProposal) + expectedErr string + }{ + { + "valid for fixed amount plan", + func(proposal *types.AddRequestProposal) {}, + "", + }, + { + "valid for ratio plan", + func(proposal *types.AddRequestProposal) { + proposal.EpochAmount = nil + proposal.EpochRatio = sdk.NewDecWithPrec(5, 2) + }, + "", + }, + { + "ambiguous plan type #1", + func(proposal *types.AddRequestProposal) { + proposal.EpochRatio = sdk.NewDecWithPrec(5, 2) + }, + "only one of epoch amount or epoch ratio must be provided: invalid request", + }, + { + "ambiguous plan type #2", + func(proposal *types.AddRequestProposal) { + proposal.EpochAmount = nil + }, + "only one of epoch amount or epoch ratio must be provided: invalid request", + }, + { + "empty name", + func(proposal *types.AddRequestProposal) { + proposal.Name = "" + }, + "", + }, + { + "too long name", + func(proposal *types.AddRequestProposal) { + proposal.Name = strings.Repeat("a", 256) + }, + "plan name cannot be longer than max length of 140: invalid plan name length", + }, + { + "invalid farming pool addr", + func(proposal *types.AddRequestProposal) { + proposal.FarmingPoolAddress = "invalid" + }, + "invalid farming pool address \"invalid\": decoding bech32 failed: invalid bech32 string length 7: invalid address", + }, + { + "invalid termination addr", + func(proposal *types.AddRequestProposal) { + proposal.TerminationAddress = "invalid" + }, + "invalid termination address \"invalid\": decoding bech32 failed: invalid bech32 string length 7: invalid address", + }, + { + "invalid staking coin weights - empty", + func(proposal *types.AddRequestProposal) { + proposal.StakingCoinWeights = nil + }, + "staking coin weights must not be empty: invalid request", + }, + { + "invalid staking coin weights - invalid", + func(proposal *types.AddRequestProposal) { + proposal.StakingCoinWeights = sdk.DecCoins{ + sdk.DecCoin{Denom: "stake1", Amount: sdk.ZeroDec()}, + } + }, + "invalid staking coin weights: coin 0.000000000000000000stake1 amount is not positive: invalid request", + }, + { + "invalid staking coin weights - invalid sum of weights", + func(proposal *types.AddRequestProposal) { + proposal.StakingCoinWeights = sdk.NewDecCoins( + sdk.NewDecCoinFromDec("stake1", sdk.NewDecWithPrec(5, 1)), + sdk.NewDecCoinFromDec("stake2", sdk.NewDecWithPrec(6, 1)), + ) + }, + "total weight must be 1: invalid request", + }, + { + "invalid start/end time", + func(proposal *types.AddRequestProposal) { + proposal.StartTime = types.ParseTime("2021-10-01T00:00:00Z") + proposal.EndTime = types.ParseTime("2021-09-01T00:00:00Z") + }, + "end time 2021-09-01 00:00:00 +0000 UTC must be greater than start time 2021-10-01 00:00:00 +0000 UTC: invalid plan end time", + }, + } { + t.Run(tc.name, func(t *testing.T) { + proposal := types.NewAddRequestProposal( + "name", + sdk.AccAddress(crypto.AddressHash([]byte("address"))).String(), + sdk.AccAddress(crypto.AddressHash([]byte("address"))).String(), + sdk.NewDecCoins(sdk.NewInt64DecCoin("stake1", 1)), + types.ParseTime("0001-01-01T00:00:00Z"), + types.ParseTime("9999-12-31T00:00:00Z"), + sdk.NewCoins(sdk.NewInt64Coin("reward1", 10000000)), + sdk.Dec{}, + ) + tc.malleate(proposal) + err := proposal.Validate() + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestUpdateRequestProposal_Validate(t *testing.T) { + for _, tc := range []struct { + name string + malleate func(*types.UpdateRequestProposal) + expectedErr string + }{ + { + "valid for fixed amount plan", + func(proposal *types.UpdateRequestProposal) {}, + "", + }, + { + "valid for ratio plan", + func(proposal *types.UpdateRequestProposal) { + proposal.EpochAmount = nil + proposal.EpochRatio = sdk.NewDecWithPrec(5, 2) + }, + "", + }, + { + "ambiguous plan type #1", + func(proposal *types.UpdateRequestProposal) { + proposal.EpochRatio = sdk.NewDecWithPrec(5, 2) + }, + "only one of epoch amount or epoch ratio must be provided: invalid request", + }, + { + "ambiguous plan type #2", + func(proposal *types.UpdateRequestProposal) { + proposal.EpochAmount = nil + }, + "only one of epoch amount or epoch ratio must be provided: invalid request", + }, + { + "invalid plan id", + func(proposal *types.UpdateRequestProposal) { + proposal.PlanId = 0 + }, + "invalid plan id: 0: invalid request", + }, + { + "empty name", + func(proposal *types.UpdateRequestProposal) { + proposal.Name = "" + }, + "", + }, + { + "too long name", + func(proposal *types.UpdateRequestProposal) { + proposal.Name = strings.Repeat("a", 256) + }, + "plan name cannot be longer than max length of 140: invalid plan name length", + }, + { + "invalid farming pool addr", + func(proposal *types.UpdateRequestProposal) { + proposal.FarmingPoolAddress = "invalid" + }, + "invalid farming pool address \"invalid\": decoding bech32 failed: invalid bech32 string length 7: invalid address", + }, + { + "invalid termination addr", + func(proposal *types.UpdateRequestProposal) { + proposal.TerminationAddress = "invalid" + }, + "invalid termination address \"invalid\": decoding bech32 failed: invalid bech32 string length 7: invalid address", + }, + { + "invalid staking coin weights - empty", + func(proposal *types.UpdateRequestProposal) { + proposal.StakingCoinWeights = nil + }, + "staking coin weights must not be empty: invalid request", + }, + { + "invalid staking coin weights - invalid", + func(proposal *types.UpdateRequestProposal) { + proposal.StakingCoinWeights = sdk.DecCoins{ + sdk.DecCoin{Denom: "stake1", Amount: sdk.ZeroDec()}, + } + }, + "invalid staking coin weights: coin 0.000000000000000000stake1 amount is not positive: invalid request", + }, + { + "invalid staking coin weights - invalid sum of weights", + func(proposal *types.UpdateRequestProposal) { + proposal.StakingCoinWeights = sdk.NewDecCoins( + sdk.NewDecCoinFromDec("stake1", sdk.NewDecWithPrec(5, 1)), + sdk.NewDecCoinFromDec("stake2", sdk.NewDecWithPrec(6, 1)), + ) + }, + "total weight must be 1: invalid request", + }, + { + "invalid start/end time", + func(proposal *types.UpdateRequestProposal) { + t := types.ParseTime("2021-10-01T00:00:00Z") + proposal.StartTime = &t + t2 := types.ParseTime("2021-09-01T00:00:00Z") + proposal.EndTime = &t2 + }, + "end time 2021-09-01 00:00:00 +0000 UTC must be greater than start time 2021-10-01 00:00:00 +0000 UTC: invalid plan end time", + }, + { + "update only start time", + func(proposal *types.UpdateRequestProposal) { + t := types.ParseTime("2021-10-01T00:00:00Z") + proposal.StartTime = &t + }, + "", + }, + { + "update only end time", + func(proposal *types.UpdateRequestProposal) { + t := types.ParseTime("2021-10-01T00:00:00Z") + proposal.EndTime = &t + }, + "", + }, + } { + t.Run(tc.name, func(t *testing.T) { + proposal := types.NewUpdateRequestProposal( + 1, + "name", + sdk.AccAddress(crypto.AddressHash([]byte("address"))).String(), + sdk.AccAddress(crypto.AddressHash([]byte("address"))).String(), + sdk.NewDecCoins(sdk.NewInt64DecCoin("stake1", 1)), + types.ParseTime("0001-01-01T00:00:00Z"), + types.ParseTime("9999-12-31T00:00:00Z"), + sdk.NewCoins(sdk.NewInt64Coin("reward1", 10000000)), + sdk.Dec{}, + ) + tc.malleate(proposal) + err := proposal.Validate() + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestDeleteRequestProposal_Validate(t *testing.T) { + for _, tc := range []struct { + name string + malleate func(*types.DeleteRequestProposal) + expectedErr string + }{ + { + "happy case", + func(proposal *types.DeleteRequestProposal) {}, + "", + }, + { + "invalid plan id", + func(proposal *types.DeleteRequestProposal) { + proposal.PlanId = 0 + }, + "invalid plan id: 0: invalid request", + }, + } { + t.Run(tc.name, func(t *testing.T) { + proposal := types.NewDeleteRequestProposal(1) + tc.malleate(proposal) + err := proposal.Validate() + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +}