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  }