cosmos-sdk/x/upgrade/plan/info_test.go

336 lines
10 KiB
Go
Raw Normal View History

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 09:44:33 -08:00
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)
}
})
}
}