github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/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/api"
    12  	"github.com/cli/cli/internal/ghrepo"
    13  	shared "github.com/cli/cli/pkg/cmd/pr/shared"
    14  	"github.com/cli/cli/pkg/cmdutil"
    15  	"github.com/cli/cli/pkg/httpmock"
    16  	"github.com/cli/cli/pkg/iostreams"
    17  	"github.com/google/shlex"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  func TestNewCmdEdit(t *testing.T) {
    23  	tmpFile := filepath.Join(t.TempDir(), "my-body.md")
    24  	err := ioutil.WriteFile(tmpFile, []byte("a body from file"), 0600)
    25  	require.NoError(t, err)
    26  
    27  	tests := []struct {
    28  		name     string
    29  		input    string
    30  		stdin    string
    31  		output   EditOptions
    32  		wantsErr bool
    33  	}{
    34  		{
    35  			name:  "no argument",
    36  			input: "",
    37  			output: EditOptions{
    38  				SelectorArg: "",
    39  				Interactive: true,
    40  			},
    41  			wantsErr: false,
    42  		},
    43  		{
    44  			name:     "two arguments",
    45  			input:    "1 2",
    46  			output:   EditOptions{},
    47  			wantsErr: true,
    48  		},
    49  		{
    50  			name:  "pull request number argument",
    51  			input: "23",
    52  			output: EditOptions{
    53  				SelectorArg: "23",
    54  				Interactive: true,
    55  			},
    56  			wantsErr: false,
    57  		},
    58  		{
    59  			name:  "title flag",
    60  			input: "23 --title test",
    61  			output: EditOptions{
    62  				SelectorArg: "23",
    63  				Editable: shared.Editable{
    64  					Title: shared.EditableString{
    65  						Value:  "test",
    66  						Edited: true,
    67  					},
    68  				},
    69  			},
    70  			wantsErr: false,
    71  		},
    72  		{
    73  			name:  "body flag",
    74  			input: "23 --body test",
    75  			output: EditOptions{
    76  				SelectorArg: "23",
    77  				Editable: shared.Editable{
    78  					Body: shared.EditableString{
    79  						Value:  "test",
    80  						Edited: true,
    81  					},
    82  				},
    83  			},
    84  			wantsErr: false,
    85  		},
    86  		{
    87  			name:  "body from stdin",
    88  			input: "23 --body-file -",
    89  			stdin: "this is on standard input",
    90  			output: EditOptions{
    91  				SelectorArg: "23",
    92  				Editable: shared.Editable{
    93  					Body: shared.EditableString{
    94  						Value:  "this is on standard input",
    95  						Edited: true,
    96  					},
    97  				},
    98  			},
    99  			wantsErr: false,
   100  		},
   101  		{
   102  			name:  "body from file",
   103  			input: fmt.Sprintf("23 --body-file '%s'", tmpFile),
   104  			output: EditOptions{
   105  				SelectorArg: "23",
   106  				Editable: shared.Editable{
   107  					Body: shared.EditableString{
   108  						Value:  "a body from file",
   109  						Edited: true,
   110  					},
   111  				},
   112  			},
   113  			wantsErr: false,
   114  		},
   115  		{
   116  			name:  "base flag",
   117  			input: "23 --base base-branch-name",
   118  			output: EditOptions{
   119  				SelectorArg: "23",
   120  				Editable: shared.Editable{
   121  					Base: shared.EditableString{
   122  						Value:  "base-branch-name",
   123  						Edited: true,
   124  					},
   125  				},
   126  			},
   127  			wantsErr: false,
   128  		},
   129  		{
   130  			name:  "add-reviewer flag",
   131  			input: "23 --add-reviewer monalisa,owner/core",
   132  			output: EditOptions{
   133  				SelectorArg: "23",
   134  				Editable: shared.Editable{
   135  					Reviewers: shared.EditableSlice{
   136  						Add:    []string{"monalisa", "owner/core"},
   137  						Edited: true,
   138  					},
   139  				},
   140  			},
   141  			wantsErr: false,
   142  		},
   143  		{
   144  			name:  "remove-reviewer flag",
   145  			input: "23 --remove-reviewer monalisa,owner/core",
   146  			output: EditOptions{
   147  				SelectorArg: "23",
   148  				Editable: shared.Editable{
   149  					Reviewers: shared.EditableSlice{
   150  						Remove: []string{"monalisa", "owner/core"},
   151  						Edited: true,
   152  					},
   153  				},
   154  			},
   155  			wantsErr: false,
   156  		},
   157  		{
   158  			name:  "add-assignee flag",
   159  			input: "23 --add-assignee monalisa,hubot",
   160  			output: EditOptions{
   161  				SelectorArg: "23",
   162  				Editable: shared.Editable{
   163  					Assignees: shared.EditableSlice{
   164  						Add:    []string{"monalisa", "hubot"},
   165  						Edited: true,
   166  					},
   167  				},
   168  			},
   169  			wantsErr: false,
   170  		},
   171  		{
   172  			name:  "remove-assignee flag",
   173  			input: "23 --remove-assignee monalisa,hubot",
   174  			output: EditOptions{
   175  				SelectorArg: "23",
   176  				Editable: shared.Editable{
   177  					Assignees: shared.EditableSlice{
   178  						Remove: []string{"monalisa", "hubot"},
   179  						Edited: true,
   180  					},
   181  				},
   182  			},
   183  			wantsErr: false,
   184  		},
   185  		{
   186  			name:  "add-label flag",
   187  			input: "23 --add-label feature,TODO,bug",
   188  			output: EditOptions{
   189  				SelectorArg: "23",
   190  				Editable: shared.Editable{
   191  					Labels: shared.EditableSlice{
   192  						Add:    []string{"feature", "TODO", "bug"},
   193  						Edited: true,
   194  					},
   195  				},
   196  			},
   197  			wantsErr: false,
   198  		},
   199  		{
   200  			name:  "remove-label flag",
   201  			input: "23 --remove-label feature,TODO,bug",
   202  			output: EditOptions{
   203  				SelectorArg: "23",
   204  				Editable: shared.Editable{
   205  					Labels: shared.EditableSlice{
   206  						Remove: []string{"feature", "TODO", "bug"},
   207  						Edited: true,
   208  					},
   209  				},
   210  			},
   211  			wantsErr: false,
   212  		},
   213  		{
   214  			name:  "add-project flag",
   215  			input: "23 --add-project Cleanup,Roadmap",
   216  			output: EditOptions{
   217  				SelectorArg: "23",
   218  				Editable: shared.Editable{
   219  					Projects: shared.EditableSlice{
   220  						Add:    []string{"Cleanup", "Roadmap"},
   221  						Edited: true,
   222  					},
   223  				},
   224  			},
   225  			wantsErr: false,
   226  		},
   227  		{
   228  			name:  "remove-project flag",
   229  			input: "23 --remove-project Cleanup,Roadmap",
   230  			output: EditOptions{
   231  				SelectorArg: "23",
   232  				Editable: shared.Editable{
   233  					Projects: shared.EditableSlice{
   234  						Remove: []string{"Cleanup", "Roadmap"},
   235  						Edited: true,
   236  					},
   237  				},
   238  			},
   239  			wantsErr: false,
   240  		},
   241  		{
   242  			name:  "milestone flag",
   243  			input: "23 --milestone GA",
   244  			output: EditOptions{
   245  				SelectorArg: "23",
   246  				Editable: shared.Editable{
   247  					Milestone: shared.EditableString{
   248  						Value:  "GA",
   249  						Edited: true,
   250  					},
   251  				},
   252  			},
   253  			wantsErr: false,
   254  		},
   255  	}
   256  	for _, tt := range tests {
   257  		t.Run(tt.name, func(t *testing.T) {
   258  			io, stdin, _, _ := iostreams.Test()
   259  			io.SetStdoutTTY(true)
   260  			io.SetStdinTTY(true)
   261  			io.SetStderrTTY(true)
   262  
   263  			if tt.stdin != "" {
   264  				_, _ = stdin.WriteString(tt.stdin)
   265  			}
   266  
   267  			f := &cmdutil.Factory{
   268  				IOStreams: io,
   269  			}
   270  
   271  			argv, err := shlex.Split(tt.input)
   272  			assert.NoError(t, err)
   273  
   274  			var gotOpts *EditOptions
   275  			cmd := NewCmdEdit(f, func(opts *EditOptions) error {
   276  				gotOpts = opts
   277  				return nil
   278  			})
   279  			cmd.Flags().BoolP("help", "x", false, "")
   280  
   281  			cmd.SetArgs(argv)
   282  			cmd.SetIn(&bytes.Buffer{})
   283  			cmd.SetOut(&bytes.Buffer{})
   284  			cmd.SetErr(&bytes.Buffer{})
   285  
   286  			_, err = cmd.ExecuteC()
   287  			if tt.wantsErr {
   288  				assert.Error(t, err)
   289  				return
   290  			}
   291  
   292  			assert.NoError(t, err)
   293  			assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg)
   294  			assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
   295  			assert.Equal(t, tt.output.Editable, gotOpts.Editable)
   296  		})
   297  	}
   298  }
   299  
   300  func Test_editRun(t *testing.T) {
   301  	tests := []struct {
   302  		name      string
   303  		input     *EditOptions
   304  		httpStubs func(*testing.T, *httpmock.Registry)
   305  		stdout    string
   306  		stderr    string
   307  	}{
   308  		{
   309  			name: "non-interactive",
   310  			input: &EditOptions{
   311  				SelectorArg: "123",
   312  				Finder: shared.NewMockFinder("123", &api.PullRequest{
   313  					URL: "https://github.com/OWNER/REPO/pull/123",
   314  				}, ghrepo.New("OWNER", "REPO")),
   315  				Interactive: false,
   316  				Editable: shared.Editable{
   317  					Title: shared.EditableString{
   318  						Value:  "new title",
   319  						Edited: true,
   320  					},
   321  					Body: shared.EditableString{
   322  						Value:  "new body",
   323  						Edited: true,
   324  					},
   325  					Base: shared.EditableString{
   326  						Value:  "base-branch-name",
   327  						Edited: true,
   328  					},
   329  					Reviewers: shared.EditableSlice{
   330  						Add:    []string{"OWNER/core", "OWNER/external", "monalisa", "hubot"},
   331  						Remove: []string{"dependabot"},
   332  						Edited: true,
   333  					},
   334  					Assignees: shared.EditableSlice{
   335  						Add:    []string{"monalisa", "hubot"},
   336  						Remove: []string{"octocat"},
   337  						Edited: true,
   338  					},
   339  					Labels: shared.EditableSlice{
   340  						Add:    []string{"feature", "TODO", "bug"},
   341  						Remove: []string{"docs"},
   342  						Edited: true,
   343  					},
   344  					Projects: shared.EditableSlice{
   345  						Add:    []string{"Cleanup", "Roadmap"},
   346  						Remove: []string{"Features"},
   347  						Edited: true,
   348  					},
   349  					Milestone: shared.EditableString{
   350  						Value:  "GA",
   351  						Edited: true,
   352  					},
   353  				},
   354  				Fetcher: testFetcher{},
   355  			},
   356  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   357  				mockRepoMetadata(t, reg, false)
   358  				mockPullRequestUpdate(t, reg)
   359  				mockPullRequestReviewersUpdate(t, reg)
   360  			},
   361  			stdout: "https://github.com/OWNER/REPO/pull/123\n",
   362  		},
   363  		{
   364  			name: "non-interactive skip reviewers",
   365  			input: &EditOptions{
   366  				SelectorArg: "123",
   367  				Finder: shared.NewMockFinder("123", &api.PullRequest{
   368  					URL: "https://github.com/OWNER/REPO/pull/123",
   369  				}, ghrepo.New("OWNER", "REPO")),
   370  				Interactive: false,
   371  				Editable: shared.Editable{
   372  					Title: shared.EditableString{
   373  						Value:  "new title",
   374  						Edited: true,
   375  					},
   376  					Body: shared.EditableString{
   377  						Value:  "new body",
   378  						Edited: true,
   379  					},
   380  					Base: shared.EditableString{
   381  						Value:  "base-branch-name",
   382  						Edited: true,
   383  					},
   384  					Assignees: shared.EditableSlice{
   385  						Add:    []string{"monalisa", "hubot"},
   386  						Remove: []string{"octocat"},
   387  						Edited: true,
   388  					},
   389  					Labels: shared.EditableSlice{
   390  						Value:  []string{"feature", "TODO", "bug"},
   391  						Remove: []string{"docs"},
   392  						Edited: true,
   393  					},
   394  					Projects: shared.EditableSlice{
   395  						Value:  []string{"Cleanup", "Roadmap"},
   396  						Remove: []string{"Features"},
   397  						Edited: true,
   398  					},
   399  					Milestone: shared.EditableString{
   400  						Value:  "GA",
   401  						Edited: true,
   402  					},
   403  				},
   404  				Fetcher: testFetcher{},
   405  			},
   406  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   407  				mockRepoMetadata(t, reg, true)
   408  				mockPullRequestUpdate(t, reg)
   409  			},
   410  			stdout: "https://github.com/OWNER/REPO/pull/123\n",
   411  		},
   412  		{
   413  			name: "interactive",
   414  			input: &EditOptions{
   415  				SelectorArg: "123",
   416  				Finder: shared.NewMockFinder("123", &api.PullRequest{
   417  					URL: "https://github.com/OWNER/REPO/pull/123",
   418  				}, ghrepo.New("OWNER", "REPO")),
   419  				Interactive:     true,
   420  				Surveyor:        testSurveyor{},
   421  				Fetcher:         testFetcher{},
   422  				EditorRetriever: testEditorRetriever{},
   423  			},
   424  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   425  				mockRepoMetadata(t, reg, false)
   426  				mockPullRequestUpdate(t, reg)
   427  				mockPullRequestReviewersUpdate(t, reg)
   428  			},
   429  			stdout: "https://github.com/OWNER/REPO/pull/123\n",
   430  		},
   431  		{
   432  			name: "interactive skip reviewers",
   433  			input: &EditOptions{
   434  				SelectorArg: "123",
   435  				Finder: shared.NewMockFinder("123", &api.PullRequest{
   436  					URL: "https://github.com/OWNER/REPO/pull/123",
   437  				}, ghrepo.New("OWNER", "REPO")),
   438  				Interactive:     true,
   439  				Surveyor:        testSurveyor{skipReviewers: true},
   440  				Fetcher:         testFetcher{},
   441  				EditorRetriever: testEditorRetriever{},
   442  			},
   443  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   444  				mockRepoMetadata(t, reg, true)
   445  				mockPullRequestUpdate(t, reg)
   446  			},
   447  			stdout: "https://github.com/OWNER/REPO/pull/123\n",
   448  		},
   449  	}
   450  	for _, tt := range tests {
   451  		io, _, stdout, stderr := iostreams.Test()
   452  		io.SetStdoutTTY(true)
   453  		io.SetStdinTTY(true)
   454  		io.SetStderrTTY(true)
   455  
   456  		reg := &httpmock.Registry{}
   457  		defer reg.Verify(t)
   458  		tt.httpStubs(t, reg)
   459  
   460  		httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
   461  
   462  		tt.input.IO = io
   463  		tt.input.HttpClient = httpClient
   464  
   465  		t.Run(tt.name, func(t *testing.T) {
   466  			err := editRun(tt.input)
   467  			assert.NoError(t, err)
   468  			assert.Equal(t, tt.stdout, stdout.String())
   469  			assert.Equal(t, tt.stderr, stderr.String())
   470  		})
   471  	}
   472  }
   473  
   474  func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry, skipReviewers bool) {
   475  	reg.Register(
   476  		httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
   477  		httpmock.StringResponse(`
   478  		{ "data": { "repository": { "assignableUsers": {
   479  			"nodes": [
   480  				{ "login": "hubot", "id": "HUBOTID" },
   481  				{ "login": "MonaLisa", "id": "MONAID" }
   482  			],
   483  			"pageInfo": { "hasNextPage": false }
   484  		} } } }
   485  		`))
   486  	reg.Register(
   487  		httpmock.GraphQL(`query RepositoryLabelList\b`),
   488  		httpmock.StringResponse(`
   489  		{ "data": { "repository": { "labels": {
   490  			"nodes": [
   491  				{ "name": "feature", "id": "FEATUREID" },
   492  				{ "name": "TODO", "id": "TODOID" },
   493  				{ "name": "bug", "id": "BUGID" }
   494  			],
   495  			"pageInfo": { "hasNextPage": false }
   496  		} } } }
   497  		`))
   498  	reg.Register(
   499  		httpmock.GraphQL(`query RepositoryMilestoneList\b`),
   500  		httpmock.StringResponse(`
   501  		{ "data": { "repository": { "milestones": {
   502  			"nodes": [
   503  				{ "title": "GA", "id": "GAID" },
   504  				{ "title": "Big One.oh", "id": "BIGONEID" }
   505  			],
   506  			"pageInfo": { "hasNextPage": false }
   507  		} } } }
   508  		`))
   509  	reg.Register(
   510  		httpmock.GraphQL(`query RepositoryProjectList\b`),
   511  		httpmock.StringResponse(`
   512  		{ "data": { "repository": { "projects": {
   513  			"nodes": [
   514  				{ "name": "Cleanup", "id": "CLEANUPID" },
   515  				{ "name": "Roadmap", "id": "ROADMAPID" }
   516  			],
   517  			"pageInfo": { "hasNextPage": false }
   518  		} } } }
   519  		`))
   520  	reg.Register(
   521  		httpmock.GraphQL(`query OrganizationProjectList\b`),
   522  		httpmock.StringResponse(`
   523  		{ "data": { "organization": { "projects": {
   524  			"nodes": [
   525  				{ "name": "Triage", "id": "TRIAGEID" }
   526  			],
   527  			"pageInfo": { "hasNextPage": false }
   528  		} } } }
   529  		`))
   530  	if !skipReviewers {
   531  		reg.Register(
   532  			httpmock.GraphQL(`query OrganizationTeamList\b`),
   533  			httpmock.StringResponse(`
   534  		{ "data": { "organization": { "teams": {
   535  			"nodes": [
   536  				{ "slug": "external", "id": "EXTERNALID" },
   537  				{ "slug": "core", "id": "COREID" }
   538  			],
   539  			"pageInfo": { "hasNextPage": false }
   540  		} } } }
   541  		`))
   542  	}
   543  }
   544  
   545  func mockPullRequestUpdate(t *testing.T, reg *httpmock.Registry) {
   546  	reg.Register(
   547  		httpmock.GraphQL(`mutation PullRequestUpdate\b`),
   548  		httpmock.StringResponse(`{}`))
   549  }
   550  
   551  func mockPullRequestReviewersUpdate(t *testing.T, reg *httpmock.Registry) {
   552  	reg.Register(
   553  		httpmock.GraphQL(`mutation PullRequestUpdateRequestReviews\b`),
   554  		httpmock.StringResponse(`{}`))
   555  }
   556  
   557  type testFetcher struct{}
   558  type testSurveyor struct {
   559  	skipReviewers bool
   560  }
   561  type testEditorRetriever struct{}
   562  
   563  func (f testFetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error {
   564  	return shared.FetchOptions(client, repo, opts)
   565  }
   566  
   567  func (s testSurveyor) FieldsToEdit(e *shared.Editable) error {
   568  	e.Title.Edited = true
   569  	e.Body.Edited = true
   570  	if !s.skipReviewers {
   571  		e.Reviewers.Edited = true
   572  	}
   573  	e.Assignees.Edited = true
   574  	e.Labels.Edited = true
   575  	e.Projects.Edited = true
   576  	e.Milestone.Edited = true
   577  	return nil
   578  }
   579  
   580  func (s testSurveyor) EditFields(e *shared.Editable, _ string) error {
   581  	e.Title.Value = "new title"
   582  	e.Body.Value = "new body"
   583  	if !s.skipReviewers {
   584  		e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"}
   585  	}
   586  	e.Assignees.Value = []string{"monalisa", "hubot"}
   587  	e.Labels.Value = []string{"feature", "TODO", "bug"}
   588  	e.Projects.Value = []string{"Cleanup", "Roadmap"}
   589  	e.Milestone.Value = "GA"
   590  	return nil
   591  }
   592  
   593  func (t testEditorRetriever) Retrieve() (string, error) {
   594  	return "vim", nil
   595  }