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

300 lines
10 KiB
Go

package plan
import (
"archive/zip"
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type DownloaderTestSuite struct {
suite.Suite
// Home is a temporary directory for use in these tests.
// It will have a src/ for things to download.
Home string
}
func (s *DownloaderTestSuite) SetupTest() {
s.Home = s.T().TempDir()
s.Assert().NoError(os.MkdirAll(filepath.Join(s.Home, "src"), 0777), "creating src/ dir")
s.T().Logf("Home: [%s]", s.Home)
}
func TestDownloaderTestSuite(t *testing.T) {
suite.Run(t, new(DownloaderTestSuite))
}
// TestFile represents a file that will be used for a test.
type TestFile struct {
// Name is the relative path and name of the file.
Name string
// Contents is the contents of the file.
Contents []byte
}
func NewTestFile(name, contents string) *TestFile {
return &TestFile{
Name: name,
Contents: []byte(contents),
}
}
// SaveIn saves this TestFile in the given path.
// The full path to the file is returned.
func (f TestFile) SaveIn(path string) (string, error) {
name := filepath.Join(path, f.Name)
file, err := os.Create(name)
if err != nil {
return name, err
}
defer file.Close()
_, err = file.Write(f.Contents)
return name, err
}
// TestZip represents a collection of TestFile objects to be zipped into an archive.
type TestZip []*TestFile
func NewTestZip(testFiles ...*TestFile) TestZip {
tz := make([]*TestFile, len(testFiles))
for i, tf := range testFiles {
tz[i] = tf
}
return tz
}
// SaveAs saves this TestZip at the given path.
func (z TestZip) SaveAs(path string) error {
archive, err := os.Create(path)
if err != nil {
return err
}
defer archive.Close()
zipper := zip.NewWriter(archive)
for _, tf := range z {
zfw, zfwerr := zipper.Create(tf.Name)
if zfwerr != nil {
return zfwerr
}
_, err = zfw.Write(tf.Contents)
if err != nil {
return err
}
}
return zipper.Close()
}
// saveTestZip saves a TestZip in this test's Home/src directory with the given name.
// The full path to the saved archive is returned.
func (s DownloaderTestSuite) saveSrcTestZip(name string, z TestZip) string {
fullName := filepath.Join(s.Home, "src", name)
s.Require().NoError(z.SaveAs(fullName), "saving test zip %s", name)
return fullName
}
// saveSrcTestFile saves a TestFile in this test's Home/src directory.
// The full path to the saved file is returned.
func (s DownloaderTestSuite) saveSrcTestFile(f *TestFile) string {
path := filepath.Join(s.Home, "src")
fullName, err := f.SaveIn(path)
s.Require().NoError(err, "saving test file %s", f.Name)
return fullName
}
// requireFileExistsAndIsExecutable requires that the given file exists and is executable.
func requireFileExistsAndIsExecutable(t *testing.T, path string) {
info, err := os.Stat(path)
require.NoError(t, err, "stat error")
perm := info.Mode().Perm()
// Checks if at least one executable bit is set (user, group, or other)
isExe := perm&0111 != 0
require.True(t, isExe, "is executable: permissions = %s", perm)
}
// requireFileEquals requires that the contents of the file at the given path
// is equal to the contents of the given TestFile.
func requireFileEquals(t *testing.T, path string, tf *TestFile) {
file, err := os.ReadFile(path)
require.NoError(t, err, "reading file")
require.Equal(t, string(tf.Contents), string(file), "file contents")
}
// makeFileUrl converts the given path to a URL with the correct checksum query parameter.
func makeFileURL(t *testing.T, path string) string {
f, err := os.Open(path)
require.NoError(t, err, "opening file")
defer f.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, f)
require.NoError(t, err, "copying file to hasher")
return fmt.Sprintf("file://%s?checksum=sha256:%x", path, hasher.Sum(nil))
}
func (s *DownloaderTestSuite) TestDownloadUpgrade() {
justAFile := NewTestFile("just-a-file", "#!/usr/bin\necho 'I am just a file'\n")
someFileName := "some-file"
someFileInBin := NewTestFile("bin"+someFileName, "#!/usr/bin\necho 'I am some file in bin'\n")
anotherFile := NewTestFile("another-file", "#!/usr/bin\necho 'I am just another file'\n")
justAFilePath := s.saveSrcTestFile(justAFile)
justAFileZip := s.saveSrcTestZip(justAFile.Name+".zip", NewTestZip(justAFile))
someFileInBinZip := s.saveSrcTestZip(someFileInBin.Name+".zip", NewTestZip(someFileInBin))
allFilesZip := s.saveSrcTestZip(anotherFile.Name+".zip", NewTestZip(justAFile, someFileInBin, anotherFile))
getDstDir := func(testName string) string {
_, tName := filepath.Split(testName)
return s.Home + "/dst/" + tName
}
s.T().Run("url does not exist", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := "file:///never/gonna/be/a/thing.zip?checksum=sha256:2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48"
err := DownloadUpgrade(dstRoot, url, "nothing")
require.Error(t, err)
assert.Contains(t, err.Error(), "no such file or directory")
})
s.T().Run("url does not have checksum", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := "file://" + justAFilePath
err := DownloadUpgrade(dstRoot, url, justAFile.Name)
require.Error(t, err)
require.Contains(t, err.Error(), "missing checksum query parameter")
})
s.T().Run("url has incorrect checksum", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
badChecksum := "2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48"
url := "file://" + justAFilePath + "?checksum=sha256:" + badChecksum
err := DownloadUpgrade(dstRoot, url, justAFile.Name)
require.Error(t, err)
assert.Contains(t, err.Error(), "Checksums did not match")
assert.Contains(t, err.Error(), "Expected: "+badChecksum)
})
s.T().Run("url returns single file", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := makeFileURL(t, justAFilePath)
err := DownloadUpgrade(dstRoot, url, justAFile.Name)
require.NoError(t, err)
expectedFile := filepath.Join(dstRoot, "bin", justAFile.Name)
requireFileExistsAndIsExecutable(t, expectedFile)
requireFileEquals(t, expectedFile, justAFile)
})
s.T().Run("url returns archive with file in bin", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := makeFileURL(t, someFileInBinZip)
err := DownloadUpgrade(dstRoot, url, someFileName)
require.NoError(t, err)
expectedFile := filepath.Join(dstRoot, "bin", someFileName)
requireFileExistsAndIsExecutable(t, expectedFile)
requireFileEquals(t, expectedFile, someFileInBin)
})
s.T().Run("url returns archive with just expected file", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := makeFileURL(t, justAFileZip)
err := DownloadUpgrade(dstRoot, url, justAFile.Name)
require.NoError(t, err)
expectedFile := filepath.Join(dstRoot, "bin", justAFile.Name)
requireFileExistsAndIsExecutable(t, expectedFile)
requireFileEquals(t, expectedFile, justAFile)
})
s.T().Run("url returns archive without expected file", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := makeFileURL(t, allFilesZip)
err := DownloadUpgrade(dstRoot, url, "not-expected")
require.Error(t, err)
require.Contains(t, err.Error(), "result does not contain a bin/not-expected or not-expected file")
})
}
func (s *DownloaderTestSuite) TestEnsureBinary() {
nonExeName := s.saveSrcTestFile(NewTestFile("non-exe.txt", "Not executable"))
s.Require().NoError(os.Chmod(nonExeName, 0600), "chmod error nonExeName")
isExeName := s.saveSrcTestFile(NewTestFile("is-exe.sh", "#!/bin/bash\necho 'executing'\n"))
s.Require().NoError(os.Chmod(isExeName, 0777), "chmod error isExeName")
s.T().Run("file does not exist", func(t *testing.T) {
name := filepath.Join(s.Home, "does-not-exist.txt")
actual := EnsureBinary(name)
require.Error(t, actual)
})
s.T().Run("file is a directory", func(t *testing.T) {
name := filepath.Join(s.Home, "src")
actual := EnsureBinary(name)
require.EqualError(t, actual, fmt.Sprintf("%s is not a regular file", "src"))
})
s.T().Run("file exists and becomes executable", func(t *testing.T) {
name := nonExeName
actual := EnsureBinary(name)
require.NoError(t, actual, "EnsureBinary error")
requireFileExistsAndIsExecutable(t, name)
})
s.T().Run("file is already executable", func(t *testing.T) {
name := isExeName
actual := EnsureBinary(name)
require.NoError(t, actual, "EnsureBinary error")
requireFileExistsAndIsExecutable(t, name)
})
}
func (s *DownloaderTestSuite) TestDownloadURLWithChecksum() {
planContents := `{"binaries":{"xxx/yyy":"url"}}`
planFile := NewTestFile("plan-info.json", planContents)
planPath := s.saveSrcTestFile(planFile)
planChecksum := fmt.Sprintf("%x", sha256.Sum256(planFile.Contents))
emptyFile := NewTestFile("empty-plan-info.json", "")
emptyPlanPath := s.saveSrcTestFile(emptyFile)
emptyChecksum := fmt.Sprintf("%x", sha256.Sum256(emptyFile.Contents))
s.T().Run("url does not exist", func(t *testing.T) {
url := "file:///never-gonna-be-a-thing?checksum=sha256:2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48"
_, err := DownloadURLWithChecksum(url)
require.Error(t, err)
assert.Contains(t, err.Error(), "could not download url")
})
s.T().Run("without checksum", func(t *testing.T) {
url := "file://" + planPath
_, err := DownloadURLWithChecksum(url)
require.Error(t, err)
assert.Contains(t, err.Error(), "missing checksum query parameter")
})
s.T().Run("with correct checksum", func(t *testing.T) {
url := "file://" + planPath + "?checksum=sha256:" + planChecksum
actual, err := DownloadURLWithChecksum(url)
require.NoError(t, err)
require.Equal(t, planContents, actual)
})
s.T().Run("with incorrect checksum", func(t *testing.T) {
badChecksum := "2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48"
url := "file://" + planPath + "?checksum=sha256:" + badChecksum
_, err := DownloadURLWithChecksum(url)
require.Error(t, err)
assert.Contains(t, err.Error(), "Checksums did not match")
assert.Contains(t, err.Error(), "Expected: "+badChecksum)
assert.Contains(t, err.Error(), "Got: "+planChecksum)
})
s.T().Run("plan is empty", func(t *testing.T) {
url := "file://" + emptyPlanPath + "?checksum=sha256:" + emptyChecksum
_, err := DownloadURLWithChecksum(url)
require.Error(t, err)
assert.Contains(t, err.Error(), "no content returned")
})
}