From 5309e7f5cf86936636b33449222a5fb11d8374b8 Mon Sep 17 00:00:00 2001 From: Andrei Aaron Date: Thu, 2 Oct 2025 01:24:38 +0300 Subject: [PATCH] chore: increase/stabilize go test coverage (#3411) * chore: increase/stabilize coverage for the local storage driver Signed-off-by: Andrei Aaron * chore: add/stabilize coverage for soring ImageSummary objects Signed-off-by: Andrei Aaron * chore: stabilize coverage in sync tests Signed-off-by: Andrei Aaron --------- Signed-off-by: Andrei Aaron --- go.mod | 2 - go.sum | 4 - pkg/extensions/search/resolver_test.go | 130 +++ pkg/extensions/sync/sync_internal_test.go | 115 +++ pkg/storage/local/driver.go | 16 +- pkg/storage/local/driver_internal_test.go | 65 ++ pkg/storage/local/driver_test.go | 1125 +++++++++++++++++++++ pkg/test/mocks/sync_remote_mock.go | 126 ++- 8 files changed, 1527 insertions(+), 56 deletions(-) create mode 100644 pkg/storage/local/driver_internal_test.go diff --git a/go.mod b/go.mod index c24a48ac..5b3a89ab 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,6 @@ require ( github.com/chartmuseum/auth v0.5.0 github.com/cloudevents/sdk-go/protocol/nats/v2 v2.16.2 github.com/cloudevents/sdk-go/v2 v2.16.2 - github.com/containers/image/v5 v5.36.2 github.com/dchest/siphash v1.2.3 github.com/didip/tollbooth/v7 v7.0.2 github.com/distribution/distribution/v3 v3.0.0 @@ -213,7 +212,6 @@ require ( github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect - github.com/containers/storage v1.59.1 // indirect github.com/coreos/go-oidc/v3 v3.14.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect diff --git a/go.sum b/go.sum index 9bc532f0..edb9e0a4 100644 --- a/go.sum +++ b/go.sum @@ -1022,10 +1022,6 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= -github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk= -github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4= -github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= -github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index bfc17e76..bee9075a 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "sort" "testing" "time" @@ -2667,6 +2668,135 @@ func TestExpandedRepoInfoErrors(t *testing.T) { }) } +func TestImageSummarySort(t *testing.T) { + Convey("Test sorting ImageSummary", t, func() { + Convey("Swap elements at valid indices", func() { + // Create test data with different timestamps + time1 := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + time2 := time.Date(2023, 1, 2, 12, 0, 0, 0, time.UTC) + time3 := time.Date(2023, 1, 3, 12, 0, 0, 0, time.UTC) + + image1 := &gql_generated.ImageSummary{ + Tag: ref("tag1"), + LastUpdated: &time1, + } + image2 := &gql_generated.ImageSummary{ + Tag: ref("tag2"), + LastUpdated: &time2, + } + image3 := &gql_generated.ImageSummary{ + Tag: ref("tag3"), + LastUpdated: &time3, + } + + // Create timeSlice with test data + timeSliceData := timeSlice{image1, image2, image3} + + // Verify initial order + So(*timeSliceData[0].Tag, ShouldEqual, "tag1") + So(*timeSliceData[1].Tag, ShouldEqual, "tag2") + So(*timeSliceData[2].Tag, ShouldEqual, "tag3") + + // Swap elements at indices 0 and 2 + timeSliceData.Swap(0, 2) + + // Verify elements are swapped + So(*timeSliceData[0].Tag, ShouldEqual, "tag3") + So(*timeSliceData[1].Tag, ShouldEqual, "tag2") + So(*timeSliceData[2].Tag, ShouldEqual, "tag1") + + // Swap elements at indices 1 and 2 + timeSliceData.Swap(1, 2) + + // Verify elements are swapped again + So(*timeSliceData[0].Tag, ShouldEqual, "tag3") + So(*timeSliceData[1].Tag, ShouldEqual, "tag1") + So(*timeSliceData[2].Tag, ShouldEqual, "tag2") + }) + + Convey("Swap elements at same index (no-op)", func() { + time1 := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + time2 := time.Date(2023, 1, 2, 12, 0, 0, 0, time.UTC) + + image1 := &gql_generated.ImageSummary{ + Tag: ref("tag1"), + LastUpdated: &time1, + } + image2 := &gql_generated.ImageSummary{ + Tag: ref("tag2"), + LastUpdated: &time2, + } + + timeSliceData := timeSlice{image1, image2} + + // Swap element with itself + timeSliceData.Swap(0, 0) + + // Verify no change + So(*timeSliceData[0].Tag, ShouldEqual, "tag1") + So(*timeSliceData[1].Tag, ShouldEqual, "tag2") + }) + + Convey("Swap with single element slice", func() { + time1 := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + + image1 := &gql_generated.ImageSummary{ + Tag: ref("tag1"), + LastUpdated: &time1, + } + + timeSliceData := timeSlice{image1} + + // Swap single element with itself + timeSliceData.Swap(0, 0) + + // Verify no change + So(*timeSliceData[0].Tag, ShouldEqual, "tag1") + }) + + Convey("Swap with empty slice", func() { + timeSliceData := timeSlice{} + + // Verify slice is empty + // Note: Calling Swap on empty slice would panic, which is expected behavior + // The Swap method doesn't check bounds, so it's the caller's responsibility + // to ensure valid indices. This is consistent with Go's standard library behavior. + So(len(timeSliceData), ShouldEqual, 0) + }) + + Convey("Integration test with sort.Sort", func() { + // Create test data with unsorted timestamps + time1 := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + time2 := time.Date(2023, 1, 3, 12, 0, 0, 0, time.UTC) // Latest + time3 := time.Date(2023, 1, 2, 12, 0, 0, 0, time.UTC) + + image1 := &gql_generated.ImageSummary{ + Tag: ref("tag1"), + LastUpdated: &time1, + } + image2 := &gql_generated.ImageSummary{ + Tag: ref("tag2"), + LastUpdated: &time2, + } + image3 := &gql_generated.ImageSummary{ + Tag: ref("tag3"), + LastUpdated: &time3, + } + + // Create timeSlice with unsorted data + timeSliceData := timeSlice{image1, image2, image3} + + // Sort using sort.Sort (which will call Swap internally) + sort.Sort(timeSliceData) + + // Verify sorted order (newest first due to Less implementation) + So(*timeSliceData[0].Tag, ShouldEqual, "tag2") // Latest time + So(*timeSliceData[1].Tag, ShouldEqual, "tag3") // Middle time + So(*timeSliceData[2].Tag, ShouldEqual, "tag1") // Oldest time + }) + }) +} + func TestUtils(t *testing.T) { Convey("utils", t, func() { Convey("", func() { diff --git a/pkg/extensions/sync/sync_internal_test.go b/pkg/extensions/sync/sync_internal_test.go index 381c549f..92753d88 100644 --- a/pkg/extensions/sync/sync_internal_test.go +++ b/pkg/extensions/sync/sync_internal_test.go @@ -7,11 +7,13 @@ import ( "bytes" "context" "encoding/json" + "errors" "os" "testing" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/regclient/regclient/types/ref" . "github.com/smartystreets/goconvey/convey" zerr "zotregistry.dev/zot/errors" @@ -41,6 +43,119 @@ func TestService(t *testing.T) { err = service.SyncRepo(context.Background(), "repo") So(err, ShouldNotBeNil) }) + + Convey("test context cancellation in SyncRepo without mock", t, func() { + conf := syncconf.RegistryConfig{ + URLs: []string{"http://localhost"}, + } + + service, err := New(conf, "", nil, os.TempDir(), storage.StoreController{}, mocks.MetaDBMock{}, log.Logger{}) + So(err, ShouldBeNil) + + // Create a context that's already cancelled + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + err = service.SyncRepo(ctx, "repo") + So(err, ShouldNotBeNil) + // This will fail at getTags before reaching the cancellation check + }) + + Convey("test context cancellation in SyncRepo with mock", t, func() { + conf := syncconf.RegistryConfig{ + URLs: []string{"http://localhost"}, + } + + service, err := New(conf, "", nil, os.TempDir(), storage.StoreController{}, mocks.MetaDBMock{}, log.Logger{}) + So(err, ShouldBeNil) + + // Create a mock remote that returns tags so we can reach the loop + mockRemote := &mocks.SyncRemoteMock{ + GetTagsFn: func(ctx context.Context, repo string) ([]string, error) { + return []string{"tag1", "tag2", "tag3"}, nil + }, + } + service.remote = mockRemote + + // Create a context that's already cancelled + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + err = service.SyncRepo(ctx, "repo") + So(err, ShouldNotBeNil) + So(errors.Is(err, context.Canceled), ShouldBeTrue) + }) + + Convey("test SyncReferrers ReferrerList error", t, func() { + conf := syncconf.RegistryConfig{ + URLs: []string{"http://localhost"}, + } + + service, err := New(conf, "", nil, os.TempDir(), storage.StoreController{}, mocks.MetaDBMock{}, log.Logger{}) + So(err, ShouldBeNil) + + // Create a minimal mock remote that only returns tags + mockRemote := &mocks.SyncRemoteMock{ + GetTagsFn: func(ctx context.Context, repo string) ([]string, error) { + return []string{"tag1"}, nil + }, + } + service.remote = mockRemote + + // Set rc to nil to force a panic at ReferrerList call + service.rc = nil + + // Use defer to catch the panic - this confirms we reached the ReferrerList call + var panicOccurred bool + defer func() { + if r := recover(); r != nil { + panicOccurred = true + t.Logf("SyncReferrers panic (expected): %v", r) + } + }() + + ctx := context.Background() + err = service.SyncReferrers(ctx, "repo", "tag1", []string{"signature"}) + + // We expect a panic when rc is nil, which confirms we reached the ReferrerList call + So(panicOccurred, ShouldBeTrue) + }) + + Convey("test syncImage ReferrerList error with OnlySigned", t, func() { + onlySigned := true + conf := syncconf.RegistryConfig{ + URLs: []string{"http://invalid-registry-that-does-not-exist:9999"}, + OnlySigned: &onlySigned, + } + + service, err := New(conf, "", nil, os.TempDir(), storage.StoreController{}, mocks.MetaDBMock{}, log.Logger{}) + So(err, ShouldBeNil) + + // Create a mock remote that returns necessary data + mockRemote := &mocks.SyncRemoteMock{ + GetImageReferenceFn: func(repo string, tag string) (ref.Ref, error) { + return ref.New("invalid-registry-that-does-not-exist:9999/" + repo + ":" + tag) + }, + GetDigestFn: func(ctx context.Context, repo, tag string) (godigest.Digest, error) { + return godigest.Digest("sha256:abc123"), nil + }, + } + service.remote = mockRemote + + // Create a mock destination + mockDest := &mocks.SyncDestinationMock{ + GetImageReferenceFn: func(repo string, tag string) (ref.Ref, error) { + return ref.New("local/" + repo + ":" + tag) + }, + } + service.destination = mockDest + + ctx := context.Background() + err = service.syncImage(ctx, "localrepo", "remoterepo", "tag1", []string{}, true) + + // We expect an error when ReferrerList fails (network/connection error in this case) + So(err, ShouldNotBeNil) + }) } func TestDestinationRegistry(t *testing.T) { diff --git a/pkg/storage/local/driver.go b/pkg/storage/local/driver.go index 177e5d02..1b383522 100644 --- a/pkg/storage/local/driver.go +++ b/pkg/storage/local/driver.go @@ -359,8 +359,15 @@ func (fi fileInfo) IsDir() bool { return fi.FileInfo.IsDir() } +// fileInternal defines the interface for file operations (internal). +type fileInternal interface { + io.WriteCloser + Sync() error + Name() string +} + type fileWriter struct { - file *os.File + file fileInternal size int64 bw *bufio.Writer closed bool @@ -369,7 +376,7 @@ type fileWriter struct { commit bool } -func newFileWriter(file *os.File, size int64, commit bool) *fileWriter { +func newFileWriter(file fileInternal, size int64, commit bool) *fileWriter { return &fileWriter{ file: file, size: size, @@ -378,6 +385,11 @@ func newFileWriter(file *os.File, size int64, commit bool) *fileWriter { } } +// NewFileWriter creates a new fileWriter for testing purposes. +func NewFileWriter(file fileInternal, size int64, commit bool) *fileWriter { + return newFileWriter(file, size, commit) +} + func (fw *fileWriter) Write(buf []byte) (int, error) { //nolint: gocritic if fw.closed { diff --git a/pkg/storage/local/driver_internal_test.go b/pkg/storage/local/driver_internal_test.go new file mode 100644 index 00000000..b4109b39 --- /dev/null +++ b/pkg/storage/local/driver_internal_test.go @@ -0,0 +1,65 @@ +package local + +import ( + "errors" + "testing" + + storagedriver "github.com/distribution/distribution/v3/registry/storage/driver" + . "github.com/smartystreets/goconvey/convey" +) + +func TestFormatErr(t *testing.T) { + Convey("Test formatErr error handling", t, func() { + driver := New(true) + + Convey("Test formatErr with nil error", func() { + err := driver.formatErr(nil) + So(err, ShouldBeNil) + }) + + Convey("Test formatErr with PathNotFoundError", func() { + pathErr := storagedriver.PathNotFoundError{Path: "/test"} + formatted := driver.formatErr(pathErr) + So(formatted, ShouldNotBeNil) + + // Check if it's still a PathNotFoundError with driver name set + var pathNotFoundErr storagedriver.PathNotFoundError + So(errors.As(formatted, &pathNotFoundErr), ShouldBeTrue) + So(pathNotFoundErr.DriverName, ShouldEqual, "local") + }) + + Convey("Test formatErr with InvalidPathError", func() { + invalidErr := storagedriver.InvalidPathError{Path: "/test"} + formatted := driver.formatErr(invalidErr) + So(formatted, ShouldNotBeNil) + + // Check if it's still an InvalidPathError with driver name set + var invalidPathErr storagedriver.InvalidPathError + So(errors.As(formatted, &invalidPathErr), ShouldBeTrue) + So(invalidPathErr.DriverName, ShouldEqual, "local") + }) + + Convey("Test formatErr with InvalidOffsetError", func() { + offsetErr := storagedriver.InvalidOffsetError{Path: "/test", Offset: 100} + formatted := driver.formatErr(offsetErr) + So(formatted, ShouldNotBeNil) + + // Check if it's still an InvalidOffsetError with driver name set + var invalidOffsetErr storagedriver.InvalidOffsetError + So(errors.As(formatted, &invalidOffsetErr), ShouldBeTrue) + So(invalidOffsetErr.DriverName, ShouldEqual, "local") + }) + + Convey("Test formatErr with generic error", func() { + genericErr := errors.New("generic error") + formatted := driver.formatErr(genericErr) + So(formatted, ShouldNotBeNil) + + // Check if it's wrapped in a storagedriver.Error + var storageErr storagedriver.Error + So(errors.As(formatted, &storageErr), ShouldBeTrue) + So(storageErr.DriverName, ShouldEqual, "local") + So(storageErr.Detail, ShouldEqual, genericErr) + }) + }) +} diff --git a/pkg/storage/local/driver_test.go b/pkg/storage/local/driver_test.go index c525db3d..7eb1a6c7 100644 --- a/pkg/storage/local/driver_test.go +++ b/pkg/storage/local/driver_test.go @@ -1,14 +1,19 @@ package local_test import ( + "context" + "errors" + "io" "os" "path" + "path/filepath" "strings" "testing" storagedriver "github.com/distribution/distribution/v3/registry/storage/driver" . "github.com/smartystreets/goconvey/convey" + zerr "zotregistry.dev/zot/errors" storageConstants "zotregistry.dev/zot/pkg/storage/constants" "zotregistry.dev/zot/pkg/storage/local" ) @@ -58,6 +63,11 @@ func TestStorageDriver(t *testing.T) { result = driver.DirExists(path.Join(rootDir, repoName)) So(result, ShouldBeTrue) + + // Invalid UTF-8 path should return false + invalidUTF8Path := string([]byte{0xff, 0xfe, 0xfd}) // Invalid UTF-8 sequence + result = driver.DirExists(invalidUTF8Path) + So(result, ShouldBeFalse) }) Convey("Test Walk", t, func() { @@ -114,3 +124,1118 @@ func TestStorageDriver(t *testing.T) { }) }) } + +func TestMove(t *testing.T) { + Convey("Test Move file/directory operations", t, func() { + driver := local.New(true) + rootDir := t.TempDir() + + Convey("Test moving non-existent file", func() { + err := driver.Move("/nonexistent", "/destination") + So(err, ShouldNotBeNil) + + var pathNotFoundErr storagedriver.PathNotFoundError + + So(errors.As(err, &pathNotFoundErr), ShouldBeTrue) + }) + + Convey("Test successful file move", func() { + srcFile := path.Join(rootDir, "source.txt") + destFile := path.Join(rootDir, "dest.txt") + + // Create source file + err := os.WriteFile(srcFile, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Move file + err = driver.Move(srcFile, destFile) + So(err, ShouldBeNil) + + // Verify move + _, err = os.Stat(srcFile) + So(err, ShouldNotBeNil) + _, err = os.Stat(destFile) + So(err, ShouldBeNil) + }) + + Convey("Test moving to non-existent directory", func() { + srcFile := path.Join(rootDir, "source.txt") + destFile := path.Join(rootDir, "nonexistent", "dest.txt") + + // Create source file + err := os.WriteFile(srcFile, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Move file (should create destination directory) + err = driver.Move(srcFile, destFile) + So(err, ShouldBeNil) + + // Verify move + _, err = os.Stat(srcFile) + So(err, ShouldNotBeNil) + _, err = os.Stat(destFile) + So(err, ShouldBeNil) + }) + + Convey("Test Move() with os.MkdirAll error to trigger formatErr", func() { + srcFile := path.Join(rootDir, "source.txt") + // Use invalid path to trigger os.MkdirAll error + destFile := string([]byte{0x00}) + "/dest.txt" + + // Create source file + err := os.WriteFile(srcFile, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Move should return a formatted error + err = driver.Move(srcFile, destFile) + So(err, ShouldNotBeNil) + + // Should be a formatted error + var storageErr storagedriver.Error + + So(errors.As(err, &storageErr), ShouldBeTrue) + So(storageErr.DriverName, ShouldEqual, "local") + }) + + Convey("Test Move() with os.Rename error to trigger formatErr", func() { + srcFile := path.Join(rootDir, "source.txt") + destFile := path.Join(rootDir, "dest.txt") + + // Create source file + err := os.WriteFile(srcFile, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Create destination file to cause rename conflict + err = os.WriteFile(destFile, []byte("existing content"), 0o600) + So(err, ShouldBeNil) + + // Move should return a formatted error (rename conflict) + err = driver.Move(srcFile, destFile) + // Note: On some systems, os.Rename might succeed by overwriting + // So we just verify it doesn't panic and handle the result appropriately + _ = err + }) + }) +} + +func TestValidateHardLink(t *testing.T) { + Convey("Test ValidateHardLink functionality", t, func() { + rootDir := t.TempDir() + + Convey("Test successful hardlink validation", func() { + err := local.ValidateHardLink(rootDir) + So(err, ShouldBeNil) + }) + + Convey("Test hardlink validation on non-existent directory", func() { + err := local.ValidateHardLink("/nonexistent/directory") + // This might succeed or fail depending on system permissions + // We're just testing that it doesn't panic + _ = err + }) + }) +} + +func TestWriteFile(t *testing.T) { + Convey("Test WriteFile operations", t, func() { + driver := local.New(true) + rootDir := t.TempDir() + + Convey("Test successful file write", func() { + content := []byte("test content") + filePath := path.Join(rootDir, "test.txt") + + n, err := driver.WriteFile(filePath, content) + So(err, ShouldBeNil) + So(n, ShouldEqual, len(content)) + + // Verify file was created + _, err = os.Stat(filePath) + So(err, ShouldBeNil) + }) + + Convey("Test write to non-existent directory", func() { + content := []byte("test content") + filePath := "/nonexistent/path/file.txt" + + n, err := driver.WriteFile(filePath, content) + So(err, ShouldNotBeNil) + So(n, ShouldEqual, -1) + }) + + Convey("Test write empty content", func() { + content := []byte("") + filePath := path.Join(rootDir, "empty.txt") + + n, err := driver.WriteFile(filePath, content) + So(err, ShouldBeNil) + So(n, ShouldEqual, 0) + }) + + Convey("Test WriteFile() with io.Copy error to trigger formatErr", func() { + // Create a file + filePath := path.Join(rootDir, "test.txt") + err := os.WriteFile(filePath, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // WriteFile should succeed normally + content := []byte("new content") + n, err := driver.WriteFile(filePath, content) + So(err, ShouldBeNil) + So(n, ShouldEqual, len(content)) + + // Test with invalid path to trigger formatErr path + // This will cause Writer to fail, which WriteFile will pass through + invalidPath := string([]byte{0x00}) // Null byte in path + n, err = driver.WriteFile(invalidPath, content) + So(err, ShouldNotBeNil) + So(n, ShouldEqual, -1) + + // Should be a formatted error + var storageErr storagedriver.Error + + So(errors.As(err, &storageErr), ShouldBeTrue) + So(storageErr.DriverName, ShouldEqual, "local") + }) + }) +} + +func TestLink(t *testing.T) { + Convey("Test Link hardlink operations", t, func() { + driver := local.New(true) + rootDir := t.TempDir() + + Convey("Test successful hardlink creation", func() { + // Create source file + srcFile := path.Join(rootDir, "source.txt") + err := os.WriteFile(srcFile, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Create hardlink + destFile := path.Join(rootDir, "link.txt") + err = driver.Link(srcFile, destFile) + So(err, ShouldBeNil) + + // Verify link exists + _, err = os.Stat(destFile) + So(err, ShouldBeNil) + }) + + Convey("Test linking non-existent file", func() { + destFile := path.Join(rootDir, "link.txt") + err := driver.Link("/nonexistent", destFile) + So(err, ShouldNotBeNil) + }) + + Convey("Test linking to existing destination", func() { + // Create source file + srcFile := path.Join(rootDir, "source.txt") + err := os.WriteFile(srcFile, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Create existing destination file + destFile := path.Join(rootDir, "existing.txt") + err = os.WriteFile(destFile, []byte("existing content"), 0o600) + So(err, ShouldBeNil) + + // Create hardlink (should remove existing file first) + err = driver.Link(srcFile, destFile) + So(err, ShouldBeNil) + + // Verify link exists + _, err = os.Stat(destFile) + So(err, ShouldBeNil) + }) + + Convey("Test Link() with os.Remove error to trigger return err", func() { + // Link should return os.Remove error + err := driver.Link("", string([]byte{0x00})) + So(err, ShouldNotBeNil) + }) + }) +} + +func TestDelete(t *testing.T) { + Convey("Test Delete operations", t, func() { + driver := local.New(true) + rootDir := t.TempDir() + + Convey("Test deleting non-existent file", func() { + err := driver.Delete("/nonexistent") + So(err, ShouldNotBeNil) + + var pathNotFoundErr storagedriver.PathNotFoundError + + So(errors.As(err, &pathNotFoundErr), ShouldBeTrue) + }) + + Convey("Test successful file deletion", func() { + filePath := path.Join(rootDir, "test.txt") + err := os.WriteFile(filePath, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + err = driver.Delete(filePath) + So(err, ShouldBeNil) + + // Verify deletion + _, err = os.Stat(filePath) + So(err, ShouldNotBeNil) + }) + + Convey("Test deleting directory", func() { + dirPath := path.Join(rootDir, "testdir") + err := os.Mkdir(dirPath, 0o755) + So(err, ShouldBeNil) + + // Create file in directory + filePath := path.Join(dirPath, "test.txt") + err = os.WriteFile(filePath, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + err = driver.Delete(dirPath) + So(err, ShouldBeNil) + + // Verify deletion + _, err = os.Stat(dirPath) + So(err, ShouldNotBeNil) + }) + + Convey("Test Delete() with invalid path to trigger formatErr", func() { + // Use an invalid path that will cause os.Stat to fail with a non-IsNotExist error + invalidPath := string([]byte{0x00}) // Null byte in path is invalid on most systems + + // Delete should return a formatted error (not PathNotFoundError) + err := driver.Delete(invalidPath) + So(err, ShouldNotBeNil) + + // Should not be a PathNotFoundError since it's not an IsNotExist error + var pathNotFoundErr storagedriver.PathNotFoundError + + So(errors.As(err, &pathNotFoundErr), ShouldBeFalse) + + // Should be a formatted error + var storageErr storagedriver.Error + + So(errors.As(err, &storageErr), ShouldBeTrue) + So(storageErr.DriverName, ShouldEqual, "local") + }) + }) +} + +func TestFileInfoSize(t *testing.T) { + Convey("Test fileInfo.Size method", t, func() { + driver := local.New(true) + rootDir := t.TempDir() + + Convey("Test file size calculation", func() { + filePath := path.Join(rootDir, "test.txt") + content := []byte("test content") + err := os.WriteFile(filePath, content, 0o600) + So(err, ShouldBeNil) + + fileInfo, err := driver.Stat(filePath) + So(err, ShouldBeNil) + So(fileInfo.Size(), ShouldEqual, int64(len(content))) + }) + + Convey("Test directory size (should be 0)", func() { + dirPath := path.Join(rootDir, "testdir") + err := os.Mkdir(dirPath, 0o755) + So(err, ShouldBeNil) + + dirInfo, err := driver.Stat(dirPath) + So(err, ShouldBeNil) + So(dirInfo.Size(), ShouldEqual, int64(0)) + }) + + Convey("Test empty file size", func() { + filePath := path.Join(rootDir, "empty.txt") + err := os.WriteFile(filePath, []byte(""), 0o600) + So(err, ShouldBeNil) + + fileInfo, err := driver.Stat(filePath) + So(err, ShouldBeNil) + So(fileInfo.Size(), ShouldEqual, int64(0)) + }) + + Convey("Test Stat() with permission error", func() { + // Create a file + filePath := path.Join(rootDir, "permission_test.txt") + err := os.WriteFile(filePath, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Remove read permission + err = os.Chmod(filePath, 0o000) + So(err, ShouldBeNil) + + defer func() { + _ = os.Chmod(filePath, 0o600) // Restore permissions + }() + + // Stat should return a formatted error (not PathNotFoundError) + _, err = driver.Stat(filePath) + // Note: On some systems, Stat() might still succeed even with 0000 permissions + // We just verify it doesn't panic and handles the case appropriately + _ = err + }) + + Convey("Test Stat() with invalid path to trigger formatErr", func() { + // Use an invalid path that will cause os.Stat to fail with a non-IsNotExist error + invalidPath := string([]byte{0x00}) // Null byte in path is invalid on most systems + + // Stat should return a formatted error (not PathNotFoundError) + _, err := driver.Stat(invalidPath) + So(err, ShouldNotBeNil) + + // Should not be a PathNotFoundError since it's not an IsNotExist error + var pathNotFoundErr storagedriver.PathNotFoundError + + So(errors.As(err, &pathNotFoundErr), ShouldBeFalse) + + // Should be a formatted error + var storageErr storagedriver.Error + + So(errors.As(err, &storageErr), ShouldBeTrue) + So(storageErr.DriverName, ShouldEqual, "local") + }) + + Convey("Test Stat() with non-existent file", func() { + // Stat on non-existent file should return PathNotFoundError + _, err := driver.Stat(path.Join(rootDir, "nonexistent.txt")) + So(err, ShouldNotBeNil) + + // Should be a PathNotFoundError + var pathNotFoundErr storagedriver.PathNotFoundError + + So(errors.As(err, &pathNotFoundErr), ShouldBeTrue) + So(pathNotFoundErr.Path, ShouldContainSubstring, "nonexistent.txt") + }) + }) +} + +func TestReader(t *testing.T) { + Convey("Test Reader operations", t, func() { + driver := local.New(true) + rootDir := t.TempDir() + + Convey("Test reading non-existent file", func() { + _, err := driver.Reader("/nonexistent", 0) + So(err, ShouldNotBeNil) + + var pathNotFoundErr storagedriver.PathNotFoundError + + So(errors.As(err, &pathNotFoundErr), ShouldBeTrue) + }) + + Convey("Test reading with invalid offset", func() { + filePath := path.Join(rootDir, "test.txt") + content := []byte("test content") + err := os.WriteFile(filePath, content, 0o600) + So(err, ShouldBeNil) + + _, err = driver.Reader(filePath, 1000) // Offset beyond file size + // Note: This might not always return an error depending on the implementation + // We just verify it doesn't panic + _ = err + }) + + Convey("Test successful read from beginning", func() { + filePath := path.Join(rootDir, "test.txt") + content := []byte("test content") + err := os.WriteFile(filePath, content, 0o600) + So(err, ShouldBeNil) + + reader, err := driver.Reader(filePath, 0) + So(err, ShouldBeNil) + defer reader.Close() + + readContent, err := io.ReadAll(reader) + So(err, ShouldBeNil) + So(string(readContent), ShouldEqual, string(content)) + }) + + Convey("Test successful read with offset", func() { + filePath := path.Join(rootDir, "test.txt") + content := []byte("test content") + err := os.WriteFile(filePath, content, 0o600) + So(err, ShouldBeNil) + + reader, err := driver.Reader(filePath, 5) // Start from offset 5 + So(err, ShouldBeNil) + defer reader.Close() + + readContent, err := io.ReadAll(reader) + So(err, ShouldBeNil) + So(string(readContent), ShouldEqual, "content") + }) + + Convey("Test ReadFile() with io.ReadAll error to trigger formatErr", func() { + // Create a file + filePath := path.Join(rootDir, "test.txt") + err := os.WriteFile(filePath, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // ReadFile should succeed normally + content, err := driver.ReadFile(filePath) + So(err, ShouldBeNil) + So(string(content), ShouldEqual, "test content") + + // Test with non-existent file to trigger formatErr path + // This will cause Reader to fail, which ReadFile will pass through + _, err = driver.ReadFile("/nonexistent") + So(err, ShouldNotBeNil) + + // Should be a PathNotFoundError (from Reader) + var pathNotFoundErr storagedriver.PathNotFoundError + + So(errors.As(err, &pathNotFoundErr), ShouldBeTrue) + }) + + Convey("Test Reader() with file.Seek error to trigger formatErr", func() { + // Create a file + filePath := path.Join(rootDir, "test.txt") + err := os.WriteFile(filePath, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Use invalid offset to trigger file.Seek error + _, err = driver.Reader(filePath, -1) // Negative offset should cause Seek error + So(err, ShouldNotBeNil) + + // Should be a formatted error + var storageErr storagedriver.Error + + So(errors.As(err, &storageErr), ShouldBeTrue) + So(storageErr.DriverName, ShouldEqual, "local") + }) + }) +} + +func TestWriter(t *testing.T) { + Convey("Test Writer operations", t, func() { + driver := local.New(true) + rootDir := t.TempDir() + + Convey("Test append mode with non-existent file", func() { + filePath := path.Join(rootDir, "test.txt") + _, err := driver.Writer(filePath, true) // append=true + So(err, ShouldNotBeNil) + + var pathNotFoundErr storagedriver.PathNotFoundError + + So(errors.As(err, &pathNotFoundErr), ShouldBeTrue) + }) + + Convey("Test successful writer creation (non-append)", func() { + filePath := path.Join(rootDir, "test.txt") + writer, err := driver.Writer(filePath, false) + So(err, ShouldBeNil) + + defer writer.Close() + + // Write some content + _, err = writer.Write([]byte("test content")) + So(err, ShouldBeNil) + + // Close and verify + err = writer.Close() + So(err, ShouldBeNil) + + // Verify file was created + _, err = os.Stat(filePath) + So(err, ShouldBeNil) + }) + + Convey("Test append mode with existing file", func() { + filePath := path.Join(rootDir, "test.txt") + initialContent := []byte("initial ") + err := os.WriteFile(filePath, initialContent, 0o600) + So(err, ShouldBeNil) + + writer, err := driver.Writer(filePath, true) + So(err, ShouldBeNil) + defer writer.Close() + + // Append content + _, err = writer.Write([]byte("appended")) + So(err, ShouldBeNil) + + // Close and verify + err = writer.Close() + So(err, ShouldBeNil) + + // Verify content was appended + content, err := os.ReadFile(filePath) + So(err, ShouldBeNil) + So(string(content), ShouldEqual, "initial appended") + }) + + Convey("Test writer with non-existent parent directory", func() { + filePath := path.Join(rootDir, "nonexistent", "test.txt") + writer, err := driver.Writer(filePath, false) + So(err, ShouldBeNil) + + defer writer.Close() + + // Write some content + _, err = writer.Write([]byte("test content")) + So(err, ShouldBeNil) + + // Close and verify + err = writer.Close() + So(err, ShouldBeNil) + + // Verify file was created + _, err = os.Stat(filePath) + So(err, ShouldBeNil) + }) + }) +} + +var ( + errMockCloseFailure = errors.New("close failed") + errMockSyncOnClose = errors.New("sync failed on close") + errMockSyncOnCommit = errors.New("sync failed on commit") +) + +// mockFile implements FileInterface for testing sync behavior. +type mockFile struct { + *os.File + syncCalled bool + syncError error + closeError error +} + +func (mf *mockFile) Sync() error { + mf.syncCalled = true + if mf.syncError != nil { + return mf.syncError + } + + return mf.File.Sync() +} + +func (mf *mockFile) Close() error { + if mf.closeError != nil { + return mf.closeError + } + + return mf.File.Close() +} + +func TestFileWriterClose(t *testing.T) { + Convey("Test fileWriter.Close() error handling", t, func() { + driver := local.New(true) + dir := t.TempDir() + filePath := filepath.Join(dir, "testfile") + + Convey("Test Close() with commit=true", func() { + // Create fileWriter with commit=true using the driver + writer, err := driver.Writer(filePath, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Close should succeed + err = writer.Close() + So(err, ShouldBeNil) + }) + + Convey("Test Close() with commit=true using mock to verify Sync()", func() { + // Create a real file first + realFile, err := os.Create(filePath) + So(err, ShouldBeNil) + + // Create a mock file wrapper + mockFile := &mockFile{File: realFile} + + // Create fileWriter with commit=true using the mock + writer := local.NewFileWriter(mockFile, 0, true) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Close should call Sync() and succeed + err = writer.Close() + So(err, ShouldBeNil) + + // Verify Sync() was called + So(mockFile.syncCalled, ShouldBeTrue) + }) + + Convey("Test Close() with commit=false", func() { + // Create a new test file + filePath2 := filepath.Join(dir, "testfile2") + writer, err := driver.Writer(filePath2, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Close should succeed + err = writer.Close() + So(err, ShouldBeNil) + }) + + Convey("Test Close() with commit=false using mock to verify Sync() NOT called", func() { + // Create a real file first + realFile, err := os.Create(filePath) + So(err, ShouldBeNil) + + // Create a mock file wrapper + mockFile := &mockFile{File: realFile} + + // Create fileWriter with commit=false using the mock + writer := local.NewFileWriter(mockFile, 0, false) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Close should NOT call Sync() and succeed + err = writer.Close() + So(err, ShouldBeNil) + + // Verify Sync() was NOT called + So(mockFile.syncCalled, ShouldBeFalse) + }) + + Convey("Test Close() on already closed file", func() { + // Create a test file + filePath3 := filepath.Join(dir, "testfile3") + writer, err := driver.Writer(filePath3, false) + So(err, ShouldBeNil) + + // Close once + err = writer.Close() + So(err, ShouldBeNil) + + // Close again should return error + err = writer.Close() + So(err, ShouldNotBeNil) + }) + + Convey("Test Close() with file.Close() error", func() { + // Create a real file first + realFile, err := os.Create(filePath) + So(err, ShouldBeNil) + + // Create a mock file wrapper with Close() error + mockFile := &mockFile{ + File: realFile, + closeError: errMockCloseFailure, + } + + // Create fileWriter using the mock + writer := local.NewFileWriter(mockFile, 0, false) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Close should return Close() error + err = writer.Close() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "close failed") + }) + }) +} + +func TestFileWriterCancel(t *testing.T) { + Convey("Test fileWriter.Cancel()", t, func() { + driver := local.New(true) + dir := t.TempDir() + filePath := filepath.Join(dir, "testfile") + + Convey("Test Cancel() on open file", func() { + // Create a test file + writer, err := driver.Writer(filePath, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Cancel should succeed and remove the file + err = writer.Cancel(context.Background()) + So(err, ShouldBeNil) + + // File should be removed + _, err = os.Stat(filePath) + So(err, ShouldNotBeNil) + }) + + Convey("Test Cancel() on already closed file", func() { + // Create a test file + filePath2 := filepath.Join(dir, "testfile2") + writer, err := driver.Writer(filePath2, false) + So(err, ShouldBeNil) + + // Close first + err = writer.Close() + So(err, ShouldBeNil) + + // Cancel on closed file should return error + err = writer.Cancel(context.Background()) + So(err, ShouldNotBeNil) + }) + }) +} + +func TestFileWriterCommit(t *testing.T) { + Convey("Test fileWriter.Commit()", t, func() { + driver := local.New(true) + dir := t.TempDir() + filePath := filepath.Join(dir, "testfile") + + Convey("Test Commit() on open file", func() { + // Create a test file + writer, err := driver.Writer(filePath, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Commit should succeed + err = writer.Commit(context.Background()) + So(err, ShouldBeNil) + + // File should still exist + _, err = os.Stat(filePath) + So(err, ShouldBeNil) + }) + + Convey("Test Commit() with commit=true using mock to verify Sync()", func() { + // Create a real file first + realFile, err := os.Create(filePath) + So(err, ShouldBeNil) + + // Create a mock file wrapper + mockFile := &mockFile{File: realFile} + + // Create fileWriter with commit=true using the mock + writer := local.NewFileWriter(mockFile, 0, true) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Commit should call Sync() and succeed + err = writer.Commit(context.Background()) + So(err, ShouldBeNil) + + // Verify Sync() was called + So(mockFile.syncCalled, ShouldBeTrue) + }) + + Convey("Test Commit() on already committed file", func() { + // Create a test file + filePath2 := filepath.Join(dir, "testfile2") + writer, err := driver.Writer(filePath2, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Commit first time + err = writer.Commit(context.Background()) + So(err, ShouldBeNil) + + // Commit again should return error + err = writer.Commit(context.Background()) + So(err, ShouldNotBeNil) + }) + + Convey("Test Commit() with commit=false using mock to verify Sync() NOT called", func() { + // Create a real file first + realFile, err := os.Create(filePath) + So(err, ShouldBeNil) + + // Create a mock file wrapper + mockFile := &mockFile{File: realFile} + + // Create fileWriter with commit=false using the mock + writer := local.NewFileWriter(mockFile, 0, false) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Commit should NOT call Sync() and succeed + err = writer.Commit(context.Background()) + So(err, ShouldBeNil) + + // Verify Sync() was NOT called + So(mockFile.syncCalled, ShouldBeFalse) + }) + + Convey("Test Commit() on already closed file", func() { + // Create a test file + filePath3 := filepath.Join(dir, "testfile3") + writer, err := driver.Writer(filePath3, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Close first + err = writer.Close() + So(err, ShouldBeNil) + + // Commit on closed file should return error + err = writer.Commit(context.Background()) + So(err, ShouldNotBeNil) + }) + + Convey("Test Commit() on already cancelled file", func() { + // Create a test file + filePath4 := filepath.Join(dir, "testfile4") + writer, err := driver.Writer(filePath4, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Cancel first + err = writer.Cancel(context.Background()) + So(err, ShouldBeNil) + + // Commit on cancelled file should return ErrFileAlreadyCancelled + err = writer.Commit(context.Background()) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrFileAlreadyCancelled) + }) + }) +} + +func TestFileWriterWrite(t *testing.T) { + Convey("Test fileWriter.Write()", t, func() { + dir := t.TempDir() + filePath := filepath.Join(dir, "testfile") + + Convey("Test Write() on open file", func() { + // Create a test file + driver := local.New(true) + writer, err := driver.Writer(filePath, false) + So(err, ShouldBeNil) + + // Write should succeed + n, err := writer.Write([]byte("test data")) + So(err, ShouldBeNil) + So(n, ShouldEqual, 9) + + // Size should be updated + So(writer.Size(), ShouldEqual, 9) + }) + + Convey("Test Sync() error handling on Close()", func() { + // Create a real file first + realFile, err := os.Create(filePath) + So(err, ShouldBeNil) + + // Create a mock file wrapper with Sync() error + mockFile := &mockFile{ + File: realFile, + syncError: errMockSyncOnClose, + } + + // Create fileWriter with commit=true using the mock + writer := local.NewFileWriter(mockFile, 0, true) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Close should return Sync() error + err = writer.Close() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "sync failed on close") + + // Verify Sync() was called + So(mockFile.syncCalled, ShouldBeTrue) + }) + + Convey("Test Write() on closed file", func() { + // Create a test file + filePath2 := filepath.Join(dir, "testfile2") + driver := local.New(true) + writer, err := driver.Writer(filePath2, false) + So(err, ShouldBeNil) + + // Close first + err = writer.Close() + So(err, ShouldBeNil) + + // Write on closed file should return error + _, err = writer.Write([]byte("test data")) + So(err, ShouldNotBeNil) + }) + + Convey("Test Write() on committed file", func() { + // Create a test file + filePath3 := filepath.Join(dir, "testfile3") + driver := local.New(true) + writer, err := driver.Writer(filePath3, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Commit + err = writer.Commit(context.Background()) + So(err, ShouldBeNil) + + // Write on committed file should return error + _, err = writer.Write([]byte("more data")) + So(err, ShouldNotBeNil) + }) + + Convey("Test Write() on cancelled file", func() { + // Create a test file + filePath4 := filepath.Join(dir, "testfile4") + driver := local.New(true) + writer, err := driver.Writer(filePath4, false) + So(err, ShouldBeNil) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Cancel + err = writer.Cancel(context.Background()) + So(err, ShouldBeNil) + + // Write on cancelled file should return ErrFileAlreadyCancelled + _, err = writer.Write([]byte("more data")) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrFileAlreadyCancelled) + }) + + Convey("Test Sync() error handling on Commit()", func() { + // Create a real file first + realFile, err := os.Create(filePath) + So(err, ShouldBeNil) + + // Create a mock file wrapper with Sync() error + mockFile := &mockFile{ + File: realFile, + syncError: errMockSyncOnCommit, + } + + // Create fileWriter with commit=true using the mock + writer := local.NewFileWriter(mockFile, 0, true) + + // Write some data + _, err = writer.Write([]byte("test data")) + So(err, ShouldBeNil) + + // Commit should return Sync() error + err = writer.Commit(context.Background()) + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "sync failed on commit") + + // Verify Sync() was called + So(mockFile.syncCalled, ShouldBeTrue) + }) + }) +} + +func TestSameFile(t *testing.T) { + Convey("Test SameFile operations", t, func() { + driver := local.New(true) + rootDir := t.TempDir() + + Convey("Test SameFile with identical paths", func() { + filePath := path.Join(rootDir, "test.txt") + err := os.WriteFile(filePath, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Same file should return true + result := driver.SameFile(filePath, filePath) + So(result, ShouldBeTrue) + }) + + Convey("Test SameFile with different files", func() { + filePath1 := path.Join(rootDir, "test1.txt") + filePath2 := path.Join(rootDir, "test2.txt") + + err := os.WriteFile(filePath1, []byte("test content 1"), 0o600) + So(err, ShouldBeNil) + err = os.WriteFile(filePath2, []byte("test content 2"), 0o600) + So(err, ShouldBeNil) + + // Different files should return false + result := driver.SameFile(filePath1, filePath2) + So(result, ShouldBeFalse) + }) + + Convey("Test SameFile with hard linked files", func() { + filePath1 := path.Join(rootDir, "test1.txt") + filePath2 := path.Join(rootDir, "test2.txt") + + err := os.WriteFile(filePath1, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Create hardlink + err = os.Link(filePath1, filePath2) + So(err, ShouldBeNil) + + // Hard linked files should return true + result := driver.SameFile(filePath1, filePath2) + So(result, ShouldBeTrue) + }) + + Convey("Test SameFile with non-existent first file", func() { + filePath1 := "/nonexistent1.txt" + filePath2 := path.Join(rootDir, "test.txt") + + err := os.WriteFile(filePath2, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Non-existent first file should return false + result := driver.SameFile(filePath1, filePath2) + So(result, ShouldBeFalse) + }) + + Convey("Test SameFile with non-existent second file", func() { + filePath1 := path.Join(rootDir, "test.txt") + filePath2 := "/nonexistent2.txt" + + err := os.WriteFile(filePath1, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Non-existent second file should return false + result := driver.SameFile(filePath1, filePath2) + So(result, ShouldBeFalse) + }) + + Convey("Test SameFile with both non-existent files", func() { + filePath1 := "/nonexistent1.txt" + filePath2 := "/nonexistent2.txt" + + // Both non-existent files should return false + result := driver.SameFile(filePath1, filePath2) + So(result, ShouldBeFalse) + }) + + Convey("Test SameFile with invalid path", func() { + filePath1 := string([]byte{0x00}) // Invalid path + filePath2 := path.Join(rootDir, "test.txt") + + err := os.WriteFile(filePath2, []byte("test content"), 0o600) + So(err, ShouldBeNil) + + // Invalid path should return false + result := driver.SameFile(filePath1, filePath2) + So(result, ShouldBeFalse) + }) + }) +} diff --git a/pkg/test/mocks/sync_remote_mock.go b/pkg/test/mocks/sync_remote_mock.go index 3fb5d01c..db3c1421 100644 --- a/pkg/test/mocks/sync_remote_mock.go +++ b/pkg/test/mocks/sync_remote_mock.go @@ -3,54 +3,30 @@ package mocks import ( "context" - "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" + "github.com/regclient/regclient/types/ref" ) -type SyncRemote struct { - // Get temporary ImageReference, is used by functions in containers/image package - GetImageReferenceFn func(repo string, tag string) (types.ImageReference, error) - - // Get local oci layout context, is used by functions in containers/image package - GetContextFn func() *types.SystemContext - - // Get a list of repos (catalog) - GetRepositoriesFn func(ctx context.Context) ([]string, error) - - // Get a list of tags given a repo - GetRepoTagsFn func(repo string) ([]string, error) - - GetDockerRemoteRepoFn func(repo string) string - - // Get manifest content, mediaType, digest given an ImageReference - GetManifestContentFn func(imageReference types.ImageReference) ([]byte, string, digest.Digest, error) +type SyncRemoteMock struct { + // Methods required by sync Remote interface. + GetHostNameFn func() string + GetRepositoriesFn func(ctx context.Context) ([]string, error) + GetTagsFn func(ctx context.Context, repo string) ([]string, error) + GetOCIDigestFn func(ctx context.Context, repo, tag string) (digest.Digest, digest.Digest, bool, error) + GetDigestFn func(ctx context.Context, repo, tag string) (digest.Digest, error) + GetImageReferenceFn func(repo string, tag string) (ref.Ref, error) } -func (remote SyncRemote) GetDockerRemoteRepo(repo string) string { - if remote.GetDockerRemoteRepoFn != nil { - return remote.GetDockerRemoteRepoFn(repo) +// Methods required by sync Remote interface. +func (remote SyncRemoteMock) GetHostName() string { + if remote.GetHostNameFn != nil { + return remote.GetHostNameFn() } - return "" + return "mock-host" } -func (remote SyncRemote) GetImageReference(repo string, tag string) (types.ImageReference, error) { - if remote.GetImageReferenceFn != nil { - return remote.GetImageReferenceFn(repo, tag) - } - - return nil, nil //nolint:nilnil -} - -func (remote SyncRemote) GetContext() *types.SystemContext { - if remote.GetContextFn != nil { - return remote.GetContextFn() - } - - return nil -} - -func (remote SyncRemote) GetRepositories(ctx context.Context) ([]string, error) { +func (remote SyncRemoteMock) GetRepositories(ctx context.Context) ([]string, error) { if remote.GetRepositoriesFn != nil { return remote.GetRepositoriesFn(ctx) } @@ -58,23 +34,77 @@ func (remote SyncRemote) GetRepositories(ctx context.Context) ([]string, error) return []string{}, nil } -func (remote SyncRemote) GetRepoTags(repo string) ([]string, error) { - if remote.GetRepoTagsFn != nil { - return remote.GetRepoTagsFn(repo) +func (remote SyncRemoteMock) GetTags(ctx context.Context, repo string) ([]string, error) { + if remote.GetTagsFn != nil { + return remote.GetTagsFn(ctx, repo) } return []string{}, nil } -func (remote SyncRemote) GetManifestContent(imageReference types.ImageReference) ( - []byte, string, digest.Digest, error, +func (remote SyncRemoteMock) GetOCIDigest(ctx context.Context, repo, tag string) ( + digest.Digest, digest.Digest, bool, error, ) { - if remote.GetManifestContentFn != nil { - return remote.GetManifestContentFn(imageReference) + if remote.GetOCIDigestFn != nil { + return remote.GetOCIDigestFn(ctx, repo, tag) } - return nil, "", "", nil + return digest.Digest("sha256:abc123"), digest.Digest("sha256:def456"), false, nil } -func (remote SyncRemote) SetUpstreamAuthConfig(username, password string) { +func (remote SyncRemoteMock) GetDigest(ctx context.Context, repo, tag string) (digest.Digest, error) { + if remote.GetDigestFn != nil { + return remote.GetDigestFn(ctx, repo, tag) + } + + return digest.Digest("sha256:abc123"), nil +} + +func (remote SyncRemoteMock) GetImageReference(repo string, tag string) (ref.Ref, error) { + if remote.GetImageReferenceFn != nil { + return remote.GetImageReferenceFn(repo, tag) + } + + return ref.New("mock-registry/" + repo + ":" + tag) +} + +type SyncDestinationMock struct { + // Methods required by sync Destination interface. + GetImageReferenceFn func(repo string, tag string) (ref.Ref, error) + CanSkipImageFn func(repo string, tag string, digest digest.Digest) (bool, error) + CommitAllFn func(repo string, imageReference ref.Ref) error + CleanupImageFn func(imageReference ref.Ref, repo string) error +} + +// Methods required by sync Destination interface. +func (dest SyncDestinationMock) GetImageReference(repo string, tag string) (ref.Ref, error) { + if dest.GetImageReferenceFn != nil { + return dest.GetImageReferenceFn(repo, tag) + } + + return ref.New("mock-local/" + repo + ":" + tag) +} + +func (dest SyncDestinationMock) CanSkipImage(repo string, tag string, digest digest.Digest) (bool, error) { + if dest.CanSkipImageFn != nil { + return dest.CanSkipImageFn(repo, tag, digest) + } + + return false, nil +} + +func (dest SyncDestinationMock) CommitAll(repo string, imageReference ref.Ref) error { + if dest.CommitAllFn != nil { + return dest.CommitAllFn(repo, imageReference) + } + + return nil +} + +func (dest SyncDestinationMock) CleanupImage(imageReference ref.Ref, repo string) error { + if dest.CleanupImageFn != nil { + return dest.CleanupImageFn(imageReference, repo) + } + + return nil }