cosmos-sdk/x/upgrade/plan/info_test.go
Daniel Wedul 181ba0e41a
feat: Add upgrade proposal plan validation to CLI (#10379)
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->

## Description

Closes: #10286

When submitting a software upgrade proposal (e.g. `$DAEMON tx gov submit-proposal software-upgrade`)
* Validate the plan info by default.
* Add flag `--no-validate` to allow skipping that validation.
* Add flag `--daemon-name` to designate the executable name (needed for validation).
* The daemon name comes first from the `--daemon-name` flag. If that's not provided, it looks for a `DAEMON_NAME` environment variable (to match what's used by Cosmovisor). If that's not set, the name of the currently running executable is used.

Things that are validated:
* The plan info cannot be empty or blank.
* If the plan info is a url:
  * It must have a `checksum` query parameter.
  * It must return properly formatted plan info JSON.
  * The `checksum` is correct.
* If the plan info is not a url:
  * It must be propery formatted plan info JSON.
* There is at least one entry in the `binaries` field.
* The keys of the `binaries` field are either "any" or in the format of "os/arch".
* All URLs contain a `checksum` query parameter.
* Each URL contains a usable response.
* The `checksum` is correct for each URL.

Note: With this change, either a valid `--upgrade-info` will need to be provided, or else `--no-validate` must be provided. If no `--upgrade-info` is given, a validation error is returned.

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] ~~added `!` to the type prefix if API or client breaking change~~ _N/A_
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [x] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [x] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-11-12 17:44:33 +00:00

336 lines
10 KiB
Go

package plan
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type InfoTestSuite struct {
suite.Suite
// Home is a temporary directory for use in these tests.
Home string
}
func (s *InfoTestSuite) SetupTest() {
s.Home = s.T().TempDir()
s.T().Logf("Home: [%s]", s.Home)
}
func TestInfoTestSuite(t *testing.T) {
suite.Run(t, new(InfoTestSuite))
}
// saveSrcTestFile saves a TestFile in this test's Home/src directory.
// The full path to the saved file is returned.
func (s InfoTestSuite) saveTestFile(f *TestFile) string {
fullName, err := f.SaveIn(s.Home)
s.Require().NoError(err, "saving test file %s", f.Name)
return fullName
}
func (s InfoTestSuite) TestParseInfo() {
goodJSON := `{"binaries":{"os1/arch1":"url1","os2/arch2":"url2"}}`
binariesWrongJSON := `{"binaries":["foo","bar"]}`
binariesWrongValueJSON := `{"binaries":{"os1/arch1":1,"os2/arch2":2}}`
goodJSONPath := s.saveTestFile(NewTestFile("good.json", goodJSON))
binariesWrongJSONPath := s.saveTestFile(NewTestFile("binaries-wrong.json", binariesWrongJSON))
binariesWrongValueJSONPath := s.saveTestFile(NewTestFile("binaries-wrong-value.json", binariesWrongValueJSON))
goodJSONAsInfo := &Info{
Binaries: BinaryDownloadURLMap{
"os1/arch1": "url1",
"os2/arch2": "url2",
},
}
makeInfoStrFuncString := func(val string) func(t *testing.T) string {
return func(t *testing.T) string {
return val
}
}
makeInfoStrFuncURL := func(file string) func(t *testing.T) string {
return func(t *testing.T) string {
return makeFileURL(t, file)
}
}
tests := []struct {
name string
infoStrMaker func(t *testing.T) string
expectedInfo *Info
expectedInError []string
}{
{
name: "json good",
infoStrMaker: makeInfoStrFuncString(goodJSON),
expectedInfo: goodJSONAsInfo,
expectedInError: nil,
},
{
name: "blank string",
infoStrMaker: makeInfoStrFuncString(" "),
expectedInfo: nil,
expectedInError: []string{"plan info must not be blank"},
},
{
name: "json binaries is wrong data type",
infoStrMaker: makeInfoStrFuncString(binariesWrongJSON),
expectedInfo: nil,
expectedInError: []string{"could not parse plan info", "cannot unmarshal array into Go struct field Info.binaries"},
},
{
name: "json wrong data type in binaries value",
infoStrMaker: makeInfoStrFuncString(binariesWrongValueJSON),
expectedInfo: nil,
expectedInError: []string{"could not parse plan info", "cannot unmarshal number into Go struct field Info.binaries"},
},
{
name: "url does not exist",
infoStrMaker: makeInfoStrFuncString("file:///this/file/does/not/exist?checksum=sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
expectedInfo: nil,
expectedInError: []string{"could not download url", "file:///this/file/does/not/exist"},
},
{
name: "url good",
infoStrMaker: makeInfoStrFuncURL(goodJSONPath),
expectedInfo: goodJSONAsInfo,
expectedInError: nil,
},
{
name: "url binaries is wrong data type",
infoStrMaker: makeInfoStrFuncURL(binariesWrongJSONPath),
expectedInfo: nil,
expectedInError: []string{"could not parse plan info", "cannot unmarshal array into Go struct field Info.binaries"},
},
{
name: "url wrong data type in binaries value",
infoStrMaker: makeInfoStrFuncURL(binariesWrongValueJSONPath),
expectedInfo: nil,
expectedInError: []string{"could not parse plan info", "cannot unmarshal number into Go struct field Info.binaries"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
infoStr := tc.infoStrMaker(t)
actualInfo, actualErr := ParseInfo(infoStr)
if len(tc.expectedInError) > 0 {
require.Error(t, actualErr)
for _, expectedErr := range tc.expectedInError {
assert.Contains(t, actualErr.Error(), expectedErr)
}
} else {
require.NoError(t, actualErr)
}
assert.Equal(t, tc.expectedInfo, actualInfo)
})
}
}
func (s InfoTestSuite) TestInfoValidateFull() {
darwinAMD64File := NewTestFile("darwin_amd64", "#!/usr/bin\necho 'darwin/amd64'\n")
linux386File := NewTestFile("linux_386", "#!/usr/bin\necho 'darwin/amd64'\n")
darwinAMD64Path := s.saveTestFile(darwinAMD64File)
linux386Path := s.saveTestFile(linux386File)
darwinAMD64URL := makeFileURL(s.T(), darwinAMD64Path)
linux386URL := makeFileURL(s.T(), linux386Path)
tests := []struct {
name string
planInfo *Info
errs []string
}{
// Positive test case
{
name: "two good entries",
planInfo: &Info{
Binaries: BinaryDownloadURLMap{
"darwin/amd64": darwinAMD64URL,
"linux/386": linux386URL,
},
},
errs: nil,
},
// a failure from BinaryDownloadURLMap.ValidateBasic
{
name: "empty binaries",
planInfo: &Info{Binaries: BinaryDownloadURLMap{}},
errs: []string{"no \"binaries\" entries found"},
},
// a failure from BinaryDownloadURLMap.CheckURLS
{
name: "url does not exist",
planInfo: &Info{
Binaries: BinaryDownloadURLMap{
"darwin/arm64": "file:///no/such/file/exists/hopefully.zip?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259",
},
},
errs: []string{"error downloading binary", "darwin/arm64", "no such file or directory"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
actualErr := tc.planInfo.ValidateFull("daemon")
if len(tc.errs) > 0 {
require.Error(t, actualErr)
for _, expectedErr := range tc.errs {
assert.Contains(t, actualErr.Error(), expectedErr)
}
} else {
require.NoError(t, actualErr)
}
})
}
}
func (s InfoTestSuite) TestBinaryDownloadURLMapValidateBasic() {
addDummyChecksum := func(url string) string {
return url + "?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259"
}
tests := []struct {
name string
urlMap BinaryDownloadURLMap
errs []string
}{
{
name: "empty map",
urlMap: BinaryDownloadURLMap{},
errs: []string{"no \"binaries\" entries found"},
},
{
name: "key with empty string",
urlMap: BinaryDownloadURLMap{
"": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: []string{"invalid os/arch", `""`},
},
{
name: "invalid key format",
urlMap: BinaryDownloadURLMap{
"badkey": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: []string{"invalid os/arch", "badkey"},
},
{
name: "any key is valid",
urlMap: BinaryDownloadURLMap{
"any": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: nil,
},
{
name: "os arch key is valid",
urlMap: BinaryDownloadURLMap{
"darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: nil,
},
{
name: "not a url",
urlMap: BinaryDownloadURLMap{
"isa/url": addDummyChecksum("https://v1.cosmos.network/sdk"),
"nota/url": addDummyChecksum("https://v1.cosmos.network:not-a-port/sdk"),
},
errs: []string{"invalid url", "nota/url", "invalid port"},
},
{
name: "url without checksum",
urlMap: BinaryDownloadURLMap{
"darwin/amd64": "https://v1.cosmos.network/sdk",
},
errs: []string{"invalid url", "darwin/amd64", "missing checksum query parameter"},
},
{
name: "multiple valid entries but one bad url",
urlMap: BinaryDownloadURLMap{
"any": addDummyChecksum("https://v1.cosmos.network/sdk"),
"darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"),
"darwin/arm64": addDummyChecksum("https://v1.cosmos.network/sdk"),
"windows/bad": addDummyChecksum("https://v1.cosmos.network:not-a-port/sdk"),
"linux/386": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: []string{"invalid url", "windows/bad", "invalid port"},
},
{
name: "multiple valid entries but one bad key",
urlMap: BinaryDownloadURLMap{
"any": addDummyChecksum("https://v1.cosmos.network/sdk"),
"darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"),
"badkey": addDummyChecksum("https://v1.cosmos.network/sdk"),
"darwin/arm64": addDummyChecksum("https://v1.cosmos.network/sdk"),
"linux/386": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: []string{"invalid os/arch", "badkey"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
actualErr := tc.urlMap.ValidateBasic()
if len(tc.errs) > 0 {
require.Error(t, actualErr)
for _, expectedErr := range tc.errs {
assert.Contains(t, actualErr.Error(), expectedErr)
}
} else {
require.NoError(t, actualErr)
}
})
}
}
func (s InfoTestSuite) TestBinaryDownloadURLMapCheckURLs() {
darwinAMD64File := NewTestFile("darwin_amd64", "#!/usr/bin\necho 'darwin/amd64'\n")
linux386File := NewTestFile("linux_386", "#!/usr/bin\necho 'darwin/amd64'\n")
darwinAMD64Path := s.saveTestFile(darwinAMD64File)
linux386Path := s.saveTestFile(linux386File)
darwinAMD64URL := makeFileURL(s.T(), darwinAMD64Path)
linux386URL := makeFileURL(s.T(), linux386Path)
tests := []struct {
name string
urlMap BinaryDownloadURLMap
errs []string
}{
{
name: "two good entries",
urlMap: BinaryDownloadURLMap{
"darwin/amd64": darwinAMD64URL,
"linux/386": linux386URL,
},
errs: nil,
},
{
name: "url does not exist",
urlMap: BinaryDownloadURLMap{
"darwin/arm64": "file:///no/such/file/exists/hopefully.zip?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259",
},
errs: []string{"error downloading binary", "darwin/arm64", "no such file or directory"},
},
{
name: "bad checksum",
urlMap: BinaryDownloadURLMap{
"darwin/amd64": "file://" + darwinAMD64Path + "?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259",
},
errs: []string{"error downloading binary", "darwin/amd64", "Checksums did not match", "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
actualErr := tc.urlMap.CheckURLs("daemon")
if len(tc.errs) > 0 {
require.Error(t, actualErr)
for _, expectedErr := range tc.errs {
assert.Contains(t, actualErr.Error(), expectedErr)
}
} else {
require.NoError(t, actualErr)
}
})
}
}