github.com/secman-team/gh-api@v1.8.2/pkg/cmd/auth/login/login_test.go (about) 1 package login 2 3 import ( 4 "bytes" 5 "net/http" 6 "os" 7 "regexp" 8 "testing" 9 10 "github.com/MakeNowJust/heredoc" 11 "github.com/secman-team/gh-api/core/config" 12 "github.com/secman-team/gh-api/core/run" 13 "github.com/secman-team/gh-api/pkg/cmdutil" 14 "github.com/secman-team/gh-api/pkg/httpmock" 15 "github.com/secman-team/gh-api/pkg/iostreams" 16 "github.com/secman-team/gh-api/pkg/prompt" 17 "github.com/google/shlex" 18 "github.com/stretchr/testify/assert" 19 ) 20 21 func Test_NewCmdLogin(t *testing.T) { 22 tests := []struct { 23 name string 24 cli string 25 stdin string 26 stdinTTY bool 27 wants LoginOptions 28 wantsErr bool 29 }{ 30 { 31 name: "nontty, with-token", 32 stdin: "abc123\n", 33 cli: "--with-token", 34 wants: LoginOptions{ 35 Hostname: "github.com", 36 Token: "abc123", 37 }, 38 }, 39 { 40 name: "tty, with-token", 41 stdinTTY: true, 42 stdin: "def456", 43 cli: "--with-token", 44 wants: LoginOptions{ 45 Hostname: "github.com", 46 Token: "def456", 47 }, 48 }, 49 { 50 name: "nontty, hostname", 51 stdinTTY: false, 52 cli: "--hostname claire.redfield", 53 wantsErr: true, 54 }, 55 { 56 name: "nontty", 57 stdinTTY: false, 58 cli: "", 59 wantsErr: true, 60 }, 61 { 62 name: "nontty, with-token, hostname", 63 cli: "--hostname claire.redfield --with-token", 64 stdin: "abc123\n", 65 wants: LoginOptions{ 66 Hostname: "claire.redfield", 67 Token: "abc123", 68 }, 69 }, 70 { 71 name: "tty, with-token, hostname", 72 stdinTTY: true, 73 stdin: "ghi789", 74 cli: "--with-token --hostname brad.vickers", 75 wants: LoginOptions{ 76 Hostname: "brad.vickers", 77 Token: "ghi789", 78 }, 79 }, 80 { 81 name: "tty, hostname", 82 stdinTTY: true, 83 cli: "--hostname barry.burton", 84 wants: LoginOptions{ 85 Hostname: "barry.burton", 86 Token: "", 87 Interactive: true, 88 }, 89 }, 90 { 91 name: "tty", 92 stdinTTY: true, 93 cli: "", 94 wants: LoginOptions{ 95 Hostname: "", 96 Token: "", 97 Interactive: true, 98 }, 99 }, 100 { 101 name: "tty web", 102 stdinTTY: true, 103 cli: "--web", 104 wants: LoginOptions{ 105 Hostname: "github.com", 106 Web: true, 107 }, 108 }, 109 { 110 name: "nontty web", 111 cli: "--web", 112 wants: LoginOptions{ 113 Hostname: "github.com", 114 Web: true, 115 }, 116 }, 117 { 118 name: "web and with-token", 119 cli: "--web --with-token", 120 wantsErr: true, 121 }, 122 { 123 name: "tty one scope", 124 stdinTTY: true, 125 cli: "--scopes repo:invite", 126 wants: LoginOptions{ 127 Hostname: "", 128 Scopes: []string{"repo:invite"}, 129 Token: "", 130 Interactive: true, 131 }, 132 }, 133 { 134 name: "tty scopes", 135 stdinTTY: true, 136 cli: "--scopes repo:invite,read:public_key", 137 wants: LoginOptions{ 138 Hostname: "", 139 Scopes: []string{"repo:invite", "read:public_key"}, 140 Token: "", 141 Interactive: true, 142 }, 143 }, 144 } 145 146 for _, tt := range tests { 147 t.Run(tt.name, func(t *testing.T) { 148 io, stdin, _, _ := iostreams.Test() 149 f := &cmdutil.Factory{ 150 IOStreams: io, 151 } 152 153 io.SetStdoutTTY(true) 154 io.SetStdinTTY(tt.stdinTTY) 155 if tt.stdin != "" { 156 stdin.WriteString(tt.stdin) 157 } 158 159 argv, err := shlex.Split(tt.cli) 160 assert.NoError(t, err) 161 162 var gotOpts *LoginOptions 163 cmd := NewCmdLogin(f, func(opts *LoginOptions) error { 164 gotOpts = opts 165 return nil 166 }) 167 // TODO cobra hack-around 168 cmd.Flags().BoolP("help", "x", false, "") 169 170 cmd.SetArgs(argv) 171 cmd.SetIn(&bytes.Buffer{}) 172 cmd.SetOut(&bytes.Buffer{}) 173 cmd.SetErr(&bytes.Buffer{}) 174 175 _, err = cmd.ExecuteC() 176 if tt.wantsErr { 177 assert.Error(t, err) 178 return 179 } 180 assert.NoError(t, err) 181 182 assert.Equal(t, tt.wants.Token, gotOpts.Token) 183 assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) 184 assert.Equal(t, tt.wants.Web, gotOpts.Web) 185 assert.Equal(t, tt.wants.Interactive, gotOpts.Interactive) 186 assert.Equal(t, tt.wants.Scopes, gotOpts.Scopes) 187 }) 188 } 189 } 190 191 func Test_loginRun_nontty(t *testing.T) { 192 tests := []struct { 193 name string 194 opts *LoginOptions 195 httpStubs func(*httpmock.Registry) 196 env map[string]string 197 wantHosts string 198 wantErr string 199 wantStderr string 200 }{ 201 { 202 name: "with token", 203 opts: &LoginOptions{ 204 Hostname: "github.com", 205 Token: "abc123", 206 }, 207 httpStubs: func(reg *httpmock.Registry) { 208 reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) 209 }, 210 wantHosts: "github.com:\n oauth_token: abc123\n", 211 }, 212 { 213 name: "with token and non-default host", 214 opts: &LoginOptions{ 215 Hostname: "albert.wesker", 216 Token: "abc123", 217 }, 218 httpStubs: func(reg *httpmock.Registry) { 219 reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) 220 }, 221 wantHosts: "albert.wesker:\n oauth_token: abc123\n", 222 }, 223 { 224 name: "missing repo scope", 225 opts: &LoginOptions{ 226 Hostname: "github.com", 227 Token: "abc456", 228 }, 229 httpStubs: func(reg *httpmock.Registry) { 230 reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org")) 231 }, 232 wantErr: `error validating token: missing required scope 'repo'`, 233 }, 234 { 235 name: "missing read scope", 236 opts: &LoginOptions{ 237 Hostname: "github.com", 238 Token: "abc456", 239 }, 240 httpStubs: func(reg *httpmock.Registry) { 241 reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo")) 242 }, 243 wantErr: `error validating token: missing required scope 'read:org'`, 244 }, 245 { 246 name: "has admin scope", 247 opts: &LoginOptions{ 248 Hostname: "github.com", 249 Token: "abc456", 250 }, 251 httpStubs: func(reg *httpmock.Registry) { 252 reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,admin:org")) 253 }, 254 wantHosts: "github.com:\n oauth_token: abc456\n", 255 }, 256 { 257 name: "github.com token from environment", 258 opts: &LoginOptions{ 259 Hostname: "github.com", 260 Token: "abc456", 261 }, 262 env: map[string]string{ 263 "GH_TOKEN": "value_from_env", 264 }, 265 wantErr: "SilentError", 266 wantStderr: heredoc.Doc(` 267 The value of the GH_TOKEN environment variable is being used for authentication. 268 To have GitHub CLI store credentials instead, first clear the value from the environment. 269 `), 270 }, 271 { 272 name: "GHE token from environment", 273 opts: &LoginOptions{ 274 Hostname: "ghe.io", 275 Token: "abc456", 276 }, 277 env: map[string]string{ 278 "GH_ENTERPRISE_TOKEN": "value_from_env", 279 }, 280 wantErr: "SilentError", 281 wantStderr: heredoc.Doc(` 282 The value of the GH_ENTERPRISE_TOKEN environment variable is being used for authentication. 283 To have GitHub CLI store credentials instead, first clear the value from the environment. 284 `), 285 }, 286 } 287 288 for _, tt := range tests { 289 io, _, stdout, stderr := iostreams.Test() 290 291 io.SetStdinTTY(false) 292 io.SetStdoutTTY(false) 293 294 tt.opts.Config = func() (config.Config, error) { 295 cfg := config.NewBlankConfig() 296 return config.InheritEnv(cfg), nil 297 } 298 299 tt.opts.IO = io 300 t.Run(tt.name, func(t *testing.T) { 301 reg := &httpmock.Registry{} 302 tt.opts.HttpClient = func() (*http.Client, error) { 303 return &http.Client{Transport: reg}, nil 304 } 305 306 old_GH_TOKEN := os.Getenv("GH_TOKEN") 307 os.Setenv("GH_TOKEN", tt.env["GH_TOKEN"]) 308 old_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN") 309 os.Setenv("GITHUB_TOKEN", tt.env["GITHUB_TOKEN"]) 310 old_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN") 311 os.Setenv("GH_ENTERPRISE_TOKEN", tt.env["GH_ENTERPRISE_TOKEN"]) 312 old_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN") 313 os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.env["GITHUB_ENTERPRISE_TOKEN"]) 314 defer func() { 315 os.Setenv("GH_TOKEN", old_GH_TOKEN) 316 os.Setenv("GITHUB_TOKEN", old_GITHUB_TOKEN) 317 os.Setenv("GH_ENTERPRISE_TOKEN", old_GH_ENTERPRISE_TOKEN) 318 os.Setenv("GITHUB_ENTERPRISE_TOKEN", old_GITHUB_ENTERPRISE_TOKEN) 319 }() 320 321 if tt.httpStubs != nil { 322 tt.httpStubs(reg) 323 } 324 325 _, restoreRun := run.Stub() 326 defer restoreRun(t) 327 328 mainBuf := bytes.Buffer{} 329 hostsBuf := bytes.Buffer{} 330 defer config.StubWriteConfig(&mainBuf, &hostsBuf)() 331 332 err := loginRun(tt.opts) 333 if tt.wantErr != "" { 334 assert.EqualError(t, err, tt.wantErr) 335 } else { 336 assert.NoError(t, err) 337 } 338 339 assert.Equal(t, "", stdout.String()) 340 assert.Equal(t, tt.wantStderr, stderr.String()) 341 assert.Equal(t, tt.wantHosts, hostsBuf.String()) 342 reg.Verify(t) 343 }) 344 } 345 } 346 347 func Test_loginRun_Survey(t *testing.T) { 348 tests := []struct { 349 name string 350 opts *LoginOptions 351 httpStubs func(*httpmock.Registry) 352 askStubs func(*prompt.AskStubber) 353 runStubs func(*run.CommandStubber) 354 wantHosts string 355 wantErrOut *regexp.Regexp 356 cfg func(config.Config) 357 }{ 358 { 359 name: "already authenticated", 360 opts: &LoginOptions{ 361 Interactive: true, 362 }, 363 cfg: func(cfg config.Config) { 364 _ = cfg.Set("github.com", "oauth_token", "ghi789") 365 }, 366 httpStubs: func(reg *httpmock.Registry) { 367 reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) 368 // reg.Register( 369 // httpmock.GraphQL(`query UserCurrent\b`), 370 // httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) 371 }, 372 askStubs: func(as *prompt.AskStubber) { 373 as.StubOne(0) // host type github.com 374 as.StubOne(false) // do not continue 375 }, 376 wantHosts: "", // nothing should have been written to hosts 377 wantErrOut: nil, 378 }, 379 { 380 name: "hostname set", 381 opts: &LoginOptions{ 382 Hostname: "rebecca.chambers", 383 Interactive: true, 384 }, 385 wantHosts: heredoc.Doc(` 386 rebecca.chambers: 387 oauth_token: def456 388 user: jillv 389 git_protocol: https 390 `), 391 askStubs: func(as *prompt.AskStubber) { 392 as.StubOne("HTTPS") // git_protocol 393 as.StubOne(false) // cache credentials 394 as.StubOne(1) // auth mode: token 395 as.StubOne("def456") // auth token 396 }, 397 runStubs: func(rs *run.CommandStubber) { 398 rs.Register(`git config credential\.https:/`, 1, "") 399 rs.Register(`git config credential\.helper`, 1, "") 400 }, 401 httpStubs: func(reg *httpmock.Registry) { 402 reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) 403 reg.Register( 404 httpmock.GraphQL(`query UserCurrent\b`), 405 httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) 406 }, 407 wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://rebecca.chambers/settings/tokens"), 408 }, 409 { 410 name: "choose enterprise", 411 wantHosts: heredoc.Doc(` 412 brad.vickers: 413 oauth_token: def456 414 user: jillv 415 git_protocol: https 416 `), 417 opts: &LoginOptions{ 418 Interactive: true, 419 }, 420 askStubs: func(as *prompt.AskStubber) { 421 as.StubOne(1) // host type enterprise 422 as.StubOne("brad.vickers") // hostname 423 as.StubOne("HTTPS") // git_protocol 424 as.StubOne(false) // cache credentials 425 as.StubOne(1) // auth mode: token 426 as.StubOne("def456") // auth token 427 }, 428 runStubs: func(rs *run.CommandStubber) { 429 rs.Register(`git config credential\.https:/`, 1, "") 430 rs.Register(`git config credential\.helper`, 1, "") 431 }, 432 httpStubs: func(reg *httpmock.Registry) { 433 reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) 434 reg.Register( 435 httpmock.GraphQL(`query UserCurrent\b`), 436 httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) 437 }, 438 wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://brad.vickers/settings/tokens"), 439 }, 440 { 441 name: "choose github.com", 442 wantHosts: heredoc.Doc(` 443 github.com: 444 oauth_token: def456 445 user: jillv 446 git_protocol: https 447 `), 448 opts: &LoginOptions{ 449 Interactive: true, 450 }, 451 askStubs: func(as *prompt.AskStubber) { 452 as.StubOne(0) // host type github.com 453 as.StubOne("HTTPS") // git_protocol 454 as.StubOne(false) // cache credentials 455 as.StubOne(1) // auth mode: token 456 as.StubOne("def456") // auth token 457 }, 458 runStubs: func(rs *run.CommandStubber) { 459 rs.Register(`git config credential\.https:/`, 1, "") 460 rs.Register(`git config credential\.helper`, 1, "") 461 }, 462 wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://github.com/settings/tokens"), 463 }, 464 { 465 name: "sets git_protocol", 466 wantHosts: heredoc.Doc(` 467 github.com: 468 oauth_token: def456 469 user: jillv 470 git_protocol: ssh 471 `), 472 opts: &LoginOptions{ 473 Interactive: true, 474 }, 475 askStubs: func(as *prompt.AskStubber) { 476 as.StubOne(0) // host type github.com 477 as.StubOne("SSH") // git_protocol 478 as.StubOne(10) // TODO: SSH key selection 479 as.StubOne(1) // auth mode: token 480 as.StubOne("def456") // auth token 481 }, 482 wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://github.com/settings/tokens"), 483 }, 484 // TODO how to test browser auth? 485 } 486 487 for _, tt := range tests { 488 if tt.opts == nil { 489 tt.opts = &LoginOptions{} 490 } 491 io, _, _, stderr := iostreams.Test() 492 493 io.SetStdinTTY(true) 494 io.SetStderrTTY(true) 495 io.SetStdoutTTY(true) 496 497 tt.opts.IO = io 498 499 cfg := config.NewBlankConfig() 500 501 if tt.cfg != nil { 502 tt.cfg(cfg) 503 } 504 tt.opts.Config = func() (config.Config, error) { 505 return cfg, nil 506 } 507 508 t.Run(tt.name, func(t *testing.T) { 509 reg := &httpmock.Registry{} 510 tt.opts.HttpClient = func() (*http.Client, error) { 511 return &http.Client{Transport: reg}, nil 512 } 513 if tt.httpStubs != nil { 514 tt.httpStubs(reg) 515 } else { 516 reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) 517 reg.Register( 518 httpmock.GraphQL(`query UserCurrent\b`), 519 httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) 520 } 521 522 mainBuf := bytes.Buffer{} 523 hostsBuf := bytes.Buffer{} 524 defer config.StubWriteConfig(&mainBuf, &hostsBuf)() 525 526 as, teardown := prompt.InitAskStubber() 527 defer teardown() 528 if tt.askStubs != nil { 529 tt.askStubs(as) 530 } 531 532 rs, restoreRun := run.Stub() 533 defer restoreRun(t) 534 if tt.runStubs != nil { 535 tt.runStubs(rs) 536 } 537 538 err := loginRun(tt.opts) 539 if err != nil { 540 t.Fatalf("unexpected error: %s", err) 541 } 542 543 assert.Equal(t, tt.wantHosts, hostsBuf.String()) 544 if tt.wantErrOut == nil { 545 assert.Equal(t, "", stderr.String()) 546 } else { 547 assert.Regexp(t, tt.wantErrOut, stderr.String()) 548 } 549 reg.Verify(t) 550 }) 551 } 552 }