diff --git a/pkg/cmd/attestation/attestation.go b/pkg/cmd/attestation/attestation.go index d3b5f57f9..ec74e08ce 100644 --- a/pkg/cmd/attestation/attestation.go +++ b/pkg/cmd/attestation/attestation.go @@ -29,9 +29,9 @@ func NewCmdAttestation(f *cmdutil.Factory) *cobra.Command { `, "`"), } - root.AddCommand(download.NewDownloadCmd(f)) - root.AddCommand(inspect.NewInspectCmd(f)) - root.AddCommand(verify.NewVerifyCmd(f)) + root.AddCommand(download.NewDownloadCmd(f, nil)) + root.AddCommand(inspect.NewInspectCmd(f, nil)) + root.AddCommand(verify.NewVerifyCmd(f, nil)) root.AddCommand(tufrootverify.NewTUFRootVerifyCmd(f)) return root diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index 4a8b289bb..7cb1e15b9 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -17,7 +17,7 @@ import ( "github.com/spf13/cobra" ) -func NewDownloadCmd(f *cmdutil.Factory) *cobra.Command { +func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command { opts := &Options{} downloadCmd := &cobra.Command{ Use: "download [ | oci://]", @@ -85,6 +85,11 @@ func NewDownloadCmd(f *cmdutil.Factory) *cobra.Command { if err := auth.IsHostSupported(); err != nil { return err } + + if runF != nil { + return runF(opts) + } + if err := runDownload(opts); err != nil { return fmt.Errorf("Failed to download the artifact's bundle(s): %w", err) } diff --git a/pkg/cmd/attestation/download/download_test.go b/pkg/cmd/attestation/download/download_test.go index 4a988331a..800403401 100644 --- a/pkg/cmd/attestation/download/download_test.go +++ b/pkg/cmd/attestation/download/download_test.go @@ -2,7 +2,9 @@ package download import ( "bufio" + "bytes" "fmt" + "net/http" "os" "path" "testing" @@ -11,10 +13,164 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" "github.com/cli/cli/v2/pkg/cmd/attestation/logging" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestNewDownloadCmd(t *testing.T) { + testIO, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: testIO, + HttpClient: func() (*http.Client, error) { + reg := &httpmock.Registry{} + client := &http.Client{} + httpmock.ReplaceTripper(client, reg) + return client, nil + }, + } + tempDir := t.TempDir() + + testcases := []struct { + name string + cli string + wants Options + wantsErr bool + }{ + { + name: "Invalid digest-alg flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore --digest-alg sha384", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha384", + Owner: "sigstore", + OutputPath: tempDir, + Limit: 30, + }, + wantsErr: true, + }, + { + name: "Missing digest-alg flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + OutputPath: tempDir, + Limit: 30, + }, + wantsErr: false, + }, + { + name: "Missing owner and repo flags", + cli: "../test/data/sigstore-js-2.1.0.tgz", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + OutputPath: tempDir, + Limit: 30, + }, + wantsErr: true, + }, + { + name: "Has both owner and repo flags", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore --repo sigstore/sigstore-js", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + OutputPath: tempDir, + Repo: "sigstore/sigstore-js", + Limit: 30, + }, + wantsErr: true, + }, + { + name: "Uses default limit flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + OutputPath: tempDir, + Limit: 30, + }, + wantsErr: false, + }, + { + name: "Uses custom limit flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore --limit 101", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + OutputPath: tempDir, + Limit: 101, + }, + wantsErr: false, + }, + { + name: "Uses invalid limit flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore --limit 0", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + OutputPath: tempDir, + Limit: 0, + }, + wantsErr: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var opts *Options + cmd := NewDownloadCmd(f, func(o *Options) error { + opts = o + return nil + }) + + argv, err := shlex.Split(tc.cli) + assert.NoError(t, err) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + _, err = cmd.ExecuteC() + if tc.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm) + assert.Equal(t, tc.wants.Limit, opts.Limit) + assert.Equal(t, tc.wants.Owner, opts.Owner) + assert.Equal(t, tc.wants.Repo, opts.Repo) + }) + } +} + func TestRunDownload(t *testing.T) { tempDir := t.TempDir() diff --git a/pkg/cmd/attestation/download/options.go b/pkg/cmd/attestation/download/options.go index 0c353c039..215316a8a 100644 --- a/pkg/cmd/attestation/download/options.go +++ b/pkg/cmd/attestation/download/options.go @@ -4,11 +4,15 @@ import ( "fmt" "github.com/cli/cli/v2/pkg/cmd/attestation/api" - "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/digest" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" "github.com/cli/cli/v2/pkg/cmd/attestation/logging" ) +const ( + minLimit = 1 + maxLimit = 1000 +) + type Options struct { APIClient api.Client ArtifactPath string @@ -23,22 +27,9 @@ type Options struct { } func (opts *Options) AreFlagsValid() error { - if opts.Owner == "" { - return fmt.Errorf("owner must be provided") - } - - // DigestAlgorithm must not be empty - if opts.DigestAlgorithm == "" { - return fmt.Errorf("digest-alg cannot be empty") - } - - if !digest.IsValidDigestAlgorithm(opts.DigestAlgorithm) { - return fmt.Errorf("invalid digest algorithm '%s' provided in digest-alg", opts.DigestAlgorithm) - } - // Check that limit is between 1 and 1000 - if opts.Limit < 1 || opts.Limit > 1000 { - return fmt.Errorf("limit %d not allowed, must be between 1 and 1000", opts.Limit) + if opts.Limit < minLimit || opts.Limit > maxLimit { + return fmt.Errorf("limit %d not allowed, must be between %d and %d", opts.Limit, minLimit, maxLimit) } return nil diff --git a/pkg/cmd/attestation/download/options_test.go b/pkg/cmd/attestation/download/options_test.go index 5ac734568..800691d79 100644 --- a/pkg/cmd/attestation/download/options_test.go +++ b/pkg/cmd/attestation/download/options_test.go @@ -1,53 +1,34 @@ package download import ( + "fmt" "testing" "github.com/stretchr/testify/require" ) func TestAreFlagsValid(t *testing.T) { - t.Run("missing Owner", func(t *testing.T) { + tests := []struct { + name string + limit int + }{ + { + name: "Limit is too low", + limit: 0, + }, + { + name: "Limit is too high", + limit: 1001, + }, + } + for _, tc := range tests { opts := Options{ - DigestAlgorithm: "sha512", + Limit: tc.limit, } err := opts.AreFlagsValid() require.Error(t, err) - require.ErrorContains(t, err, "owner must be provided") - }) - - t.Run("missing DigestAlgorithm", func(t *testing.T) { - opts := Options{ - Owner: "github", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "digest-alg cannot be empty") - }) - - t.Run("Limit is too low", func(t *testing.T) { - opts := Options{ - DigestAlgorithm: "sha512", - Limit: 0, - Owner: "github", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "limit 0 not allowed, must be between 1 and 1000") - }) - - t.Run("Limit is too high", func(t *testing.T) { - opts := Options{ - DigestAlgorithm: "sha512", - Limit: 1001, - Owner: "github", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "limit 1001 not allowed, must be between 1 and 1000") - }) + expectedErrMsg := fmt.Sprintf("limit %d not allowed, must be between 1 and 1000", tc.limit) + require.ErrorContains(t, err, expectedErrMsg) + } } diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index 63631d7ba..8c034eff1 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -15,7 +15,7 @@ import ( "github.com/spf13/cobra" ) -func NewInspectCmd(f *cmdutil.Factory) *cobra.Command { +func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command { opts := &Options{} inspectCmd := &cobra.Command{ Use: "inspect [ | oci://] --bundle ", @@ -56,11 +56,6 @@ func NewInspectCmd(f *cmdutil.Factory) *cobra.Command { // set the artifact path opts.ArtifactPath = args[0] - // Check that the given flag combination is valid - if err := opts.AreFlagsValid(); err != nil { - return err - } - // Clean file path options opts.Clean() @@ -72,6 +67,11 @@ func NewInspectCmd(f *cmdutil.Factory) *cobra.Command { if err := auth.IsHostSupported(); err != nil { return err } + + if runF != nil { + return runF(opts) + } + if err := runInspect(opts); err != nil { return fmt.Errorf("Failed to inspect the artifact and bundle: %w", err) } diff --git a/pkg/cmd/attestation/inspect/inspect_test.go b/pkg/cmd/attestation/inspect/inspect_test.go index c72a52231..981cba810 100644 --- a/pkg/cmd/attestation/inspect/inspect_test.go +++ b/pkg/cmd/attestation/inspect/inspect_test.go @@ -1,12 +1,20 @@ package inspect import ( + "bytes" + "net/http" "testing" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" "github.com/cli/cli/v2/pkg/cmd/attestation/logging" "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,6 +28,113 @@ var ( bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json") ) +func TestNewInspectCmd(t *testing.T) { + testIO, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: testIO, + HttpClient: func() (*http.Client, error) { + reg := &httpmock.Registry{} + client := &http.Client{} + httpmock.ReplaceTripper(client, reg) + return client, nil + }, + } + + testcases := []struct { + name string + cli string + wants Options + wantsErr bool + }{ + { + name: "Invalid digest-alg flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --bundle ../test/data/sigstore-js-2.1.0-bundle.json --digest-alg sha384", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + DigestAlgorithm: "sha384", + OCIClient: oci.MockClient{}, + }, + wantsErr: true, + }, + { + name: "Use default digest-alg value", + cli: "../test/data/sigstore-js-2.1.0.tgz --bundle ../test/data/sigstore-js-2.1.0-bundle.json", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + DigestAlgorithm: "sha256", + OCIClient: oci.MockClient{}, + }, + wantsErr: false, + }, + { + name: "Use custom digest-alg value", + cli: "../test/data/sigstore-js-2.1.0.tgz --bundle ../test/data/sigstore-js-2.1.0-bundle.json --digest-alg sha512", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + DigestAlgorithm: "sha512", + OCIClient: oci.MockClient{}, + }, + wantsErr: false, + }, + { + name: "Missing bundle flag", + cli: "../test/data/sigstore-js-2.1.0.tgz", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + DigestAlgorithm: "sha256", + OCIClient: oci.MockClient{}, + }, + wantsErr: true, + }, + { + name: "Has both verbose and quiet flags", + cli: "../test/data/sigstore-js-2.1.0.tgz --bundle ../test/data/sigstore-js-2.1.0-bundle.json --verbose --quiet", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + DigestAlgorithm: "sha256", + OCIClient: oci.MockClient{}, + Verbose: true, + Quiet: true, + }, + wantsErr: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var opts *Options + cmd := NewInspectCmd(f, func(o *Options) error { + opts = o + return nil + }) + + argv, err := shlex.Split(tc.cli) + assert.NoError(t, err) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + _, err = cmd.ExecuteC() + if tc.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tc.wants.ArtifactPath, opts.ArtifactPath) + assert.Equal(t, tc.wants.BundlePath, opts.BundlePath) + assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm) + assert.Equal(t, tc.wants.JsonResult, opts.JsonResult) + assert.Equal(t, tc.wants.Verbose, opts.Verbose) + assert.Equal(t, tc.wants.Quiet, opts.Quiet) + }) + } +} + func TestRunInspect(t *testing.T) { opts := Options{ ArtifactPath: artifactPath, diff --git a/pkg/cmd/attestation/inspect/options.go b/pkg/cmd/attestation/inspect/options.go index eaedd73f1..4e0a3fc49 100644 --- a/pkg/cmd/attestation/inspect/options.go +++ b/pkg/cmd/attestation/inspect/options.go @@ -1,10 +1,8 @@ package inspect import ( - "fmt" "path/filepath" - "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/digest" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" "github.com/cli/cli/v2/pkg/cmd/attestation/logging" ) @@ -25,23 +23,3 @@ type Options struct { func (opts *Options) Clean() { opts.BundlePath = filepath.Clean(opts.BundlePath) } - -// AreFlagsValid checks that the provided flag combination is valid -// and returns an error otherwise -func (opts *Options) AreFlagsValid() error { - // either BundlePath or Repo must be set to configure offline or online mode - if opts.BundlePath == "" { - return fmt.Errorf("bundle must be provided") - } - - // DigestAlgorithm must not be empty - if opts.DigestAlgorithm == "" { - return fmt.Errorf("digest-alg cannot be empty") - } - - if !digest.IsValidDigestAlgorithm(opts.DigestAlgorithm) { - return fmt.Errorf("invalid digest algorithm '%s' provided in digest-alg", opts.DigestAlgorithm) - } - - return nil -} diff --git a/pkg/cmd/attestation/inspect/options_test.go b/pkg/cmd/attestation/inspect/options_test.go deleted file mode 100644 index 5994dff56..000000000 --- a/pkg/cmd/attestation/inspect/options_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package inspect - -import ( - "testing" - - "github.com/cli/cli/v2/pkg/cmd/attestation/test" - - "github.com/stretchr/testify/require" -) - -func TestAreFlagsValid(t *testing.T) { - artifactPath := test.NormalizeRelativePath("../test/data/public-good/sigstore-js-2.1.0.tgz") - bundlePath := test.NormalizeRelativePath("../test/data/public-good/sigstore-js-2.1.0-bundle.json") - - t.Run("missing BundlePath", func(t *testing.T) { - opts := Options{ - ArtifactPath: artifactPath, - DigestAlgorithm: "sha512", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "bundle must be provided") - }) - - t.Run("missing DigestAlgorithm", func(t *testing.T) { - opts := Options{ - ArtifactPath: artifactPath, - BundlePath: bundlePath, - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "digest-alg cannot be empty") - }) - - t.Run("invalid DigestAlgorithm", func(t *testing.T) { - opts := Options{ - ArtifactPath: artifactPath, - BundlePath: bundlePath, - DigestAlgorithm: "sha1", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "invalid digest algorithm 'sha1' provided in digest-alg") - }) -} diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index 19b1f8008..b38f31369 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/cli/cli/v2/pkg/cmd/attestation/api" - "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/digest" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" "github.com/cli/cli/v2/pkg/cmd/attestation/logging" ) @@ -63,35 +62,6 @@ func (opts *Options) SetPolicyFlags() { // AreFlagsValid checks that the provided flag combination is valid // and returns an error otherwise func (opts *Options) AreFlagsValid() error { - // either BundlePath or Repo must be set to configure offline or online mode - if opts.BundlePath == "" && opts.Repo == "" && opts.Owner == "" { - return fmt.Errorf("either bundle or repo or owner must be provided") - } - - // DigestAlgorithm must not be empty - if opts.DigestAlgorithm == "" { - return fmt.Errorf("digest-alg cannot be empty") - } - - if !digest.IsValidDigestAlgorithm(opts.DigestAlgorithm) { - return fmt.Errorf("invalid digtest algorithm '%s' provided in digest-alg", opts.DigestAlgorithm) - } - - // OIDCIssuer must not be empty - if opts.OIDCIssuer == "" { - return fmt.Errorf("cert-oidc-issuer cannot be empty") - } - - // either Owner or Repo must be supplied - if opts.Owner == "" && opts.Repo == "" { - return fmt.Errorf("owner or repo must be provided") - } - - // SAN or SAN regex are mutually exclusive, only one can be provided - if opts.SAN != "" && opts.SANRegex != "" { - return fmt.Errorf("cert-identity and cert-identity-regex cannot both be provided") - } - // check that Repo is in the expected format if provided if opts.Repo != "" { // we expect the repo argument to be in the format / diff --git a/pkg/cmd/attestation/verify/options_test.go b/pkg/cmd/attestation/verify/options_test.go index 305f54f95..a7430017e 100644 --- a/pkg/cmd/attestation/verify/options_test.go +++ b/pkg/cmd/attestation/verify/options_test.go @@ -14,60 +14,6 @@ var ( ) func TestAreFlagsValid(t *testing.T) { - t.Run("missing BundlePath, Repo, and Owner", func(t *testing.T) { - opts := Options{ - ArtifactPath: publicGoodArtifactPath, - DigestAlgorithm: "sha512", - OIDCIssuer: "some issuer", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "either bundle or repo or owner must be provided") - }) - - t.Run("missing DigestAlgorithm", func(t *testing.T) { - opts := Options{ - ArtifactPath: publicGoodArtifactPath, - BundlePath: publicGoodBundlePath, - OIDCIssuer: "some issuer", - Owner: "sigstore", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "digest-alg cannot be empty") - }) - - t.Run("missing Owner and Repo", func(t *testing.T) { - opts := Options{ - ArtifactPath: publicGoodArtifactPath, - BundlePath: publicGoodBundlePath, - DigestAlgorithm: "sha512", - OIDCIssuer: "some issuer", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "owner or repo must be provided") - }) - - t.Run("has both SAN and SANRegex", func(t *testing.T) { - opts := Options{ - ArtifactPath: publicGoodArtifactPath, - BundlePath: publicGoodBundlePath, - DigestAlgorithm: "sha512", - OIDCIssuer: "some issuer", - Owner: "sigstore", - SAN: "some san", - SANRegex: "^some san regex$", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "cert-identity and cert-identity-regex cannot both be provided") - }) - t.Run("has invalid Repo value", func(t *testing.T) { opts := Options{ ArtifactPath: publicGoodArtifactPath, @@ -81,19 +27,6 @@ func TestAreFlagsValid(t *testing.T) { require.ErrorContains(t, err, "invalid value provided for repo") }) - t.Run("missing OIDCIssuer", func(t *testing.T) { - opts := Options{ - ArtifactPath: publicGoodArtifactPath, - BundlePath: publicGoodBundlePath, - DigestAlgorithm: "sha512", - Owner: "sigstore", - } - - err := opts.AreFlagsValid() - require.Error(t, err) - require.ErrorContains(t, err, "cert-oidc-issuer cannot be empty") - }) - t.Run("invalid limit < 0", func(t *testing.T) { opts := Options{ ArtifactPath: publicGoodArtifactPath, diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index a379e2a2a..baa42373d 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -19,7 +19,7 @@ import ( var ErrNoMatchingSLSAPredicate = fmt.Errorf("the attestation does not have the expected SLSA predicate type: %s", SLSAPredicateType) -func NewVerifyCmd(f *cmdutil.Factory) *cobra.Command { +func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command { opts := &Options{} verifyCmd := &cobra.Command{ Use: "verify ", @@ -100,6 +100,11 @@ func NewVerifyCmd(f *cmdutil.Factory) *cobra.Command { if err := auth.IsHostSupported(); err != nil { return err } + + if runF != nil { + return runF(opts) + } + if err := runVerify(opts); err != nil { return fmt.Errorf("Failed to verify the artifact: %w", err) } diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index d7b9cdba3..929a1ee73 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -1,6 +1,8 @@ package verify import ( + "bytes" + "net/http" "testing" "github.com/cli/cli/v2/pkg/cmd/attestation/api" @@ -8,6 +10,12 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/logging" "github.com/cli/cli/v2/pkg/cmd/attestation/test" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" "github.com/in-toto/in-toto-golang/in_toto" "github.com/sigstore/sigstore-go/pkg/verify" @@ -20,6 +28,199 @@ const ( SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/" ) +func TestNewVerifyCmd(t *testing.T) { + testIO, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: testIO, + HttpClient: func() (*http.Client, error) { + reg := &httpmock.Registry{} + client := &http.Client{} + httpmock.ReplaceTripper(client, reg) + return client, nil + }, + } + + testcases := []struct { + name string + cli string + wants Options + wantsErr bool + }{ + { + name: "Invalid digest-alg flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --bundle ../test/data/sigstore-js-2.1.0-bundle.json --digest-alg sha384 --owner sigstore", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + DigestAlgorithm: "sha384", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + }, + wantsErr: true, + }, + { + name: "Use default digest-alg value", + cli: "../test/data/sigstore-js-2.1.0.tgz --bundle ../test/data/sigstore-js-2.1.0-bundle.json --owner sigstore", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: false, + }, + { + name: "Use custom digest-alg value", + cli: "../test/data/sigstore-js-2.1.0.tgz --bundle ../test/data/sigstore-js-2.1.0-bundle.json --digest-alg sha512 --owner sigstore", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + DigestAlgorithm: "sha512", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: false, + }, + { + name: "Missing owner and repo flags", + cli: "../test/data/sigstore-js-2.1.0.tgz", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + Limit: 30, + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: true, + }, + { + name: "Has both owner and repo flags", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore --repo sigstore/sigstore-js", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + Repo: "sigstore/sigstore-js", + Limit: 30, + }, + wantsErr: true, + }, + { + name: "Uses default limit flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: false, + }, + { + name: "Uses custom limit flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore --limit 101", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + Limit: 101, + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: false, + }, + { + name: "Uses invalid limit flag", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore --limit 0", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + Limit: 0, + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: true, + }, + { + name: "Has both verbose and quiet flags", + cli: "../test/data/sigstore-js-2.1.0.tgz --bundle ../test/data/sigstore-js-2.1.0-bundle.json --owner sigstore --verbose --quiet", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Verbose: true, + Quiet: true, + }, + wantsErr: true, + }, + { + name: "Has both cert-identity and cert-identity-regex flags", + cli: "../test/data/sigstore-js-2.1.0.tgz --owner sigstore --cert-identity https://github.com/sigstore/ --cert-identity-regex ^https://github.com/sigstore/", + wants: Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SAN: "https://github.com/sigstore/", + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var opts *Options + cmd := NewVerifyCmd(f, func(o *Options) error { + opts = o + return nil + }) + + argv, err := shlex.Split(tc.cli) + assert.NoError(t, err) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + _, err = cmd.ExecuteC() + if tc.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tc.wants.ArtifactPath, opts.ArtifactPath) + assert.Equal(t, tc.wants.BundlePath, opts.BundlePath) + assert.Equal(t, tc.wants.CustomTrustedRoot, opts.CustomTrustedRoot) + assert.Equal(t, tc.wants.DenySelfHostedRunner, opts.DenySelfHostedRunner) + assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm) + assert.Equal(t, tc.wants.JsonResult, opts.JsonResult) + assert.Equal(t, tc.wants.Limit, opts.Limit) + assert.Equal(t, tc.wants.NoPublicGood, opts.NoPublicGood) + assert.Equal(t, tc.wants.OIDCIssuer, opts.OIDCIssuer) + assert.Equal(t, tc.wants.Owner, opts.Owner) + assert.Equal(t, tc.wants.Quiet, opts.Quiet) + assert.Equal(t, tc.wants.Repo, opts.Repo) + assert.Equal(t, tc.wants.SAN, opts.SAN) + assert.Equal(t, tc.wants.SANRegex, opts.SANRegex) + assert.Equal(t, tc.wants.Verbose, opts.Verbose) + }) + } +} + func TestRunVerify(t *testing.T) { logger := logging.NewTestLogger()