refactor(pkg/test): split logic in pkg/test/common.go into multiple packages (#1861)

Which could be imported independently. See more details:
1. "zotregistry.io/zot/pkg/test/common" - currently used as
   tcommon "zotregistry.io/zot/pkg/test/common" - inside pkg/test
   test "zotregistry.io/zot/pkg/test/common" - in tests
   . "zotregistry.io/zot/pkg/test/common" - in tests
Decouple zb from code in test/pkg in order to keep the size small.

2. "zotregistry.io/zot/pkg/test/image-utils" - curently used as
   . "zotregistry.io/zot/pkg/test/image-utils"

3. "zotregistry.io/zot/pkg/test/deprecated" -  curently used as
   "zotregistry.io/zot/pkg/test/deprecated"
This one will bre replaced gradually by image-utils in the future.

4. "zotregistry.io/zot/pkg/test/signature" - (cosign + notation) use as
   "zotregistry.io/zot/pkg/test/signature"

5. "zotregistry.io/zot/pkg/test/auth" - (bearer + oidc)  curently used as
   authutils "zotregistry.io/zot/pkg/test/auth"

 6. "zotregistry.io/zot/pkg/test/oci-utils" -  curently used as
   ociutils "zotregistry.io/zot/pkg/test/oci-utils"

Some unused functions were removed, some were replaced, and in
a few cases specific funtions were moved to the files they were used in.

Added an interface for the StoreController, this reduces the number of imports
of the entire image store, decreasing binary size for tests.
If the zb code was still coupled with pkg/test, this would have reflected in zb size.

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
Andrei Aaron
2023-09-27 21:34:48 +03:00
committed by GitHub
parent c3801dc3d3
commit ba6f347d8d
82 changed files with 3362 additions and 3243 deletions
+246
View File
@@ -0,0 +1,246 @@
package common
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
)
var ErrNoGoModFileFound = errors.New("test: no go.mod file found in parent directories")
func GetProjectRootDir() (string, error) {
workDir, err := os.Getwd()
if err != nil {
return "", err
}
for {
goModPath := filepath.Join(workDir, "go.mod")
_, err := os.Stat(goModPath)
if err == nil {
return workDir, nil
}
if workDir == filepath.Dir(workDir) {
return "", ErrNoGoModFileFound
}
workDir = filepath.Dir(workDir)
}
}
func CopyFile(sourceFilePath, destFilePath string) error {
destFile, err := os.Create(destFilePath)
if err != nil {
return err
}
defer destFile.Close()
sourceFile, err := os.Open(sourceFilePath)
if err != nil {
return err
}
defer sourceFile.Close()
if _, err = io.Copy(destFile, sourceFile); err != nil {
return err
}
return nil
}
func CopyFiles(sourceDir, destDir string) error {
sourceMeta, err := os.Stat(sourceDir)
if err != nil {
return fmt.Errorf("CopyFiles os.Stat failed: %w", err)
}
if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil {
return fmt.Errorf("CopyFiles os.MkdirAll failed: %w", err)
}
files, err := os.ReadDir(sourceDir)
if err != nil {
return fmt.Errorf("CopyFiles os.ReadDir failed: %w", err)
}
for _, file := range files {
sourceFilePath := path.Join(sourceDir, file.Name())
destFilePath := path.Join(destDir, file.Name())
if file.IsDir() {
if strings.HasPrefix(file.Name(), "_") {
// Some tests create the trivy related folders under test/_trivy
continue
}
if err = CopyFiles(sourceFilePath, destFilePath); err != nil {
return err
}
} else {
sourceFile, err := os.Open(sourceFilePath)
if err != nil {
return fmt.Errorf("CopyFiles os.Open failed: %w", err)
}
defer sourceFile.Close()
destFile, err := os.Create(destFilePath)
if err != nil {
return fmt.Errorf("CopyFiles os.Create failed: %w", err)
}
defer destFile.Close()
if _, err = io.Copy(destFile, sourceFile); err != nil {
return fmt.Errorf("io.Copy failed: %w", err)
}
}
}
return nil
}
func CopyTestKeysAndCerts(destDir string) error {
files := []string{
"ca.crt", "ca.key", "client.cert", "client.csr",
"client.key", "server.cert", "server.csr", "server.key",
}
rootPath, err := GetProjectRootDir()
if err != nil {
return err
}
sourceDir := filepath.Join(rootPath, "test/data")
sourceMeta, err := os.Stat(sourceDir)
if err != nil {
return fmt.Errorf("CopyFiles os.Stat failed: %w", err)
}
if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil {
return err
}
for _, file := range files {
err = CopyFile(filepath.Join(sourceDir, file), filepath.Join(destDir, file))
if err != nil {
return err
}
}
return nil
}
func WriteFileWithPermission(path string, data []byte, perm fs.FileMode, overwrite bool) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
flag := os.O_WRONLY | os.O_CREATE
if overwrite {
flag |= os.O_TRUNC
} else {
flag |= os.O_EXCL
}
file, err := os.OpenFile(path, flag, perm)
if err != nil {
return err
}
_, err = file.Write(data)
if err != nil {
file.Close()
return err
}
return file.Close()
}
func ReadLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
for {
select {
case <-ctx.Done():
return false, nil
default:
content, err := os.ReadFile(logPath)
if err != nil {
return false, err
}
if strings.Contains(string(content), stringToMatch) {
return true, nil
}
}
}
}
func ReadLogFileAndCountStringOccurence(logPath string, stringToMatch string,
timeout time.Duration, count int,
) (bool, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
for {
select {
case <-ctx.Done():
return false, nil
default:
content, err := os.ReadFile(logPath)
if err != nil {
return false, err
}
if strings.Count(string(content), stringToMatch) >= count {
return true, nil
}
}
}
}
func MakeHtpasswdFile() string {
// bcrypt(username="test", passwd="test")
content := "test:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n"
return MakeHtpasswdFileFromString(content)
}
func GetCredString(username, password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
panic(err)
}
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
return usernameAndHash
}
func MakeHtpasswdFileFromString(fileContent string) string {
htpasswdFile, err := os.CreateTemp("", "htpasswd-")
if err != nil {
panic(err)
}
// bcrypt(username="test", passwd="test")
content := []byte(fileContent)
if err := os.WriteFile(htpasswdFile.Name(), content, 0o600); err != nil { //nolint:gomnd
panic(err)
}
return htpasswdFile.Name()
}
+219
View File
@@ -0,0 +1,219 @@
package common_test
import (
"encoding/json"
"errors"
"os"
"path"
"path/filepath"
"testing"
"time"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
tcommon "zotregistry.io/zot/pkg/test/common"
)
var ErrTestError = errors.New("ErrTestError")
func TestCopyFiles(t *testing.T) {
Convey("sourceDir does not exist", t, func() {
err := tcommon.CopyFiles("/path/to/some/unexisting/directory", os.TempDir())
So(err, ShouldNotBeNil)
})
Convey("destDir is a file", t, func() {
dir := t.TempDir()
err := tcommon.CopyFiles("../../../test/data", dir)
So(err, ShouldBeNil)
err = tcommon.CopyFiles(dir, "/etc/passwd")
So(err, ShouldNotBeNil)
})
Convey("sourceDir does not have read permissions", t, func() {
dir := t.TempDir()
err := os.Chmod(dir, 0o300)
So(err, ShouldBeNil)
err = tcommon.CopyFiles(dir, os.TempDir())
So(err, ShouldNotBeNil)
})
Convey("sourceDir has a subfolder that does not have read permissions", t, func() {
dir := t.TempDir()
sdir := "subdir"
err := os.Mkdir(path.Join(dir, sdir), 0o300)
So(err, ShouldBeNil)
err = tcommon.CopyFiles(dir, os.TempDir())
So(err, ShouldNotBeNil)
})
Convey("sourceDir has a file that does not have read permissions", t, func() {
dir := t.TempDir()
filePath := path.Join(dir, "file.txt")
err := os.WriteFile(filePath, []byte("some dummy file content"), 0o644) //nolint: gosec
if err != nil {
panic(err)
}
err = os.Chmod(filePath, 0o300)
So(err, ShouldBeNil)
err = tcommon.CopyFiles(dir, os.TempDir())
So(err, ShouldNotBeNil)
})
Convey("sourceDir contains a folder starting with invalid characters", t, func() {
srcDir := t.TempDir()
dstDir := t.TempDir()
err := os.MkdirAll(path.Join(srcDir, "_trivy", "db"), 0o755)
if err != nil {
panic(err)
}
err = os.MkdirAll(path.Join(srcDir, "test-index"), 0o755)
if err != nil {
panic(err)
}
filePathTrivy := path.Join(srcDir, "_trivy", "db", "trivy.db")
err = os.WriteFile(filePathTrivy, []byte("some dummy file content"), 0o644) //nolint: gosec
if err != nil {
panic(err)
}
var index ispec.Index
content, err := json.Marshal(index)
if err != nil {
panic(err)
}
err = os.WriteFile(path.Join(srcDir, "test-index", "index.json"), content, 0o644) //nolint: gosec
if err != nil {
panic(err)
}
err = tcommon.CopyFiles(srcDir, dstDir)
So(err, ShouldBeNil)
_, err = os.Stat(path.Join(dstDir, "_trivy", "db", "trivy.db"))
So(err, ShouldNotBeNil)
So(os.IsNotExist(err), ShouldBeTrue)
_, err = os.Stat(path.Join(dstDir, "test-index", "index.json"))
So(err, ShouldBeNil)
})
}
func TestCopyFile(t *testing.T) {
Convey("destFilePath does not exist", t, func() {
err := tcommon.CopyFile("/path/to/srcFile", "~/path/to/some/unexisting/destDir/file")
So(err, ShouldNotBeNil)
})
Convey("sourceFile does not exist", t, func() {
err := tcommon.CopyFile("/path/to/some/unexisting/file", path.Join(t.TempDir(), "destFile.txt"))
So(err, ShouldNotBeNil)
})
}
func TestReadLogFileAndSearchString(t *testing.T) {
logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
if err != nil {
panic(err)
}
logPath := logFile.Name()
defer os.Remove(logPath)
Convey("Invalid path", t, func() {
_, err = tcommon.ReadLogFileAndSearchString("invalidPath",
"DB update completed, next update scheduled", 1*time.Second)
So(err, ShouldNotBeNil)
})
Convey("Time too short", t, func() {
ok, err := tcommon.ReadLogFileAndSearchString(logPath, "invalid string", time.Microsecond)
So(err, ShouldBeNil)
So(ok, ShouldBeFalse)
})
}
func TestReadLogFileAndCountStringOccurence(t *testing.T) {
logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
if err != nil {
panic(err)
}
_, err = logFile.Write([]byte("line1\n line2\n line3 line1 line2\n line1"))
if err != nil {
panic(err)
}
logPath := logFile.Name()
defer os.Remove(logPath)
Convey("Invalid path", t, func() {
_, err = tcommon.ReadLogFileAndCountStringOccurence("invalidPath",
"DB update completed, next update scheduled", 1*time.Second, 1)
So(err, ShouldNotBeNil)
})
Convey("Time too short", t, func() {
ok, err := tcommon.ReadLogFileAndCountStringOccurence(logPath, "invalid string", time.Microsecond, 1)
So(err, ShouldBeNil)
So(ok, ShouldBeFalse)
})
Convey("Count occurrence working", t, func() {
ok, err := tcommon.ReadLogFileAndCountStringOccurence(logPath, "line1", 90*time.Second, 3)
So(err, ShouldBeNil)
So(ok, ShouldBeTrue)
})
}
func TestCopyTestKeysAndCerts(t *testing.T) {
Convey("CopyTestKeysAndCerts", t, func() {
// ------- Make test files unreadable -------
dir := t.TempDir()
file := filepath.Join(dir, "ca.crt")
_, err := os.Create(file)
So(err, ShouldBeNil)
err = os.Chmod(file, 0o000)
So(err, ShouldBeNil)
err = tcommon.CopyTestKeysAndCerts(dir)
So(err, ShouldNotBeNil)
err = os.Chmod(file, 0o777)
So(err, ShouldBeNil)
// ------- Copy fails -------
err = os.Chmod(dir, 0o000)
So(err, ShouldBeNil)
err = tcommon.CopyTestKeysAndCerts(file)
So(err, ShouldNotBeNil)
err = os.Chmod(dir, 0o777)
So(err, ShouldBeNil)
// ------- Folder creation fails -------
file = filepath.Join(dir, "a-file.file")
_, err = os.Create(file)
So(err, ShouldBeNil)
_, err = os.Stat(file)
So(err, ShouldBeNil)
err = tcommon.CopyTestKeysAndCerts(file)
So(err, ShouldNotBeNil)
})
}
+146 -19
View File
@@ -1,15 +1,45 @@
package common
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"path"
"time"
"github.com/phayes/freeport"
"gopkg.in/resty.v1"
)
var ErrNoGoModFileFound = errors.New("test: no go.mod file found in parent directories")
const (
BaseURL = "http://127.0.0.1:%s"
BaseSecureURL = "https://127.0.0.1:%s"
SleepTime = 100 * time.Millisecond
)
type isser interface {
Is(string) bool
}
// Index returns the index of the first occurrence of name in s,
// or -1 if not present.
func Index[E isser](s []E, name string) int {
for i, v := range s {
if v.Is(name) {
return i
}
}
return -1
}
// Contains reports whether name is present in s.
func Contains[E isser](s []E, name string) bool {
return Index(s, name) >= 0
}
func Location(baseURL string, resp *resty.Response) string {
// For some API responses, the Location header is set and is supposed to
@@ -29,24 +59,121 @@ func Location(baseURL string, resp *resty.Response) string {
return baseURL + path
}
func GetProjectRootDir() (string, error) {
workDir, err := os.Getwd()
if err != nil {
return "", err
}
type Controller interface {
Init(ctx context.Context) error
Run(ctx context.Context) error
Shutdown()
GetPort() int
}
for {
goModPath := filepath.Join(workDir, "go.mod")
type ControllerManager struct {
controller Controller
// used to stop background tasks(goroutines)
cancelRoutinesFunc context.CancelFunc
}
_, err := os.Stat(goModPath)
if err == nil {
return workDir, nil
}
if workDir == filepath.Dir(workDir) {
return "", ErrNoGoModFileFound
}
workDir = filepath.Dir(workDir)
func (cm *ControllerManager) RunServer(ctx context.Context) {
// Useful to be able to call in the same goroutine for testing purposes
if err := cm.controller.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}
func (cm *ControllerManager) StartServer() {
ctx, cancel := context.WithCancel(context.Background())
cm.cancelRoutinesFunc = cancel
if err := cm.controller.Init(ctx); err != nil {
panic(err)
}
go func() {
cm.RunServer(ctx)
}()
}
func (cm *ControllerManager) StopServer() {
// stop background tasks
if cm.cancelRoutinesFunc != nil {
cm.cancelRoutinesFunc()
}
cm.controller.Shutdown()
}
func (cm *ControllerManager) WaitServerToBeReady(port string) {
url := GetBaseURL(port)
WaitTillServerReady(url)
}
func (cm *ControllerManager) StartAndWait(port string) {
cm.StartServer()
url := GetBaseURL(port)
WaitTillServerReady(url)
}
func NewControllerManager(controller Controller) ControllerManager {
cm := ControllerManager{
controller: controller,
}
return cm
}
func WaitTillServerReady(url string) {
for {
_, err := resty.R().Get(url)
if err == nil {
break
}
time.Sleep(SleepTime)
}
}
func WaitTillTrivyDBDownloadStarted(rootDir string) {
for {
if _, err := os.Stat(path.Join(rootDir, "_trivy", "db", "trivy.db")); err == nil {
break
}
time.Sleep(SleepTime)
}
}
func GetFreePort() string {
port, err := freeport.GetFreePort()
if err != nil {
panic(err)
}
return fmt.Sprint(port)
}
func GetBaseURL(port string) string {
return fmt.Sprintf(BaseURL, port)
}
func GetSecureBaseURL(port string) string {
return fmt.Sprintf(BaseSecureURL, port)
}
func CustomRedirectPolicy(noOfRedirect int) resty.RedirectPolicy {
return resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
if len(via) >= noOfRedirect {
return fmt.Errorf("stopped after %d redirects", noOfRedirect) //nolint: goerr113
}
for key, val := range via[len(via)-1].Header {
req.Header[key] = val
}
respCookies := req.Response.Cookies()
for _, cookie := range respCookies {
req.AddCookie(cookie)
}
return nil
})
}
+63
View File
@@ -0,0 +1,63 @@
package common_test
import (
"context"
"os"
"path"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
tcommon "zotregistry.io/zot/pkg/test/common"
)
func TestWaitTillTrivyDBDownloadStarted(t *testing.T) {
Convey("finishes successfully", t, func() {
tempDir := t.TempDir()
go func() {
tcommon.WaitTillTrivyDBDownloadStarted(tempDir)
}()
time.Sleep(tcommon.SleepTime)
_, err := os.Create(path.Join(tempDir, "trivy.db"))
So(err, ShouldBeNil)
})
}
func TestControllerManager(t *testing.T) {
Convey("Test StartServer Init() panic", t, func() {
port := tcommon.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlrManager := tcommon.NewControllerManager(ctlr)
// No storage configured
So(func() { ctlrManager.StartServer() }, ShouldPanic)
})
Convey("Test RunServer panic", t, func() {
tempDir := t.TempDir()
// Invalid port
conf := config.New()
conf.HTTP.Port = "999999"
conf.Storage.RootDirectory = tempDir
ctlr := api.NewController(conf)
ctlrManager := tcommon.NewControllerManager(ctlr)
ctx := context.Background()
err := ctlr.Init(ctx)
So(err, ShouldBeNil)
So(func() { ctlrManager.RunServer(ctx) }, ShouldPanic)
})
}