github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/issue/edit/edit_test.go (about)

     1  package edit
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"path/filepath"
     9  	"testing"
    10  
    11  	"github.com/cli/cli/internal/ghrepo"
    12  	prShared "github.com/cli/cli/pkg/cmd/pr/shared"
    13  	"github.com/cli/cli/pkg/cmdutil"
    14  	"github.com/cli/cli/pkg/httpmock"
    15  	"github.com/cli/cli/pkg/iostreams"
    16  	"github.com/google/shlex"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func TestNewCmdEdit(t *testing.T) {
    22  	tmpFile := filepath.Join(t.TempDir(), "my-body.md")
    23  	err := ioutil.WriteFile(tmpFile, []byte("a body from file"), 0600)
    24  	require.NoError(t, err)
    25  
    26  	tests := []struct {
    27  		name     string
    28  		input    string
    29  		stdin    string
    30  		output   EditOptions
    31  		wantsErr bool
    32  	}{
    33  		{
    34  			name:     "no argument",
    35  			input:    "",
    36  			output:   EditOptions{},
    37  			wantsErr: true,
    38  		},
    39  		{
    40  			name:  "issue number argument",
    41  			input: "23",
    42  			output: EditOptions{
    43  				SelectorArg: "23",
    44  				Interactive: true,
    45  			},
    46  			wantsErr: false,
    47  		},
    48  		{
    49  			name:  "title flag",
    50  			input: "23 --title test",
    51  			output: EditOptions{
    52  				SelectorArg: "23",
    53  				Editable: prShared.Editable{
    54  					Title: prShared.EditableString{
    55  						Value:  "test",
    56  						Edited: true,
    57  					},
    58  				},
    59  			},
    60  			wantsErr: false,
    61  		},
    62  		{
    63  			name:  "body flag",
    64  			input: "23 --body test",
    65  			output: EditOptions{
    66  				SelectorArg: "23",
    67  				Editable: prShared.Editable{
    68  					Body: prShared.EditableString{
    69  						Value:  "test",
    70  						Edited: true,
    71  					},
    72  				},
    73  			},
    74  			wantsErr: false,
    75  		},
    76  		{
    77  			name:  "body from stdin",
    78  			input: "23 --body-file -",
    79  			stdin: "this is on standard input",
    80  			output: EditOptions{
    81  				SelectorArg: "23",
    82  				Editable: prShared.Editable{
    83  					Body: prShared.EditableString{
    84  						Value:  "this is on standard input",
    85  						Edited: true,
    86  					},
    87  				},
    88  			},
    89  			wantsErr: false,
    90  		},
    91  		{
    92  			name:  "body from file",
    93  			input: fmt.Sprintf("23 --body-file '%s'", tmpFile),
    94  			output: EditOptions{
    95  				SelectorArg: "23",
    96  				Editable: prShared.Editable{
    97  					Body: prShared.EditableString{
    98  						Value:  "a body from file",
    99  						Edited: true,
   100  					},
   101  				},
   102  			},
   103  			wantsErr: false,
   104  		},
   105  		{
   106  			name:  "add-assignee flag",
   107  			input: "23 --add-assignee monalisa,hubot",
   108  			output: EditOptions{
   109  				SelectorArg: "23",
   110  				Editable: prShared.Editable{
   111  					Assignees: prShared.EditableSlice{
   112  						Add:    []string{"monalisa", "hubot"},
   113  						Edited: true,
   114  					},
   115  				},
   116  			},
   117  			wantsErr: false,
   118  		},
   119  		{
   120  			name:  "remove-assignee flag",
   121  			input: "23 --remove-assignee monalisa,hubot",
   122  			output: EditOptions{
   123  				SelectorArg: "23",
   124  				Editable: prShared.Editable{
   125  					Assignees: prShared.EditableSlice{
   126  						Remove: []string{"monalisa", "hubot"},
   127  						Edited: true,
   128  					},
   129  				},
   130  			},
   131  			wantsErr: false,
   132  		},
   133  		{
   134  			name:  "add-label flag",
   135  			input: "23 --add-label feature,TODO,bug",
   136  			output: EditOptions{
   137  				SelectorArg: "23",
   138  				Editable: prShared.Editable{
   139  					Labels: prShared.EditableSlice{
   140  						Add:    []string{"feature", "TODO", "bug"},
   141  						Edited: true,
   142  					},
   143  				},
   144  			},
   145  			wantsErr: false,
   146  		},
   147  		{
   148  			name:  "remove-label flag",
   149  			input: "23 --remove-label feature,TODO,bug",
   150  			output: EditOptions{
   151  				SelectorArg: "23",
   152  				Editable: prShared.Editable{
   153  					Labels: prShared.EditableSlice{
   154  						Remove: []string{"feature", "TODO", "bug"},
   155  						Edited: true,
   156  					},
   157  				},
   158  			},
   159  			wantsErr: false,
   160  		},
   161  		{
   162  			name:  "add-project flag",
   163  			input: "23 --add-project Cleanup,Roadmap",
   164  			output: EditOptions{
   165  				SelectorArg: "23",
   166  				Editable: prShared.Editable{
   167  					Projects: prShared.EditableSlice{
   168  						Add:    []string{"Cleanup", "Roadmap"},
   169  						Edited: true,
   170  					},
   171  				},
   172  			},
   173  			wantsErr: false,
   174  		},
   175  		{
   176  			name:  "remove-project flag",
   177  			input: "23 --remove-project Cleanup,Roadmap",
   178  			output: EditOptions{
   179  				SelectorArg: "23",
   180  				Editable: prShared.Editable{
   181  					Projects: prShared.EditableSlice{
   182  						Remove: []string{"Cleanup", "Roadmap"},
   183  						Edited: true,
   184  					},
   185  				},
   186  			},
   187  			wantsErr: false,
   188  		},
   189  		{
   190  			name:  "milestone flag",
   191  			input: "23 --milestone GA",
   192  			output: EditOptions{
   193  				SelectorArg: "23",
   194  				Editable: prShared.Editable{
   195  					Milestone: prShared.EditableString{
   196  						Value:  "GA",
   197  						Edited: true,
   198  					},
   199  				},
   200  			},
   201  			wantsErr: false,
   202  		},
   203  	}
   204  	for _, tt := range tests {
   205  		t.Run(tt.name, func(t *testing.T) {
   206  			io, stdin, _, _ := iostreams.Test()
   207  			io.SetStdoutTTY(true)
   208  			io.SetStdinTTY(true)
   209  			io.SetStderrTTY(true)
   210  
   211  			if tt.stdin != "" {
   212  				_, _ = stdin.WriteString(tt.stdin)
   213  			}
   214  
   215  			f := &cmdutil.Factory{
   216  				IOStreams: io,
   217  			}
   218  
   219  			argv, err := shlex.Split(tt.input)
   220  			assert.NoError(t, err)
   221  
   222  			var gotOpts *EditOptions
   223  			cmd := NewCmdEdit(f, func(opts *EditOptions) error {
   224  				gotOpts = opts
   225  				return nil
   226  			})
   227  			cmd.Flags().BoolP("help", "x", false, "")
   228  
   229  			cmd.SetArgs(argv)
   230  			cmd.SetIn(&bytes.Buffer{})
   231  			cmd.SetOut(&bytes.Buffer{})
   232  			cmd.SetErr(&bytes.Buffer{})
   233  
   234  			_, err = cmd.ExecuteC()
   235  			if tt.wantsErr {
   236  				assert.Error(t, err)
   237  				return
   238  			}
   239  
   240  			assert.NoError(t, err)
   241  			assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg)
   242  			assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
   243  			assert.Equal(t, tt.output.Editable, gotOpts.Editable)
   244  		})
   245  	}
   246  }
   247  
   248  func Test_editRun(t *testing.T) {
   249  	tests := []struct {
   250  		name      string
   251  		input     *EditOptions
   252  		httpStubs func(*testing.T, *httpmock.Registry)
   253  		stdout    string
   254  		stderr    string
   255  	}{
   256  		{
   257  			name: "non-interactive",
   258  			input: &EditOptions{
   259  				SelectorArg: "123",
   260  				Interactive: false,
   261  				Editable: prShared.Editable{
   262  					Title: prShared.EditableString{
   263  						Value:  "new title",
   264  						Edited: true,
   265  					},
   266  					Body: prShared.EditableString{
   267  						Value:  "new body",
   268  						Edited: true,
   269  					},
   270  					Assignees: prShared.EditableSlice{
   271  						Add:    []string{"monalisa", "hubot"},
   272  						Remove: []string{"octocat"},
   273  						Edited: true,
   274  					},
   275  					Labels: prShared.EditableSlice{
   276  						Add:    []string{"feature", "TODO", "bug"},
   277  						Remove: []string{"docs"},
   278  						Edited: true,
   279  					},
   280  					Projects: prShared.EditableSlice{
   281  						Add:    []string{"Cleanup", "Roadmap"},
   282  						Remove: []string{"Features"},
   283  						Edited: true,
   284  					},
   285  					Milestone: prShared.EditableString{
   286  						Value:  "GA",
   287  						Edited: true,
   288  					},
   289  				},
   290  				FetchOptions: prShared.FetchOptions,
   291  			},
   292  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   293  				mockIssueGet(t, reg)
   294  				mockRepoMetadata(t, reg)
   295  				mockIssueUpdate(t, reg)
   296  			},
   297  			stdout: "https://github.com/OWNER/REPO/issue/123\n",
   298  		},
   299  		{
   300  			name: "interactive",
   301  			input: &EditOptions{
   302  				SelectorArg: "123",
   303  				Interactive: true,
   304  				FieldsToEditSurvey: func(eo *prShared.Editable) error {
   305  					eo.Title.Edited = true
   306  					eo.Body.Edited = true
   307  					eo.Assignees.Edited = true
   308  					eo.Labels.Edited = true
   309  					eo.Projects.Edited = true
   310  					eo.Milestone.Edited = true
   311  					return nil
   312  				},
   313  				EditFieldsSurvey: func(eo *prShared.Editable, _ string) error {
   314  					eo.Title.Value = "new title"
   315  					eo.Body.Value = "new body"
   316  					eo.Assignees.Value = []string{"monalisa", "hubot"}
   317  					eo.Labels.Value = []string{"feature", "TODO", "bug"}
   318  					eo.Projects.Value = []string{"Cleanup", "Roadmap"}
   319  					eo.Milestone.Value = "GA"
   320  					return nil
   321  				},
   322  				FetchOptions:    prShared.FetchOptions,
   323  				DetermineEditor: func() (string, error) { return "vim", nil },
   324  			},
   325  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   326  				mockIssueGet(t, reg)
   327  				mockRepoMetadata(t, reg)
   328  				mockIssueUpdate(t, reg)
   329  			},
   330  			stdout: "https://github.com/OWNER/REPO/issue/123\n",
   331  		},
   332  	}
   333  	for _, tt := range tests {
   334  		io, _, stdout, stderr := iostreams.Test()
   335  		io.SetStdoutTTY(true)
   336  		io.SetStdinTTY(true)
   337  		io.SetStderrTTY(true)
   338  
   339  		reg := &httpmock.Registry{}
   340  		defer reg.Verify(t)
   341  		tt.httpStubs(t, reg)
   342  
   343  		httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
   344  		baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
   345  
   346  		tt.input.IO = io
   347  		tt.input.HttpClient = httpClient
   348  		tt.input.BaseRepo = baseRepo
   349  
   350  		t.Run(tt.name, func(t *testing.T) {
   351  			err := editRun(tt.input)
   352  			assert.NoError(t, err)
   353  			assert.Equal(t, tt.stdout, stdout.String())
   354  			assert.Equal(t, tt.stderr, stderr.String())
   355  		})
   356  	}
   357  }
   358  
   359  func mockIssueGet(_ *testing.T, reg *httpmock.Registry) {
   360  	reg.Register(
   361  		httpmock.GraphQL(`query IssueByNumber\b`),
   362  		httpmock.StringResponse(`
   363  			{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
   364  				"number": 123,
   365  				"url": "https://github.com/OWNER/REPO/issue/123"
   366  			} } } }`),
   367  	)
   368  }
   369  
   370  func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) {
   371  	reg.Register(
   372  		httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
   373  		httpmock.StringResponse(`
   374  		{ "data": { "repository": { "assignableUsers": {
   375  			"nodes": [
   376  				{ "login": "hubot", "id": "HUBOTID" },
   377  				{ "login": "MonaLisa", "id": "MONAID" }
   378  			],
   379  			"pageInfo": { "hasNextPage": false }
   380  		} } } }
   381  		`))
   382  	reg.Register(
   383  		httpmock.GraphQL(`query RepositoryLabelList\b`),
   384  		httpmock.StringResponse(`
   385  		{ "data": { "repository": { "labels": {
   386  			"nodes": [
   387  				{ "name": "feature", "id": "FEATUREID" },
   388  				{ "name": "TODO", "id": "TODOID" },
   389  				{ "name": "bug", "id": "BUGID" }
   390  			],
   391  			"pageInfo": { "hasNextPage": false }
   392  		} } } }
   393  		`))
   394  	reg.Register(
   395  		httpmock.GraphQL(`query RepositoryMilestoneList\b`),
   396  		httpmock.StringResponse(`
   397  		{ "data": { "repository": { "milestones": {
   398  			"nodes": [
   399  				{ "title": "GA", "id": "GAID" },
   400  				{ "title": "Big One.oh", "id": "BIGONEID" }
   401  			],
   402  			"pageInfo": { "hasNextPage": false }
   403  		} } } }
   404  		`))
   405  	reg.Register(
   406  		httpmock.GraphQL(`query RepositoryProjectList\b`),
   407  		httpmock.StringResponse(`
   408  		{ "data": { "repository": { "projects": {
   409  			"nodes": [
   410  				{ "name": "Cleanup", "id": "CLEANUPID" },
   411  				{ "name": "Roadmap", "id": "ROADMAPID" }
   412  			],
   413  			"pageInfo": { "hasNextPage": false }
   414  		} } } }
   415  		`))
   416  	reg.Register(
   417  		httpmock.GraphQL(`query OrganizationProjectList\b`),
   418  		httpmock.StringResponse(`
   419  		{ "data": { "organization": { "projects": {
   420  			"nodes": [
   421  				{ "name": "Triage", "id": "TRIAGEID" }
   422  			],
   423  			"pageInfo": { "hasNextPage": false }
   424  		} } } }
   425  		`))
   426  }
   427  
   428  func mockIssueUpdate(t *testing.T, reg *httpmock.Registry) {
   429  	reg.Register(
   430  		httpmock.GraphQL(`mutation IssueUpdate\b`),
   431  		httpmock.GraphQLMutation(`
   432  				{ "data": { "updateIssue": { "issue": {
   433  					"id": "123"
   434  				} } } }`,
   435  			func(inputs map[string]interface{}) {}),
   436  	)
   437  }