diff --git a/.github/.codecov.yml b/.github/.codecov.yml index 38ac600b..fae45030 100644 --- a/.github/.codecov.yml +++ b/.github/.codecov.yml @@ -15,7 +15,7 @@ coverage: status: project: default: - target: 75% + target: 80% if_ci_failed: error patch: default: diff --git a/content/graph_test.go b/content/graph_test.go index d90dc81a..d656f553 100644 --- a/content/graph_test.go +++ b/content/graph_test.go @@ -19,12 +19,14 @@ import ( "bytes" "context" "encoding/json" + "errors" "reflect" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/cas" "oras.land/oras-go/v2/internal/docker" "oras.land/oras-go/v2/internal/spec" @@ -389,3 +391,100 @@ func TestSuccessors_otherMediaType(t *testing.T) { t.Errorf("Successors() = %v, want nil", got) } } + +func TestSuccessors_ErrNotFound(t *testing.T) { + tests := []struct { + name string + desc ocispec.Descriptor + }{ + { + name: "docker manifest", + desc: ocispec.Descriptor{ + MediaType: docker.MediaTypeManifest, + }, + }, + { + name: "image manifest", + desc: ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + }, + }, + { + name: "docker manifest list", + desc: ocispec.Descriptor{ + MediaType: docker.MediaTypeManifestList, + }, + }, + { + name: "image index", + desc: ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageIndex, + }, + }, + { + name: "artifact manifest", + desc: ocispec.Descriptor{ + MediaType: spec.MediaTypeArtifactManifest, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + fetcher := cas.NewMemory() + if _, err := content.Successors(ctx, fetcher, tt.desc); !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("Successors() error = %v, wantErr = %v", err, errdef.ErrNotFound) + } + }) + } +} + +func TestSuccessors_UnmarshalError(t *testing.T) { + tests := []struct { + name string + mediaType string + }{ + { + name: "docker manifest", + mediaType: docker.MediaTypeManifest, + }, + { + name: "image manifest", + mediaType: ocispec.MediaTypeImageManifest, + }, + { + name: "docker manifest list", + mediaType: docker.MediaTypeManifestList, + }, + { + name: "image index", + mediaType: ocispec.MediaTypeImageIndex, + }, + { + name: "artifact manifest", + mediaType: spec.MediaTypeArtifactManifest, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + fetcher := cas.NewMemory() + + // prepare test content + data := "invalid json" + desc := ocispec.Descriptor{ + MediaType: tt.mediaType, + Digest: digest.FromString(data), + Size: int64(len(data)), + } + if err := fetcher.Push(ctx, desc, bytes.NewReader([]byte(data))); err != nil { + t.Fatalf("failed to push test content to fetcher: %v", err) + } + + // test Successors + if _, err := content.Successors(ctx, fetcher, desc); err == nil { + t.Error("Successors() error = nil, wantErr = true") + } + }) + } +} diff --git a/content/limitedstorage_test.go b/content/limitedstorage_test.go new file mode 100644 index 00000000..7423bf7c --- /dev/null +++ b/content/limitedstorage_test.go @@ -0,0 +1,116 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content_test + +import ( + "bytes" + "context" + "errors" + "io" + "reflect" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/cas" +) + +func TestLimitedStorage_Push(t *testing.T) { + data := []byte("test") + size := int64(len(data)) + dgst := digest.FromBytes(data) + mediaType := "application/vnd.test" + + tests := []struct { + name string + desc ocispec.Descriptor + limit int64 + wantErr error + }{ + { + name: "descriptor size matches actual size and is within limit", + desc: ocispec.Descriptor{ + MediaType: mediaType, + Size: size, + Digest: dgst, + }, + limit: size, + wantErr: nil, + }, + { + name: "descriptor size matches actual size but exeeds limit", + desc: ocispec.Descriptor{ + MediaType: mediaType, + Size: size, + Digest: dgst, + }, + limit: size - 1, + wantErr: errdef.ErrSizeExceedsLimit, + }, + { + name: "descriptor size mismatches actual size and is within limit", + desc: ocispec.Descriptor{ + MediaType: mediaType, + Size: size - 1, + Digest: dgst, + }, + limit: size, + wantErr: content.ErrMismatchedDigest, + }, + { + name: "descriptor size mismatches actual size and exceeds limit", + desc: ocispec.Descriptor{ + MediaType: mediaType, + Size: size + 1, + Digest: dgst, + }, + limit: size, + wantErr: errdef.ErrSizeExceedsLimit, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + ls := content.LimitStorage(cas.NewMemory(), tt.limit) + + // test Push + err := ls.Push(ctx, tt.desc, bytes.NewReader(data)) + if err != nil { + if errors.Is(err, tt.wantErr) { + return + } + t.Errorf("LimitedStorage.Push() error = %v, wantErr %v", err, tt.wantErr) + } + + // verify + rc, err := ls.Storage.Fetch(ctx, tt.desc) + if err != nil { + t.Fatalf("LimitedStorage.Fetch() error = %v", err) + } + defer rc.Close() + + got, err := io.ReadAll(rc) + if err != nil { + t.Fatalf("io.ReadAll() error = %v", err) + } + if !reflect.DeepEqual(got, data) { + t.Errorf("LimitedStorage.Fetch() = %v, want %v", got, data) + } + }) + } +} diff --git a/content/storage_test.go b/content/storage_test.go new file mode 100644 index 00000000..8a27722c --- /dev/null +++ b/content/storage_test.go @@ -0,0 +1,58 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content + +import ( + "bytes" + "context" + "errors" + "io" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestFetcherFunc_Fetch(t *testing.T) { + data := []byte("test content") + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(data), + Size: int64(len(data)), + } + + fetcherFunc := FetcherFunc(func(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + if target.Digest != desc.Digest { + return nil, errors.New("content not found") + } + return io.NopCloser(bytes.NewReader(data)), nil + }) + + ctx := context.Background() + rc, err := fetcherFunc.Fetch(ctx, desc) + if err != nil { + t.Fatalf("FetcherFunc.Fetch() error = %v", err) + } + defer rc.Close() + + got, err := io.ReadAll(rc) + if err != nil { + t.Fatalf("FetcherFunc.Fetch().Read() error = %v", err) + } + if !bytes.Equal(got, data) { + t.Errorf("FetcherFunc.Fetch() = %v, want %v", got, data) + } +} diff --git a/copy_test.go b/copy_test.go index c9e9bfcd..a9b3fab3 100644 --- a/copy_test.go +++ b/copy_test.go @@ -62,6 +62,19 @@ func (t *storageTracker) Exists(ctx context.Context, target ocispec.Descriptor) return t.Storage.Exists(ctx, target) } +type mockReferencePusher struct { + oras.Target + pushReference int64 +} + +func (p *mockReferencePusher) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { + atomic.AddInt64(&p.pushReference, 1) + if err := p.Target.Push(ctx, expected, content); err != nil { + return err + } + return p.Target.Tag(ctx, expected, reference) +} + func TestCopy_FullCopy(t *testing.T) { src := memory.New() dst := memory.New() @@ -2210,3 +2223,129 @@ func TestCopyGraph_ForeignLayers_Mixed(t *testing.T) { t.Errorf("count(dst.Exists()) = %v, want %v", got, want) } } + +func TestCopy_ReferencePusher(t *testing.T) { + ctx := context.Background() + src := memory.New() + dst := &mockReferencePusher{Target: memory.New()} + + // generate test content + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + generateManifest(descs[0], descs[1:3]...) // Blob 3 + + for i := range blobs { + err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + root := descs[len(descs)-1] + tag := "latest" + if err := src.Tag(ctx, root, tag); err != nil { + t.Fatalf("failed to tag manifest: %v", err) + } + + // test copying to a reference pusher + var preCopyCount int64 + var postCopyCount int64 + opts := oras.CopyOptions{ + CopyGraphOptions: oras.CopyGraphOptions{ + PreCopy: func(ctx context.Context, desc ocispec.Descriptor) error { + atomic.AddInt64(&preCopyCount, 1) + return nil + }, + PostCopy: func(ctx context.Context, desc ocispec.Descriptor) error { + atomic.AddInt64(&postCopyCount, 1) + return nil + }, + }, + } + gotManifestDesc, err := oras.Copy(ctx, src, tag, dst, tag, opts) + if err != nil { + t.Fatalf("Copy() error = %v, wantErr %v", err, false) + } + if !reflect.DeepEqual(gotManifestDesc, root) { + t.Errorf("Copy() = %v, want %v", gotManifestDesc, root) + } + + // verify contents + for i, desc := range descs { + exists, err := dst.Exists(ctx, desc) + if err != nil { + t.Fatalf("dst.Exists(%d) error = %v", i, err) + } + if !exists { + t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true) + } + } + + // verify tag + gotDesc, err := dst.Resolve(ctx, tag) + if err != nil { + t.Fatal("dst.Resolve() error =", err) + } + if !reflect.DeepEqual(gotDesc, root) { + t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root) + } + + // verify API counts + if got, want := preCopyCount, int64(4); got != want { + t.Errorf("count(PreCopy()) = %v, want %v", got, want) + } + if got, want := postCopyCount, int64(4); got != want { + t.Errorf("count(PostCopy()) = %v, want %v", got, want) + } +} + +func TestCopy_Error(t *testing.T) { + t.Run("src target is nil", func(t *testing.T) { + ctx := context.Background() + dst := memory.New() + if _, err := oras.Copy(ctx, nil, "", dst, "", oras.DefaultCopyOptions); err == nil { + t.Errorf("Copy() error = %v, wantErr %v", err, true) + } + }) + + t.Run("dst target is nil", func(t *testing.T) { + ctx := context.Background() + src := memory.New() + if _, err := oras.Copy(ctx, src, "", nil, "", oras.DefaultCopyOptions); err == nil { + t.Errorf("Copy() error = %v, wantErr %v", err, true) + } + }) + + t.Run("failed to resolve reference", func(t *testing.T) { + ctx := context.Background() + src := memory.New() + dst := memory.New() + if _, err := oras.Copy(ctx, src, "whatever", dst, "", oras.DefaultCopyOptions); err == nil { + t.Errorf("Copy() error = %v, wantErr %v", err, true) + } + }) +} diff --git a/extendedcopy_test.go b/extendedcopy_test.go index 2a4c18ad..af73349a 100644 --- a/extendedcopy_test.go +++ b/extendedcopy_test.go @@ -1829,3 +1829,21 @@ func TestExtendedCopyGraph_FilterArtifactTypeAndAnnotationWithMultipleRegex_Refe uncopiedIndice := []int{1, 2, 4, 5, 6, 8, 9} verifyCopy(dst, copiedIndice, uncopiedIndice) } + +func TestExtededCopy_Error(t *testing.T) { + t.Run("src target is nil", func(t *testing.T) { + ctx := context.Background() + dst := memory.New() + if _, err := oras.ExtendedCopy(ctx, nil, "", dst, "", oras.DefaultExtendedCopyOptions); err == nil { + t.Errorf("Copy() error = %v, wantErr %v", err, true) + } + }) + + t.Run("dst target is nil", func(t *testing.T) { + ctx := context.Background() + src := memory.New() + if _, err := oras.ExtendedCopy(ctx, src, "", nil, "", oras.DefaultExtendedCopyOptions); err == nil { + t.Errorf("Copy() error = %v, wantErr %v", err, true) + } + }) +} diff --git a/internal/fs/tarfs/tarfs_test.go b/internal/fs/tarfs/tarfs_test.go index e80df155..d05edfee 100644 --- a/internal/fs/tarfs/tarfs_test.go +++ b/internal/fs/tarfs/tarfs_test.go @@ -82,6 +82,14 @@ func TestTarFS_Open_Success(t *testing.T) { t.Fatalf("TarFS.Open(%s) error = %v, wantErr %v", name, err, nil) } + fi, err := f.Stat() + if err != nil { + t.Fatalf("failed to get FileInfo for %s: %v", name, err) + } + if got, want := fi.Name(), filepath.Base(name); got != want { + t.Errorf("FileInfo.Name() = %v, want %v", got, want) + } + got, err := io.ReadAll(f) if err != nil { t.Fatalf("failed to read %s: %v", name, err) @@ -397,3 +405,19 @@ func TestTarFS_Stat_Unsupported(t *testing.T) { }) } } + +func TestTarFs_New_Error(t *testing.T) { + t.Run("not existing path", func(t *testing.T) { + _, err := New("testdata/ghost.tar") + if err == nil { + t.Error("New() error = nil, wantErr = true") + } + }) + + t.Run("invalid file path", func(t *testing.T) { + _, err := New(string([]byte{0x00})) + if err == nil { + t.Error("New() error = nil, wantErr = true") + } + }) +} diff --git a/internal/manifestutil/parser_test.go b/internal/manifestutil/parser_test.go index 487e11dc..f11f2445 100644 --- a/internal/manifestutil/parser_test.go +++ b/internal/manifestutil/parser_test.go @@ -53,14 +53,6 @@ func (s *testStorage) Fetch(ctx context.Context, target ocispec.Descriptor) (io. return s.store.Fetch(ctx, target) } -// func (s *testStorage) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { -// return s.store.Exists(ctx, target) -// } - -// func (s *testStorage) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { -// return s.store.Predecessors(ctx, node) -// } - func TestConfig(t *testing.T) { storage := cas.NewMemory() @@ -138,6 +130,35 @@ func TestConfig(t *testing.T) { } } +func TestConfig_ErrorPath(t *testing.T) { + data := []byte("data") + desc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(data), + Size: int64(len(data)), + } + + t.Run("Fetch error", func(t *testing.T) { + storage := cas.NewMemory() + ctx := context.Background() + if _, err := Config(ctx, storage, desc); err == nil { + t.Error("Config() error = nil, wantErr = true") + } + }) + + t.Run("Unmarshal error", func(t *testing.T) { + storage := cas.NewMemory() + ctx := context.Background() + if err := storage.Push(ctx, desc, bytes.NewReader(data)); err != nil { + t.Fatalf("failed to push test content to src: %v", err) + } + _, err := Config(ctx, storage, desc) + if err == nil { + t.Error("Config() error = nil, wantErr = true") + } + }) +} + func TestManifests(t *testing.T) { storage := cas.NewMemory() @@ -234,6 +255,35 @@ func TestManifests(t *testing.T) { } } +func TestManifests_ErrorPath(t *testing.T) { + data := []byte("data") + desc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageIndex, + Digest: digest.FromBytes(data), + Size: int64(len(data)), + } + + t.Run("Fetch error", func(t *testing.T) { + storage := cas.NewMemory() + ctx := context.Background() + if _, err := Manifests(ctx, storage, desc); err == nil { + t.Error("Manifests() error = nil, wantErr = true") + } + }) + + t.Run("Unmarshal error", func(t *testing.T) { + storage := cas.NewMemory() + ctx := context.Background() + if err := storage.Push(ctx, desc, bytes.NewReader(data)); err != nil { + t.Fatalf("failed to push test content to src: %v", err) + } + _, err := Manifests(ctx, storage, desc) + if err == nil { + t.Error("Manifests() error = nil, wantErr = true") + } + }) +} + func TestSubject(t *testing.T) { storage := cas.NewMemory() @@ -289,65 +339,87 @@ func TestSubject(t *testing.T) { } func TestSubject_ErrorPath(t *testing.T) { - s := testStorage{ - store: memory.New(), - badFetch: set.New[digest.Digest](), - } - ctx := context.Background() - // generate test content - var blobs [][]byte - var descs []ocispec.Descriptor - appendBlob := func(mediaType string, artifactType string, blob []byte) { - blobs = append(blobs, blob) - descs = append(descs, ocispec.Descriptor{ - MediaType: mediaType, - ArtifactType: artifactType, - Annotations: map[string]string{"test": "content"}, - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), - }) - } - generateImageManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) { - manifest := ocispec.Manifest{ - MediaType: ocispec.MediaTypeImageManifest, - Config: config, - Subject: subject, - Layers: layers, - Annotations: map[string]string{"test": "content"}, + t.Run("Fetch error", func(t *testing.T) { + s := testStorage{ + store: memory.New(), + badFetch: set.New[digest.Digest](), } - manifestJSON, err := json.Marshal(manifest) - if err != nil { - t.Fatal(err) + ctx := context.Background() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, artifactType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + ArtifactType: artifactType, + Annotations: map[string]string{"test": "content"}, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) } - appendBlob(ocispec.MediaTypeImageManifest, manifest.Config.MediaType, manifestJSON) - } - appendBlob("image manifest", "image config", []byte("config")) // Blob 0 - appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("foo")) // Blob 1 - appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("bar")) // Blob 2 - appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("hello")) // Blob 3 - generateImageManifest(descs[0], nil, descs[1]) // Blob 4 - generateImageManifest(descs[0], &descs[4], descs[2]) // Blob 5 - s.badFetch.Add(descs[5].Digest) - - eg, egCtx := errgroup.WithContext(ctx) - for i := range blobs { - eg.Go(func(i int) func() error { - return func() error { - err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i])) - if err != nil { - return fmt.Errorf("failed to push test content to src: %d: %v", i, err) - } - return nil + generateImageManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Config: config, + Subject: subject, + Layers: layers, + Annotations: map[string]string{"test": "content"}, } - }(i)) - } - if err := eg.Wait(); err != nil { - t.Fatal(err) - } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifest.Config.MediaType, manifestJSON) + } + appendBlob("image manifest", "image config", []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("bar")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("hello")) // Blob 3 + generateImageManifest(descs[0], nil, descs[1]) // Blob 4 + generateImageManifest(descs[0], &descs[4], descs[2]) // Blob 5 + s.badFetch.Add(descs[5].Digest) - _, err := Subject(ctx, &s, descs[5]) - if !errors.Is(err, ErrBadFetch) { - t.Errorf("Store.Referrers() error = %v, want %v", err, ErrBadFetch) - } + eg, egCtx := errgroup.WithContext(ctx) + for i := range blobs { + eg.Go(func(i int) func() error { + return func() error { + err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + return fmt.Errorf("failed to push test content to src: %d: %v", i, err) + } + return nil + } + }(i)) + } + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + // test fetch error + if _, err := Subject(ctx, &s, descs[5]); !errors.Is(err, ErrBadFetch) { + t.Errorf("Subject() error = %v, want %v", err, ErrBadFetch) + } + }) + + t.Run("Unmarshal error", func(t *testing.T) { + data := []byte("data") + desc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageIndex, + Digest: digest.FromBytes(data), + Size: int64(len(data)), + } + + storage := cas.NewMemory() + ctx := context.Background() + if err := storage.Push(ctx, desc, bytes.NewReader(data)); err != nil { + t.Fatalf("failed to push test content to src: %v", err) + } + _, err := Subject(ctx, storage, desc) + if err == nil { + t.Error("Subject() error = nil, wantErr = true") + } + }) } diff --git a/registry/reference_test.go b/registry/reference_test.go index 414b33bb..17518ffa 100644 --- a/registry/reference_test.go +++ b/registry/reference_test.go @@ -152,3 +152,201 @@ func TestParseReferenceUglies(t *testing.T) { }) } } + +func TestReference_Validate(t *testing.T) { + tests := []struct { + name string + reference Reference + wantErr bool + }{ + { + name: "valid reference with tag", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: "v1.0.0", + }, + wantErr: false, + }, + { + name: "valid reference with digest", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: ValidDigest, + }, + wantErr: false, + }, + { + name: "valid reference without tag or digest", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + }, + wantErr: false, + }, + { + name: "invalid registry", + reference: Reference{ + Registry: "invalid registry", + Repository: "hello-world", + Reference: "v1.0.0", + }, + wantErr: true, + }, + { + name: "invalid repository", + reference: Reference{ + Registry: "registry.example.com", + Repository: "INVALID_REPO", + Reference: "v1.0.0", + }, + wantErr: true, + }, + { + name: "invalid tag", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: "INVALID_TAG!", + }, + wantErr: true, + }, + { + name: "invalid digest", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: InvalidDigest, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.reference.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Reference.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestReference_Host(t *testing.T) { + tests := []struct { + name string + registry string + want string + }{ + { + name: "docker.io", + registry: "docker.io", + want: "registry-1.docker.io", + }, + { + name: "other registry", + registry: "registry.example.com", + want: "registry.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref := Reference{ + Registry: tt.registry, + } + if got := ref.Host(); got != tt.want { + t.Errorf("Reference.Host() = %v, want %v", got, tt.want) + } + }) + } +} +func TestReference_ReferenceOrDefault(t *testing.T) { + tests := []struct { + name string + reference Reference + want string + }{ + { + name: "empty reference", + reference: Reference{ + Reference: "", + }, + want: "latest", + }, + { + name: "non-empty reference", + reference: Reference{ + Reference: "v1.0.0", + }, + want: "v1.0.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.reference.ReferenceOrDefault(); got != tt.want { + t.Errorf("Reference.ReferenceOrDefault() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestReference_String(t *testing.T) { + tests := []struct { + name string + reference Reference + want string + }{ + { + name: "only registry", + reference: Reference{ + Registry: "registry.example.com", + }, + want: "registry.example.com", + }, + { + name: "registry and repository", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + }, + want: "registry.example.com/hello-world", + }, + { + name: "registry, repository and tag", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: "v1.0.0", + }, + want: "registry.example.com/hello-world:v1.0.0", + }, + { + name: "registry, repository and digest", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: ValidDigest, + }, + want: fmt.Sprintf("registry.example.com/hello-world@%s", ValidDigest), + }, + { + name: "registry, repository and invalid digest", + reference: Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: InvalidDigest, + }, + want: fmt.Sprintf("registry.example.com/hello-world:%s", InvalidDigest), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.reference.String(); got != tt.want { + t.Errorf("Reference.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/registry/remote/credentials/trace/trace_test.go b/registry/remote/credentials/trace/trace_test.go new file mode 100644 index 00000000..b55ebfec --- /dev/null +++ b/registry/remote/credentials/trace/trace_test.go @@ -0,0 +1,134 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package trace + +import ( + "context" + "testing" +) + +func TestWithExecutableTrace(t *testing.T) { + ctx := context.Background() + + t.Run("trace is nil", func(t *testing.T) { + newCtx := WithExecutableTrace(ctx, nil) + if newCtx != ctx { + t.Errorf("expected context to be unchanged when trace is nil") + } + }) + + t.Run("adding a new trace", func(t *testing.T) { + trace := &ExecutableTrace{} + newCtx := WithExecutableTrace(ctx, trace) + if newCtx == ctx { + t.Errorf("expected context to be changed when adding a new trace") + } + if got := ContextExecutableTrace(newCtx); got != trace { + t.Errorf("expected trace to be added to context") + } + }) + + t.Run("adding a new emtpy trace with existing trace", func(t *testing.T) { + oldExecStartCount := 0 + oldExecDoneCount := 0 + oldTrace := &ExecutableTrace{ + ExecuteStart: func(executableName, action string) { + oldExecStartCount++ + }, + ExecuteDone: func(executableName, action string, err error) { + oldExecDoneCount++ + }, + } + ctx = WithExecutableTrace(ctx, oldTrace) + + newTrace := &ExecutableTrace{} + newCtx := WithExecutableTrace(ctx, newTrace) + + // verify new trace + gotTrace1 := ContextExecutableTrace(newCtx) + if gotTrace1 != newTrace { + t.Error("expected new trace to be added to context") + } + + // verify old trace + gotTrace2 := ContextExecutableTrace(newCtx) + if gotTrace2 == oldTrace { + t.Errorf("expected old trace to be composed with new trace") + } + gotTrace2.ExecuteStart("oldExec", "oldAction") + if want := 1; oldExecStartCount != want { + t.Errorf("oldExecStartCount: got %d, expected: %v", oldExecStartCount, want) + } + gotTrace2.ExecuteDone("oldExec", "oldAction", nil) + if want := 1; oldExecDoneCount != want { + t.Errorf("oldExecDoneCount: got %d, expected: %v", oldExecDoneCount, want) + } + }) + + t.Run("adding a new trace with existing trace", func(t *testing.T) { + oldExecStartCount := 0 + oldExecDoneCount := 0 + oldTrace := &ExecutableTrace{ + ExecuteStart: func(executableName, action string) { + oldExecStartCount++ + }, + ExecuteDone: func(executableName, action string, err error) { + oldExecDoneCount++ + }, + } + ctx = WithExecutableTrace(ctx, oldTrace) + + newExecStartCount := 0 + newExecDoneCount := 0 + newTrace := &ExecutableTrace{ + ExecuteStart: func(executableName, action string) { + newExecStartCount++ + }, + ExecuteDone: func(executableName, action string, err error) { + newExecDoneCount++ + }, + } + newCtx := WithExecutableTrace(ctx, newTrace) + + // verify new trace + gotTrace1 := ContextExecutableTrace(newCtx) + if gotTrace1 != newTrace { + t.Error("expected new trace to be added to context") + } + gotTrace1.ExecuteStart("newExec", "newAction") + if want := 1; newExecStartCount != want { + t.Errorf("newExecStartCount: got %d, expected: %v", newExecStartCount, want) + } + gotTrace1.ExecuteDone("newExec", "newAction", nil) + if want := 1; newExecDoneCount != want { + t.Errorf("newExecDoneCount: got %d, expected: %v", newExecDoneCount, want) + } + + // verify old trace + gotTrace2 := ContextExecutableTrace(newCtx) + if gotTrace2 == oldTrace { + t.Errorf("expected old trace to be composed with new trace") + } + gotTrace2.ExecuteStart("oldExec", "oldAction") + if want := 2; oldExecStartCount != want { + t.Errorf("oldExecStartCount: got %d, expected: %v", oldExecStartCount, want) + } + gotTrace2.ExecuteDone("oldExec", "oldAction", nil) + if want := 2; oldExecDoneCount != want { + t.Errorf("oldExecDoneCount: got %d, expected: %v", oldExecDoneCount, want) + } + }) +} diff --git a/registry/remote/registry_test.go b/registry/remote/registry_test.go index 8f91c4e1..1a3e4112 100644 --- a/registry/remote/registry_test.go +++ b/registry/remote/registry_test.go @@ -67,41 +67,83 @@ func TestRegistry_TLS(t *testing.T) { } func TestRegistry_Ping(t *testing.T) { - v2Implemented := true - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet || r.URL.Path != "/v2/" { - t.Errorf("unexpected access: %s %s", r.Method, r.URL) - w.WriteHeader(http.StatusNotFound) - return + t.Run("Ping success", func(t *testing.T) { + v2Implemented := true + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/v2/" { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + return + } + + if v2Implemented { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) } - if v2Implemented { - w.WriteHeader(http.StatusOK) - } else { - w.WriteHeader(http.StatusNotFound) + reg, err := NewRegistry(uri.Host) + if err != nil { + t.Fatalf("NewRegistry() error = %v", err) } - })) - defer ts.Close() - uri, err := url.Parse(ts.URL) - if err != nil { - t.Fatalf("invalid test http server: %v", err) - } + reg.PlainHTTP = true - reg, err := NewRegistry(uri.Host) - if err != nil { - t.Fatalf("NewRegistry() error = %v", err) - } - reg.PlainHTTP = true + ctx := context.Background() + if err := reg.Ping(ctx); err != nil { + t.Errorf("Registry.Ping() error = %v", err) + } - ctx := context.Background() - if err := reg.Ping(ctx); err != nil { - t.Errorf("Registry.Ping() error = %v", err) - } + v2Implemented = false + if err := reg.Ping(ctx); err == nil { + t.Errorf("Registry.Ping() error = %v, wantErr %v", err, errdef.ErrNotFound) + } + }) - v2Implemented = false - if err := reg.Ping(ctx); err == nil { - t.Errorf("Registry.Ping() error = %v, wantErr %v", err, errdef.ErrNotFound) - } + t.Run("Ping failed for server error", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/v2/" { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusInternalServerError) + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + reg, err := NewRegistry(uri.Host) + if err != nil { + t.Fatalf("NewRegistry() error = %v", err) + } + reg.PlainHTTP = true + + ctx := context.Background() + if err := reg.Ping(ctx); err == nil { + t.Error("Registry.Ping() error = nil, wantErr = true") + } + }) + + t.Run("Ping failed for connection error", func(t *testing.T) { + reg, err := NewRegistry("localhost:9876") + if err != nil { + t.Fatalf("NewRegistry() error = %v", err) + } + + ctx := context.Background() + if err := reg.Ping(ctx); err == nil { + t.Error("Registry.Ping() error = nil, wantErr = true") + } + }) } func TestRegistry_Repositories(t *testing.T) { @@ -380,6 +422,38 @@ func TestRegistry_do(t *testing.T) { } } +func TestNewRegistry(t *testing.T) { + tests := []struct { + name string + regName string + wantErr bool + }{ + { + name: "Valid registry name", + regName: "localhost:5000", + wantErr: false, + }, + { + name: "Invalid registry name", + regName: "invalid registry name", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewRegistry(tt.regName) + if (err != nil) != tt.wantErr { + t.Errorf("NewRegistry() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got == nil { + t.Errorf("NewRegistry() = %v, want non-nil", got) + } + }) + } +} + // indexOf returns the index of an element within a slice func indexOf(element string, data []string) int { for ind, val := range data { diff --git a/registry/remote/repository_test.go b/registry/remote/repository_test.go index a3dc77f9..fbc88f4f 100644 --- a/registry/remote/repository_test.go +++ b/registry/remote/repository_test.go @@ -57,6 +57,16 @@ type testIOStruct struct { const theAmazingBanClan = "Ban Gu, Ban Chao, Ban Zhao" const theAmazingBanDigest = "b526a4f2be963a2f9b0990c001255669eab8a254ab1a6e3f84f1820212ac7078" +type badReader struct{} + +func (r *badReader) Read(p []byte) (n int, err error) { + return 0, errors.New("read error") +} + +func (r *badReader) Close() error { + return nil +} + // The following truth table aims to cover the expected GET/HEAD request outcome // for all possible permutations of the client/server "containing a digest", for // both Manifests and Blobs. Where the results between the two differ, the index @@ -120,6 +130,45 @@ func getTestIOStructMapForGetDescriptorClass() map[string]testIOStruct { } } +func TestNewRepository(t *testing.T) { + tests := []struct { + name string + reference string + wantErr error + }{ + { + name: "valid reference", + reference: "localhost:5000/hello-world", + wantErr: nil, + }, + { + name: "invalid reference", + reference: "invalid reference", + wantErr: errdef.ErrInvalidReference, + }, + { + name: "empty reference", + reference: "", + wantErr: errdef.ErrInvalidReference, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewRepository(tt.reference) + if err != nil { + if errors.Is(err, tt.wantErr) { + return + } + t.Fatalf("NewRepository() error = %v, wantErr %v", err, tt.wantErr) + } + if got.Reference.String() != tt.reference { + t.Errorf("NewRepository() got = %v, want %v", got.Reference.String(), tt.reference) + } + }) + } +} + func TestRepository_Fetch(t *testing.T) { blob := []byte("hello world") blobDesc := ocispec.Descriptor{ @@ -2881,69 +2930,132 @@ func Test_BlobStore_Resolve(t *testing.T) { Digest: digest.FromBytes(blob), Size: int64(len(blob)), } - ref := "foobar" - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodHead { - t.Errorf("unexpected access: %s %s", r.Method, r.URL) - w.WriteHeader(http.StatusMethodNotAllowed) - return + + t.Run("Successfully resolve", func(t *testing.T) { + ref := "foobar" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + switch r.URL.Path { + case "/v2/test/blobs/" + blobDesc.Digest.String(): + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size))) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) } - switch r.URL.Path { - case "/v2/test/blobs/" + blobDesc.Digest.String(): - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) - w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size))) - default: - w.WriteHeader(http.StatusNotFound) + + repoName := uri.Host + "/test" + repo, err := NewRepository(repoName) + if err != nil { + t.Fatalf("NewRepository() error = %v", err) } - })) - defer ts.Close() - uri, err := url.Parse(ts.URL) - if err != nil { - t.Fatalf("invalid test http server: %v", err) - } + repo.PlainHTTP = true + store := repo.Blobs() + ctx := context.Background() - repoName := uri.Host + "/test" - repo, err := NewRepository(repoName) - if err != nil { - t.Fatalf("NewRepository() error = %v", err) - } - repo.PlainHTTP = true - store := repo.Blobs() - ctx := context.Background() + got, err := store.Resolve(ctx, blobDesc.Digest.String()) + if err != nil { + t.Fatalf("Blobs.Resolve() error = %v", err) + } + if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size { + t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc) + } - got, err := store.Resolve(ctx, blobDesc.Digest.String()) - if err != nil { - t.Fatalf("Blobs.Resolve() error = %v", err) - } - if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size { - t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc) - } + _, err = store.Resolve(ctx, ref) + if !errors.Is(err, digest.ErrDigestInvalidFormat) { + t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, digest.ErrDigestInvalidFormat) + } - _, err = store.Resolve(ctx, ref) - if !errors.Is(err, digest.ErrDigestInvalidFormat) { - t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, digest.ErrDigestInvalidFormat) - } + fqdnRef := repoName + "@" + blobDesc.Digest.String() + got, err = store.Resolve(ctx, fqdnRef) + if err != nil { + t.Fatalf("Blobs.Resolve() error = %v", err) + } + if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size { + t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc) + } - fqdnRef := repoName + "@" + blobDesc.Digest.String() - got, err = store.Resolve(ctx, fqdnRef) - if err != nil { - t.Fatalf("Blobs.Resolve() error = %v", err) - } - if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size { - t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc) - } + content := []byte("foobar") + contentDesc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + _, err = store.Resolve(ctx, contentDesc.Digest.String()) + if !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) + } + }) - content := []byte("foobar") - contentDesc := ocispec.Descriptor{ - MediaType: "test", - Digest: digest.FromBytes(content), - Size: int64(len(content)), - } - _, err = store.Resolve(ctx, contentDesc.Digest.String()) - if !errors.Is(err, errdef.ErrNotFound) { - t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) - } + t.Run("Resolve failed with server error", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + switch r.URL.Path { + case "/v2/test/blobs/" + blobDesc.Digest.String(): + w.WriteHeader(http.StatusInternalServerError) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + repoName := uri.Host + "/test" + repo, err := NewRepository(repoName) + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Blobs() + ctx := context.Background() + + if _, err := store.Resolve(ctx, blobDesc.Digest.String()); err == nil { + t.Error("Blobs.Resolve() error = nil, wantErr = true") + } + }) + + t.Run("Resolve failed with bad reference", func(t *testing.T) { + ref := "sha256:bad" + repo, err := NewRepository("localhost:5000/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + store := repo.Blobs() + ctx := context.Background() + if _, err := store.Resolve(ctx, ref); err == nil { + t.Error("Blobs.Resolve() error = nil, wantErr = true") + } + }) + + t.Run("Resolve failed with connection error", func(t *testing.T) { + ref := blobDesc.Digest.String() + repo, err := NewRepository("localhost:9876/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + store := repo.Blobs() + ctx := context.Background() + if _, err := store.Resolve(ctx, ref); err == nil { + t.Error("Blobs.Resolve() error = nil, wantErr = true") + } + }) } func Test_BlobStore_FetchReference(t *testing.T) { @@ -3249,100 +3361,275 @@ func Test_ManifestStore_Fetch(t *testing.T) { Digest: digest.FromBytes(manifest), Size: int64(len(manifest)), } - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - t.Errorf("unexpected access: %s %s", r.Method, r.URL) - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - switch r.URL.Path { - case "/v2/test/manifests/" + manifestDesc.Digest.String(): - if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { - t.Errorf("manifest not convertable: %s", accept) - w.WriteHeader(http.StatusBadRequest) + + t.Run("successfull fetch", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) return } - w.Header().Set("Content-Type", manifestDesc.MediaType) - w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) - if _, err := w.Write(manifest); err != nil { - t.Errorf("failed to write %q: %v", r.URL, err) + switch r.URL.Path { + case "/v2/test/manifests/" + manifestDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", manifestDesc.MediaType) + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + if _, err := w.Write(manifest); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + default: + w.WriteHeader(http.StatusNotFound) } - default: - w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) } - })) - defer ts.Close() - uri, err := url.Parse(ts.URL) - if err != nil { - t.Fatalf("invalid test http server: %v", err) - } - repo, err := NewRepository(uri.Host + "/test") - if err != nil { - t.Fatalf("NewRepository() error = %v", err) - } - repo.PlainHTTP = true - store := repo.Manifests() - ctx := context.Background() + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() - rc, err := store.Fetch(ctx, manifestDesc) - if err != nil { - t.Fatalf("Manifests.Fetch() error = %v", err) - } - buf := bytes.NewBuffer(nil) - if _, err := buf.ReadFrom(rc); err != nil { - t.Errorf("fail to read: %v", err) - } - if err := rc.Close(); err != nil { - t.Errorf("fail to close: %v", err) - } - if got := buf.Bytes(); !bytes.Equal(got, manifest) { - t.Errorf("Manifests.Fetch() = %v, want %v", got, manifest) - } + rc, err := store.Fetch(ctx, manifestDesc) + if err != nil { + t.Fatalf("Manifests.Fetch() error = %v", err) + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(rc); err != nil { + t.Errorf("fail to read: %v", err) + } + if err := rc.Close(); err != nil { + t.Errorf("fail to close: %v", err) + } + if got := buf.Bytes(); !bytes.Equal(got, manifest) { + t.Errorf("Manifests.Fetch() = %v, want %v", got, manifest) + } - content := []byte(`{"manifests":[]}`) - contentDesc := ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageIndex, - Digest: digest.FromBytes(content), - Size: int64(len(content)), - } - _, err = store.Fetch(ctx, contentDesc) - if !errors.Is(err, errdef.ErrNotFound) { - t.Errorf("Manifests.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound) - } -} + content := []byte(`{"manifests":[]}`) + contentDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageIndex, + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + _, err = store.Fetch(ctx, contentDesc) + if !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("Manifests.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound) + } + }) -func Test_ManifestStore_Push(t *testing.T) { - manifest := []byte(`{"layers":[]}`) - manifestDesc := ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageManifest, - Digest: digest.FromBytes(manifest), - Size: int64(len(manifest)), - } - var gotManifest []byte - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): - if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { - w.WriteHeader(http.StatusBadRequest) - break + t.Run("fail with invalid Content-Type", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) + return } - buf := bytes.NewBuffer(nil) - if _, err := buf.ReadFrom(r.Body); err != nil { - t.Errorf("fail to read: %v", err) + switch r.URL.Path { + case "/v2/test/manifests/" + manifestDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "invalid content type") + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + if _, err := w.Write(manifest); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + default: + w.WriteHeader(http.StatusNotFound) } - gotManifest = buf.Bytes() - w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) - w.WriteHeader(http.StatusCreated) - return - default: - w.WriteHeader(http.StatusForbidden) + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) } - t.Errorf("unexpected access: %s %s", r.Method, r.URL) - })) - defer ts.Close() - uri, err := url.Parse(ts.URL) - if err != nil { + + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() + + _, err = store.Fetch(ctx, manifestDesc) + if err == nil { + t.Error("Manifests.Fetch() error = nil, wantErr = true") + } + }) + + t.Run("fail with mismatching Content-Type", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + switch r.URL.Path { + case "/v2/test/manifests/" + manifestDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/vnd.other") + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + if _, err := w.Write(manifest); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() + + _, err = store.Fetch(ctx, manifestDesc) + if err == nil { + t.Error("Manifests.Fetch() error = nil, wantErr = true") + } + }) + + t.Run("fail with mismatching Content-Length", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + switch r.URL.Path { + case "/v2/test/manifests/" + manifestDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", manifestDesc.MediaType) + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + if _, err := w.Write([]byte("random")); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() + + _, err = store.Fetch(ctx, manifestDesc) + if err == nil { + t.Error("Manifests.Fetch() error = nil, wantErr = true") + } + }) + + t.Run("fail with mismatching digest", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + switch r.URL.Path { + case "/v2/test/manifests/" + manifestDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", manifestDesc.MediaType) + w.Header().Set("Docker-Content-Digest", digest.FromBytes([]byte("random")).String()) + if _, err := w.Write(manifest); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() + + _, err = store.Fetch(ctx, manifestDesc) + if err == nil { + t.Error("Manifests.Fetch() error = nil, wantErr = true") + } + }) +} + +func Test_ManifestStore_Push(t *testing.T) { + manifest := []byte(`{"layers":[]}`) + manifestDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(manifest), + Size: int64(len(manifest)), + } + var gotManifest []byte + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + return + default: + w.WriteHeader(http.StatusForbidden) + } + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { t.Fatalf("invalid test http server: %v", err) } @@ -5206,86 +5493,154 @@ func Test_ManifestStore_Resolve(t *testing.T) { Size: int64(len(manifest)), } ref := "foobar" - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodHead { - t.Errorf("unexpected access: %s %s", r.Method, r.URL) - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - switch r.URL.Path { - case "/v2/test/manifests/" + manifestDesc.Digest.String(), - "/v2/test/manifests/" + ref: - if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { - t.Errorf("manifest not convertable: %s", accept) - w.WriteHeader(http.StatusBadRequest) + + t.Run("Successfully resolve", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) return } - w.Header().Set("Content-Type", manifestDesc.MediaType) - w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) - w.Header().Set("Content-Length", strconv.Itoa(int(manifestDesc.Size))) - default: - w.WriteHeader(http.StatusNotFound) + switch r.URL.Path { + case "/v2/test/manifests/" + manifestDesc.Digest.String(), + "/v2/test/manifests/" + ref: + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", manifestDesc.MediaType) + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(int(manifestDesc.Size))) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) } - })) - defer ts.Close() - uri, err := url.Parse(ts.URL) - if err != nil { - t.Fatalf("invalid test http server: %v", err) - } - repoName := uri.Host + "/test" - repo, err := NewRepository(repoName) - if err != nil { - t.Fatalf("NewRepository() error = %v", err) - } - repo.PlainHTTP = true - store := repo.Manifests() - ctx := context.Background() + repoName := uri.Host + "/test" + repo, err := NewRepository(repoName) + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() - got, err := store.Resolve(ctx, manifestDesc.Digest.String()) - if err != nil { - t.Fatalf("Manifests.Resolve() error = %v", err) - } - if !reflect.DeepEqual(got, manifestDesc) { - t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) - } + got, err := store.Resolve(ctx, manifestDesc.Digest.String()) + if err != nil { + t.Fatalf("Manifests.Resolve() error = %v", err) + } + if !reflect.DeepEqual(got, manifestDesc) { + t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) + } - got, err = store.Resolve(ctx, ref) - if err != nil { - t.Fatalf("Manifests.Resolve() error = %v", err) - } - if !reflect.DeepEqual(got, manifestDesc) { - t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) - } + got, err = store.Resolve(ctx, ref) + if err != nil { + t.Fatalf("Manifests.Resolve() error = %v", err) + } + if !reflect.DeepEqual(got, manifestDesc) { + t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) + } - tagDigestRef := "whatever" + "@" + manifestDesc.Digest.String() - got, err = repo.Resolve(ctx, tagDigestRef) - if err != nil { - t.Fatalf("Manifests.Resolve() error = %v", err) - } - if !reflect.DeepEqual(got, manifestDesc) { - t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) - } + tagDigestRef := "whatever" + "@" + manifestDesc.Digest.String() + got, err = repo.Resolve(ctx, tagDigestRef) + if err != nil { + t.Fatalf("Manifests.Resolve() error = %v", err) + } + if !reflect.DeepEqual(got, manifestDesc) { + t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) + } - fqdnRef := repoName + ":" + tagDigestRef - got, err = repo.Resolve(ctx, fqdnRef) - if err != nil { - t.Fatalf("Manifests.Resolve() error = %v", err) - } - if !reflect.DeepEqual(got, manifestDesc) { - t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) - } + fqdnRef := repoName + ":" + tagDigestRef + got, err = repo.Resolve(ctx, fqdnRef) + if err != nil { + t.Fatalf("Manifests.Resolve() error = %v", err) + } + if !reflect.DeepEqual(got, manifestDesc) { + t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) + } - content := []byte(`{"manifests":[]}`) - contentDesc := ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageIndex, - Digest: digest.FromBytes(content), - Size: int64(len(content)), - } - _, err = store.Resolve(ctx, contentDesc.Digest.String()) - if !errors.Is(err, errdef.ErrNotFound) { - t.Errorf("Manifests.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) - } + content := []byte(`{"manifests":[]}`) + contentDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageIndex, + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + _, err = store.Resolve(ctx, contentDesc.Digest.String()) + if !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("Manifests.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) + } + }) + + t.Run("Resolve failed with server error", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + switch r.URL.Path { + case "/v2/test/manifests/" + manifestDesc.Digest.String(), + "/v2/test/manifests/" + ref: + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusInternalServerError) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + repoName := uri.Host + "/test" + repo, err := NewRepository(repoName) + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() + + if _, err := store.Resolve(ctx, ref); err == nil { + t.Error("Manifests.Resolve() error = nil, wantErr = true") + } + }) + + t.Run("Resolve failed with bad reference", func(t *testing.T) { + ref := "sha256:bad" + repo, err := NewRepository("localhost:5000/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + store := repo.Manifests() + ctx := context.Background() + if _, err := store.Resolve(ctx, ref); err == nil { + t.Error("Manifests.Resolve() error = nil, wantErr = true") + } + }) + + t.Run("Resolve failed with connection error", func(t *testing.T) { + repo, err := NewRepository("localhost:9876/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + store := repo.Manifests() + ctx := context.Background() + if _, err := store.Resolve(ctx, ref); err == nil { + t.Error("Manifests.Resolve() error = nil, wantErr = true") + } + }) } func Test_ManifestStore_FetchReference(t *testing.T) { @@ -7248,6 +7603,36 @@ func TestRepository_do(t *testing.T) { } } +func TestRepository_newRepositoryWithOptions(t *testing.T) { + t.Run("valid reference and options", func(t *testing.T) { + ref := registry.Reference{ + Registry: "registry.example.com", + Repository: "test", + Reference: "latest", + } + opts := &RepositoryOptions{ + PlainHTTP: true, + } + repo, err := newRepositoryWithOptions(ref, opts) + if err != nil { + t.Fatalf("newRepositoryWithOptions() error = %v", err) + } + if repo.PlainHTTP != opts.PlainHTTP { + t.Errorf("Repository.PlainHTTP = %v, want %v", repo.PlainHTTP, opts.PlainHTTP) + } + if !reflect.DeepEqual(repo.Reference, ref) { + t.Errorf("Repository.Reference = %v, want %v", repo.Reference, ref) + } + }) + + t.Run("invalid reference", func(t *testing.T) { + ref := registry.Reference{} + if _, err := newRepositoryWithOptions(ref, nil); err == nil { + t.Error("newRepositoryWithOptions() error = nil, wantErr") + } + }) +} + func TestRepository_clone(t *testing.T) { repo, err := NewRepository("localhost:1234/repo/image") if err != nil { @@ -7268,3 +7653,397 @@ func TestRepository_clone(t *testing.T) { t.Fatal("referrersMergePool should be different") } } + +func TestManifestStore_ParseReference(t *testing.T) { + tests := []struct { + name string + reference string + want registry.Reference + wantErr bool + }{ + { + name: "valid tag", + reference: "foobar", + want: registry.Reference{ + Reference: "foobar", + }, + wantErr: false, + }, + { + name: "valid digest", + reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + want: registry.Reference{ + Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + }, + wantErr: false, + }, + { + name: "valid tag@digest", + reference: "foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + want: registry.Reference{ + Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + }, + wantErr: false, + }, + { + name: "invalid reference", + reference: "invalid@reference", + want: registry.Reference{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &Repository{} + s := &manifestStore{repo: repo} + got, err := s.ParseReference(tt.reference) + if (err != nil) != tt.wantErr { + t.Errorf("ParseReference() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseReference() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestManifestStore_generateDescriptor(t *testing.T) { + data := []byte("test") + dataSize := int64(len(data)) + dataDigest := digest.FromBytes(data) + mediaType := "application/vnd.test" + + tests := []struct { + name string + resp *http.Response + ref registry.Reference + httpMethod string + wantDescriptor ocispec.Descriptor + wantErr bool + }{ + { + name: "valid response with Content-Type and Docker-Content-Digest", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + "Docker-Content-Digest": []string{dataDigest.String()}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + ref: registry.Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: dataDigest.String(), + }, + httpMethod: http.MethodGet, + wantDescriptor: ocispec.Descriptor{ + MediaType: mediaType, + Digest: dataDigest, + Size: dataSize, + }, + wantErr: false, + }, + { + name: "invalid Content-Type", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{"invalid content type"}, + "Docker-Content-Digest": []string{dataDigest.String()}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + ref: registry.Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: dataDigest.String(), + }, + httpMethod: http.MethodGet, + wantDescriptor: ocispec.Descriptor{}, + wantErr: true, + }, + { + name: "unknown Content-Length", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + "Docker-Content-Digest": []string{dataDigest.String()}, + }, + ContentLength: -1, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + ref: registry.Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: dataDigest.String(), + }, + httpMethod: http.MethodGet, + wantDescriptor: ocispec.Descriptor{}, + wantErr: true, + }, + { + name: "bad Docker-Content-Digest", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + "Docker-Content-Digest": []string{"not-a-digest"}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + ref: registry.Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: dataDigest.String(), + }, + httpMethod: http.MethodGet, + wantDescriptor: ocispec.Descriptor{}, + wantErr: true, + }, + { + name: "resp with body, missing Docker-Content-Digest", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + Body: io.NopCloser(bytes.NewReader(data)), + }, + ref: registry.Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: dataDigest.String(), + }, + httpMethod: http.MethodGet, + wantDescriptor: ocispec.Descriptor{ + MediaType: mediaType, + Digest: dataDigest, + Size: dataSize, + }, + wantErr: false, + }, + { + name: "failed to read resp with body, missing Docker-Content-Digest", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + Body: &badReader{}, + }, + ref: registry.Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: dataDigest.String(), + }, + httpMethod: http.MethodGet, + wantDescriptor: ocispec.Descriptor{}, + wantErr: true, + }, + { + name: "digest mismatch", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + "Docker-Content-Digest": []string{string(dataDigest)}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + ref: registry.Reference{ + Registry: "registry.example.com", + Repository: "hello-world", + Reference: string(digest.FromBytes([]byte("whatever"))), + }, + httpMethod: http.MethodGet, + wantDescriptor: ocispec.Descriptor{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &manifestStore{ + repo: &Repository{ + MaxMetadataBytes: 1024, + }, + } + got, err := s.generateDescriptor(tt.resp, tt.ref, tt.httpMethod) + if (err != nil) != tt.wantErr { + t.Errorf("generateDescriptor() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.wantDescriptor) { + t.Errorf("generateDescriptor() = %v, want %v", got, tt.wantDescriptor) + } + }) + } +} + +func Test_generateBlobDescriptor(t *testing.T) { + data := []byte("test") + dataSize := int64(len(data)) + dataDigest := digest.FromBytes(data) + mediaType := "application/vnd.test" + + tests := []struct { + name string + resp *http.Response + refDigest digest.Digest + wantDescriptor ocispec.Descriptor + wantErr bool + }{ + { + name: "valid response with Content-Type and Docker-Content-Digest", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + "Docker-Content-Digest": []string{dataDigest.String()}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + refDigest: dataDigest, + wantDescriptor: ocispec.Descriptor{ + MediaType: mediaType, + Digest: dataDigest, + Size: dataSize, + }, + wantErr: false, + }, + { + name: "missing Content-Type", + resp: &http.Response{ + Header: http.Header{ + "Docker-Content-Digest": []string{dataDigest.String()}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + refDigest: dataDigest, + wantDescriptor: ocispec.Descriptor{ + MediaType: "application/octet-stream", + Digest: dataDigest, + Size: dataSize, + }, + wantErr: false, + }, + { + name: "invalid Content-Type", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{"invalid content type"}, + "Docker-Content-Digest": []string{dataDigest.String()}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + refDigest: dataDigest, + wantDescriptor: ocispec.Descriptor{ + MediaType: "application/octet-stream", + Digest: dataDigest, + Size: dataSize, + }, + wantErr: false, + }, + { + name: "unknown Content-Length", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + "Docker-Content-Digest": []string{dataDigest.String()}, + }, + ContentLength: -1, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + refDigest: dataDigest, + wantDescriptor: ocispec.Descriptor{}, + wantErr: true, + }, + { + name: "bad Docker-Content-Digest", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + "Docker-Content-Digest": []string{"not-a-digest"}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + refDigest: dataDigest, + wantDescriptor: ocispec.Descriptor{}, + wantErr: true, + }, + { + name: "digest mismatch", + resp: &http.Response{ + Header: http.Header{ + "Content-Type": []string{mediaType}, + "Docker-Content-Digest": []string{string(dataDigest)}, + }, + ContentLength: dataSize, + Request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Path: "/test"}, + }, + }, + refDigest: digest.FromBytes([]byte("mismatch")), + wantDescriptor: ocispec.Descriptor{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := generateBlobDescriptor(tt.resp, tt.refDigest) + if (err != nil) != tt.wantErr { + t.Errorf("generateBlobDescriptor() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.wantDescriptor) { + t.Errorf("generateBlobDescriptor() = %v, want %v", got, tt.wantDescriptor) + } + }) + } +} diff --git a/registry/remote/utils_test.go b/registry/remote/utils_test.go index 89dc47c1..ef364344 100644 --- a/registry/remote/utils_test.go +++ b/registry/remote/utils_test.go @@ -52,7 +52,13 @@ func Test_parseLink(t *testing.T) { want: "https://localhost:5001/v2/_catalog?last=alpine&n=1", }, { - name: "invalid header", + name: "invalid header, missing <", + url: "https://localhost:5000/v2/_catalog", + header: `/v2/_catalog>`, + wantErr: true, + }, + { + name: "invalid header, missing >", url: "https://localhost:5000/v2/_catalog", header: `