package edit import ( "bytes" "fmt" "io/ioutil" "net/http" "path/filepath" "testing" "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 := ioutil.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) { io, stdin, _, _ := iostreams.Test() io.SetStdoutTTY(true) io.SetStdinTTY(true) io.SetStderrTTY(true) if tt.stdin != "" { _, _ = stdin.WriteString(tt.stdin) } f := &cmdutil.Factory{ IOStreams: io, } 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, }, }, FetchOptions: prShared.FetchOptions, }, 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", }, { 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 { io, _, stdout, stderr := iostreams.Test() io.SetStdoutTTY(true) io.SetStdinTTY(true) io.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 = io 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" } ], "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": { "issue": { "id": "123" } } } }`, func(inputs map[string]interface{}) {}), ) }