Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
rchincha marked this conversation as resolved.
github.com/dchest/siphash v1.2.3
github.com/didip/tollbooth/v7 v7.0.2
github.com/distribution/distribution/v3 v3.0.0
Expand Down Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
130 changes: 130 additions & 0 deletions pkg/extensions/search/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"errors"
"fmt"
"sort"
"testing"
"time"

Expand Down Expand Up @@ -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() {
Expand Down
115 changes: 115 additions & 0 deletions pkg/extensions/sync/sync_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 14 additions & 2 deletions pkg/storage/local/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
Loading
Loading