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 }