From b8508d04e9065e3787c72a7d280e474d812e7210 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 29 May 2024 01:33:03 +0800 Subject: [PATCH] test: improve test coverage to 80% (#405) Test: - improved test coverage to over 80.16% Fix: - added ORAS copy library to copy the OCI Layout test data to a temporary directory and avoid generating test time temporary files in the repository directory. --------- Signed-off-by: Junjie Gao --- .gitignore | 5 +- config/config_test.go | 12 + dir/fs_test.go | 26 +- errors_test.go | 93 ++++++ internal/mock/mocks.go | 5 + internal/mock/ocilayout/ocilayout.go | 47 +++ internal/mock/ocilayout/ocilayout_test.go | 60 ++++ log/log_test.go | 41 +++ notation_test.go | 271 +++++++++++++--- plugin/testdata/plugins/foo/libfoo | 0 registry/repository_test.go | 365 +++++++++++++++++++--- 11 files changed, 839 insertions(+), 86 deletions(-) create mode 100644 errors_test.go create mode 100644 internal/mock/ocilayout/ocilayout.go create mode 100644 internal/mock/ocilayout/ocilayout_test.go create mode 100644 log/log_test.go create mode 100644 plugin/testdata/plugins/foo/libfoo diff --git a/.gitignore b/.gitignore index e9ac268a..c1644c70 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,7 @@ *.sublime-workspace # Custom -coverage.txt \ No newline at end of file +coverage.txt + +# tmp directory was generated by example_remoteVerify_test.go +tmp/ \ No newline at end of file diff --git a/config/config_test.go b/config/config_test.go index ceb7ba83..db279c2b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -51,3 +51,15 @@ func TestSaveFile(t *testing.T) { t.Fatal("save config file failed.") } } + +func TestLoadNonExistedConfig(t *testing.T) { + dir.UserConfigDir = "./testdata/non-existed" + got, err := LoadConfig() + if err != nil { + t.Fatalf("LoadConfig() error. err = %v", err) + } + + if !reflect.DeepEqual(got, NewConfig()) { + t.Errorf("loadFile() = %v, want %v", got, NewConfig()) + } +} diff --git a/dir/fs_test.go b/dir/fs_test.go index 0011276c..d2c976cf 100644 --- a/dir/fs_test.go +++ b/dir/fs_test.go @@ -19,7 +19,7 @@ import ( "testing" ) -func Test_sysFS_SysPath(t *testing.T) { +func TestSysFS_SysPath(t *testing.T) { wantPath := filepath.FromSlash("/path/notation/config.json") fsys := NewSysFS("/path/notation") path, err := fsys.SysPath(PathConfigFile) @@ -31,7 +31,7 @@ func Test_sysFS_SysPath(t *testing.T) { } } -func Test_OsFs(t *testing.T) { +func TestOsFs(t *testing.T) { wantData := []byte("data") fsys := NewSysFS("./testdata") @@ -49,3 +49,25 @@ func Test_OsFs(t *testing.T) { t.Fatalf("SysFS read failed. got data = %v, want %v", data, wantData) } } + +func TestConfigFS(t *testing.T) { + configFS := ConfigFS() + path, err := configFS.SysPath(PathConfigFile) + if err != nil { + t.Fatalf("SysPath() failed. err = %v", err) + } + if path != filepath.Join(UserConfigDir, PathConfigFile) { + t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(UserConfigDir, PathConfigFile)) + } +} + +func TestPluginFS(t *testing.T) { + pluginFS := PluginFS() + path, err := pluginFS.SysPath("plugin") + if err != nil { + t.Fatalf("SysPath() failed. err = %v", err) + } + if path != filepath.Join(UserLibexecDir, PathPlugins, "plugin") { + t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(UserLibexecDir, PathPlugins, "plugin")) + } +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 00000000..29aed6ba --- /dev/null +++ b/errors_test.go @@ -0,0 +1,93 @@ +// Copyright The Notary Project 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 notation + +import "testing" + +func TestErrorMessages(t *testing.T) { + tests := []struct { + name string + err error + want string + }{ + { + name: "ErrorPushSignatureFailed with message", + err: ErrorPushSignatureFailed{Msg: "test message"}, + want: "failed to push signature to registry with error: test message", + }, + { + name: "ErrorPushSignatureFailed without message", + err: ErrorPushSignatureFailed{}, + want: "failed to push signature to registry", + }, + { + name: "ErrorVerificationInconclusive with message", + err: ErrorVerificationInconclusive{Msg: "test message"}, + want: "test message", + }, + { + name: "ErrorVerificationInconclusive without message", + err: ErrorVerificationInconclusive{}, + want: "signature verification was inclusive due to an unexpected error", + }, + { + name: "ErrorNoApplicableTrustPolicy with message", + err: ErrorNoApplicableTrustPolicy{Msg: "test message"}, + want: "test message", + }, + { + name: "ErrorNoApplicableTrustPolicy without message", + err: ErrorNoApplicableTrustPolicy{}, + want: "there is no applicable trust policy for the given artifact", + }, + { + name: "ErrorSignatureRetrievalFailed with message", + err: ErrorSignatureRetrievalFailed{Msg: "test message"}, + want: "test message", + }, + { + name: "ErrorSignatureRetrievalFailed without message", + err: ErrorSignatureRetrievalFailed{}, + want: "unable to retrieve the digital signature from the registry", + }, + { + name: "ErrorVerificationFailed with message", + err: ErrorVerificationFailed{Msg: "test message"}, + want: "test message", + }, + { + name: "ErrorVerificationFailed without message", + err: ErrorVerificationFailed{}, + want: "signature verification failed", + }, + { + name: "ErrorUserMetadataVerificationFailed with message", + err: ErrorUserMetadataVerificationFailed{Msg: "test message"}, + want: "test message", + }, + { + name: "ErrorUserMetadataVerificationFailed without message", + err: ErrorUserMetadataVerificationFailed{}, + want: "unable to find specified metadata in the signature", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.err.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/mock/mocks.go b/internal/mock/mocks.go index bc07c51f..4b4f0c4f 100644 --- a/internal/mock/mocks.go +++ b/internal/mock/mocks.go @@ -125,6 +125,7 @@ type Repository struct { FetchSignatureBlobError error MissMatchDigest bool ExceededNumOfSignatures bool + PushSignatureError error } func NewRepository() Repository { @@ -163,6 +164,10 @@ func (t Repository) FetchSignatureBlob(ctx context.Context, desc ocispec.Descrip } func (t Repository) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) { + if t.PushSignatureError != nil { + return ocispec.Descriptor{}, ocispec.Descriptor{}, t.PushSignatureError + } + return ocispec.Descriptor{}, ocispec.Descriptor{}, nil } diff --git a/internal/mock/ocilayout/ocilayout.go b/internal/mock/ocilayout/ocilayout.go new file mode 100644 index 00000000..367079f1 --- /dev/null +++ b/internal/mock/ocilayout/ocilayout.go @@ -0,0 +1,47 @@ +// Copyright The Notary Project 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 ocilayout + +import ( + "context" + "os" + + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content/oci" +) + +// Copy creates a temporary OCI layout for testing +// and returns the path to the layout. +func Copy(sourcePath, destPath, tag string) error { + ctx := context.Background() + + srcStore, err := oci.NewFromFS(ctx, os.DirFS(sourcePath)) + if err != nil { + return err + } + + // create a dest store for store the generated oci layout. + destStore, err := oci.New(destPath) + if err != nil { + return err + } + + // copy data + _, err = oras.ExtendedCopy(ctx, srcStore, tag, destStore, "", oras.DefaultExtendedCopyOptions) + if err != nil { + return err + } + + return nil +} diff --git a/internal/mock/ocilayout/ocilayout_test.go b/internal/mock/ocilayout/ocilayout_test.go new file mode 100644 index 00000000..81b464f7 --- /dev/null +++ b/internal/mock/ocilayout/ocilayout_test.go @@ -0,0 +1,60 @@ +// Copyright The Notary Project 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 ocilayout + +import ( + "os" + "testing" +) + +func TestCopy(t *testing.T) { + t.Run("empty oci layout", func(t *testing.T) { + err := Copy("", "", "v2") + if err == nil { + t.Errorf("expected error, got nil") + } + }) + + t.Run("invalid target path", func(t *testing.T) { + tempDir := t.TempDir() + // change the permission of the tempDir to make it invalid + if err := os.Chmod(tempDir, 0); err != nil { + t.Fatalf("failed to change the permission of the tempDir: %v", err) + } + err := Copy("../../testdata/oci-layout", tempDir, "v2") + if err == nil { + t.Errorf("expected error, got nil") + } + + if err := os.Chmod(tempDir, 0755); err != nil { + t.Fatalf("failed to change the permission of the tempDir: %v", err) + } + }) + + t.Run("copy failed", func(t *testing.T) { + tempDir := t.TempDir() + err := Copy("../../testdata/oci-layout", tempDir, "v3") + if err == nil { + t.Errorf("expected error, got nil") + } + }) + + t.Run("copy success", func(t *testing.T) { + tempDir := t.TempDir() + err := Copy("../../testdata/oci-layout", tempDir, "v2") + if err != nil { + t.Errorf("expected nil, got %v", err) + } + }) +} diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 00000000..d09874db --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,41 @@ +// Copyright The Notary Project 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 log provides logging functionality to notation. +// Users who want to enable logging option in notation should implement the +// log.Logger interface and include it in context by calling log.WithLogger. +// 3rd party loggers that implement log.Logger: github.com/uber-go/zap.SugaredLogger +// and github.com/sirupsen/logrus.Logger. +package log + +import ( + "context" + "testing" +) + +func TestWithLoggerAndGetLogger(t *testing.T) { + tl := &discardLogger{} + ctx := WithLogger(context.Background(), tl) + + if got := GetLogger(ctx); got != tl { + t.Errorf("GetLogger() = %v, want %v", got, tl) + } +} + +func TestGetLoggerWithNoLogger(t *testing.T) { + ctx := context.Background() + + if got := GetLogger(ctx); got != Discard { + t.Errorf("GetLogger() = %v, want Discard", got) + } +} diff --git a/notation_test.go b/notation_test.go index 72255f5c..3145a055 100644 --- a/notation_test.go +++ b/notation_test.go @@ -15,6 +15,7 @@ package notation import ( "context" + "encoding/json" "errors" "fmt" "io" @@ -28,7 +29,9 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/cose" "github.com/notaryproject/notation-core-go/signature/jws" + "github.com/notaryproject/notation-go/internal/envelope" "github.com/notaryproject/notation-go/internal/mock" + "github.com/notaryproject/notation-go/internal/mock/ocilayout" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation-go/verifier/trustpolicy" @@ -155,6 +158,55 @@ func TestSignSuccessWithUserMetadata(t *testing.T) { } } +func TestSignWithNilRepo(t *testing.T) { + opts := SignOptions{} + opts.ArtifactReference = mock.SampleArtifactUri + opts.SignatureMediaType = jws.MediaTypeEnvelope + + _, err := Sign(context.Background(), &dummySigner{}, nil, opts) + if err == nil { + t.Fatalf("no error occurred, expected error: repo cannot be nil") + } +} + +func TestSignResolveFailed(t *testing.T) { + repo := mock.NewRepository() + repo.ResolveError = errors.New("resolve error") + opts := SignOptions{} + opts.ArtifactReference = mock.SampleArtifactUri + opts.SignatureMediaType = jws.MediaTypeEnvelope + + _, err := Sign(context.Background(), &dummySigner{}, repo, opts) + if err == nil { + t.Fatalf("no error occurred, expected resolve error") + } +} + +func TestSignArtifactRefIsTag(t *testing.T) { + repo := mock.NewRepository() + opts := SignOptions{} + opts.ArtifactReference = "registry.acme-rockets.io/software/net-monitor:v1" + opts.SignatureMediaType = jws.MediaTypeEnvelope + + _, err := Sign(context.Background(), &dummySigner{}, repo, opts) + if err != nil { + t.Fatalf("expect no error, got %s", err) + } +} + +func TestSignWithPushSignatureError(t *testing.T) { + repo := mock.NewRepository() + repo.PushSignatureError = errors.New("error") + opts := SignOptions{} + opts.ArtifactReference = mock.SampleArtifactUri + opts.SignatureMediaType = jws.MediaTypeEnvelope + + _, err := Sign(context.Background(), &dummySigner{}, repo, opts) + if err == nil { + t.Fatalf("no error occurred, expected error: failed to delete dangling referrers index") + } +} + func TestSignWithInvalidExpiry(t *testing.T) { repo := mock.NewRepository() testCases := []struct { @@ -188,7 +240,14 @@ func TestSignWithInvalidUserMetadata(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(b *testing.T) { - _, err := Sign(context.Background(), &dummySigner{}, repo, SignOptions{UserMetadata: tc.metadata}) + opts := SignOptions{ + UserMetadata: tc.metadata, + SignerSignOptions: SignerSignOptions{ + SignatureMediaType: jws.MediaTypeEnvelope, + }, + } + + _, err := Sign(context.Background(), &dummySigner{}, repo, opts) if err == nil { b.Fatalf("Expected error but not found") } @@ -196,6 +255,37 @@ func TestSignWithInvalidUserMetadata(t *testing.T) { } } +func TestSignOptsMissingSignatureMediaType(t *testing.T) { + repo := mock.NewRepository() + opts := SignOptions{ + SignerSignOptions: SignerSignOptions{ + SignatureMediaType: "", + }, + ArtifactReference: mock.SampleArtifactUri, + } + + _, err := Sign(context.Background(), &dummySigner{}, repo, opts) + if err == nil { + t.Fatalf("expected error but not found") + } +} + +func TestSignOptsUnknownMediaType(t *testing.T) { + repo := mock.NewRepository() + opts := SignOptions{ + SignerSignOptions: SignerSignOptions{ + SignatureMediaType: "unknown", + }, + ArtifactReference: mock.SampleArtifactUri, + } + + _, err := Sign(context.Background(), &dummySigner{}, repo, opts) + if err == nil { + t.Fatalf("expected error but not found") + } + +} + func TestRegistryResolveError(t *testing.T) { policyDocument := dummyPolicyDocument() repo := mock.NewRepository() @@ -375,18 +465,47 @@ func TestExceededMaxSignatureAttempts(t *testing.T) { } func TestVerifyFailed(t *testing.T) { - policyDocument := dummyPolicyDocument() - repo := mock.NewRepository() - verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict} - expectedErr := ErrorVerificationFailed{} + t.Run("verification error", func(t *testing.T) { + policyDocument := dummyPolicyDocument() + repo := mock.NewRepository() + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict} + expectedErr := ErrorVerificationFailed{} + + // mock the repository + opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50} + _, _, err := Verify(context.Background(), &verifier, repo, opts) + + if err == nil || !errors.Is(err, expectedErr) { + t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err) + } + }) - // mock the repository - opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50} - _, _, err := Verify(context.Background(), &verifier, repo, opts) + t.Run("verifier is nil", func(t *testing.T) { + repo := mock.NewRepository() + expectedErr := errors.New("verifier cannot be nil") - if err == nil || !errors.Is(err, expectedErr) { - t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err) - } + // mock the repository + opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50} + _, _, err := Verify(context.Background(), nil, repo, opts) + + if err == nil || err.Error() != expectedErr.Error() { + t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err) + } + }) + + t.Run("repo is nil", func(t *testing.T) { + policyDocument := dummyPolicyDocument() + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} + expectedErr := errors.New("repo cannot be nil") + + // mock the repository + opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50} + _, _, err := Verify(context.Background(), &verifier, nil, opts) + + if err == nil || err.Error() != expectedErr.Error() { + t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err) + } + }) } func dummyPolicyDocument() (policyDoc trustpolicy.Document) { @@ -471,7 +590,6 @@ func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, sig } var ( - ociLayoutPath = filepath.FromSlash("./internal/testdata/oci-layout") reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0" artifactReference = "local/oci-layout@sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0" signaturePath = filepath.FromSlash("./internal/testdata/cose_signature.sig") @@ -495,37 +613,114 @@ func (s *ociDummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts return sigBlob, &content.SignerInfo, nil } -func TestSignLocalContent(t *testing.T) { - repo, err := registry.NewOCIRepository(ociLayoutPath, registry.RepositoryOptions{}) +func TestLocalContent(t *testing.T) { + // create a temp OCI layout + ociLayoutTestDataPath, err := filepath.Abs(filepath.Join("internal", "testdata", "oci-layout")) if err != nil { - t.Fatal(err) + t.Fatalf("failed to get oci layout path: %v", err) } - signOpts := SignOptions{ - SignerSignOptions: SignerSignOptions{ - SignatureMediaType: cose.MediaTypeEnvelope, - }, - ArtifactReference: reference, + newOCILayoutPath := t.TempDir() + if err := ocilayout.Copy(ociLayoutTestDataPath, newOCILayoutPath, "v2"); err != nil { + t.Fatalf("failed to create temp oci layout: %v", err) } - _, err = Sign(context.Background(), &ociDummySigner{}, repo, signOpts) + repo, err := registry.NewOCIRepository(newOCILayoutPath, registry.RepositoryOptions{}) if err != nil { - t.Fatalf("failed to Sign: %v", err) + t.Fatal(err) } + + t.Run("sign the local content", func(t *testing.T) { + // sign the artifact + signOpts := SignOptions{ + SignerSignOptions: SignerSignOptions{ + SignatureMediaType: cose.MediaTypeEnvelope, + }, + ArtifactReference: reference, + } + _, err = Sign(context.Background(), &ociDummySigner{}, repo, signOpts) + if err != nil { + t.Fatalf("failed to Sign: %v", err) + } + }) + + t.Run("verify local content", func(t *testing.T) { + // verify the artifact + verifyOpts := VerifyOptions{ + ArtifactReference: artifactReference, + MaxSignatureAttempts: math.MaxInt64, + } + policyDocument := dummyPolicyDocument() + verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} + // verify signatures inside the OCI layout folder + _, _, err = Verify(context.Background(), &verifier, repo, verifyOpts) + if err != nil { + t.Fatalf("failed to verify local content: %v", err) + } + }) } -func TestVerifyLocalContent(t *testing.T) { - repo, err := registry.NewOCIRepository(ociLayoutPath, registry.RepositoryOptions{}) - if err != nil { - t.Fatalf("failed to create oci.Store as registry.Repository: %v", err) - } - verifyOpts := VerifyOptions{ - ArtifactReference: artifactReference, - MaxSignatureAttempts: math.MaxInt64, - } - policyDocument := dummyPolicyDocument() - verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict} - // verify signatures inside the OCI layout folder - _, _, err = Verify(context.Background(), &verifier, repo, verifyOpts) - if err != nil { - t.Fatalf("failed to verify local content: %v", err) - } +func TestUserMetadata(t *testing.T) { + t.Run("EnvelopeContent is nil", func(t *testing.T) { + outcome := &VerificationOutcome{} + _, err := outcome.UserMetadata() + if err == nil { + t.Fatal("expected an error, got nil") + } + if err.Error() != "unable to find envelope content for verification outcome" { + t.Fatalf("expected error message 'unable to find envelope content for verification outcome', got '%s'", err.Error()) + } + }) + + t.Run("EnvelopeContent is valid", func(t *testing.T) { + payload := envelope.Payload{ + TargetArtifact: ocispec.Descriptor{ + Annotations: map[string]string{ + "key": "value", + }, + }, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + t.Fatalf("unexpected error marshaling payload: %v", err) + } + + outcome := &VerificationOutcome{ + EnvelopeContent: &signature.EnvelopeContent{ + Payload: signature.Payload{ + Content: payloadBytes, + }, + }, + } + metadata, err := outcome.UserMetadata() + if err != nil { + t.Fatalf("unexpected error getting user metadata: %v", err) + } + if len(metadata) != 1 || metadata["key"] != "value" { + t.Fatalf("expected metadata map[key]=value, got %v", metadata) + } + }) + + t.Run("Annotation is nil", func(t *testing.T) { + payload := envelope.Payload{ + TargetArtifact: ocispec.Descriptor{}, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + t.Fatalf("unexpected error marshaling payload: %v", err) + } + + outcome := &VerificationOutcome{ + EnvelopeContent: &signature.EnvelopeContent{ + Payload: signature.Payload{ + Content: payloadBytes, + }, + }, + } + metadata, err := outcome.UserMetadata() + if err != nil { + t.Fatalf("unexpected error getting user metadata: %v", err) + } + if len(metadata) != 0 { + t.Fatalf("expected empty metadata, got %v", metadata) + } + }) } diff --git a/plugin/testdata/plugins/foo/libfoo b/plugin/testdata/plugins/foo/libfoo new file mode 100644 index 00000000..e69de29b diff --git a/registry/repository_test.go b/registry/repository_test.go index 9224f875..50ea6885 100644 --- a/registry/repository_test.go +++ b/registry/repository_test.go @@ -27,11 +27,14 @@ import ( "testing" "github.com/notaryproject/notation-go/internal/envelope" + "github.com/notaryproject/notation-go/internal/mock/ocilayout" "github.com/notaryproject/notation-go/internal/slices" "github.com/notaryproject/notation-go/registry/internal/artifactspec" "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/content/memory" + "oras.land/oras-go/v2/content/oci" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" ) @@ -464,7 +467,6 @@ func newRepositoryClientWithImageManifest(client remote.Client, ref registry.Ref } var ( - ociLayoutPath = filepath.FromSlash("../internal/testdata/oci-layout") reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0" expectedTargetDesc = ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", @@ -487,11 +489,23 @@ var ( } ) -func TestOciLayoutRepositoryResolveAndPush(t *testing.T) { - repo, err := NewOCIRepository(ociLayoutPath, RepositoryOptions{}) +func TestOciLayoutRepositoryPushAndFetch(t *testing.T) { + // create a temp OCI layout + ociLayoutTestdataPath, err := filepath.Abs(filepath.Join("..", "internal", "testdata", "oci-layout")) + if err != nil { + t.Fatalf("failed to get oci layout path: %v", err) + } + + newOCILayoutPath := t.TempDir() + if err := ocilayout.Copy(ociLayoutTestdataPath, newOCILayoutPath, "v2"); err != nil { + t.Fatalf("failed to create temp oci layout: %v", err) + } + repo, err := NewOCIRepository(newOCILayoutPath, RepositoryOptions{}) if err != nil { t.Fatalf("failed to create oci.Store as registry.Repository: %v", err) } + + // test resolve targetDesc, err := repo.Resolve(context.Background(), reference) if err != nil { t.Fatalf("failed to resolve reference: %v", err) @@ -499,59 +513,320 @@ func TestOciLayoutRepositoryResolveAndPush(t *testing.T) { if !content.Equal(targetDesc, expectedTargetDesc) { t.Fatalf("failed to resolve reference. expected descriptor: %v, but got: %v", expectedTargetDesc, targetDesc) } - signature, err := os.ReadFile(signaturePath) - if err != nil { - t.Fatalf("failed to read signature: %v", err) - } - _, signatureManifestDesc, err := repo.PushSignature(context.Background(), joseTag, signature, targetDesc, annotations) + + t.Run("oci layout push", func(t *testing.T) { + signature, err := os.ReadFile(signaturePath) + if err != nil { + t.Fatalf("failed to read signature: %v", err) + } + _, signatureManifestDesc, err := repo.PushSignature(context.Background(), joseTag, signature, targetDesc, annotations) + if err != nil { + t.Fatalf("failed to push signature: %v", err) + } + if !content.Equal(expectedSignatureManifestDesc, signatureManifestDesc) { + t.Fatalf("expected desc: %v, got: %v", expectedSignatureManifestDesc, signatureManifestDesc) + } + expectedAnnotations := map[string]string{ + envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]", + ocispec.AnnotationCreated: "2023-03-14T08:10:02Z", + } + if !reflect.DeepEqual(expectedAnnotations, signatureManifestDesc.Annotations) { + t.Fatalf("expected annotations: %v, but got: %v", expectedAnnotations, signatureManifestDesc.Annotations) + } + }) + + t.Run("oci layout fetch", func(t *testing.T) { + err = repo.ListSignatures(context.Background(), targetDesc, func(signatureManifests []ocispec.Descriptor) error { + if len(signatureManifests) == 0 { + return fmt.Errorf("expected to find signature in the OCI layout folder, but got none") + } + var found bool + for _, sigManifestDesc := range signatureManifests { + if !content.Equal(sigManifestDesc, expectedSignatureManifestDesc) { + continue + } + _, sigDesc, err := repo.FetchSignatureBlob(context.Background(), sigManifestDesc) + if err != nil { + return fmt.Errorf("failed to fetch blob: %w", err) + } + if !content.Equal(expectedSignatureBlobDesc, sigDesc) { + return fmt.Errorf("expected to get signature blob desc: %v, got: %v", expectedSignatureBlobDesc, sigDesc) + } + found = true + } + if !found { + return fmt.Errorf("expected to find the signature with manifest desc: %v, but failed", expectedSignatureManifestDesc) + } + return nil + }) + if err != nil { + t.Fatal(err) + } + }) +} + +func TestNewRepository(t *testing.T) { + target, err := oci.New(t.TempDir()) if err != nil { - t.Fatalf("failed to push signature: %v", err) + t.Fatalf("failed to create oci.Store as registry.Repository: %v", err) } - if !content.Equal(expectedSignatureManifestDesc, signatureManifestDesc) { - t.Fatalf("expected desc: %v, got: %v", expectedSignatureManifestDesc, signatureManifestDesc) + repo := NewRepository(target) + if repo == nil { + t.Fatalf("failed to create repository") } - expectedAnnotations := map[string]string{ - envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]", - ocispec.AnnotationCreated: "2023-03-14T08:10:02Z", + repoClient, ok := repo.(*repositoryClient) + if !ok { + t.Fatalf("failed to create repositoryClient") } - if !reflect.DeepEqual(expectedAnnotations, signatureManifestDesc.Annotations) { - t.Fatalf("expected annotations: %v, but got: %v", expectedAnnotations, signatureManifestDesc.Annotations) + if target != repoClient.GraphTarget { + t.Fatalf("expected target: %v, got: %v", target, repoClient.GraphTarget) } } -func TestOciLayoutRepositoryListAndFetchBlob(t *testing.T) { - repo, err := NewOCIRepository(ociLayoutPath, RepositoryOptions{}) - if err != nil { - t.Fatalf("failed to create oci.Store as registry.Repository: %v", err) +func TestNewOCIRepositoryFailed(t *testing.T) { + t.Run("os stat failed", func(t *testing.T) { + _, err := NewOCIRepository("invalid-path", RepositoryOptions{}) + if err == nil { + t.Fatalf("expected to fail with invalid path") + } + }) + + t.Run("path is regular file", func(t *testing.T) { + // create a regular file in the temp dir + filePath := filepath.Join(t.TempDir(), "file") + file, err := os.Create(filePath) + if err != nil { + t.Fatalf("failed to create file: %v", err) + } + file.Close() + + _, err = NewOCIRepository(filePath, RepositoryOptions{}) + if err == nil { + t.Fatalf("expected to fail with regular file") + } + }) + + t.Run("no permission to create new path", func(t *testing.T) { + // create a directory in the temp dir + dirPath := filepath.Join(t.TempDir(), "dir") + err := os.Mkdir(dirPath, 0000) + if err != nil { + t.Fatalf("failed to create dir: %v", err) + } + + _, err = NewOCIRepository(dirPath, RepositoryOptions{}) + if err == nil { + t.Fatalf("expected to fail with no permission to create new path") + } + }) +} + +// testStorage implements content.ReadOnlyGraphStorage +type testStorage struct { + store *memory.Store + FetchError error + FetchContent []byte + PredecessorsError error + PredecessorsDesc []ocispec.Descriptor +} + +func (s *testStorage) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error { + return s.store.Push(ctx, expected, reader) +} + +func (s *testStorage) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + if s.FetchError != nil { + return nil, s.FetchError } - targetDesc, err := repo.Resolve(context.Background(), reference) - if err != nil { - t.Fatalf("failed to resolve reference: %v", err) + return io.NopCloser(bytes.NewReader(s.FetchContent)), nil +} + +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) { + if s.PredecessorsError != nil { + return nil, s.PredecessorsError } - err = repo.ListSignatures(context.Background(), targetDesc, func(signatureManifests []ocispec.Descriptor) error { - if len(signatureManifests) == 0 { - return fmt.Errorf("expected to find signature in the OCI layout folder, but got none") + return s.PredecessorsDesc, nil +} + +func TestSignatureReferrers(t *testing.T) { + t.Run("get predecessors failed", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsError: fmt.Errorf("failed to get predecessors"), } - var found bool - for _, sigManifestDesc := range signatureManifests { - if !content.Equal(sigManifestDesc, expectedSignatureManifestDesc) { - continue - } - _, sigDesc, err := repo.FetchSignatureBlob(context.Background(), sigManifestDesc) - if err != nil { - return fmt.Errorf("failed to fetch blob: %w", err) - } - if !content.Equal(expectedSignatureBlobDesc, sigDesc) { - return fmt.Errorf("expected to get signature blob desc: %v, got: %v", expectedSignatureBlobDesc, sigDesc) - } - found = true + _, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{}) + if err == nil { + t.Fatalf("expected to fail with getting predecessors") } - if !found { - return fmt.Errorf("expected to find the signature with manifest desc: %v, but failed", expectedSignatureManifestDesc) + }) + + t.Run("artifact manifest exceds max blob size", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsDesc: []ocispec.Descriptor{ + { + Digest: validDigestWithAlgo2, + MediaType: "application/vnd.oci.artifact.manifest.v1+json", + Size: 4*1024*1024 + 1, + }, + }, + } + _, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{ + Digest: validDigestWithAlgo2, + }) + if err == nil { + t.Fatalf("expected to fail with artifact manifest exceds max blob size") + } + }) + + t.Run("image manifest exceds max blob size", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsDesc: []ocispec.Descriptor{ + { + Digest: validDigestWithAlgo2, + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: 4*1024*1024 + 1, + }, + }, + } + _, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{ + Digest: validDigestWithAlgo2, + }) + if err == nil { + t.Fatalf("expected to fail with image manifest exceds max blob size") + } + }) + + t.Run("artifact manifest fetchAll failed", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsDesc: []ocispec.Descriptor{ + { + Digest: validDigestWithAlgo, + MediaType: "application/vnd.oci.artifact.manifest.v1+json", + Size: 481, + }, + }, + FetchError: fmt.Errorf("failed to fetch all"), + } + _, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{ + Digest: validDigestWithAlgo, + }) + if err == nil { + t.Fatalf("expected to fail with fetchAll failed") + } + }) + + t.Run("image manifest fetchAll failed", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsDesc: []ocispec.Descriptor{ + { + Digest: validDigestWithAlgo, + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: 481, + }, + }, + FetchError: fmt.Errorf("failed to fetch all"), + } + _, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{ + Digest: validDigestWithAlgo, + }) + if err == nil { + t.Fatalf("expected to fail with fetchAll failed") + } + }) + + t.Run("artifact manifest marshal failed", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsDesc: []ocispec.Descriptor{ + { + Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690", + MediaType: "application/vnd.oci.artifact.manifest.v1+json", + Size: 15, + }, + }, + FetchContent: []byte("invalid content"), + } + _, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{ + Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690", + }) + if err == nil { + t.Fatalf("expected to fail with marshal failed") + } + }) + + t.Run("image manifest marshal failed", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsDesc: []ocispec.Descriptor{ + { + Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690", + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: 15, + }, + }, + FetchContent: []byte("invalid content"), + } + _, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{ + Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690", + }) + if err == nil { + t.Fatalf("expected to fail with marshal failed") + } + }) + + t.Run("no valid artifact manifest", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsDesc: []ocispec.Descriptor{ + { + Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + MediaType: "application/vnd.oci.artifact.manifest.v1+json", + Size: 2, + }, + }, + FetchContent: []byte("{}"), + } + descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{ + Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + }) + + if err != nil { + t.Fatalf("failed to get referrers: %v", err) + } + if len(descriptors) != 0 { + t.Fatalf("expected to get no referrers, but got: %v", descriptors) + } + }) + + t.Run("no valid image manifest", func(t *testing.T) { + store := &testStorage{ + store: &memory.Store{}, + PredecessorsDesc: []ocispec.Descriptor{ + { + Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: 2, + }, + }, + FetchContent: []byte("{}"), + } + descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{ + Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + }) + + if err != nil { + t.Fatalf("failed to get referrers: %v", err) + } + if len(descriptors) != 0 { + t.Fatalf("expected to get no referrers, but got: %v", descriptors) } - return nil }) - if err != nil { - t.Fatal(err) - } }