github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/issue/edit/edit_test.go (about)

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