Merge remote-tracking branch 'upstream/trunk' into attestation-refactor-policy

This commit is contained in:
Meredith Lancaster 2024-10-30 15:29:27 -06:00
commit fa2574c1a8
7 changed files with 340 additions and 244 deletions

View file

@ -15,32 +15,54 @@ import (
)
func TestLiveSigstoreVerifier(t *testing.T) {
t.Run("with invalid signature", func(t *testing.T) {
attestations := getAttestationsFor(t, "../test/data/sigstoreBundle-invalid-signature.json")
require.NotNil(t, attestations)
type testcase struct {
name string
attestations []*api.Attestation
expectErr bool
errContains string
}
testcases := []testcase{
{
name: "with invalid signature",
attestations: getAttestationsFor(t, "../test/data/sigstoreBundle-invalid-signature.json"),
expectErr: true,
errContains: "verifying with issuer \"sigstore.dev\"",
},
{
name: "with valid artifact and JSON lines file containing multiple Sigstore bundles",
attestations: getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl"),
},
{
name: "with invalid bundle version",
attestations: getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json"),
expectErr: true,
errContains: "unsupported bundle version",
},
{
name: "with no attestations",
attestations: []*api.Attestation{},
expectErr: true,
errContains: "no attestations were verified",
},
}
for _, tc := range testcases {
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
Logger: io.NewTestHandler(),
})
res := verifier.Verify(attestations, publicGoodPolicy(t))
require.Error(t, res.Error)
require.ErrorContains(t, res.Error, "verifying with issuer \"sigstore.dev\"")
require.Nil(t, res.VerifyResults)
})
res := verifier.Verify(tc.attestations, publicGoodPolicy(t))
t.Run("with valid artifact and JSON lines file containing multiple Sigstore bundles", func(t *testing.T) {
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
require.Len(t, attestations, 2)
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
Logger: io.NewTestHandler(),
})
res := verifier.Verify(attestations, publicGoodPolicy(t))
require.Len(t, res.VerifyResults, 2)
require.NoError(t, res.Error)
})
if tc.expectErr {
require.Error(t, res.Error, "test case: %s", tc.name)
require.ErrorContains(t, res.Error, tc.errContains, "test case: %s", tc.name)
require.Nil(t, res.VerifyResults, "test case: %s", tc.name)
} else {
require.Equal(t, len(tc.attestations), len(res.VerifyResults), "test case: %s", tc.name)
require.NoError(t, res.Error, "test case: %s", tc.name)
}
}
t.Run("with GitHub Sigstore artifact", func(t *testing.T) {
githubArtifactPath := test.NormalizeRelativePath("../test/data/github_provenance_demo-0.0.12-py3-none-any.whl")
@ -72,34 +94,6 @@ func TestLiveSigstoreVerifier(t *testing.T) {
require.Len(t, res.VerifyResults, 2)
require.NoError(t, res.Error)
})
t.Run("with invalid bundle version", func(t *testing.T) {
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json")
require.Len(t, attestations, 1)
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
Logger: io.NewTestHandler(),
})
res := verifier.Verify(attestations, publicGoodPolicy(t))
require.Len(t, res.VerifyResults, 0)
require.ErrorContains(t, res.Error, "unsupported bundle version")
})
t.Run("with no attestations", func(t *testing.T) {
attestations := []*api.Attestation{}
require.Len(t, attestations, 0)
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
Logger: io.NewTestHandler(),
TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"),
})
res := verifier.Verify(attestations, publicGoodPolicy(t))
require.Len(t, res.VerifyResults, 0)
require.NotNil(t, res.Error)
})
}
func publicGoodPolicy(t *testing.T) verify.PolicyBuilder {

View file

@ -13,29 +13,28 @@ var (
publicGoodBundlePath = test.NormalizeRelativePath("../test/data/psigstore-js-2.1.0-bundle.json")
)
var baseOptions = Options{
ArtifactPath: publicGoodArtifactPath,
BundlePath: publicGoodBundlePath,
DigestAlgorithm: "sha512",
Limit: 1,
Owner: "sigstore",
OIDCIssuer: "some issuer",
}
func TestAreFlagsValid(t *testing.T) {
t.Run("has invalid Repo value", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
DigestAlgorithm: "sha512",
OIDCIssuer: "some issuer",
Repo: "sigstoresigstore-js",
}
opts := baseOptions
opts.Repo = "sigstoresigstore-js"
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "invalid value provided for repo")
})
t.Run("invalid limit < 0", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
BundlePath: publicGoodBundlePath,
DigestAlgorithm: "sha512",
Owner: "sigstore",
OIDCIssuer: "some issuer",
Limit: 0,
}
t.Run("invalid limit == 0", func(t *testing.T) {
opts := baseOptions
opts.Limit = 0
err := opts.AreFlagsValid()
require.Error(t, err)
@ -43,19 +42,43 @@ func TestAreFlagsValid(t *testing.T) {
})
t.Run("invalid limit > 1000", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
BundlePath: publicGoodBundlePath,
DigestAlgorithm: "sha512",
Owner: "sigstore",
OIDCIssuer: "some issuer",
Limit: 1001,
}
opts := baseOptions
opts.Limit = 1001
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "limit 1001 not allowed, must be between 1 and 1000")
})
t.Run("returns error when UseBundleFromRegistry is true and ArtifactPath is not an OCI path", func(t *testing.T) {
opts := baseOptions
opts.BundlePath = ""
opts.UseBundleFromRegistry = true
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "bundle-from-oci flag can only be used with OCI artifact paths")
})
t.Run("does not return error when UseBundleFromRegistry is true and ArtifactPath is an OCI path", func(t *testing.T) {
opts := baseOptions
opts.ArtifactPath = "oci://sigstore/sigstore-js:2.1.0"
opts.BundlePath = ""
opts.UseBundleFromRegistry = true
err := opts.AreFlagsValid()
require.NoError(t, err)
})
t.Run("returns error when UseBundleFromRegistry is true and BundlePath is provided", func(t *testing.T) {
opts := baseOptions
opts.ArtifactPath = "oci://sigstore/sigstore-js:2.1.0"
opts.UseBundleFromRegistry = true
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "bundle-from-oci flag cannot be used with bundle-path flag")
})
}
func TestSetPolicyFlags(t *testing.T) {
@ -116,47 +139,4 @@ func TestSetPolicyFlags(t *testing.T) {
require.Equal(t, "sigstore", opts.Owner)
require.Equal(t, "^https://github/foo", opts.SANRegex)
})
t.Run("returns error when UseBundleFromRegistry is true and ArtifactPath is not an OCI path", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
DigestAlgorithm: "sha512",
Owner: "sigstore",
UseBundleFromRegistry: true,
Limit: 1,
}
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "bundle-from-oci flag can only be used with OCI artifact paths")
})
t.Run("does not return error when UseBundleFromRegistry is true and ArtifactPath is an OCI path", func(t *testing.T) {
opts := Options{
ArtifactPath: "oci://sigstore/sigstore-js:2.1.0",
DigestAlgorithm: "sha512",
OIDCIssuer: "some issuer",
Owner: "sigstore",
UseBundleFromRegistry: true,
Limit: 1,
}
err := opts.AreFlagsValid()
require.NoError(t, err)
})
t.Run("returns error when UseBundleFromRegistry is true and BundlePath is provided", func(t *testing.T) {
opts := Options{
ArtifactPath: "oci://sigstore/sigstore-js:2.1.0",
BundlePath: publicGoodBundlePath,
DigestAlgorithm: "sha512",
Owner: "sigstore",
UseBundleFromRegistry: true,
Limit: 1,
}
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "bundle-from-oci flag cannot be used with bundle-path flag")
})
}

View file

@ -36,18 +36,28 @@ func TestValidateSignerWorkflow(t *testing.T) {
providedSignerWorkflow string
expectedWorkflowRegex string
host string
expectErr bool
errContains string
}
testcases := []testcase{
{
name: "workflow with no host specified",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectErr: true,
errContains: "unknown host",
},
{
name: "workflow with host specified",
name: "workflow with default host",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
host: "github.com",
},
{
name: "workflow with workflow URL included",
providedSignerWorkflow: "github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
host: "github.com",
},
{
name: "workflow with GH_HOST set",
@ -61,44 +71,25 @@ func TestValidateSignerWorkflow(t *testing.T) {
expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
host: "authedhost.github.com",
},
{
name: "workflow with authenticated host",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
host: "authedhost.github.com",
},
}
for _, tc := range testcases {
cmdFactory := factory.New("test")
opts := &Options{
Config: cmdFactory.Config,
Config: factory.New("test").Config,
SignerWorkflow: tc.providedSignerWorkflow,
}
// All host resolution is done verify.go:RunE
if tc.host == "" {
// Set to default host
tc.host = "github.com"
}
opts.Hostname = tc.host
workflowRegex, err := validateSignerWorkflow(opts)
require.NoError(t, err)
require.Equal(t, tc.expectedWorkflowRegex, workflowRegex)
if tc.expectErr {
require.Error(t, err)
require.ErrorContains(t, err, tc.errContains)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectedWorkflowRegex, workflowRegex)
}
}
}
func TestValidateSignerWorkflowNoHost(t *testing.T) {
cmdFactory := factory.New("test")
opts := &Options{
Config: cmdFactory.Config,
SignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
}
workflowRegex, err := validateSignerWorkflow(opts)
require.Error(t, err)
require.ErrorContains(t, err, "unknown host")
require.Equal(t, "", workflowRegex)
}

View file

@ -83,6 +83,33 @@ func TestVerifyIntegration(t *testing.T) {
require.Error(t, err)
require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://github.com/fakeowner/fakerepo, got https://github.com/sigstore/sigstore-js")
})
t.Run("with no matching OIDC issuer", func(t *testing.T) {
opts := publicGoodOpts
opts.OIDCIssuer = "some-other-issuer"
err := runVerify(&opts)
require.Error(t, err)
require.ErrorContains(t, err, "expected Issuer to be some-other-issuer, got https://token.actions.githubusercontent.com")
})
t.Run("with invalid SAN", func(t *testing.T) {
opts := publicGoodOpts
opts.SAN = "fake san"
err := runVerify(&opts)
require.Error(t, err)
require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\"")
})
t.Run("with invalid SAN regex", func(t *testing.T) {
opts := publicGoodOpts
opts.SANRegex = "^https://github.com/sigstore/not-real/"
err := runVerify(&opts)
require.Error(t, err)
require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\"")
})
}
func TestVerifyIntegrationCustomIssuer(t *testing.T) {

View file

@ -453,75 +453,6 @@ func TestRunVerify(t *testing.T) {
require.ErrorContains(t, err, "failed to fetch attestations from wrong-owner")
})
// TODO: this test can only be tested with a live SigstoreVerifier
// add integration tests or HTTP mocked sigstore verifier tests
// to test this case
t.Run("with invalid OIDC issuer", func(t *testing.T) {
t.Skip()
opts := publicGoodOpts
opts.OIDCIssuer = "not-a-real-issuer"
require.Error(t, runVerify(&opts))
})
// TODO: this test can only be tested with a live SigstoreVerifier
// add integration tests or HTTP mocked sigstore verifier tests
// to test this case
t.Run("with SAN enforcement", func(t *testing.T) {
t.Skip()
opts := Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
APIClient: api.NewTestClient(),
DigestAlgorithm: "sha512",
Logger: logger,
OIDCIssuer: verification.GitHubOIDCIssuer,
Owner: "sigstore",
SAN: SigstoreSanValue,
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
}
require.Nil(t, runVerify(&opts))
})
// TODO: this test can only be tested with a live SigstoreVerifier
// add integration tests or HTTP mocked sigstore verifier tests
// to test this case
t.Run("with invalid SAN", func(t *testing.T) {
t.Skip()
opts := publicGoodOpts
opts.SAN = "fake san"
require.Error(t, runVerify(&opts))
})
// TODO: this test can only be tested with a live SigstoreVerifier
// add integration tests or HTTP mocked sigstore verifier tests
// to test this case
t.Run("with SAN regex enforcement", func(t *testing.T) {
t.Skip()
opts := publicGoodOpts
opts.SANRegex = SigstoreSanRegex
require.Nil(t, runVerify(&opts))
})
// TODO: this test can only be tested with a live SigstoreVerifier
// add integration tests or HTTP mocked sigstore verifier tests
// to test this case
t.Run("with invalid SAN regex", func(t *testing.T) {
t.Skip()
opts := publicGoodOpts
opts.SANRegex = "^https://github.com/sigstore/not-real/"
require.Error(t, runVerify(&opts))
})
// TODO: this test can only be tested with a live SigstoreVerifier
// add integration tests or HTTP mocked sigstore verifier tests
// to test this case
t.Run("with no matching OIDC issuer", func(t *testing.T) {
t.Skip()
opts := publicGoodOpts
opts.OIDCIssuer = "some-other-issuer"
require.Error(t, runVerify(&opts))
})
t.Run("with missing API client", func(t *testing.T) {
customOpts := publicGoodOpts
customOpts.APIClient = nil

View file

@ -15,6 +15,7 @@ import (
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/set"
@ -49,15 +50,16 @@ const (
)
type EditOptions struct {
HTTPClient *http.Client
Repository ghrepo.Interface
IO *iostreams.IOStreams
Edits EditRepositoryInput
AddTopics []string
RemoveTopics []string
InteractiveMode bool
Detector fd.Detector
Prompter iprompter
HTTPClient *http.Client
Repository ghrepo.Interface
IO *iostreams.IOStreams
Edits EditRepositoryInput
AddTopics []string
RemoveTopics []string
AcceptVisibilityChangeConsequences bool
InteractiveMode bool
Detector fd.Detector
Prompter iprompter
// Cache of current repo topics to avoid retrieving them
// in multiple flows.
topicsCache []string
@ -103,7 +105,16 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
To toggle a setting off, use the %[1]s--<flag>=false%[1]s syntax.
Note that changing repository visibility to private will cause loss of stars and watchers.
Changing repository visibility can have unexpected consequences including but not limited to:
- Losing stars and watchers, affecting repository ranking
- Detaching public forks from the network
- Disabling push rulesets
- Allowing access to GitHub Actions history and logs
When the %[1]s--visibility%[1]s flag is used, %[1]s--accept-visibility-change-consequences%[1]s flag is required.
For information on all the potential consequences, see <https://gh.io/setting-repository-visibility>
`, "`"),
Args: cobra.MaximumNArgs(1),
Example: heredoc.Doc(`
@ -142,6 +153,10 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
return cmdutil.FlagErrorf("specify properties to edit when not running interactively")
}
if opts.Edits.Visibility != nil && !opts.AcceptVisibilityChangeConsequences {
return cmdutil.FlagErrorf("use of --visibility flag requires --accept-visibility-change-consequences flag")
}
if runF != nil {
return runF(opts)
}
@ -167,6 +182,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowUpdateBranch, "allow-update-branch", "", "Allow a pull request head branch that is behind its base branch to be updated")
cmd.Flags().StringSliceVar(&opts.AddTopics, "add-topic", nil, "Add repository topic")
cmd.Flags().StringSliceVar(&opts.RemoveTopics, "remove-topic", nil, "Remove repository topic")
cmd.Flags().BoolVar(&opts.AcceptVisibilityChangeConsequences, "accept-visibility-change-consequences", false, "Accept the consequences of changing the repository visibility")
return cmd
}
@ -379,23 +395,26 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
}
opts.Edits.EnableProjects = &a
case optionVisibility:
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.ErrOut, "%s Danger zone: changing repository visibility can have unexpected consequences; consult https://gh.io/setting-repository-visibility before continuing.\n", cs.WarningIcon())
visibilityOptions := []string{"public", "private", "internal"}
selected, err := p.Select("Visibility", strings.ToLower(r.Visibility), visibilityOptions)
if err != nil {
return err
}
confirmed := true
if visibilityOptions[selected] == "private" &&
(r.StargazerCount > 0 || r.Watchers.TotalCount > 0) {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.ErrOut, "%s Changing the repository visibility to private will cause permanent loss of stars and watchers.\n", cs.WarningIcon())
confirmed, err = p.Confirm("Do you want to change visibility to private?", false)
if err != nil {
return err
}
selectedVisibility := visibilityOptions[selected]
if selectedVisibility != r.Visibility && (r.StargazerCount > 0 || r.Watchers.TotalCount > 0) {
fmt.Fprintf(opts.IO.ErrOut, "%s Changing the repository visibility to %s will cause permanent loss of %s and %s.\n", cs.WarningIcon(), selectedVisibility, text.Pluralize(r.StargazerCount, "star"), text.Pluralize(r.Watchers.TotalCount, "watcher"))
}
confirmed, err := p.Confirm(fmt.Sprintf("Do you want to change visibility to %s?", selectedVisibility), false)
if err != nil {
return err
}
if confirmed {
opts.Edits.Visibility = &visibilityOptions[selected]
opts.Edits.Visibility = &selectedVisibility
}
case optionMergeOptions:
var defaultMergeOptions []string

View file

@ -34,6 +34,63 @@ func TestNewCmdEdit(t *testing.T) {
},
},
},
{
name: "deny public visibility change without accepting consequences",
args: "--visibility public",
wantOpts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{},
},
wantErr: "use of --visibility flag requires --accept-visibility-change-consequences flag",
},
{
name: "allow public visibility change with accepting consequences",
args: "--visibility public --accept-visibility-change-consequences",
wantOpts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
Visibility: sp("public"),
},
},
},
{
name: "deny private visibility change without accepting consequences",
args: "--visibility private",
wantOpts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{},
},
wantErr: "use of --visibility flag requires --accept-visibility-change-consequences flag",
},
{
name: "allow private visibility change with accepting consequences",
args: "--visibility private --accept-visibility-change-consequences",
wantOpts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
Visibility: sp("private"),
},
},
},
{
name: "deny internal visibility change without accepting consequences",
args: "--visibility internal",
wantOpts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{},
},
wantErr: "use of --visibility flag requires --accept-visibility-change-consequences flag",
},
{
name: "allow internal visibility change with accepting consequences",
args: "--visibility internal --accept-visibility-change-consequences",
wantOpts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
Visibility: sp("internal"),
},
},
},
}
for _, tt := range tests {
@ -241,6 +298,109 @@ func Test_editRun_interactive(t *testing.T) {
}))
},
},
{
name: "skipping visibility without confirmation",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
promptStubs: func(pm *prompter.MockPrompter) {
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
func(_ string, _, opts []string) ([]int, error) {
return []int{8}, nil
})
pm.RegisterSelect("Visibility", []string{"public", "private", "internal"},
func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, "private")
})
pm.RegisterConfirm("Do you want to change visibility to private?", func(_ string, _ bool) (bool, error) {
return false, nil
})
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{
"data": {
"repository": {
"visibility": "public",
"description": "description",
"homePageUrl": "https://url.com",
"defaultBranchRef": {
"name": "main"
},
"stargazerCount": 10,
"isInOrganization": false,
"repositoryTopics": {
"nodes": [{
"topic": {
"name": "x"
}
}]
}
}
}
}`))
reg.Exclude(t, httpmock.REST("PATCH", "repos/OWNER/REPO"))
},
wantsStderr: "Changing the repository visibility to private will cause permanent loss of 10 stars and 0 watchers.",
},
{
name: "changing visibility with confirmation",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
promptStubs: func(pm *prompter.MockPrompter) {
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
func(_ string, _, opts []string) ([]int, error) {
return []int{8}, nil
})
pm.RegisterSelect("Visibility", []string{"public", "private", "internal"},
func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, "private")
})
pm.RegisterConfirm("Do you want to change visibility to private?", func(_ string, _ bool) (bool, error) {
return true, nil
})
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{
"data": {
"repository": {
"visibility": "public",
"description": "description",
"homePageUrl": "https://url.com",
"defaultBranchRef": {
"name": "main"
},
"stargazerCount": 10,
"watchers": {
"totalCount": 15
},
"isInOrganization": false,
"repositoryTopics": {
"nodes": [{
"topic": {
"name": "x"
}
}]
}
}
}
}`))
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, "private", payload["visibility"])
}))
},
wantsStderr: "Changing the repository visibility to private will cause permanent loss of 10 stars and 15 watchers",
},
{
name: "the rest",
opts: EditOptions{
@ -250,7 +410,7 @@ func Test_editRun_interactive(t *testing.T) {
promptStubs: func(pm *prompter.MockPrompter) {
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
func(_ string, _, opts []string) ([]int, error) {
return []int{0, 2, 3, 5, 6, 8, 9}, nil
return []int{0, 2, 3, 5, 6, 9}, nil
})
pm.RegisterInput("Default branch name", func(_, _ string) (string, error) {
return "trunk", nil
@ -267,13 +427,6 @@ func Test_editRun_interactive(t *testing.T) {
pm.RegisterConfirm("Convert into a template repository?", func(_ string, _ bool) (bool, error) {
return true, nil
})
pm.RegisterSelect("Visibility", []string{"public", "private", "internal"},
func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, "private")
})
pm.RegisterConfirm("Do you want to change visibility to private?", func(_ string, _ bool) (bool, error) {
return true, nil
})
pm.RegisterConfirm("Enable Wikis?", func(_ string, _ bool) (bool, error) {
return true, nil
})
@ -310,7 +463,6 @@ func Test_editRun_interactive(t *testing.T) {
assert.Equal(t, "https://zombo.com", payload["homepage"])
assert.Equal(t, true, payload["has_issues"])
assert.Equal(t, true, payload["has_projects"])
assert.Equal(t, "private", payload["visibility"])
assert.Equal(t, true, payload["is_template"])
assert.Equal(t, true, payload["has_wiki"])
}))
@ -484,7 +636,7 @@ func Test_editRun_interactive(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
ios, _, _, stderr := iostreams.Test()
ios.SetStdoutTTY(true)
ios.SetStdinTTY(true)
ios.SetStderrTTY(true)
@ -509,9 +661,11 @@ func Test_editRun_interactive(t *testing.T) {
if tt.wantsErr == "" {
require.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantsErr)
require.EqualError(t, err, tt.wantsErr)
return
}
assert.Contains(t, stderr.String(), tt.wantsStderr)
})
}
}