458 lines
11 KiB
Go
458 lines
11 KiB
Go
package edit
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/cli/cli/v2/api"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
|
"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 TestNewCmdEdit(t *testing.T) {
|
|
tmpFile := filepath.Join(t.TempDir(), "my-body.md")
|
|
err := os.WriteFile(tmpFile, []byte("a body from file"), 0600)
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
stdin string
|
|
output EditOptions
|
|
wantsErr bool
|
|
}{
|
|
{
|
|
name: "no argument",
|
|
input: "",
|
|
output: EditOptions{},
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "issue number argument",
|
|
input: "23",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Interactive: true,
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "title flag",
|
|
input: "23 --title test",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Title: prShared.EditableString{
|
|
Value: "test",
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "body flag",
|
|
input: "23 --body test",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Body: prShared.EditableString{
|
|
Value: "test",
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "body from stdin",
|
|
input: "23 --body-file -",
|
|
stdin: "this is on standard input",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Body: prShared.EditableString{
|
|
Value: "this is on standard input",
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "body from file",
|
|
input: fmt.Sprintf("23 --body-file '%s'", tmpFile),
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Body: prShared.EditableString{
|
|
Value: "a body from file",
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "add-assignee flag",
|
|
input: "23 --add-assignee monalisa,hubot",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Assignees: prShared.EditableSlice{
|
|
Add: []string{"monalisa", "hubot"},
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "remove-assignee flag",
|
|
input: "23 --remove-assignee monalisa,hubot",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Assignees: prShared.EditableSlice{
|
|
Remove: []string{"monalisa", "hubot"},
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "add-label flag",
|
|
input: "23 --add-label feature,TODO,bug",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Labels: prShared.EditableSlice{
|
|
Add: []string{"feature", "TODO", "bug"},
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "remove-label flag",
|
|
input: "23 --remove-label feature,TODO,bug",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Labels: prShared.EditableSlice{
|
|
Remove: []string{"feature", "TODO", "bug"},
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "add-project flag",
|
|
input: "23 --add-project Cleanup,Roadmap",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Projects: prShared.EditableSlice{
|
|
Add: []string{"Cleanup", "Roadmap"},
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "remove-project flag",
|
|
input: "23 --remove-project Cleanup,Roadmap",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Projects: prShared.EditableSlice{
|
|
Remove: []string{"Cleanup", "Roadmap"},
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
{
|
|
name: "milestone flag",
|
|
input: "23 --milestone GA",
|
|
output: EditOptions{
|
|
SelectorArg: "23",
|
|
Editable: prShared.Editable{
|
|
Milestone: prShared.EditableString{
|
|
Value: "GA",
|
|
Edited: true,
|
|
},
|
|
},
|
|
},
|
|
wantsErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ios, stdin, _, _ := iostreams.Test()
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
|
|
if tt.stdin != "" {
|
|
_, _ = stdin.WriteString(tt.stdin)
|
|
}
|
|
|
|
f := &cmdutil.Factory{
|
|
IOStreams: ios,
|
|
}
|
|
|
|
argv, err := shlex.Split(tt.input)
|
|
assert.NoError(t, err)
|
|
|
|
var gotOpts *EditOptions
|
|
cmd := NewCmdEdit(f, func(opts *EditOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.Flags().BoolP("help", "x", false, "")
|
|
|
|
cmd.SetArgs(argv)
|
|
cmd.SetIn(&bytes.Buffer{})
|
|
cmd.SetOut(&bytes.Buffer{})
|
|
cmd.SetErr(&bytes.Buffer{})
|
|
|
|
_, err = cmd.ExecuteC()
|
|
if tt.wantsErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg)
|
|
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
|
assert.Equal(t, tt.output.Editable, gotOpts.Editable)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_editRun(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input *EditOptions
|
|
httpStubs func(*testing.T, *httpmock.Registry)
|
|
stdout string
|
|
stderr string
|
|
}{
|
|
{
|
|
name: "non-interactive",
|
|
input: &EditOptions{
|
|
SelectorArg: "123",
|
|
Interactive: false,
|
|
Editable: prShared.Editable{
|
|
Title: prShared.EditableString{
|
|
Value: "new title",
|
|
Edited: true,
|
|
},
|
|
Body: prShared.EditableString{
|
|
Value: "new body",
|
|
Edited: true,
|
|
},
|
|
Assignees: prShared.EditableSlice{
|
|
Add: []string{"monalisa", "hubot"},
|
|
Remove: []string{"octocat"},
|
|
Edited: true,
|
|
},
|
|
Labels: prShared.EditableSlice{
|
|
Add: []string{"feature", "TODO", "bug"},
|
|
Remove: []string{"docs"},
|
|
Edited: true,
|
|
},
|
|
Projects: prShared.EditableSlice{
|
|
Add: []string{"Cleanup", "Roadmap"},
|
|
Remove: []string{"Features"},
|
|
Edited: true,
|
|
},
|
|
Milestone: prShared.EditableString{
|
|
Value: "GA",
|
|
Edited: true,
|
|
},
|
|
Metadata: api.RepoMetadataResult{
|
|
Labels: []api.RepoLabel{
|
|
{Name: "docs", ID: "DOCSID"},
|
|
},
|
|
},
|
|
},
|
|
FetchOptions: prShared.FetchOptions,
|
|
},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
mockIssueGet(t, reg)
|
|
mockRepoMetadata(t, reg)
|
|
mockIssueUpdate(t, reg)
|
|
mockIssueUpdateLabels(t, reg)
|
|
},
|
|
stdout: "https://github.com/OWNER/REPO/issue/123\n",
|
|
},
|
|
{
|
|
name: "interactive",
|
|
input: &EditOptions{
|
|
SelectorArg: "123",
|
|
Interactive: true,
|
|
FieldsToEditSurvey: func(eo *prShared.Editable) error {
|
|
eo.Title.Edited = true
|
|
eo.Body.Edited = true
|
|
eo.Assignees.Edited = true
|
|
eo.Labels.Edited = true
|
|
eo.Projects.Edited = true
|
|
eo.Milestone.Edited = true
|
|
return nil
|
|
},
|
|
EditFieldsSurvey: func(eo *prShared.Editable, _ string) error {
|
|
eo.Title.Value = "new title"
|
|
eo.Body.Value = "new body"
|
|
eo.Assignees.Value = []string{"monalisa", "hubot"}
|
|
eo.Labels.Value = []string{"feature", "TODO", "bug"}
|
|
eo.Projects.Value = []string{"Cleanup", "Roadmap"}
|
|
eo.Milestone.Value = "GA"
|
|
return nil
|
|
},
|
|
FetchOptions: prShared.FetchOptions,
|
|
DetermineEditor: func() (string, error) { return "vim", nil },
|
|
},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
mockIssueGet(t, reg)
|
|
mockRepoMetadata(t, reg)
|
|
mockIssueUpdate(t, reg)
|
|
},
|
|
stdout: "https://github.com/OWNER/REPO/issue/123\n",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
ios, _, stdout, stderr := iostreams.Test()
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
|
|
reg := &httpmock.Registry{}
|
|
defer reg.Verify(t)
|
|
tt.httpStubs(t, reg)
|
|
|
|
httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
|
|
baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
|
|
|
tt.input.IO = ios
|
|
tt.input.HttpClient = httpClient
|
|
tt.input.BaseRepo = baseRepo
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := editRun(tt.input)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.stdout, stdout.String())
|
|
assert.Equal(t, tt.stderr, stderr.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func mockIssueGet(_ *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query IssueByNumber\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
|
|
"number": 123,
|
|
"url": "https://github.com/OWNER/REPO/issue/123"
|
|
} } } }`),
|
|
)
|
|
}
|
|
|
|
func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "assignableUsers": {
|
|
"nodes": [
|
|
{ "login": "hubot", "id": "HUBOTID" },
|
|
{ "login": "MonaLisa", "id": "MONAID" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryLabelList\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "labels": {
|
|
"nodes": [
|
|
{ "name": "feature", "id": "FEATUREID" },
|
|
{ "name": "TODO", "id": "TODOID" },
|
|
{ "name": "bug", "id": "BUGID" },
|
|
{ "name": "docs", "id": "DOCSID" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryMilestoneList\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "milestones": {
|
|
"nodes": [
|
|
{ "title": "GA", "id": "GAID" },
|
|
{ "title": "Big One.oh", "id": "BIGONEID" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "projects": {
|
|
"nodes": [
|
|
{ "name": "Cleanup", "id": "CLEANUPID" },
|
|
{ "name": "Roadmap", "id": "ROADMAPID" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "organization": { "projects": {
|
|
"nodes": [
|
|
{ "name": "Triage", "id": "TRIAGEID" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
}
|
|
|
|
func mockIssueUpdate(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation IssueUpdate\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "updateIssue": { "__typename": "" } } }`,
|
|
func(inputs map[string]interface{}) {}),
|
|
)
|
|
}
|
|
|
|
func mockIssueUpdateLabels(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation LabelAdd\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "addLabelsToLabelable": { "__typename": "" } } }`,
|
|
func(inputs map[string]interface{}) {}),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation LabelRemove\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{ "data": { "removeLabelsFromLabelable": { "__typename": "" } } }`,
|
|
func(inputs map[string]interface{}) {}),
|
|
)
|
|
}
|