github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/repo/create/create_test.go (about) 1 package create 2 3 import ( 4 "bytes" 5 "fmt" 6 "net/http" 7 "testing" 8 9 "github.com/ungtb10d/cli/v2/git" 10 "github.com/ungtb10d/cli/v2/internal/config" 11 "github.com/ungtb10d/cli/v2/internal/prompter" 12 "github.com/ungtb10d/cli/v2/internal/run" 13 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 14 "github.com/ungtb10d/cli/v2/pkg/httpmock" 15 "github.com/ungtb10d/cli/v2/pkg/iostreams" 16 "github.com/google/shlex" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 ) 20 21 func TestNewCmdCreate(t *testing.T) { 22 tests := []struct { 23 name string 24 tty bool 25 cli string 26 wantsErr bool 27 errMsg string 28 wantsOpts CreateOptions 29 }{ 30 { 31 name: "no args tty", 32 tty: true, 33 cli: "", 34 wantsOpts: CreateOptions{Interactive: true}, 35 }, 36 { 37 name: "no args no-tty", 38 tty: false, 39 cli: "", 40 wantsErr: true, 41 errMsg: "at least one argument required in non-interactive mode", 42 }, 43 { 44 name: "new repo from remote", 45 cli: "NEWREPO --public --clone", 46 wantsOpts: CreateOptions{ 47 Name: "NEWREPO", 48 Public: true, 49 Clone: true}, 50 }, 51 { 52 name: "no visibility", 53 tty: true, 54 cli: "NEWREPO", 55 wantsErr: true, 56 errMsg: "`--public`, `--private`, or `--internal` required when not running interactively", 57 }, 58 { 59 name: "multiple visibility", 60 tty: true, 61 cli: "NEWREPO --public --private", 62 wantsErr: true, 63 errMsg: "expected exactly one of `--public`, `--private`, or `--internal`", 64 }, 65 { 66 name: "new remote from local", 67 cli: "--source=/path/to/repo --private", 68 wantsOpts: CreateOptions{ 69 Private: true, 70 Source: "/path/to/repo"}, 71 }, 72 { 73 name: "new remote from local with remote", 74 cli: "--source=/path/to/repo --public --remote upstream", 75 wantsOpts: CreateOptions{ 76 Public: true, 77 Source: "/path/to/repo", 78 Remote: "upstream", 79 }, 80 }, 81 { 82 name: "new remote from local with push", 83 cli: "--source=/path/to/repo --push --public", 84 wantsOpts: CreateOptions{ 85 Public: true, 86 Source: "/path/to/repo", 87 Push: true, 88 }, 89 }, 90 { 91 name: "new remote from local without visibility", 92 cli: "--source=/path/to/repo --push", 93 wantsOpts: CreateOptions{ 94 Source: "/path/to/repo", 95 Push: true, 96 }, 97 wantsErr: true, 98 errMsg: "`--public`, `--private`, or `--internal` required when not running interactively", 99 }, 100 { 101 name: "source with template", 102 cli: "--source=/path/to/repo --private --template mytemplate", 103 wantsErr: true, 104 errMsg: "the `--source` option is not supported with `--clone`, `--template`, `--license`, or `--gitignore`", 105 }, 106 { 107 name: "include all branches without template", 108 cli: "--source=/path/to/repo --private --include-all-branches", 109 wantsErr: true, 110 errMsg: "the `--include-all-branches` option is only supported when using `--template`", 111 }, 112 { 113 name: "new remote from template with include all branches", 114 cli: "template-repo --template https://github.com/OWNER/REPO --public --include-all-branches", 115 wantsOpts: CreateOptions{ 116 Name: "template-repo", 117 Public: true, 118 Template: "https://github.com/OWNER/REPO", 119 IncludeAllBranches: true, 120 }, 121 }, 122 } 123 124 for _, tt := range tests { 125 t.Run(tt.name, func(t *testing.T) { 126 ios, _, _, _ := iostreams.Test() 127 ios.SetStdinTTY(tt.tty) 128 ios.SetStdoutTTY(tt.tty) 129 130 f := &cmdutil.Factory{ 131 IOStreams: ios, 132 } 133 134 var opts *CreateOptions 135 cmd := NewCmdCreate(f, func(o *CreateOptions) error { 136 opts = o 137 return nil 138 }) 139 140 // TODO STUPID HACK 141 // cobra aggressively adds help to all commands. since we're not running through the root command 142 // (which manages help when running for real) and since create has a '-h' flag (for homepage), 143 // cobra blows up when it tried to add a help flag and -h is already in use. This hack adds a 144 // dummy help flag with a random shorthand to get around this. 145 cmd.Flags().BoolP("help", "x", false, "") 146 147 args, err := shlex.Split(tt.cli) 148 require.NoError(t, err) 149 cmd.SetArgs(args) 150 cmd.SetIn(&bytes.Buffer{}) 151 cmd.SetOut(&bytes.Buffer{}) 152 cmd.SetErr(&bytes.Buffer{}) 153 154 _, err = cmd.ExecuteC() 155 if tt.wantsErr { 156 assert.Error(t, err) 157 assert.Equal(t, tt.errMsg, err.Error()) 158 return 159 } else { 160 require.NoError(t, err) 161 } 162 163 assert.Equal(t, tt.wantsOpts.Interactive, opts.Interactive) 164 assert.Equal(t, tt.wantsOpts.Source, opts.Source) 165 assert.Equal(t, tt.wantsOpts.Name, opts.Name) 166 assert.Equal(t, tt.wantsOpts.Public, opts.Public) 167 assert.Equal(t, tt.wantsOpts.Internal, opts.Internal) 168 assert.Equal(t, tt.wantsOpts.Private, opts.Private) 169 assert.Equal(t, tt.wantsOpts.Clone, opts.Clone) 170 }) 171 } 172 } 173 174 func Test_createRun(t *testing.T) { 175 tests := []struct { 176 name string 177 tty bool 178 opts *CreateOptions 179 httpStubs func(*httpmock.Registry) 180 promptStubs func(*prompter.PrompterMock) 181 execStubs func(*run.CommandStubber) 182 wantStdout string 183 wantErr bool 184 errMsg string 185 }{ 186 { 187 name: "interactive create from scratch with gitignore and license", 188 opts: &CreateOptions{Interactive: true}, 189 tty: true, 190 wantStdout: "✓ Created repository OWNER/REPO on GitHub\n", 191 promptStubs: func(p *prompter.PrompterMock) { 192 p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) { 193 switch message { 194 case "Would you like to add a README file?": 195 return false, nil 196 case "Would you like to add a .gitignore?": 197 return true, nil 198 case "Would you like to add a license?": 199 return true, nil 200 case `This will create "REPO" as a private repository on GitHub. Continue?`: 201 return defaultValue, nil 202 case "Clone the new repository locally?": 203 return defaultValue, nil 204 default: 205 return false, fmt.Errorf("unexpected confirm prompt: %s", message) 206 } 207 } 208 p.InputFunc = func(message, defaultValue string) (string, error) { 209 switch message { 210 case "Repository name": 211 return "REPO", nil 212 case "Description": 213 return "my new repo", nil 214 default: 215 return "", fmt.Errorf("unexpected input prompt: %s", message) 216 } 217 } 218 p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { 219 switch message { 220 case "What would you like to do?": 221 return prompter.IndexFor(options, "Create a new repository on GitHub from scratch") 222 case "Visibility": 223 return prompter.IndexFor(options, "Private") 224 case "Choose a license": 225 return prompter.IndexFor(options, "GNU Lesser General Public License v3.0") 226 case "Choose a .gitignore template": 227 return prompter.IndexFor(options, "Go") 228 default: 229 return 0, fmt.Errorf("unexpected select prompt: %s", message) 230 } 231 } 232 }, 233 httpStubs: func(reg *httpmock.Registry) { 234 reg.Register( 235 httpmock.REST("GET", "gitignore/templates"), 236 httpmock.StringResponse(`["Actionscript","Android","AppceleratorTitanium","Autotools","Bancha","C","C++","Go"]`)) 237 reg.Register( 238 httpmock.REST("GET", "licenses"), 239 httpmock.StringResponse(`[{"key": "mit","name": "MIT License"},{"key": "lgpl-3.0","name": "GNU Lesser General Public License v3.0"}]`)) 240 reg.Register( 241 httpmock.REST("POST", "user/repos"), 242 httpmock.StringResponse(`{"name":"REPO", "owner":{"login": "OWNER"}, "html_url":"https://github.com/OWNER/REPO"}`)) 243 244 }, 245 execStubs: func(cs *run.CommandStubber) { 246 cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "") 247 }, 248 }, 249 { 250 name: "interactive create from scratch but cancel before submit", 251 opts: &CreateOptions{Interactive: true}, 252 tty: true, 253 promptStubs: func(p *prompter.PrompterMock) { 254 p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) { 255 switch message { 256 case "Would you like to add a README file?": 257 return false, nil 258 case "Would you like to add a .gitignore?": 259 return false, nil 260 case "Would you like to add a license?": 261 return false, nil 262 case `This will create "REPO" as a private repository on GitHub. Continue?`: 263 return false, nil 264 default: 265 return false, fmt.Errorf("unexpected confirm prompt: %s", message) 266 } 267 } 268 p.InputFunc = func(message, defaultValue string) (string, error) { 269 switch message { 270 case "Repository name": 271 return "REPO", nil 272 case "Description": 273 return "my new repo", nil 274 default: 275 return "", fmt.Errorf("unexpected input prompt: %s", message) 276 } 277 } 278 p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { 279 switch message { 280 case "What would you like to do?": 281 return prompter.IndexFor(options, "Create a new repository on GitHub from scratch") 282 case "Visibility": 283 return prompter.IndexFor(options, "Private") 284 default: 285 return 0, fmt.Errorf("unexpected select prompt: %s", message) 286 } 287 } 288 }, 289 wantStdout: "", 290 wantErr: true, 291 errMsg: "CancelError", 292 }, 293 { 294 name: "interactive with existing repository public", 295 opts: &CreateOptions{Interactive: true}, 296 tty: true, 297 promptStubs: func(p *prompter.PrompterMock) { 298 p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) { 299 switch message { 300 case "Add a remote?": 301 return false, nil 302 default: 303 return false, fmt.Errorf("unexpected confirm prompt: %s", message) 304 } 305 } 306 p.InputFunc = func(message, defaultValue string) (string, error) { 307 switch message { 308 case "Path to local repository": 309 return defaultValue, nil 310 case "Repository name": 311 return "REPO", nil 312 case "Description": 313 return "my new repo", nil 314 default: 315 return "", fmt.Errorf("unexpected input prompt: %s", message) 316 } 317 } 318 p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { 319 switch message { 320 case "What would you like to do?": 321 return prompter.IndexFor(options, "Push an existing local repository to GitHub") 322 case "Visibility": 323 return prompter.IndexFor(options, "Private") 324 default: 325 return 0, fmt.Errorf("unexpected select prompt: %s", message) 326 } 327 } 328 }, 329 httpStubs: func(reg *httpmock.Registry) { 330 reg.Register( 331 httpmock.GraphQL(`mutation RepositoryCreate\b`), 332 httpmock.StringResponse(` 333 { 334 "data": { 335 "createRepository": { 336 "repository": { 337 "id": "REPOID", 338 "name": "REPO", 339 "owner": {"login":"OWNER"}, 340 "url": "https://github.com/OWNER/REPO" 341 } 342 } 343 } 344 }`)) 345 }, 346 execStubs: func(cs *run.CommandStubber) { 347 cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") 348 cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") 349 }, 350 wantStdout: "✓ Created repository OWNER/REPO on GitHub\n", 351 }, 352 { 353 name: "interactive with existing repository public add remote and push", 354 opts: &CreateOptions{Interactive: true}, 355 tty: true, 356 promptStubs: func(p *prompter.PrompterMock) { 357 p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) { 358 switch message { 359 case "Add a remote?": 360 return true, nil 361 case `Would you like to push commits from the current branch to "origin"?`: 362 return true, nil 363 default: 364 return false, fmt.Errorf("unexpected confirm prompt: %s", message) 365 } 366 } 367 p.InputFunc = func(message, defaultValue string) (string, error) { 368 switch message { 369 case "Path to local repository": 370 return defaultValue, nil 371 case "Repository name": 372 return "REPO", nil 373 case "Description": 374 return "my new repo", nil 375 case "What should the new remote be called?": 376 return defaultValue, nil 377 default: 378 return "", fmt.Errorf("unexpected input prompt: %s", message) 379 } 380 } 381 p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { 382 switch message { 383 case "What would you like to do?": 384 return prompter.IndexFor(options, "Push an existing local repository to GitHub") 385 case "Visibility": 386 return prompter.IndexFor(options, "Private") 387 default: 388 return 0, fmt.Errorf("unexpected select prompt: %s", message) 389 } 390 } 391 }, 392 httpStubs: func(reg *httpmock.Registry) { 393 reg.Register( 394 httpmock.GraphQL(`mutation RepositoryCreate\b`), 395 httpmock.StringResponse(` 396 { 397 "data": { 398 "createRepository": { 399 "repository": { 400 "id": "REPOID", 401 "name": "REPO", 402 "owner": {"login":"OWNER"}, 403 "url": "https://github.com/OWNER/REPO" 404 } 405 } 406 } 407 }`)) 408 }, 409 execStubs: func(cs *run.CommandStubber) { 410 cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") 411 cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") 412 cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") 413 cs.Register(`git -C . push --set-upstream origin HEAD`, 0, "") 414 }, 415 wantStdout: "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n", 416 }, 417 { 418 name: "noninteractive create from scratch", 419 opts: &CreateOptions{ 420 Interactive: false, 421 Name: "REPO", 422 Visibility: "PRIVATE", 423 }, 424 tty: false, 425 httpStubs: func(reg *httpmock.Registry) { 426 reg.Register( 427 httpmock.GraphQL(`mutation RepositoryCreate\b`), 428 httpmock.StringResponse(` 429 { 430 "data": { 431 "createRepository": { 432 "repository": { 433 "id": "REPOID", 434 "name": "REPO", 435 "owner": {"login":"OWNER"}, 436 "url": "https://github.com/OWNER/REPO" 437 } 438 } 439 } 440 }`)) 441 }, 442 wantStdout: "https://github.com/OWNER/REPO\n", 443 }, 444 { 445 name: "noninteractive create from source", 446 opts: &CreateOptions{ 447 Interactive: false, 448 Source: ".", 449 Name: "REPO", 450 Visibility: "PRIVATE", 451 }, 452 tty: false, 453 httpStubs: func(reg *httpmock.Registry) { 454 reg.Register( 455 httpmock.GraphQL(`mutation RepositoryCreate\b`), 456 httpmock.StringResponse(` 457 { 458 "data": { 459 "createRepository": { 460 "repository": { 461 "id": "REPOID", 462 "name": "REPO", 463 "owner": {"login":"OWNER"}, 464 "url": "https://github.com/OWNER/REPO" 465 } 466 } 467 } 468 }`)) 469 }, 470 execStubs: func(cs *run.CommandStubber) { 471 cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") 472 cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") 473 cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") 474 }, 475 wantStdout: "https://github.com/OWNER/REPO\n", 476 }, 477 } 478 for _, tt := range tests { 479 prompterMock := &prompter.PrompterMock{} 480 tt.opts.Prompter = prompterMock 481 if tt.promptStubs != nil { 482 tt.promptStubs(prompterMock) 483 } 484 485 reg := &httpmock.Registry{} 486 if tt.httpStubs != nil { 487 tt.httpStubs(reg) 488 } 489 tt.opts.HttpClient = func() (*http.Client, error) { 490 return &http.Client{Transport: reg}, nil 491 } 492 tt.opts.Config = func() (config.Config, error) { 493 return config.NewBlankConfig(), nil 494 } 495 496 tt.opts.GitClient = &git.Client{GitPath: "some/path/git"} 497 498 ios, _, stdout, stderr := iostreams.Test() 499 ios.SetStdinTTY(tt.tty) 500 ios.SetStdoutTTY(tt.tty) 501 tt.opts.IO = ios 502 503 t.Run(tt.name, func(t *testing.T) { 504 cs, restoreRun := run.Stub() 505 defer restoreRun(t) 506 if tt.execStubs != nil { 507 tt.execStubs(cs) 508 } 509 510 defer reg.Verify(t) 511 err := createRun(tt.opts) 512 if tt.wantErr { 513 assert.Error(t, err) 514 assert.Equal(t, tt.errMsg, err.Error()) 515 return 516 } 517 assert.NoError(t, err) 518 assert.Equal(t, tt.wantStdout, stdout.String()) 519 assert.Equal(t, "", stderr.String()) 520 }) 521 } 522 }