github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/extension/command_test.go (about) 1 package extension 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "net/http" 8 "net/url" 9 "os" 10 "strings" 11 "testing" 12 13 "github.com/MakeNowJust/heredoc" 14 "github.com/ungtb10d/cli/v2/internal/browser" 15 "github.com/ungtb10d/cli/v2/internal/config" 16 "github.com/ungtb10d/cli/v2/internal/ghrepo" 17 "github.com/ungtb10d/cli/v2/internal/prompter" 18 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 19 "github.com/ungtb10d/cli/v2/pkg/extensions" 20 "github.com/ungtb10d/cli/v2/pkg/httpmock" 21 "github.com/ungtb10d/cli/v2/pkg/iostreams" 22 "github.com/ungtb10d/cli/v2/pkg/search" 23 "github.com/spf13/cobra" 24 "github.com/stretchr/testify/assert" 25 ) 26 27 func TestNewCmdExtension(t *testing.T) { 28 tempDir := t.TempDir() 29 oldWd, _ := os.Getwd() 30 assert.NoError(t, os.Chdir(tempDir)) 31 t.Cleanup(func() { _ = os.Chdir(oldWd) }) 32 33 tests := []struct { 34 name string 35 args []string 36 managerStubs func(em *extensions.ExtensionManagerMock) func(*testing.T) 37 prompterStubs func(pm *prompter.PrompterMock) 38 httpStubs func(reg *httpmock.Registry) 39 browseStubs func(*browser.Stub) func(*testing.T) 40 isTTY bool 41 wantErr bool 42 errMsg string 43 wantStdout string 44 wantStderr string 45 }{ 46 { 47 name: "search for extensions", 48 args: []string{"search"}, 49 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 50 em.ListFunc = func() []extensions.Extension { 51 return []extensions.Extension{ 52 &extensions.ExtensionMock{ 53 URLFunc: func() string { 54 return "https://github.com/vilmibm/gh-screensaver" 55 }, 56 }, 57 &extensions.ExtensionMock{ 58 URLFunc: func() string { 59 return "https://github.com/github/gh-gei" 60 }, 61 }, 62 } 63 } 64 return func(t *testing.T) { 65 listCalls := em.ListCalls() 66 assert.Equal(t, 1, len(listCalls)) 67 } 68 }, 69 httpStubs: func(reg *httpmock.Registry) { 70 values := url.Values{ 71 "page": []string{"1"}, 72 "per_page": []string{"30"}, 73 "q": []string{"topic:gh-extension"}, 74 } 75 reg.Register( 76 httpmock.QueryMatcher("GET", "search/repositories", values), 77 httpmock.JSONResponse(searchResults()), 78 ) 79 }, 80 isTTY: true, 81 wantStdout: "Showing 4 of 4 extensions\n\n REPO DESCRIPTION\n✓ vilmibm/gh-screensaver terminal animations\n cli/gh-cool it's just cool ok\n samcoe/gh-triage helps with triage\n✓ github/gh-gei something something enterprise\n", 82 }, 83 { 84 name: "search for extensions non-tty", 85 args: []string{"search"}, 86 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 87 em.ListFunc = func() []extensions.Extension { 88 return []extensions.Extension{ 89 &extensions.ExtensionMock{ 90 URLFunc: func() string { 91 return "https://github.com/vilmibm/gh-screensaver" 92 }, 93 }, 94 &extensions.ExtensionMock{ 95 URLFunc: func() string { 96 return "https://github.com/github/gh-gei" 97 }, 98 }, 99 } 100 } 101 return func(t *testing.T) { 102 listCalls := em.ListCalls() 103 assert.Equal(t, 1, len(listCalls)) 104 } 105 }, 106 httpStubs: func(reg *httpmock.Registry) { 107 values := url.Values{ 108 "page": []string{"1"}, 109 "per_page": []string{"30"}, 110 "q": []string{"topic:gh-extension"}, 111 } 112 reg.Register( 113 httpmock.QueryMatcher("GET", "search/repositories", values), 114 httpmock.JSONResponse(searchResults()), 115 ) 116 }, 117 wantStdout: "installed\tvilmibm/gh-screensaver\tterminal animations\n\tcli/gh-cool\tit's just cool ok\n\tsamcoe/gh-triage\thelps with triage\ninstalled\tgithub/gh-gei\tsomething something enterprise\n", 118 }, 119 { 120 name: "search for extensions with keywords", 121 args: []string{"search", "screen"}, 122 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 123 em.ListFunc = func() []extensions.Extension { 124 return []extensions.Extension{ 125 &extensions.ExtensionMock{ 126 URLFunc: func() string { 127 return "https://github.com/vilmibm/gh-screensaver" 128 }, 129 }, 130 &extensions.ExtensionMock{ 131 URLFunc: func() string { 132 return "https://github.com/github/gh-gei" 133 }, 134 }, 135 } 136 } 137 return func(t *testing.T) { 138 listCalls := em.ListCalls() 139 assert.Equal(t, 1, len(listCalls)) 140 } 141 }, 142 httpStubs: func(reg *httpmock.Registry) { 143 values := url.Values{ 144 "page": []string{"1"}, 145 "per_page": []string{"30"}, 146 "q": []string{"screen topic:gh-extension"}, 147 } 148 results := searchResults() 149 results.Total = 1 150 results.Items = []search.Repository{results.Items[0]} 151 reg.Register( 152 httpmock.QueryMatcher("GET", "search/repositories", values), 153 httpmock.JSONResponse(results), 154 ) 155 }, 156 wantStdout: "installed\tvilmibm/gh-screensaver\tterminal animations\n", 157 }, 158 { 159 name: "search for extensions with parameter flags", 160 args: []string{"search", "--limit", "1", "--order", "asc", "--sort", "stars"}, 161 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 162 em.ListFunc = func() []extensions.Extension { 163 return []extensions.Extension{} 164 } 165 return func(t *testing.T) { 166 listCalls := em.ListCalls() 167 assert.Equal(t, 1, len(listCalls)) 168 } 169 }, 170 httpStubs: func(reg *httpmock.Registry) { 171 values := url.Values{ 172 "page": []string{"1"}, 173 "order": []string{"asc"}, 174 "sort": []string{"stars"}, 175 "per_page": []string{"1"}, 176 "q": []string{"topic:gh-extension"}, 177 } 178 results := searchResults() 179 results.Total = 1 180 results.Items = []search.Repository{results.Items[0]} 181 reg.Register( 182 httpmock.QueryMatcher("GET", "search/repositories", values), 183 httpmock.JSONResponse(results), 184 ) 185 }, 186 wantStdout: "\tvilmibm/gh-screensaver\tterminal animations\n", 187 }, 188 { 189 name: "search for extensions with qualifier flags", 190 args: []string{"search", "--license", "GPLv3", "--owner", "jillvalentine"}, 191 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 192 em.ListFunc = func() []extensions.Extension { 193 return []extensions.Extension{} 194 } 195 return func(t *testing.T) { 196 listCalls := em.ListCalls() 197 assert.Equal(t, 1, len(listCalls)) 198 } 199 }, 200 httpStubs: func(reg *httpmock.Registry) { 201 values := url.Values{ 202 "page": []string{"1"}, 203 "per_page": []string{"30"}, 204 "q": []string{"license:GPLv3 topic:gh-extension user:jillvalentine"}, 205 } 206 results := searchResults() 207 results.Total = 1 208 results.Items = []search.Repository{results.Items[0]} 209 reg.Register( 210 httpmock.QueryMatcher("GET", "search/repositories", values), 211 httpmock.JSONResponse(results), 212 ) 213 }, 214 wantStdout: "\tvilmibm/gh-screensaver\tterminal animations\n", 215 }, 216 { 217 name: "search for extensions with web mode", 218 args: []string{"search", "--web"}, 219 browseStubs: func(b *browser.Stub) func(*testing.T) { 220 return func(t *testing.T) { 221 b.Verify(t, "https://github.com/search?q=topic%3Agh-extension&type=repositories") 222 } 223 }, 224 }, 225 { 226 name: "install an extension", 227 args: []string{"install", "owner/gh-some-ext"}, 228 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 229 em.ListFunc = func() []extensions.Extension { 230 return []extensions.Extension{} 231 } 232 em.InstallFunc = func(_ ghrepo.Interface, _ string) error { 233 return nil 234 } 235 return func(t *testing.T) { 236 installCalls := em.InstallCalls() 237 assert.Equal(t, 1, len(installCalls)) 238 assert.Equal(t, "gh-some-ext", installCalls[0].InterfaceMoqParam.RepoName()) 239 listCalls := em.ListCalls() 240 assert.Equal(t, 1, len(listCalls)) 241 } 242 }, 243 }, 244 { 245 name: "install an extension with same name as existing extension", 246 args: []string{"install", "owner/gh-existing-ext"}, 247 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 248 em.ListFunc = func() []extensions.Extension { 249 e := &Extension{path: "owner2/gh-existing-ext"} 250 return []extensions.Extension{e} 251 } 252 return func(t *testing.T) { 253 calls := em.ListCalls() 254 assert.Equal(t, 1, len(calls)) 255 } 256 }, 257 wantErr: true, 258 errMsg: "there is already an installed extension that provides the \"existing-ext\" command", 259 }, 260 { 261 name: "install local extension", 262 args: []string{"install", "."}, 263 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 264 em.InstallLocalFunc = func(dir string) error { 265 return nil 266 } 267 return func(t *testing.T) { 268 calls := em.InstallLocalCalls() 269 assert.Equal(t, 1, len(calls)) 270 assert.Equal(t, tempDir, normalizeDir(calls[0].Dir)) 271 } 272 }, 273 }, 274 { 275 name: "error extension not found", 276 args: []string{"install", "owner/gh-some-ext"}, 277 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 278 em.ListFunc = func() []extensions.Extension { 279 return []extensions.Extension{} 280 } 281 em.InstallFunc = func(_ ghrepo.Interface, _ string) error { 282 return repositoryNotFoundErr 283 } 284 return func(t *testing.T) { 285 installCalls := em.InstallCalls() 286 assert.Equal(t, 1, len(installCalls)) 287 assert.Equal(t, "gh-some-ext", installCalls[0].InterfaceMoqParam.RepoName()) 288 } 289 }, 290 wantErr: true, 291 errMsg: "X Could not find extension 'owner/gh-some-ext' on host github.com", 292 }, 293 { 294 name: "install local extension with pin", 295 args: []string{"install", ".", "--pin", "v1.0.0"}, 296 wantErr: true, 297 errMsg: "local extensions cannot be pinned", 298 isTTY: true, 299 }, 300 { 301 name: "upgrade argument error", 302 args: []string{"upgrade"}, 303 wantErr: true, 304 errMsg: "specify an extension to upgrade or `--all`", 305 }, 306 { 307 name: "upgrade --all with extension name error", 308 args: []string{"upgrade", "test", "--all"}, 309 wantErr: true, 310 errMsg: "cannot use `--all` with extension name", 311 }, 312 { 313 name: "upgrade an extension", 314 args: []string{"upgrade", "hello"}, 315 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 316 em.UpgradeFunc = func(name string, force bool) error { 317 return nil 318 } 319 return func(t *testing.T) { 320 calls := em.UpgradeCalls() 321 assert.Equal(t, 1, len(calls)) 322 assert.Equal(t, "hello", calls[0].Name) 323 } 324 }, 325 isTTY: true, 326 wantStdout: "✓ Successfully upgraded extension hello\n", 327 }, 328 { 329 name: "upgrade an extension dry run", 330 args: []string{"upgrade", "hello", "--dry-run"}, 331 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 332 em.EnableDryRunModeFunc = func() {} 333 em.UpgradeFunc = func(name string, force bool) error { 334 return nil 335 } 336 return func(t *testing.T) { 337 dryRunCalls := em.EnableDryRunModeCalls() 338 assert.Equal(t, 1, len(dryRunCalls)) 339 upgradeCalls := em.UpgradeCalls() 340 assert.Equal(t, 1, len(upgradeCalls)) 341 assert.Equal(t, "hello", upgradeCalls[0].Name) 342 assert.False(t, upgradeCalls[0].Force) 343 } 344 }, 345 isTTY: true, 346 wantStdout: "✓ Would have upgraded extension hello\n", 347 }, 348 { 349 name: "upgrade an extension notty", 350 args: []string{"upgrade", "hello"}, 351 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 352 em.UpgradeFunc = func(name string, force bool) error { 353 return nil 354 } 355 return func(t *testing.T) { 356 calls := em.UpgradeCalls() 357 assert.Equal(t, 1, len(calls)) 358 assert.Equal(t, "hello", calls[0].Name) 359 } 360 }, 361 isTTY: false, 362 }, 363 { 364 name: "upgrade an up-to-date extension", 365 args: []string{"upgrade", "hello"}, 366 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 367 em.UpgradeFunc = func(name string, force bool) error { 368 return upToDateError 369 } 370 return func(t *testing.T) { 371 calls := em.UpgradeCalls() 372 assert.Equal(t, 1, len(calls)) 373 assert.Equal(t, "hello", calls[0].Name) 374 } 375 }, 376 isTTY: true, 377 wantStdout: "✓ Extension already up to date\n", 378 wantStderr: "", 379 }, 380 { 381 name: "upgrade extension error", 382 args: []string{"upgrade", "hello"}, 383 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 384 em.UpgradeFunc = func(name string, force bool) error { 385 return errors.New("oh no") 386 } 387 return func(t *testing.T) { 388 calls := em.UpgradeCalls() 389 assert.Equal(t, 1, len(calls)) 390 assert.Equal(t, "hello", calls[0].Name) 391 } 392 }, 393 isTTY: false, 394 wantErr: true, 395 errMsg: "SilentError", 396 wantStdout: "", 397 wantStderr: "X Failed upgrading extension hello: oh no\n", 398 }, 399 { 400 name: "upgrade an extension gh-prefix", 401 args: []string{"upgrade", "gh-hello"}, 402 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 403 em.UpgradeFunc = func(name string, force bool) error { 404 return nil 405 } 406 return func(t *testing.T) { 407 calls := em.UpgradeCalls() 408 assert.Equal(t, 1, len(calls)) 409 assert.Equal(t, "hello", calls[0].Name) 410 } 411 }, 412 isTTY: true, 413 wantStdout: "✓ Successfully upgraded extension hello\n", 414 }, 415 { 416 name: "upgrade an extension full name", 417 args: []string{"upgrade", "monalisa/gh-hello"}, 418 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 419 em.UpgradeFunc = func(name string, force bool) error { 420 return nil 421 } 422 return func(t *testing.T) { 423 calls := em.UpgradeCalls() 424 assert.Equal(t, 1, len(calls)) 425 assert.Equal(t, "hello", calls[0].Name) 426 } 427 }, 428 isTTY: true, 429 wantStdout: "✓ Successfully upgraded extension hello\n", 430 }, 431 { 432 name: "upgrade all", 433 args: []string{"upgrade", "--all"}, 434 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 435 em.UpgradeFunc = func(name string, force bool) error { 436 return nil 437 } 438 return func(t *testing.T) { 439 calls := em.UpgradeCalls() 440 assert.Equal(t, 1, len(calls)) 441 assert.Equal(t, "", calls[0].Name) 442 } 443 }, 444 isTTY: true, 445 wantStdout: "✓ Successfully upgraded extensions\n", 446 }, 447 { 448 name: "upgrade all dry run", 449 args: []string{"upgrade", "--all", "--dry-run"}, 450 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 451 em.EnableDryRunModeFunc = func() {} 452 em.UpgradeFunc = func(name string, force bool) error { 453 return nil 454 } 455 return func(t *testing.T) { 456 dryRunCalls := em.EnableDryRunModeCalls() 457 assert.Equal(t, 1, len(dryRunCalls)) 458 upgradeCalls := em.UpgradeCalls() 459 assert.Equal(t, 1, len(upgradeCalls)) 460 assert.Equal(t, "", upgradeCalls[0].Name) 461 assert.False(t, upgradeCalls[0].Force) 462 } 463 }, 464 isTTY: true, 465 wantStdout: "✓ Would have upgraded extensions\n", 466 }, 467 { 468 name: "upgrade all none installed", 469 args: []string{"upgrade", "--all"}, 470 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 471 em.UpgradeFunc = func(name string, force bool) error { 472 return noExtensionsInstalledError 473 } 474 return func(t *testing.T) { 475 calls := em.UpgradeCalls() 476 assert.Equal(t, 1, len(calls)) 477 assert.Equal(t, "", calls[0].Name) 478 } 479 }, 480 isTTY: true, 481 wantErr: true, 482 errMsg: "no installed extensions found", 483 }, 484 { 485 name: "upgrade all notty", 486 args: []string{"upgrade", "--all"}, 487 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 488 em.UpgradeFunc = func(name string, force bool) error { 489 return nil 490 } 491 return func(t *testing.T) { 492 calls := em.UpgradeCalls() 493 assert.Equal(t, 1, len(calls)) 494 assert.Equal(t, "", calls[0].Name) 495 } 496 }, 497 isTTY: false, 498 }, 499 { 500 name: "remove extension tty", 501 args: []string{"remove", "hello"}, 502 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 503 em.RemoveFunc = func(name string) error { 504 return nil 505 } 506 return func(t *testing.T) { 507 calls := em.RemoveCalls() 508 assert.Equal(t, 1, len(calls)) 509 assert.Equal(t, "hello", calls[0].Name) 510 } 511 }, 512 isTTY: true, 513 wantStdout: "✓ Removed extension hello\n", 514 }, 515 { 516 name: "remove extension nontty", 517 args: []string{"remove", "hello"}, 518 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 519 em.RemoveFunc = func(name string) error { 520 return nil 521 } 522 return func(t *testing.T) { 523 calls := em.RemoveCalls() 524 assert.Equal(t, 1, len(calls)) 525 assert.Equal(t, "hello", calls[0].Name) 526 } 527 }, 528 isTTY: false, 529 wantStdout: "", 530 }, 531 { 532 name: "remove extension gh-prefix", 533 args: []string{"remove", "gh-hello"}, 534 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 535 em.RemoveFunc = func(name string) error { 536 return nil 537 } 538 return func(t *testing.T) { 539 calls := em.RemoveCalls() 540 assert.Equal(t, 1, len(calls)) 541 assert.Equal(t, "hello", calls[0].Name) 542 } 543 }, 544 isTTY: false, 545 wantStdout: "", 546 }, 547 { 548 name: "remove extension full name", 549 args: []string{"remove", "monalisa/gh-hello"}, 550 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 551 em.RemoveFunc = func(name string) error { 552 return nil 553 } 554 return func(t *testing.T) { 555 calls := em.RemoveCalls() 556 assert.Equal(t, 1, len(calls)) 557 assert.Equal(t, "hello", calls[0].Name) 558 } 559 }, 560 isTTY: false, 561 wantStdout: "", 562 }, 563 { 564 name: "list extensions", 565 args: []string{"list"}, 566 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 567 em.ListFunc = func() []extensions.Extension { 568 ex1 := &Extension{path: "cli/gh-test", url: "https://github.com/cli/gh-test", currentVersion: "1"} 569 ex2 := &Extension{path: "cli/gh-test2", url: "https://github.com/cli/gh-test2", currentVersion: "1"} 570 return []extensions.Extension{ex1, ex2} 571 } 572 return func(t *testing.T) { 573 calls := em.ListCalls() 574 assert.Equal(t, 1, len(calls)) 575 } 576 }, 577 wantStdout: "gh test\tcli/gh-test\t1\ngh test2\tcli/gh-test2\t1\n", 578 }, 579 { 580 name: "create extension interactive", 581 args: []string{"create"}, 582 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 583 em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error { 584 return nil 585 } 586 return func(t *testing.T) { 587 calls := em.CreateCalls() 588 assert.Equal(t, 1, len(calls)) 589 assert.Equal(t, "gh-test", calls[0].Name) 590 } 591 }, 592 isTTY: true, 593 prompterStubs: func(pm *prompter.PrompterMock) { 594 pm.InputFunc = func(prompt, defVal string) (string, error) { 595 if prompt == "Extension name:" { 596 return "test", nil 597 } 598 return "", nil 599 } 600 pm.SelectFunc = func(prompt, defVal string, opts []string) (int, error) { 601 return prompter.IndexFor(opts, "Script (Bash, Ruby, Python, etc)") 602 } 603 }, 604 wantStdout: heredoc.Doc(` 605 ✓ Created directory gh-test 606 ✓ Initialized git repository 607 ✓ Set up extension scaffolding 608 609 gh-test is ready for development! 610 611 Next Steps 612 - run 'cd gh-test; gh extension install .; gh test' to see your new extension in action 613 - commit and use 'gh repo create' to share your extension with others 614 615 For more information on writing extensions: 616 https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions 617 `), 618 }, 619 { 620 name: "create extension with arg, --precompiled=go", 621 args: []string{"create", "test", "--precompiled", "go"}, 622 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 623 em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error { 624 return nil 625 } 626 return func(t *testing.T) { 627 calls := em.CreateCalls() 628 assert.Equal(t, 1, len(calls)) 629 assert.Equal(t, "gh-test", calls[0].Name) 630 } 631 }, 632 isTTY: true, 633 wantStdout: heredoc.Doc(` 634 ✓ Created directory gh-test 635 ✓ Initialized git repository 636 ✓ Set up extension scaffolding 637 ✓ Downloaded Go dependencies 638 ✓ Built gh-test binary 639 640 gh-test is ready for development! 641 642 Next Steps 643 - run 'cd gh-test; gh extension install .; gh test' to see your new extension in action 644 - use 'go build && gh test' to see changes in your code as you develop 645 - commit and use 'gh repo create' to share your extension with others 646 647 For more information on writing extensions: 648 https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions 649 `), 650 }, 651 { 652 name: "create extension with arg, --precompiled=other", 653 args: []string{"create", "test", "--precompiled", "other"}, 654 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 655 em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error { 656 return nil 657 } 658 return func(t *testing.T) { 659 calls := em.CreateCalls() 660 assert.Equal(t, 1, len(calls)) 661 assert.Equal(t, "gh-test", calls[0].Name) 662 } 663 }, 664 isTTY: true, 665 wantStdout: heredoc.Doc(` 666 ✓ Created directory gh-test 667 ✓ Initialized git repository 668 ✓ Set up extension scaffolding 669 670 gh-test is ready for development! 671 672 Next Steps 673 - run 'cd gh-test; gh extension install .' to install your extension locally 674 - fill in script/build.sh with your compilation script for automated builds 675 - compile a gh-test binary locally and run 'gh test' to see changes 676 - commit and use 'gh repo create' to share your extension with others 677 678 For more information on writing extensions: 679 https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions 680 `), 681 }, 682 { 683 name: "create extension tty with argument", 684 args: []string{"create", "test"}, 685 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 686 em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error { 687 return nil 688 } 689 return func(t *testing.T) { 690 calls := em.CreateCalls() 691 assert.Equal(t, 1, len(calls)) 692 assert.Equal(t, "gh-test", calls[0].Name) 693 } 694 }, 695 isTTY: true, 696 wantStdout: heredoc.Doc(` 697 ✓ Created directory gh-test 698 ✓ Initialized git repository 699 ✓ Set up extension scaffolding 700 701 gh-test is ready for development! 702 703 Next Steps 704 - run 'cd gh-test; gh extension install .; gh test' to see your new extension in action 705 - commit and use 'gh repo create' to share your extension with others 706 707 For more information on writing extensions: 708 https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions 709 `), 710 }, 711 { 712 name: "create extension notty", 713 args: []string{"create", "gh-test"}, 714 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 715 em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error { 716 return nil 717 } 718 return func(t *testing.T) { 719 calls := em.CreateCalls() 720 assert.Equal(t, 1, len(calls)) 721 assert.Equal(t, "gh-test", calls[0].Name) 722 } 723 }, 724 isTTY: false, 725 wantStdout: "", 726 }, 727 { 728 name: "exec extension missing", 729 args: []string{"exec", "invalid"}, 730 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 731 em.DispatchFunc = func(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) { 732 return false, nil 733 } 734 return func(t *testing.T) { 735 calls := em.DispatchCalls() 736 assert.Equal(t, 1, len(calls)) 737 assert.EqualValues(t, []string{"invalid"}, calls[0].Args) 738 } 739 }, 740 wantErr: true, 741 errMsg: `extension "invalid" not found`, 742 }, 743 { 744 name: "exec extension with arguments", 745 args: []string{"exec", "test", "arg1", "arg2", "--flag1"}, 746 managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { 747 em.DispatchFunc = func(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) { 748 fmt.Fprintf(stdout, "test output") 749 return true, nil 750 } 751 return func(t *testing.T) { 752 calls := em.DispatchCalls() 753 assert.Equal(t, 1, len(calls)) 754 assert.EqualValues(t, []string{"test", "arg1", "arg2", "--flag1"}, calls[0].Args) 755 } 756 }, 757 wantStdout: "test output", 758 }, 759 { 760 name: "browse", 761 args: []string{"browse"}, 762 wantErr: true, 763 errMsg: "this command runs an interactive UI and needs to be run in a terminal", 764 }, 765 } 766 767 for _, tt := range tests { 768 t.Run(tt.name, func(t *testing.T) { 769 ios, _, stdout, stderr := iostreams.Test() 770 ios.SetStdoutTTY(tt.isTTY) 771 ios.SetStderrTTY(tt.isTTY) 772 773 var assertFunc func(*testing.T) 774 em := &extensions.ExtensionManagerMock{} 775 if tt.managerStubs != nil { 776 assertFunc = tt.managerStubs(em) 777 } 778 779 pm := &prompter.PrompterMock{} 780 if tt.prompterStubs != nil { 781 tt.prompterStubs(pm) 782 } 783 784 reg := httpmock.Registry{} 785 defer reg.Verify(t) 786 client := http.Client{Transport: ®} 787 788 if tt.httpStubs != nil { 789 tt.httpStubs(®) 790 } 791 792 var assertBrowserFunc func(*testing.T) 793 browseStub := &browser.Stub{} 794 if tt.browseStubs != nil { 795 assertBrowserFunc = tt.browseStubs(browseStub) 796 } 797 798 f := cmdutil.Factory{ 799 Config: func() (config.Config, error) { 800 return config.NewBlankConfig(), nil 801 }, 802 IOStreams: ios, 803 ExtensionManager: em, 804 Prompter: pm, 805 Browser: browseStub, 806 HttpClient: func() (*http.Client, error) { 807 return &client, nil 808 }, 809 } 810 811 cmd := NewCmdExtension(&f) 812 cmd.SetArgs(tt.args) 813 cmd.SetOut(io.Discard) 814 cmd.SetErr(io.Discard) 815 816 _, err := cmd.ExecuteC() 817 if tt.wantErr { 818 assert.EqualError(t, err, tt.errMsg) 819 } else { 820 assert.NoError(t, err) 821 } 822 823 if assertFunc != nil { 824 assertFunc(t) 825 } 826 827 if assertBrowserFunc != nil { 828 assertBrowserFunc(t) 829 } 830 831 assert.Equal(t, tt.wantStdout, stdout.String()) 832 assert.Equal(t, tt.wantStderr, stderr.String()) 833 }) 834 } 835 } 836 837 func normalizeDir(d string) string { 838 return strings.TrimPrefix(d, "/private") 839 } 840 841 func Test_checkValidExtension(t *testing.T) { 842 rootCmd := &cobra.Command{} 843 rootCmd.AddCommand(&cobra.Command{Use: "help"}) 844 rootCmd.AddCommand(&cobra.Command{Use: "auth"}) 845 846 m := &extensions.ExtensionManagerMock{ 847 ListFunc: func() []extensions.Extension { 848 return []extensions.Extension{ 849 &extensions.ExtensionMock{ 850 NameFunc: func() string { return "screensaver" }, 851 }, 852 &extensions.ExtensionMock{ 853 NameFunc: func() string { return "triage" }, 854 }, 855 } 856 }, 857 } 858 859 type args struct { 860 rootCmd *cobra.Command 861 manager extensions.ExtensionManager 862 extName string 863 } 864 tests := []struct { 865 name string 866 args args 867 wantError string 868 }{ 869 { 870 name: "valid extension", 871 args: args{ 872 rootCmd: rootCmd, 873 manager: m, 874 extName: "gh-hello", 875 }, 876 }, 877 { 878 name: "invalid extension name", 879 args: args{ 880 rootCmd: rootCmd, 881 manager: m, 882 extName: "gherkins", 883 }, 884 wantError: "extension repository name must start with `gh-`", 885 }, 886 { 887 name: "clashes with built-in command", 888 args: args{ 889 rootCmd: rootCmd, 890 manager: m, 891 extName: "gh-auth", 892 }, 893 wantError: "\"auth\" matches the name of a built-in command", 894 }, 895 { 896 name: "clashes with an installed extension", 897 args: args{ 898 rootCmd: rootCmd, 899 manager: m, 900 extName: "gh-triage", 901 }, 902 wantError: "there is already an installed extension that provides the \"triage\" command", 903 }, 904 } 905 for _, tt := range tests { 906 t.Run(tt.name, func(t *testing.T) { 907 err := checkValidExtension(tt.args.rootCmd, tt.args.manager, tt.args.extName) 908 if tt.wantError == "" { 909 assert.NoError(t, err) 910 } else { 911 assert.EqualError(t, err, tt.wantError) 912 } 913 }) 914 } 915 } 916 917 func searchResults() search.RepositoriesResult { 918 return search.RepositoriesResult{ 919 IncompleteResults: false, 920 Items: []search.Repository{ 921 { 922 FullName: "vilmibm/gh-screensaver", 923 Name: "gh-screensaver", 924 Description: "terminal animations", 925 Owner: search.User{ 926 Login: "vilmibm", 927 }, 928 }, 929 { 930 FullName: "cli/gh-cool", 931 Name: "gh-cool", 932 Description: "it's just cool ok", 933 Owner: search.User{ 934 Login: "cli", 935 }, 936 }, 937 { 938 FullName: "samcoe/gh-triage", 939 Name: "gh-triage", 940 Description: "helps with triage", 941 Owner: search.User{ 942 Login: "samcoe", 943 }, 944 }, 945 { 946 FullName: "github/gh-gei", 947 Name: "gh-gei", 948 Description: "something something enterprise", 949 Owner: search.User{ 950 Login: "github", 951 }, 952 }, 953 }, 954 Total: 4, 955 } 956 }