* Fix `StatusJSONResponse` usage Signed-off-by: Babak K. Shandiz <babakks@github.com> * Replace `assert` with `require` Signed-off-by: Babak K. Shandiz <babakks@github.com> * Improve assertion against errors Signed-off-by: Babak K. Shandiz <babakks@github.com> * Add `JSONErrorResponse` helper func Signed-off-by: Babak K. Shandiz <babakks@github.com> * Use `httpmock.JSONErrorResponse` Signed-off-by: Babak K. Shandiz <babakks@github.com> * Replace `StatusJSONResponse` to `JSONErrorResponse` for better readibility Signed-off-by: Babak K. Shandiz <babakks@github.com> * Fix improper use of `StatsJSONResponse` Signed-off-by: Babak K. Shandiz <babakks@github.com> --------- Signed-off-by: Babak K. Shandiz <babakks@github.com>
398 lines
9.9 KiB
Go
398 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|