From 49eb96b48fadb848231e94fbb5c303e8a019770e Mon Sep 17 00:00:00 2001 From: Mohamed Awnallah Date: Fri, 11 Oct 2024 19:49:21 +0300 Subject: [PATCH] operator/pkg/util: unit test util In this commit, we unit test utilities on downloader operations and unpacking tar files. Signed-off-by: Mohamed Awnallah --- operator/pkg/util/util.go | 9 +- operator/pkg/util/util_test.go | 348 +++++++++++++++++++++++++++++++++ 2 files changed, 353 insertions(+), 4 deletions(-) diff --git a/operator/pkg/util/util.go b/operator/pkg/util/util.go index 99365b2d11e4..d112b9fad95d 100644 --- a/operator/pkg/util/util.go +++ b/operator/pkg/util/util.go @@ -74,11 +74,12 @@ func (d *Downloader) Read(p []byte) (n int, err error) { return } +var httpClient = http.Client{ + Timeout: 60 * time.Second, +} + // DownloadFile Download files via URL func DownloadFile(url, filePath string) error { - httpClient := http.Client{ - Timeout: 60 * time.Second, - } resp, err := httpClient.Get(url) if err != nil { return err @@ -86,7 +87,7 @@ func DownloadFile(url, filePath string) error { defer resp.Body.Close() if resp.StatusCode != 200 { - return fmt.Errorf("failed download file. url: %s code: %v", url, resp.StatusCode) + return fmt.Errorf("failed to download file. url: %s code: %v", url, resp.StatusCode) } file, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR, util.DefaultFilePerm) diff --git a/operator/pkg/util/util_test.go b/operator/pkg/util/util_test.go index 4758b6268e73..9edc46e0292f 100644 --- a/operator/pkg/util/util_test.go +++ b/operator/pkg/util/util_test.go @@ -17,10 +17,309 @@ limitations under the License. package util import ( + "archive/tar" + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" "strings" "testing" + "time" + + "k8s.io/utils/ptr" ) +// mockReader is a simple io.Reader that returns an error after being called. +type mockReader struct { + data []byte + err error +} + +func (m *mockReader) Read(p []byte) (n int, err error) { + if m.data == nil { + return 0, m.err + } + n = copy(p, m.data) + m.data = m.data[n:] + return n, m.err +} + +type mockRoundTripper struct { + response *http.Response + err error +} + +func (m *mockRoundTripper) RoundTrip(*http.Request) (*http.Response, error) { + if m.err != nil { + return nil, m.err + } + return m.response, nil +} + +func TestRead(t *testing.T) { + tests := []struct { + name string + downloader *Downloader + data string + prep func(downloader *Downloader, data string) error + wantErr bool + errMsg string + }{ + { + name: "Read_FailedToReadFromDataSource_ReadFailed", + downloader: &Downloader{ + Reader: &mockReader{ + err: errors.New("unexpected read error"), + }, + }, + prep: func(*Downloader, string) error { + return nil + }, + wantErr: true, + errMsg: "unexpected read error", + }, + { + name: "Read_FailedToReadWithEOF_ReadFailed", + downloader: &Downloader{ + Reader: &mockReader{ + err: io.EOF, + }, + }, + prep: func(*Downloader, string) error { + return nil + }, + wantErr: true, + errMsg: "EOF", + }, + { + name: "Read_FromValidDataSource_ReadSucceeded", + downloader: &Downloader{ + Current: 3, + Total: 10, + }, + data: "test data", + prep: func(downloader *Downloader, data string) error { + downloader.Reader = &mockReader{data: []byte(data)} + return nil + }, + wantErr: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.prep(test.downloader, test.data); err != nil { + t.Fatalf("failed to prep before reading the data, got: %v", err) + } + buffer := test.downloader.Reader.(*mockReader).data + _, err := test.downloader.Read(buffer) + if err == nil && test.wantErr { + t.Fatal("expected an error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error, got: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected error message %s to be in %s", test.errMsg, err.Error()) + } + if string(buffer) != test.data { + t.Errorf("expected read buffer data to be %s, but got %s", test.data, string(buffer)) + } + }) + } +} + +func TestDownloadFile(t *testing.T) { + tests := []struct { + name string + url string + filePath string + prep func(url, filePath string) error + verify func(filePath string) error + wantErr bool + errMsg string + }{ + { + name: "DownloadFile_UrlIsNotFound_FailedToGetResponse", + url: "not-found-url", + prep: func(url, _ string) error { + httpClient = http.Client{ + Transport: &mockRoundTripper{ + err: fmt.Errorf("failed to get url %s, url is not found", url), + }, + Timeout: time.Second, + } + return nil + }, + verify: func(string) error { return nil }, + wantErr: true, + errMsg: "failed to get url not-found-url, url is not found", + }, + { + name: "DownloadFile_ServiceIsUnavailable_FailedToReachTheService", + url: "https://www.example.com/test-file", + prep: func(_, _ string) error { + httpClient = http.Client{ + Transport: &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusServiceUnavailable, + }, + }, + Timeout: time.Second, + } + return nil + }, + verify: func(string) error { return nil }, + wantErr: true, + errMsg: "failed to download file", + }, + { + name: "DownloadFile_FileDownlaoded_", + url: "https://www.example.com/test-file", + filePath: filepath.Join(os.TempDir(), "temp-download-file.txt"), + prep: func(_, filePath string) error { + // Create temp download filepath. + tempFile, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create temp download file: %w", err) + } + defer tempFile.Close() + + // Create HTTP client. + httpClient = http.Client{ + Transport: &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte("Hello, World!"))), + ContentLength: int64(len("Hello, World!")), + }, + }, + Timeout: time.Second, + } + + return nil + }, + verify: func(filePath string) error { + // Read the content of the downloaded file. + content, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + // Verify the content of the file. + expected := "Hello, World!" + if string(content) != expected { + return fmt.Errorf("unexpected file content: got %q, want %q", string(content), expected) + } + + if err := os.Remove(filePath); err != nil { + return fmt.Errorf("failed to clean up %s", filePath) + } + + return nil + }, + wantErr: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.prep(test.url, test.filePath); err != nil { + t.Fatalf("failed to prep before downloading the file, got: %v", err) + } + err := DownloadFile(test.url, test.filePath) + if err == nil && test.wantErr { + t.Fatal("expected an error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error, got: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected error message %s to be in %s", test.errMsg, err.Error()) + } + if err := test.verify(test.filePath); err != nil { + t.Errorf("failed to verify the actual of download of file: %v", err) + } + }) + } +} + +func TestUnpack(t *testing.T) { + tests := []struct { + name string + tarFile string + regularFile string + targetPath *string + prep func(tarFile, regularFile string, targetPath *string) error + verify func(regularFile string, targetPath string) error + wantErr bool + errMsg string + }{ + { + name: "Unpack_InvalidGzipFileHeader_InvalidHeader", + tarFile: "invalid.tar.gz", + targetPath: ptr.To(""), + prep: func(tarFile, _ string, targetPath *string) error { + var err error + *targetPath, err = os.MkdirTemp("", "test-unpack-*") + if err != nil { + return err + } + f, err := os.Create(filepath.Join(*targetPath, tarFile)) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString("Invalid gzip content") + return err + }, + verify: func(_, targetPath string) error { + return os.RemoveAll(targetPath) + }, + wantErr: true, + errMsg: gzip.ErrHeader.Error(), + }, + { + name: "Unpack_ValidTarGzipped_UnpackedSuccessfully", + tarFile: "valid.tar.gz", + regularFile: "test-file.txt", + targetPath: ptr.To(""), + prep: verifyValidTarGzipped, + verify: func(regularFile string, targetPath string) error { + fileExpected := filepath.Join(targetPath, "test", regularFile) + _, err := os.Stat(fileExpected) + if err != nil { + return fmt.Errorf("failed to find the file %s, got error: %v", fileExpected, err) + } + return os.RemoveAll(targetPath) + }, + wantErr: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.prep(test.tarFile, test.regularFile, test.targetPath); err != nil { + t.Fatalf("failed to prep before unpacking the tar file: %v", err) + } + err := Unpack(filepath.Join(*test.targetPath, test.tarFile), *test.targetPath) + if err == nil && test.wantErr { + t.Fatal("expected an error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error, got: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected error message %s to be in %s", test.errMsg, err.Error()) + } + if err := test.verify(test.regularFile, *test.targetPath); err != nil { + t.Errorf("failed to verify unpacking process, got: %v", err) + } + }) + } +} + func TestListFileWithSuffix(t *testing.T) { suffix := ".yaml" files := ListFileWithSuffix(".", suffix) @@ -35,3 +334,52 @@ func TestListFileWithSuffix(t *testing.T) { } } } + +// verifyValidTarGzipped creates a tar.gz file in a temporary directory. +// The archive contains a "test" directory and a file with the specified name, +// containing the message "Hello, World!". +func verifyValidTarGzipped(tarFile, regularFile string, targetPath *string) error { + var err error + *targetPath, err = os.MkdirTemp("", "test-unpack-*") + if err != nil { + return err + } + f, err := os.Create(filepath.Join(*targetPath, tarFile)) + if err != nil { + return err + } + defer f.Close() + + // Create a gzip writer. + gw := gzip.NewWriter(f) + defer gw.Close() + + // Create a tar writer. + tw := tar.NewWriter(gw) + defer tw.Close() + + // Add a directory to the tar. + if err := tw.WriteHeader(&tar.Header{ + Name: "test" + string(filepath.Separator), + Typeflag: tar.TypeDir, + Mode: 0755, + }); err != nil { + return err + } + + // Add a file to the tar. + message := "Hello, World!" + if err := tw.WriteHeader(&tar.Header{ + Name: filepath.Join("test", regularFile), + Mode: 0644, + Size: int64(len(message)), + Typeflag: tar.TypeReg, + }); err != nil { + return err + } + if _, err := tw.Write([]byte(message)); err != nil { + return err + } + + return nil +}