package delete import ( "bytes" "fmt" "net/http" "testing" "time" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/cmd/gist/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" ghAPI "github.com/cli/go-gh/v2/pkg/api" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewCmdDelete(t *testing.T) { tests := []struct { name string cli string tty bool want DeleteOptions wantErr bool wantErrMsg string }{ { name: "valid selector", cli: "123", tty: true, want: DeleteOptions{ Selector: "123", }, }, { name: "valid selector, no ID supplied", cli: "", tty: true, want: DeleteOptions{ Selector: "", }, }, { name: "no ID supplied with --yes", cli: "--yes", tty: true, want: DeleteOptions{ Selector: "", }, }, { name: "selector with --yes, no tty", cli: "123 --yes", tty: false, want: DeleteOptions{ Selector: "123", }, }, { name: "ID arg without --yes, no tty", cli: "123", tty: false, want: DeleteOptions{ Selector: "", }, wantErr: true, wantErrMsg: "--yes required when not running interactively", }, { name: "no ID supplied with --yes, no tty", cli: "--yes", tty: false, want: DeleteOptions{ Selector: "", }, wantErr: true, wantErrMsg: "id or url argument required in non-interactive mode", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { io, _, _, _ := iostreams.Test() f := &cmdutil.Factory{ IOStreams: io, } io.SetStdinTTY(tt.tty) io.SetStdoutTTY(tt.tty) argv, err := shlex.Split(tt.cli) assert.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 { assert.EqualError(t, err, tt.wantErrMsg) return } assert.NoError(t, err) assert.Equal(t, tt.want.Selector, gotOpts.Selector) }) } } func Test_deleteRun(t *testing.T) { tests := []struct { name string opts *DeleteOptions cancel bool httpStubs func(*httpmock.Registry) mockPromptGists bool noGists bool wantErr bool wantStdout string wantStderr string }{ { name: "successfully delete", opts: &DeleteOptions{ Selector: "1234", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, wantStdout: "✓ Gist \"cool.txt\" deleted\n", }, { name: "successfully delete with prompt", opts: &DeleteOptions{ Selector: "", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, mockPromptGists: true, wantStdout: "✓ Gist \"cool.txt\" deleted\n", }, { name: "successfully delete with --yes", opts: &DeleteOptions{ Selector: "1234", Confirmed: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, wantStdout: "✓ Gist \"cool.txt\" deleted\n", }, { name: "successfully delete with prompt and --yes", opts: &DeleteOptions{ Selector: "", Confirmed: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, mockPromptGists: true, wantStdout: "✓ Gist \"cool.txt\" deleted\n", }, { name: "cancel delete with id", opts: &DeleteOptions{ Selector: "1234", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) }, cancel: true, wantErr: true, }, { name: "cancel delete with url", opts: &DeleteOptions{ Selector: "https://gist.github.com/myrepo/1234", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) }, cancel: true, wantErr: true, }, { name: "cancel delete with prompt", opts: &DeleteOptions{ Selector: "", }, httpStubs: func(reg *httpmock.Registry) {}, mockPromptGists: true, cancel: true, wantErr: true, }, { name: "not owned by you", opts: &DeleteOptions{ Selector: "1234", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(404, "{}")) }, wantErr: true, wantStderr: "unable to delete gist \"cool.txt\": either the gist is not found or it is not owned by you", }, { name: "not found", opts: &DeleteOptions{ Selector: "1234", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), httpmock.StatusStringResponse(404, "{}")) }, wantErr: true, wantStderr: "not found", }, { name: "no gists", opts: &DeleteOptions{ Selector: "", }, httpStubs: func(reg *httpmock.Registry) {}, mockPromptGists: true, noGists: true, wantStdout: "No gists found.\n", }, } for _, tt := range tests { pm := prompter.NewMockPrompter(t) if !tt.opts.Confirmed { pm.RegisterConfirm("Delete \"cool.txt\" gist?", func(_ string, _ bool) (bool, error) { return !tt.cancel, nil }) } reg := &httpmock.Registry{} tt.httpStubs(reg) if tt.mockPromptGists { if tt.noGists { reg.Register( httpmock.GraphQL(`query GistList\b`), httpmock.StringResponse( `{ "data": { "viewer": { "gists": { "nodes": [] }} } }`), ) } else { sixHours, _ := time.ParseDuration("6h") sixHoursAgo := time.Now().Add(-sixHours) reg.Register( httpmock.GraphQL(`query GistList\b`), httpmock.StringResponse(fmt.Sprintf( `{ "data": { "viewer": { "gists": { "nodes": [ { "name": "1234", "files": [{ "name": "cool.txt" }], "updatedAt": "%s", "isPublic": true } ] } } } }`, sixHoursAgo.Format(time.RFC3339), )), ) pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, _ []string) (int, error) { return 0, nil }) } } tt.opts.Prompter = pm tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } tt.opts.Config = func() (gh.Config, error) { return config.NewBlankConfig(), nil } ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(true) ios.SetStdinTTY(false) tt.opts.IO = ios t.Run(tt.name, func(t *testing.T) { err := deleteRun(tt.opts) reg.Verify(t) if tt.wantErr { assert.Error(t, err) if tt.wantStderr != "" { assert.EqualError(t, err, tt.wantStderr) } return } assert.NoError(t, err) assert.Equal(t, tt.wantStdout, stdout.String()) assert.Equal(t, tt.wantStderr, stderr.String()) }) } } func Test_gistDelete(t *testing.T) { tests := []struct { name string httpStubs func(*httpmock.Registry) hostname string gistID string wantErr error wantErrString string }{ { name: "successful delete", httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(204, "{}"), ) }, hostname: "github.com", gistID: "1234", }, { name: "when a gist is not found, it returns a NotFoundError", httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(404, "{}"), ) }, hostname: "github.com", gistID: "1234", wantErr: shared.NotFoundErr, // To make sure we return the pre-defined error instance. wantErrString: "not found", }, { name: "when there is a non-404 error deleting the gist, that error is returned", httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("DELETE", "gists/1234"), httpmock.JSONErrorResponse(500, ghAPI.HTTPError{ StatusCode: 500, Message: "arbitrary error", }), ) }, hostname: "github.com", gistID: "1234", wantErrString: "HTTP 500: arbitrary error (https://api.github.com/gists/1234)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} tt.httpStubs(reg) client := api.NewClientFromHTTP(&http.Client{Transport: reg}) err := deleteGist(client, tt.hostname, tt.gistID) if tt.wantErrString == "" && tt.wantErr == nil { require.NoError(t, err) } else { if tt.wantErrString != "" { require.EqualError(t, err, tt.wantErrString) } if tt.wantErr != nil { require.ErrorIs(t, err, tt.wantErr) } } }) } }