diff --git a/go.mod b/go.mod index 38837b883..9dd32099f 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( google.golang.org/protobuf v1.36.4 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 + gotest.tools/v3 v3.0.3 ) require ( diff --git a/go.sum b/go.sum index 883ac72ce..4e7b719c8 100644 --- a/go.sum +++ b/go.sum @@ -206,6 +206,7 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= @@ -360,6 +361,7 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -433,6 +435,7 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -508,6 +511,7 @@ golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCR golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -544,11 +548,13 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY= google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= diff --git a/pkg/cmd/repo/autolink/delete/delete_test.go b/pkg/cmd/repo/autolink/delete/delete_test.go new file mode 100644 index 000000000..99233fb1e --- /dev/null +++ b/pkg/cmd/repo/autolink/delete/delete_test.go @@ -0,0 +1,257 @@ +package delete + +import ( + "bytes" + "errors" + "net/http" + "testing" + + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCmdDelete(t *testing.T) { + tests := []struct { + name string + input string + output deleteOptions + wantErr bool + errMsg string + }{ + { + name: "no argument", + input: "", + wantErr: true, + errMsg: "accepts 1 arg(s), received 0", + }, + { + name: "id provided", + input: "123", + output: deleteOptions{ID: "123"}, + }, + { + name: "yes flag", + input: "123 --yes", + output: deleteOptions{ID: "123", Confirmed: true}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: ios, + } + f.HttpClient = func() (*http.Client, error) { + return &http.Client{}, nil + } + + argv, err := shlex.Split(tt.input) + require.NoError(t, err) + + var gotOpts *deleteOptions + cmd := NewCmdDelete(f, func(opts *deleteOptions) error { + gotOpts = opts + return nil + }) + + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + if tt.wantErr { + require.EqualError(t, err, tt.errMsg) + } else { + require.NoError(t, err) + assert.Equal(t, tt.output.ID, gotOpts.ID) + assert.Equal(t, tt.output.Confirmed, gotOpts.Confirmed) + } + }) + } +} + +type stubAutolinkDeleter struct { + err error +} + +func (d *stubAutolinkDeleter) Delete(repo ghrepo.Interface, id string) error { + return d.err +} + +type stubAutolinkViewer struct { + autolink *shared.Autolink + err error +} + +func (g stubAutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) { + return g.autolink, g.err +} + +var errTestPrompt = errors.New("prompt error") +var errTestAutolinkClientView = errors.New("autolink client view error") +var errTestAutolinkClientDelete = errors.New("autolink client delete error") + +func TestDeleteRun(t *testing.T) { + tests := []struct { + name string + opts *deleteOptions + stubDeleter stubAutolinkDeleter + stubViewer stubAutolinkViewer + prompterStubs func(*prompter.PrompterMock) + + wantStdout string + wantStderr string + expectedErr error + expectedErrMsg string + }{ + { + name: "delete", + opts: &deleteOptions{ + ID: "123", + }, + stubViewer: stubAutolinkViewer{ + autolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + stubDeleter: stubAutolinkDeleter{ + err: nil, + }, + prompterStubs: func(pm *prompter.PrompterMock) { + pm.ConfirmDeletionFunc = func(_ string) error { + return nil + } + }, + wantStdout: "Autolink 123 has key prefix TICKET-.✓ Autolink 123 deleted from OWNER/REPO\n", + }, + { + name: "delete with confirm flag", + opts: &deleteOptions{ + ID: "123", + Confirmed: true, + }, + stubViewer: stubAutolinkViewer{ + autolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + stubDeleter: stubAutolinkDeleter{ + err: nil, + }, + wantStdout: "✓ Autolink 123 deleted from OWNER/REPO\n", + }, + { + name: "confirmation fails", + opts: &deleteOptions{ + ID: "123", + }, + stubViewer: stubAutolinkViewer{ + autolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + stubDeleter: stubAutolinkDeleter{ + err: nil, + }, + prompterStubs: func(pm *prompter.PrompterMock) { + pm.ConfirmDeletionFunc = func(_ string) error { + return errTestPrompt + } + }, + expectedErr: errTestPrompt, + expectedErrMsg: errTestPrompt.Error(), + wantStdout: "Autolink 123 has key prefix TICKET-.", + }, + { + name: "view error", + opts: &deleteOptions{ + ID: "123", + }, + stubViewer: stubAutolinkViewer{ + err: errTestAutolinkClientView, + }, + stubDeleter: stubAutolinkDeleter{ + err: nil, + }, + expectedErr: errTestAutolinkClientView, + expectedErrMsg: "error deleting autolink: autolink client view error", + }, + { + name: "delete error", + opts: &deleteOptions{ + ID: "123", + }, + stubViewer: stubAutolinkViewer{ + autolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + stubDeleter: stubAutolinkDeleter{ + err: errTestAutolinkClientDelete, + }, + prompterStubs: func(pm *prompter.PrompterMock) { + pm.ConfirmDeletionFunc = func(_ string) error { + return nil + } + }, + expectedErr: errTestAutolinkClientDelete, + expectedErrMsg: errTestAutolinkClientDelete.Error(), + wantStdout: "Autolink 123 has key prefix TICKET-.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, stderr := iostreams.Test() + + opts := tt.opts + opts.IO = ios + opts.Browser = &browser.Stub{} + + opts.IO = ios + opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } + + opts.AutolinkDeleteClient = &tt.stubDeleter + opts.AutolinkViewClient = &tt.stubViewer + + pm := &prompter.PrompterMock{} + if tt.prompterStubs != nil { + tt.prompterStubs(pm) + } + tt.opts.Prompter = pm + + err := deleteRun(opts) + + if tt.expectedErr != nil { + require.Error(t, err, "expected error but got none") + assert.ErrorIs(t, err, tt.expectedErr, "unexpected error") + assert.Equal(t, tt.expectedErrMsg, err.Error(), "unexpected error message") + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout") + assert.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") + }) + } +} diff --git a/pkg/cmd/repo/autolink/delete/http_test.go b/pkg/cmd/repo/autolink/delete/http_test.go new file mode 100644 index 000000000..a67e08acc --- /dev/null +++ b/pkg/cmd/repo/autolink/delete/http_test.go @@ -0,0 +1 @@ +package delete diff --git a/pkg/cmd/repo/autolink/view/view_test.go b/pkg/cmd/repo/autolink/view/view_test.go index bc0a1bf7f..4192822ea 100644 --- a/pkg/cmd/repo/autolink/view/view_test.go +++ b/pkg/cmd/repo/autolink/view/view_test.go @@ -49,13 +49,12 @@ func TestNewCmdView(t *testing.T) { { name: "json flag", input: "123 --json id", - output: viewOptions{}, + output: viewOptions{ID: "123"}, wantExporter: true, }, { name: "invalid json flag", input: "123 --json invalid", - output: viewOptions{}, wantErr: true, errMsg: "Unknown JSON field: \"invalid\"\nAvailable fields:\n id\n isAlphanumeric\n keyPrefix\n urlTemplate", }, @@ -91,17 +90,18 @@ func TestNewCmdView(t *testing.T) { } else { require.NoError(t, err) assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil) + assert.Equal(t, tt.output.ID, gotOpts.ID) } }) } } -type stubAutoLinkViewer struct { +type stubAutolinkViewer struct { autolink *shared.Autolink err error } -func (g stubAutoLinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) { +func (g stubAutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) { return g.autolink, g.err } @@ -115,7 +115,7 @@ func TestViewRun(t *testing.T) { tests := []struct { name string opts *viewOptions - stubViewer stubAutoLinkViewer + stubViewer stubAutolinkViewer expectedErr error wantStdout string }{ @@ -124,7 +124,7 @@ func TestViewRun(t *testing.T) { opts: &viewOptions{ ID: "1", }, - stubViewer: stubAutoLinkViewer{ + stubViewer: stubAutolinkViewer{ autolink: &shared.Autolink{ ID: 1, KeyPrefix: "TICKET-", @@ -150,7 +150,7 @@ func TestViewRun(t *testing.T) { return exporter }(), }, - stubViewer: stubAutoLinkViewer{ + stubViewer: stubAutolinkViewer{ autolink: &shared.Autolink{ ID: 1, KeyPrefix: "TICKET-", @@ -163,7 +163,7 @@ func TestViewRun(t *testing.T) { { name: "client error", opts: &viewOptions{}, - stubViewer: stubAutoLinkViewer{ + stubViewer: stubAutolinkViewer{ autolink: nil, err: testAutolinkClientViewError{}, },