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 }