github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/release/create/create_test.go (about) 1 package create 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "path/filepath" 11 "testing" 12 13 "github.com/ungtb10d/cli/v2/git" 14 "github.com/ungtb10d/cli/v2/internal/config" 15 "github.com/ungtb10d/cli/v2/internal/ghrepo" 16 "github.com/ungtb10d/cli/v2/internal/run" 17 "github.com/ungtb10d/cli/v2/pkg/cmd/release/shared" 18 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 19 "github.com/ungtb10d/cli/v2/pkg/httpmock" 20 "github.com/ungtb10d/cli/v2/pkg/iostreams" 21 "github.com/ungtb10d/cli/v2/pkg/prompt" 22 "github.com/google/shlex" 23 "github.com/stretchr/testify/assert" 24 "github.com/stretchr/testify/require" 25 ) 26 27 func Test_NewCmdCreate(t *testing.T) { 28 tempDir := t.TempDir() 29 tf, err := os.CreateTemp(tempDir, "release-create") 30 require.NoError(t, err) 31 fmt.Fprint(tf, "MY NOTES") 32 tf.Close() 33 af1, err := os.Create(filepath.Join(tempDir, "windows.zip")) 34 require.NoError(t, err) 35 af1.Close() 36 af2, err := os.Create(filepath.Join(tempDir, "linux.tgz")) 37 require.NoError(t, err) 38 af2.Close() 39 40 tests := []struct { 41 name string 42 args string 43 isTTY bool 44 stdin string 45 want CreateOptions 46 wantErr string 47 }{ 48 { 49 name: "no arguments tty", 50 args: "", 51 isTTY: true, 52 want: CreateOptions{ 53 TagName: "", 54 Target: "", 55 Name: "", 56 Body: "", 57 BodyProvided: false, 58 Draft: false, 59 Prerelease: false, 60 RepoOverride: "", 61 Concurrency: 5, 62 Assets: []*shared.AssetForUpload(nil), 63 }, 64 }, 65 { 66 name: "no arguments notty", 67 args: "", 68 isTTY: false, 69 wantErr: "tag required when not running interactively", 70 }, 71 { 72 name: "only tag name", 73 args: "v1.2.3", 74 isTTY: true, 75 want: CreateOptions{ 76 TagName: "v1.2.3", 77 Target: "", 78 Name: "", 79 Body: "", 80 BodyProvided: false, 81 Draft: false, 82 Prerelease: false, 83 RepoOverride: "", 84 Concurrency: 5, 85 Assets: []*shared.AssetForUpload(nil), 86 }, 87 }, 88 { 89 name: "asset files", 90 args: fmt.Sprintf("v1.2.3 '%s' '%s#Linux build'", af1.Name(), af2.Name()), 91 isTTY: true, 92 want: CreateOptions{ 93 TagName: "v1.2.3", 94 Target: "", 95 Name: "", 96 Body: "", 97 BodyProvided: false, 98 Draft: false, 99 Prerelease: false, 100 RepoOverride: "", 101 Concurrency: 5, 102 Assets: []*shared.AssetForUpload{ 103 { 104 Name: "windows.zip", 105 Label: "", 106 }, 107 { 108 Name: "linux.tgz", 109 Label: "Linux build", 110 }, 111 }, 112 }, 113 }, 114 { 115 name: "provide title and body", 116 args: "v1.2.3 -t mytitle -n mynotes", 117 isTTY: true, 118 want: CreateOptions{ 119 TagName: "v1.2.3", 120 Target: "", 121 Name: "mytitle", 122 Body: "mynotes", 123 BodyProvided: true, 124 Draft: false, 125 Prerelease: false, 126 RepoOverride: "", 127 Concurrency: 5, 128 Assets: []*shared.AssetForUpload(nil), 129 }, 130 }, 131 { 132 name: "notes from file", 133 args: fmt.Sprintf(`v1.2.3 -F '%s'`, tf.Name()), 134 isTTY: true, 135 want: CreateOptions{ 136 TagName: "v1.2.3", 137 Target: "", 138 Name: "", 139 Body: "MY NOTES", 140 BodyProvided: true, 141 Draft: false, 142 Prerelease: false, 143 RepoOverride: "", 144 Concurrency: 5, 145 Assets: []*shared.AssetForUpload(nil), 146 }, 147 }, 148 { 149 name: "notes from stdin", 150 args: "v1.2.3 -F -", 151 isTTY: true, 152 stdin: "MY NOTES", 153 want: CreateOptions{ 154 TagName: "v1.2.3", 155 Target: "", 156 Name: "", 157 Body: "MY NOTES", 158 BodyProvided: true, 159 Draft: false, 160 Prerelease: false, 161 RepoOverride: "", 162 Concurrency: 5, 163 Assets: []*shared.AssetForUpload(nil), 164 }, 165 }, 166 { 167 name: "set draft and prerelease", 168 args: "v1.2.3 -d -p", 169 isTTY: true, 170 want: CreateOptions{ 171 TagName: "v1.2.3", 172 Target: "", 173 Name: "", 174 Body: "", 175 BodyProvided: false, 176 Draft: true, 177 Prerelease: true, 178 RepoOverride: "", 179 Concurrency: 5, 180 Assets: []*shared.AssetForUpload(nil), 181 }, 182 }, 183 { 184 name: "discussion category", 185 args: "v1.2.3 --discussion-category 'General'", 186 isTTY: true, 187 want: CreateOptions{ 188 TagName: "v1.2.3", 189 Target: "", 190 Name: "", 191 Body: "", 192 BodyProvided: false, 193 Draft: false, 194 Prerelease: false, 195 RepoOverride: "", 196 Concurrency: 5, 197 Assets: []*shared.AssetForUpload(nil), 198 DiscussionCategory: "General", 199 }, 200 }, 201 { 202 name: "discussion category for draft release", 203 args: "v1.2.3 -d --discussion-category 'General'", 204 isTTY: true, 205 wantErr: "discussions for draft releases not supported", 206 }, 207 { 208 name: "generate release notes", 209 args: "v1.2.3 --generate-notes", 210 isTTY: true, 211 want: CreateOptions{ 212 TagName: "v1.2.3", 213 Target: "", 214 Name: "", 215 Body: "", 216 BodyProvided: true, 217 Draft: false, 218 Prerelease: false, 219 RepoOverride: "", 220 Concurrency: 5, 221 Assets: []*shared.AssetForUpload(nil), 222 GenerateNotes: true, 223 }, 224 }, 225 { 226 name: "generate release notes with notes tag", 227 args: "v1.2.3 --generate-notes --notes-start-tag v1.1.0", 228 isTTY: true, 229 want: CreateOptions{ 230 TagName: "v1.2.3", 231 Target: "", 232 Name: "", 233 Body: "", 234 BodyProvided: true, 235 Draft: false, 236 Prerelease: false, 237 RepoOverride: "", 238 Concurrency: 5, 239 Assets: []*shared.AssetForUpload(nil), 240 GenerateNotes: true, 241 NotesStartTag: "v1.1.0", 242 }, 243 }, 244 { 245 name: "notes tag", 246 args: "--notes-start-tag v1.1.0", 247 isTTY: true, 248 want: CreateOptions{ 249 TagName: "", 250 Target: "", 251 Name: "", 252 Body: "", 253 BodyProvided: false, 254 Draft: false, 255 Prerelease: false, 256 RepoOverride: "", 257 Concurrency: 5, 258 Assets: []*shared.AssetForUpload(nil), 259 GenerateNotes: false, 260 NotesStartTag: "v1.1.0", 261 }, 262 }, 263 { 264 name: "latest", 265 args: "--latest v1.1.0", 266 isTTY: false, 267 want: CreateOptions{ 268 TagName: "v1.1.0", 269 Target: "", 270 Name: "", 271 Body: "", 272 BodyProvided: false, 273 Draft: false, 274 Prerelease: false, 275 IsLatest: boolPtr(true), 276 RepoOverride: "", 277 Concurrency: 5, 278 Assets: []*shared.AssetForUpload(nil), 279 GenerateNotes: false, 280 NotesStartTag: "", 281 }, 282 }, 283 { 284 name: "not latest", 285 args: "--latest=false v1.1.0", 286 isTTY: false, 287 want: CreateOptions{ 288 TagName: "v1.1.0", 289 Target: "", 290 Name: "", 291 Body: "", 292 BodyProvided: false, 293 Draft: false, 294 Prerelease: false, 295 IsLatest: boolPtr(false), 296 RepoOverride: "", 297 Concurrency: 5, 298 Assets: []*shared.AssetForUpload(nil), 299 GenerateNotes: false, 300 NotesStartTag: "", 301 }, 302 }, 303 } 304 for _, tt := range tests { 305 t.Run(tt.name, func(t *testing.T) { 306 ios, stdin, _, _ := iostreams.Test() 307 if tt.stdin == "" { 308 ios.SetStdinTTY(tt.isTTY) 309 } else { 310 ios.SetStdinTTY(false) 311 fmt.Fprint(stdin, tt.stdin) 312 } 313 ios.SetStdoutTTY(tt.isTTY) 314 ios.SetStderrTTY(tt.isTTY) 315 316 f := &cmdutil.Factory{ 317 IOStreams: ios, 318 } 319 320 var opts *CreateOptions 321 cmd := NewCmdCreate(f, func(o *CreateOptions) error { 322 opts = o 323 return nil 324 }) 325 cmd.PersistentFlags().StringP("repo", "R", "", "") 326 327 argv, err := shlex.Split(tt.args) 328 require.NoError(t, err) 329 cmd.SetArgs(argv) 330 331 cmd.SetIn(&bytes.Buffer{}) 332 cmd.SetOut(io.Discard) 333 cmd.SetErr(io.Discard) 334 335 _, err = cmd.ExecuteC() 336 if tt.wantErr != "" { 337 require.EqualError(t, err, tt.wantErr) 338 return 339 } else { 340 require.NoError(t, err) 341 } 342 343 assert.Equal(t, tt.want.TagName, opts.TagName) 344 assert.Equal(t, tt.want.Target, opts.Target) 345 assert.Equal(t, tt.want.Name, opts.Name) 346 assert.Equal(t, tt.want.Body, opts.Body) 347 assert.Equal(t, tt.want.BodyProvided, opts.BodyProvided) 348 assert.Equal(t, tt.want.Draft, opts.Draft) 349 assert.Equal(t, tt.want.Prerelease, opts.Prerelease) 350 assert.Equal(t, tt.want.Concurrency, opts.Concurrency) 351 assert.Equal(t, tt.want.RepoOverride, opts.RepoOverride) 352 assert.Equal(t, tt.want.DiscussionCategory, opts.DiscussionCategory) 353 assert.Equal(t, tt.want.GenerateNotes, opts.GenerateNotes) 354 assert.Equal(t, tt.want.NotesStartTag, opts.NotesStartTag) 355 assert.Equal(t, tt.want.IsLatest, opts.IsLatest) 356 357 require.Equal(t, len(tt.want.Assets), len(opts.Assets)) 358 for i := range tt.want.Assets { 359 assert.Equal(t, tt.want.Assets[i].Name, opts.Assets[i].Name) 360 assert.Equal(t, tt.want.Assets[i].Label, opts.Assets[i].Label) 361 } 362 }) 363 } 364 } 365 366 func Test_createRun(t *testing.T) { 367 tests := []struct { 368 name string 369 isTTY bool 370 opts CreateOptions 371 httpStubs func(t *testing.T, reg *httpmock.Registry) 372 wantErr string 373 wantStdout string 374 wantStderr string 375 }{ 376 { 377 name: "create a release", 378 isTTY: true, 379 opts: CreateOptions{ 380 TagName: "v1.2.3", 381 Name: "The Big 1.2", 382 Body: "* Fixed bugs", 383 BodyProvided: true, 384 Target: "", 385 }, 386 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 387 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 388 "url": "https://api.github.com/releases/123", 389 "upload_url": "https://api.github.com/assets/upload", 390 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 391 }`, func(params map[string]interface{}) { 392 assert.Equal(t, map[string]interface{}{ 393 "tag_name": "v1.2.3", 394 "name": "The Big 1.2", 395 "body": "* Fixed bugs", 396 "draft": false, 397 "prerelease": false, 398 }, params) 399 })) 400 }, 401 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 402 wantStderr: ``, 403 }, 404 { 405 name: "with discussion category", 406 isTTY: true, 407 opts: CreateOptions{ 408 TagName: "v1.2.3", 409 Name: "The Big 1.2", 410 Body: "* Fixed bugs", 411 BodyProvided: true, 412 Target: "", 413 DiscussionCategory: "General", 414 }, 415 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 416 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 417 "url": "https://api.github.com/releases/123", 418 "upload_url": "https://api.github.com/assets/upload", 419 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 420 }`, func(params map[string]interface{}) { 421 assert.Equal(t, map[string]interface{}{ 422 "tag_name": "v1.2.3", 423 "name": "The Big 1.2", 424 "body": "* Fixed bugs", 425 "draft": false, 426 "prerelease": false, 427 "discussion_category_name": "General", 428 }, params) 429 })) 430 }, 431 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 432 wantStderr: ``, 433 }, 434 { 435 name: "with target commitish", 436 isTTY: true, 437 opts: CreateOptions{ 438 TagName: "v1.2.3", 439 Name: "", 440 Body: "", 441 BodyProvided: true, 442 Target: "main", 443 }, 444 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 445 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 446 "url": "https://api.github.com/releases/123", 447 "upload_url": "https://api.github.com/assets/upload", 448 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 449 }`, func(params map[string]interface{}) { 450 assert.Equal(t, map[string]interface{}{ 451 "tag_name": "v1.2.3", 452 "draft": false, 453 "prerelease": false, 454 "target_commitish": "main", 455 }, params) 456 })) 457 }, 458 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 459 wantStderr: ``, 460 }, 461 { 462 name: "as draft", 463 isTTY: true, 464 opts: CreateOptions{ 465 TagName: "v1.2.3", 466 Name: "", 467 Body: "", 468 BodyProvided: true, 469 Draft: true, 470 Target: "", 471 }, 472 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 473 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 474 "url": "https://api.github.com/releases/123", 475 "upload_url": "https://api.github.com/assets/upload", 476 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 477 }`, func(params map[string]interface{}) { 478 assert.Equal(t, map[string]interface{}{ 479 "tag_name": "v1.2.3", 480 "draft": true, 481 "prerelease": false, 482 }, params) 483 })) 484 }, 485 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 486 wantStderr: ``, 487 }, 488 { 489 name: "with latest", 490 isTTY: false, 491 opts: CreateOptions{ 492 TagName: "v1.2.3", 493 Name: "", 494 Body: "", 495 Target: "", 496 IsLatest: boolPtr(true), 497 BodyProvided: true, 498 GenerateNotes: false, 499 }, 500 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 501 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 502 "url": "https://api.github.com/releases/123", 503 "upload_url": "https://api.github.com/assets/upload", 504 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 505 }`, func(params map[string]interface{}) { 506 assert.Equal(t, map[string]interface{}{ 507 "tag_name": "v1.2.3", 508 "draft": false, 509 "prerelease": false, 510 "make_latest": "true", 511 }, params) 512 })) 513 }, 514 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 515 wantErr: "", 516 }, 517 { 518 name: "with generate notes", 519 isTTY: true, 520 opts: CreateOptions{ 521 TagName: "v1.2.3", 522 Name: "", 523 Body: "", 524 Target: "", 525 BodyProvided: true, 526 GenerateNotes: true, 527 }, 528 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 529 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 530 "url": "https://api.github.com/releases/123", 531 "upload_url": "https://api.github.com/assets/upload", 532 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 533 }`, func(params map[string]interface{}) { 534 assert.Equal(t, map[string]interface{}{ 535 "tag_name": "v1.2.3", 536 "draft": false, 537 "prerelease": false, 538 "generate_release_notes": true, 539 }, params) 540 })) 541 }, 542 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 543 wantErr: "", 544 }, 545 { 546 name: "with generate notes and notes tag", 547 isTTY: true, 548 opts: CreateOptions{ 549 TagName: "v1.2.3", 550 Name: "", 551 Body: "", 552 Target: "", 553 BodyProvided: true, 554 GenerateNotes: true, 555 NotesStartTag: "v1.1.0", 556 }, 557 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 558 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 559 httpmock.RESTPayload(200, `{ 560 "name": "generated name", 561 "body": "generated body" 562 }`, func(params map[string]interface{}) { 563 assert.Equal(t, map[string]interface{}{ 564 "tag_name": "v1.2.3", 565 "previous_tag_name": "v1.1.0", 566 }, params) 567 })) 568 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 569 "url": "https://api.github.com/releases/123", 570 "upload_url": "https://api.github.com/assets/upload", 571 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 572 }`, func(params map[string]interface{}) { 573 assert.Equal(t, map[string]interface{}{ 574 "tag_name": "v1.2.3", 575 "draft": false, 576 "prerelease": false, 577 "body": "generated body", 578 "name": "generated name", 579 }, params) 580 })) 581 }, 582 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 583 wantErr: "", 584 }, 585 { 586 name: "with generate notes and notes tag and body and name", 587 isTTY: true, 588 opts: CreateOptions{ 589 TagName: "v1.2.3", 590 Name: "name", 591 Body: "body", 592 Target: "", 593 BodyProvided: true, 594 GenerateNotes: true, 595 NotesStartTag: "v1.1.0", 596 }, 597 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 598 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 599 httpmock.RESTPayload(200, `{ 600 "name": "generated name", 601 "body": "generated body" 602 }`, func(params map[string]interface{}) { 603 assert.Equal(t, map[string]interface{}{ 604 "tag_name": "v1.2.3", 605 "previous_tag_name": "v1.1.0", 606 }, params) 607 })) 608 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 609 "url": "https://api.github.com/releases/123", 610 "upload_url": "https://api.github.com/assets/upload", 611 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 612 }`, func(params map[string]interface{}) { 613 assert.Equal(t, map[string]interface{}{ 614 "tag_name": "v1.2.3", 615 "draft": false, 616 "prerelease": false, 617 "body": "body\ngenerated body", 618 "name": "name", 619 }, params) 620 })) 621 }, 622 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 623 wantErr: "", 624 }, 625 { 626 name: "publish after uploading files", 627 isTTY: true, 628 opts: CreateOptions{ 629 TagName: "v1.2.3", 630 Name: "", 631 Body: "", 632 BodyProvided: true, 633 Draft: false, 634 Target: "", 635 Assets: []*shared.AssetForUpload{ 636 { 637 Name: "ball.tgz", 638 Open: func() (io.ReadCloser, error) { 639 return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil 640 }, 641 }, 642 }, 643 Concurrency: 1, 644 }, 645 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 646 reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``)) 647 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 648 "url": "https://api.github.com/releases/123", 649 "upload_url": "https://api.github.com/assets/upload", 650 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 651 }`, func(params map[string]interface{}) { 652 assert.Equal(t, map[string]interface{}{ 653 "tag_name": "v1.2.3", 654 "draft": true, 655 "prerelease": false, 656 }, params) 657 })) 658 reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) { 659 q := req.URL.Query() 660 assert.Equal(t, "ball.tgz", q.Get("name")) 661 assert.Equal(t, "", q.Get("label")) 662 return &http.Response{ 663 StatusCode: 201, 664 Request: req, 665 Body: io.NopCloser(bytes.NewBufferString(`{}`)), 666 Header: map[string][]string{ 667 "Content-Type": {"application/json"}, 668 }, 669 }, nil 670 }) 671 reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{ 672 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final" 673 }`, func(params map[string]interface{}) { 674 assert.Equal(t, map[string]interface{}{ 675 "draft": false, 676 }, params) 677 })) 678 }, 679 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n", 680 wantStderr: ``, 681 }, 682 { 683 name: "upload files but release already exists", 684 isTTY: true, 685 opts: CreateOptions{ 686 TagName: "v1.2.3", 687 Name: "", 688 Body: "", 689 BodyProvided: true, 690 Draft: false, 691 Target: "", 692 Assets: []*shared.AssetForUpload{ 693 { 694 Name: "ball.tgz", 695 Open: func() (io.ReadCloser, error) { 696 return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil 697 }, 698 }, 699 }, 700 Concurrency: 1, 701 }, 702 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 703 reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(200, ``)) 704 }, 705 wantStdout: ``, 706 wantStderr: ``, 707 wantErr: `a release with the same tag name already exists: v1.2.3`, 708 }, 709 { 710 name: "upload files and create discussion", 711 isTTY: true, 712 opts: CreateOptions{ 713 TagName: "v1.2.3", 714 Name: "", 715 Body: "", 716 BodyProvided: true, 717 Draft: false, 718 Target: "", 719 Assets: []*shared.AssetForUpload{ 720 { 721 Name: "ball.tgz", 722 Open: func() (io.ReadCloser, error) { 723 return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil 724 }, 725 }, 726 }, 727 DiscussionCategory: "general", 728 Concurrency: 1, 729 }, 730 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 731 reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``)) 732 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ 733 "url": "https://api.github.com/releases/123", 734 "upload_url": "https://api.github.com/assets/upload", 735 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 736 }`, func(params map[string]interface{}) { 737 assert.Equal(t, map[string]interface{}{ 738 "tag_name": "v1.2.3", 739 "draft": true, 740 "prerelease": false, 741 "discussion_category_name": "general", 742 }, params) 743 })) 744 reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) { 745 q := req.URL.Query() 746 assert.Equal(t, "ball.tgz", q.Get("name")) 747 assert.Equal(t, "", q.Get("label")) 748 return &http.Response{ 749 StatusCode: 201, 750 Request: req, 751 Body: io.NopCloser(bytes.NewBufferString(`{}`)), 752 Header: map[string][]string{ 753 "Content-Type": {"application/json"}, 754 }, 755 }, nil 756 }) 757 reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{ 758 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final" 759 }`, func(params map[string]interface{}) { 760 assert.Equal(t, map[string]interface{}{ 761 "draft": false, 762 "discussion_category_name": "general", 763 }, params) 764 })) 765 }, 766 wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n", 767 wantStderr: ``, 768 }, 769 } 770 for _, tt := range tests { 771 t.Run(tt.name, func(t *testing.T) { 772 ios, _, stdout, stderr := iostreams.Test() 773 ios.SetStdoutTTY(tt.isTTY) 774 ios.SetStdinTTY(tt.isTTY) 775 ios.SetStderrTTY(tt.isTTY) 776 777 fakeHTTP := &httpmock.Registry{} 778 if tt.httpStubs != nil { 779 tt.httpStubs(t, fakeHTTP) 780 } 781 defer fakeHTTP.Verify(t) 782 783 tt.opts.IO = ios 784 tt.opts.HttpClient = func() (*http.Client, error) { 785 return &http.Client{Transport: fakeHTTP}, nil 786 } 787 tt.opts.BaseRepo = func() (ghrepo.Interface, error) { 788 return ghrepo.FromFullName("OWNER/REPO") 789 } 790 791 tt.opts.GitClient = &git.Client{GitPath: "some/path/git"} 792 793 err := createRun(&tt.opts) 794 if tt.wantErr != "" { 795 require.EqualError(t, err, tt.wantErr) 796 return 797 } else { 798 require.NoError(t, err) 799 } 800 801 assert.Equal(t, tt.wantStdout, stdout.String()) 802 assert.Equal(t, tt.wantStderr, stderr.String()) 803 }) 804 } 805 } 806 807 func Test_createRun_interactive(t *testing.T) { 808 tests := []struct { 809 name string 810 httpStubs func(*httpmock.Registry) 811 askStubs func(*prompt.AskStubber) 812 runStubs func(*run.CommandStubber) 813 opts *CreateOptions 814 wantParams map[string]interface{} 815 wantOut string 816 wantErr string 817 }{ 818 { 819 name: "create a release from existing tag", 820 opts: &CreateOptions{}, 821 askStubs: func(as *prompt.AskStubber) { 822 as.StubPrompt("Choose a tag"). 823 AssertOptions([]string{"v1.2.3", "v1.2.2", "v1.0.0", "v0.1.2", "Create a new tag"}). 824 AnswerWith("v1.2.3") 825 as.StubPrompt("Title (optional)").AnswerWith("") 826 as.StubPrompt("Release notes"). 827 AssertOptions([]string{"Write my own", "Write using generated notes as template", "Leave blank"}). 828 AnswerWith("Leave blank") 829 as.StubPrompt("Is this a prerelease?").AnswerWith(false) 830 as.StubPrompt("Submit?"). 831 AssertOptions([]string{"Publish release", "Save as draft", "Cancel"}).AnswerWith("Publish release") 832 }, 833 runStubs: func(rs *run.CommandStubber) { 834 rs.Register(`git tag --list`, 1, "") 835 }, 836 httpStubs: func(reg *httpmock.Registry) { 837 reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/tags"), httpmock.StatusStringResponse(200, `[ 838 { "name": "v1.2.3" }, { "name": "v1.2.2" }, { "name": "v1.0.0" }, { "name": "v0.1.2" } 839 ]`)) 840 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 841 httpmock.StatusStringResponse(200, `{ 842 "name": "generated name", 843 "body": "generated body" 844 }`)) 845 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{ 846 "url": "https://api.github.com/releases/123", 847 "upload_url": "https://api.github.com/assets/upload", 848 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 849 }`)) 850 }, 851 wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 852 }, 853 { 854 name: "create a release from new tag", 855 opts: &CreateOptions{}, 856 askStubs: func(as *prompt.AskStubber) { 857 as.StubPrompt("Choose a tag").AnswerWith("Create a new tag") 858 as.StubPrompt("Tag name").AnswerWith("v1.2.3") 859 as.StubPrompt("Title (optional)").AnswerWith("") 860 as.StubPrompt("Release notes"). 861 AssertOptions([]string{"Write my own", "Write using generated notes as template", "Leave blank"}). 862 AnswerWith("Leave blank") 863 as.StubPrompt("Is this a prerelease?").AnswerWith(false) 864 as.StubPrompt("Submit?").AnswerWith("Publish release") 865 }, 866 runStubs: func(rs *run.CommandStubber) { 867 rs.Register(`git tag --list`, 1, "") 868 }, 869 httpStubs: func(reg *httpmock.Registry) { 870 reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/tags"), httpmock.StatusStringResponse(200, `[ 871 { "name": "v1.2.2" }, { "name": "v1.0.0" }, { "name": "v0.1.2" } 872 ]`)) 873 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 874 httpmock.StatusStringResponse(200, `{ 875 "name": "generated name", 876 "body": "generated body" 877 }`)) 878 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{ 879 "url": "https://api.github.com/releases/123", 880 "upload_url": "https://api.github.com/assets/upload", 881 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 882 }`)) 883 }, 884 wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 885 }, 886 { 887 name: "create a release using generated notes", 888 opts: &CreateOptions{ 889 TagName: "v1.2.3", 890 }, 891 askStubs: func(as *prompt.AskStubber) { 892 as.StubPrompt("Title (optional)").AnswerDefault() 893 as.StubPrompt("Release notes"). 894 AssertOptions([]string{"Write my own", "Write using generated notes as template", "Leave blank"}). 895 AnswerWith("Write using generated notes as template") 896 as.StubPrompt("Is this a prerelease?").AnswerWith(false) 897 as.StubPrompt("Submit?").AnswerWith("Publish release") 898 }, 899 runStubs: func(rs *run.CommandStubber) { 900 rs.Register(`git tag --list`, 1, "") 901 }, 902 httpStubs: func(reg *httpmock.Registry) { 903 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 904 httpmock.StatusStringResponse(200, `{ 905 "name": "generated name", 906 "body": "generated body" 907 }`)) 908 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), 909 httpmock.StatusStringResponse(201, `{ 910 "url": "https://api.github.com/releases/123", 911 "upload_url": "https://api.github.com/assets/upload", 912 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 913 }`)) 914 }, 915 wantParams: map[string]interface{}{ 916 "body": "generated body", 917 "draft": false, 918 "name": "generated name", 919 "prerelease": false, 920 "tag_name": "v1.2.3", 921 }, 922 wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 923 }, 924 { 925 name: "create a release using commit log as notes", 926 opts: &CreateOptions{ 927 TagName: "v1.2.3", 928 }, 929 askStubs: func(as *prompt.AskStubber) { 930 as.StubPrompt("Title (optional)").AnswerDefault() 931 as.StubPrompt("Release notes"). 932 AssertOptions([]string{"Write my own", "Write using commit log as template", "Leave blank"}). 933 AnswerWith("Write using commit log as template") 934 as.StubPrompt("Is this a prerelease?").AnswerWith(false) 935 as.StubPrompt("Submit?").AnswerWith("Publish release") 936 }, 937 runStubs: func(rs *run.CommandStubber) { 938 rs.Register(`git tag --list`, 1, "") 939 rs.Register(`git describe --tags --abbrev=0 HEAD\^`, 0, "v1.2.2\n") 940 rs.Register(`git .+log .+v1\.2\.2\.\.HEAD$`, 0, "commit subject\n\ncommit body\n") 941 }, 942 httpStubs: func(reg *httpmock.Registry) { 943 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 944 httpmock.StatusStringResponse(404, `{}`)) 945 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), 946 httpmock.StatusStringResponse(201, `{ 947 "url": "https://api.github.com/releases/123", 948 "upload_url": "https://api.github.com/assets/upload", 949 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 950 }`)) 951 }, 952 wantParams: map[string]interface{}{ 953 "body": "* commit subject\n\n commit body\n ", 954 "draft": false, 955 "prerelease": false, 956 "tag_name": "v1.2.3", 957 }, 958 wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 959 }, 960 { 961 name: "create using annotated tag as notes", 962 opts: &CreateOptions{ 963 TagName: "v1.2.3", 964 }, 965 askStubs: func(as *prompt.AskStubber) { 966 as.StubPrompt("Title (optional)").AnswerDefault() 967 as.StubPrompt("Release notes"). 968 AssertOptions([]string{"Write my own", "Write using git tag message as template", "Leave blank"}). 969 AnswerWith("Write using git tag message as template") 970 as.StubPrompt("Is this a prerelease?").AnswerWith(false) 971 as.StubPrompt("Submit?").AnswerWith("Publish release") 972 }, 973 runStubs: func(rs *run.CommandStubber) { 974 rs.Register(`git tag --list`, 0, "hello from annotated tag") 975 rs.Register(`git describe --tags --abbrev=0 v1\.2\.3\^`, 1, "") 976 }, 977 httpStubs: func(reg *httpmock.Registry) { 978 reg.Register(httpmock.GraphQL("RepositoryFindRef"), 979 httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`)) 980 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 981 httpmock.StatusStringResponse(404, `{}`)) 982 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), 983 httpmock.StatusStringResponse(201, `{ 984 "url": "https://api.github.com/releases/123", 985 "upload_url": "https://api.github.com/assets/upload", 986 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 987 }`)) 988 }, 989 wantParams: map[string]interface{}{ 990 "body": "hello from annotated tag", 991 "draft": false, 992 "prerelease": false, 993 "tag_name": "v1.2.3", 994 }, 995 wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 996 }, 997 { 998 name: "error when unpublished local tag and target not specified", 999 opts: &CreateOptions{ 1000 TagName: "v1.2.3", 1001 }, 1002 runStubs: func(rs *run.CommandStubber) { 1003 rs.Register(`git tag --list`, 0, "tag exists") 1004 }, 1005 httpStubs: func(reg *httpmock.Registry) { 1006 reg.Register(httpmock.GraphQL("RepositoryFindRef"), 1007 httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": ""}}}}`)) 1008 }, 1009 wantErr: "tag v1.2.3 exists locally but has not been pushed to OWNER/REPO, please push it before continuing or specify the `--target` flag to create a new tag", 1010 }, 1011 { 1012 name: "create a release when unpublished local tag and target specified", 1013 opts: &CreateOptions{ 1014 TagName: "v1.2.3", 1015 Target: "main", 1016 }, 1017 askStubs: func(as *prompt.AskStubber) { 1018 as.StubPrompt("Title (optional)").AnswerWith("") 1019 as.StubPrompt("Release notes"). 1020 AssertOptions([]string{"Write my own", "Write using generated notes as template", "Write using git tag message as template", "Leave blank"}). 1021 AnswerWith("Leave blank") 1022 as.StubPrompt("Is this a prerelease?").AnswerWith(false) 1023 as.StubPrompt("Submit?").AnswerWith("Publish release") 1024 }, 1025 runStubs: func(rs *run.CommandStubber) { 1026 rs.Register(`git tag --list`, 0, "tag exists") 1027 }, 1028 httpStubs: func(reg *httpmock.Registry) { 1029 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 1030 httpmock.StatusStringResponse(200, `{ 1031 "name": "generated name", 1032 "body": "generated body" 1033 }`)) 1034 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{ 1035 "url": "https://api.github.com/releases/123", 1036 "upload_url": "https://api.github.com/assets/upload", 1037 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 1038 }`)) 1039 }, 1040 wantParams: map[string]interface{}{ 1041 "draft": false, 1042 "prerelease": false, 1043 "tag_name": "v1.2.3", 1044 "target_commitish": "main", 1045 }, 1046 wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 1047 }, 1048 { 1049 name: "create a release using generated notes with previous tag", 1050 opts: &CreateOptions{ 1051 TagName: "v1.2.3", 1052 NotesStartTag: "v1.1.0", 1053 }, 1054 askStubs: func(as *prompt.AskStubber) { 1055 as.StubPrompt("Title (optional)").AnswerDefault() 1056 as.StubPrompt("Release notes"). 1057 AssertOptions([]string{"Write my own", "Write using generated notes as template", "Leave blank"}). 1058 AnswerWith("Write using generated notes as template") 1059 as.StubPrompt("Is this a prerelease?").AnswerWith(false) 1060 as.StubPrompt("Submit?").AnswerWith("Publish release") 1061 }, 1062 runStubs: func(rs *run.CommandStubber) { 1063 rs.Register(`git tag --list`, 1, "") 1064 }, 1065 httpStubs: func(reg *httpmock.Registry) { 1066 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 1067 httpmock.RESTPayload(200, `{ 1068 "name": "generated name", 1069 "body": "generated body" 1070 }`, func(params map[string]interface{}) { 1071 assert.Equal(t, map[string]interface{}{ 1072 "tag_name": "v1.2.3", 1073 "previous_tag_name": "v1.1.0", 1074 }, params) 1075 })) 1076 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), 1077 httpmock.StatusStringResponse(201, `{ 1078 "url": "https://api.github.com/releases/123", 1079 "upload_url": "https://api.github.com/assets/upload", 1080 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 1081 }`)) 1082 }, 1083 wantParams: map[string]interface{}{ 1084 "body": "generated body", 1085 "draft": false, 1086 "name": "generated name", 1087 "prerelease": false, 1088 "tag_name": "v1.2.3", 1089 }, 1090 wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 1091 }, 1092 { 1093 name: "create a release using commit log as notes with previous tag", 1094 opts: &CreateOptions{ 1095 TagName: "v1.2.3", 1096 NotesStartTag: "v1.1.0", 1097 }, 1098 askStubs: func(as *prompt.AskStubber) { 1099 as.StubPrompt("Title (optional)").AnswerDefault() 1100 as.StubPrompt("Release notes"). 1101 AssertOptions([]string{"Write my own", "Write using commit log as template", "Leave blank"}). 1102 AnswerWith("Write using commit log as template") 1103 as.StubPrompt("Is this a prerelease?").AnswerWith(false) 1104 as.StubPrompt("Submit?").AnswerWith("Publish release") 1105 }, 1106 runStubs: func(rs *run.CommandStubber) { 1107 rs.Register(`git tag --list`, 1, "") 1108 rs.Register(`git .+log .+v1\.1\.0\.\.HEAD$`, 0, "commit subject\n\ncommit body\n") 1109 }, 1110 httpStubs: func(reg *httpmock.Registry) { 1111 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"), 1112 httpmock.StatusStringResponse(404, `{}`)) 1113 reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), 1114 httpmock.StatusStringResponse(201, `{ 1115 "url": "https://api.github.com/releases/123", 1116 "upload_url": "https://api.github.com/assets/upload", 1117 "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" 1118 }`)) 1119 }, 1120 wantParams: map[string]interface{}{ 1121 "body": "* commit subject\n\n commit body\n ", 1122 "draft": false, 1123 "prerelease": false, 1124 "tag_name": "v1.2.3", 1125 }, 1126 wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", 1127 }, 1128 } 1129 for _, tt := range tests { 1130 ios, _, stdout, stderr := iostreams.Test() 1131 ios.SetStdoutTTY(true) 1132 ios.SetStdinTTY(true) 1133 ios.SetStderrTTY(true) 1134 tt.opts.IO = ios 1135 1136 reg := &httpmock.Registry{} 1137 defer reg.Verify(t) 1138 tt.httpStubs(reg) 1139 tt.opts.HttpClient = func() (*http.Client, error) { 1140 return &http.Client{Transport: reg}, nil 1141 } 1142 1143 tt.opts.BaseRepo = func() (ghrepo.Interface, error) { 1144 return ghrepo.FromFullName("OWNER/REPO") 1145 } 1146 1147 tt.opts.Config = func() (config.Config, error) { 1148 return config.NewBlankConfig(), nil 1149 } 1150 1151 tt.opts.Edit = func(_, _, val string, _ io.Reader, _, _ io.Writer) (string, error) { 1152 return val, nil 1153 } 1154 1155 tt.opts.GitClient = &git.Client{GitPath: "some/path/git"} 1156 1157 t.Run(tt.name, func(t *testing.T) { 1158 //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock 1159 as := prompt.NewAskStubber(t) 1160 if tt.askStubs != nil { 1161 tt.askStubs(as) 1162 } 1163 1164 rs, teardown := run.Stub() 1165 defer teardown(t) 1166 if tt.runStubs != nil { 1167 tt.runStubs(rs) 1168 } 1169 1170 err := createRun(tt.opts) 1171 1172 if tt.wantErr != "" { 1173 require.EqualError(t, err, tt.wantErr) 1174 return 1175 } else { 1176 require.NoError(t, err) 1177 } 1178 1179 if tt.wantParams != nil { 1180 var r *http.Request 1181 for _, req := range reg.Requests { 1182 if req.URL.Path == "/repos/OWNER/REPO/releases" { 1183 r = req 1184 break 1185 } 1186 } 1187 if r == nil { 1188 t.Fatalf("no http requests for creating a release found") 1189 } 1190 bb, err := io.ReadAll(r.Body) 1191 assert.NoError(t, err) 1192 var params map[string]interface{} 1193 err = json.Unmarshal(bb, ¶ms) 1194 assert.NoError(t, err) 1195 assert.Equal(t, tt.wantParams, params) 1196 } 1197 1198 assert.Equal(t, tt.wantOut, stdout.String()) 1199 assert.Equal(t, "", stderr.String()) 1200 }) 1201 } 1202 } 1203 1204 func boolPtr(b bool) *bool { 1205 return &b 1206 }