github.com/pulumi/terraform@v1.4.0/pkg/command/login_test.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"net/http/httptest"
     6  	"path/filepath"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/mitchellh/cli"
    11  
    12  	svchost "github.com/hashicorp/terraform-svchost"
    13  	"github.com/hashicorp/terraform-svchost/disco"
    14  	"github.com/pulumi/terraform/pkg/command/cliconfig"
    15  	oauthserver "github.com/pulumi/terraform/pkg/command/testdata/login-oauth-server"
    16  	tfeserver "github.com/pulumi/terraform/pkg/command/testdata/login-tfe-server"
    17  	"github.com/pulumi/terraform/pkg/command/webbrowser"
    18  	"github.com/pulumi/terraform/pkg/httpclient"
    19  	"github.com/pulumi/terraform/version"
    20  )
    21  
    22  func TestLogin(t *testing.T) {
    23  	// oauthserver.Handler is a stub OAuth server implementation that will,
    24  	// on success, always issue a bearer token named "good-token".
    25  	s := httptest.NewServer(oauthserver.Handler)
    26  	defer s.Close()
    27  
    28  	// tfeserver.Handler is a stub TFE API implementation which will respond
    29  	// to ping and current account requests, when requests are authenticated
    30  	// with token "good-token"
    31  	ts := httptest.NewServer(tfeserver.Handler)
    32  	defer ts.Close()
    33  
    34  	loginTestCase := func(test func(t *testing.T, c *LoginCommand, ui *cli.MockUi)) func(t *testing.T) {
    35  		return func(t *testing.T) {
    36  			t.Helper()
    37  			workDir := t.TempDir()
    38  
    39  			// We'll use this context to avoid asynchronous tasks outliving
    40  			// a single test run.
    41  			ctx, cancel := context.WithCancel(context.Background())
    42  			defer cancel()
    43  
    44  			// Do not use the NewMockUi initializer here, as we want to delay
    45  			// the call to init until after setting up the input mocks
    46  			ui := new(cli.MockUi)
    47  
    48  			browserLauncher := webbrowser.NewMockLauncher(ctx)
    49  			creds := cliconfig.EmptyCredentialsSourceForTests(filepath.Join(workDir, "credentials.tfrc.json"))
    50  			svcs := disco.NewWithCredentialsSource(creds)
    51  			svcs.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
    52  
    53  			svcs.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{
    54  				"login.v1": map[string]interface{}{
    55  					// For this fake hostname we'll use a conventional OAuth flow,
    56  					// with browser-based consent that we'll mock away using a
    57  					// mock browser launcher below.
    58  					"client": "anything-goes",
    59  					"authz":  s.URL + "/authz",
    60  					"token":  s.URL + "/token",
    61  				},
    62  			})
    63  			svcs.ForceHostServices(svchost.Hostname("with-scopes.example.com"), map[string]interface{}{
    64  				"login.v1": map[string]interface{}{
    65  					// with scopes
    66  					// mock browser launcher below.
    67  					"client": "scopes_test",
    68  					"authz":  s.URL + "/authz",
    69  					"token":  s.URL + "/token",
    70  					"scopes": []interface{}{"app1.full_access", "app2.read_only"},
    71  				},
    72  			})
    73  			svcs.ForceHostServices(svchost.Hostname("app.terraform.io"), map[string]interface{}{
    74  				// This represents Terraform Cloud, which does not yet support the
    75  				// login API, but does support its own bespoke tokens API.
    76  				"tfe.v2":   ts.URL + "/api/v2",
    77  				"tfe.v2.1": ts.URL + "/api/v2",
    78  				"tfe.v2.2": ts.URL + "/api/v2",
    79  				"motd.v1":  ts.URL + "/api/terraform/motd",
    80  			})
    81  			svcs.ForceHostServices(svchost.Hostname("tfe.acme.com"), map[string]interface{}{
    82  				// This represents a Terraform Enterprise instance which does not
    83  				// yet support the login API, but does support its own bespoke tokens API.
    84  				"tfe.v2":   ts.URL + "/api/v2",
    85  				"tfe.v2.1": ts.URL + "/api/v2",
    86  				"tfe.v2.2": ts.URL + "/api/v2",
    87  			})
    88  			svcs.ForceHostServices(svchost.Hostname("unsupported.example.net"), map[string]interface{}{
    89  				// This host intentionally left blank.
    90  			})
    91  
    92  			c := &LoginCommand{
    93  				Meta: Meta{
    94  					Ui:              ui,
    95  					BrowserLauncher: browserLauncher,
    96  					Services:        svcs,
    97  				},
    98  			}
    99  
   100  			test(t, c, ui)
   101  		}
   102  	}
   103  
   104  	t.Run("app.terraform.io (no login support)", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   105  		// Enter "yes" at the consent prompt, then paste a token with some
   106  		// accidental whitespace.
   107  		defer testInputMap(t, map[string]string{
   108  			"approve": "yes",
   109  			"token":   "  good-token ",
   110  		})()
   111  		status := c.Run([]string{"app.terraform.io"})
   112  		if status != 0 {
   113  			t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
   114  		}
   115  
   116  		credsSrc := c.Services.CredentialsSource()
   117  		creds, err := credsSrc.ForHost(svchost.Hostname("app.terraform.io"))
   118  		if err != nil {
   119  			t.Errorf("failed to retrieve credentials: %s", err)
   120  		}
   121  		if got, want := creds.Token(), "good-token"; got != want {
   122  			t.Errorf("wrong token %q; want %q", got, want)
   123  		}
   124  		if got, want := ui.OutputWriter.String(), "Welcome to Terraform Cloud!"; !strings.Contains(got, want) {
   125  			t.Errorf("expected output to contain %q, but was:\n%s", want, got)
   126  		}
   127  	}))
   128  
   129  	t.Run("example.com with authorization code flow", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   130  		// Enter "yes" at the consent prompt.
   131  		defer testInputMap(t, map[string]string{
   132  			"approve": "yes",
   133  		})()
   134  		status := c.Run([]string{"example.com"})
   135  		if status != 0 {
   136  			t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
   137  		}
   138  
   139  		credsSrc := c.Services.CredentialsSource()
   140  		creds, err := credsSrc.ForHost(svchost.Hostname("example.com"))
   141  		if err != nil {
   142  			t.Errorf("failed to retrieve credentials: %s", err)
   143  		}
   144  		if got, want := creds.Token(), "good-token"; got != want {
   145  			t.Errorf("wrong token %q; want %q", got, want)
   146  		}
   147  
   148  		if got, want := ui.OutputWriter.String(), "Terraform has obtained and saved an API token."; !strings.Contains(got, want) {
   149  			t.Errorf("expected output to contain %q, but was:\n%s", want, got)
   150  		}
   151  	}))
   152  
   153  	t.Run("example.com results in no scopes", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   154  
   155  		host, _ := c.Services.Discover("example.com")
   156  		client, _ := host.ServiceOAuthClient("login.v1")
   157  		if len(client.Scopes) != 0 {
   158  			t.Errorf("unexpected scopes %q; expected none", client.Scopes)
   159  		}
   160  	}))
   161  
   162  	t.Run("with-scopes.example.com with authorization code flow and scopes", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   163  		// Enter "yes" at the consent prompt.
   164  		defer testInputMap(t, map[string]string{
   165  			"approve": "yes",
   166  		})()
   167  		status := c.Run([]string{"with-scopes.example.com"})
   168  		if status != 0 {
   169  			t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
   170  		}
   171  
   172  		credsSrc := c.Services.CredentialsSource()
   173  		creds, err := credsSrc.ForHost(svchost.Hostname("with-scopes.example.com"))
   174  
   175  		if err != nil {
   176  			t.Errorf("failed to retrieve credentials: %s", err)
   177  		}
   178  
   179  		if got, want := creds.Token(), "good-token"; got != want {
   180  			t.Errorf("wrong token %q; want %q", got, want)
   181  		}
   182  
   183  		if got, want := ui.OutputWriter.String(), "Terraform has obtained and saved an API token."; !strings.Contains(got, want) {
   184  			t.Errorf("expected output to contain %q, but was:\n%s", want, got)
   185  		}
   186  	}))
   187  
   188  	t.Run("with-scopes.example.com results in expected scopes", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   189  
   190  		host, _ := c.Services.Discover("with-scopes.example.com")
   191  		client, _ := host.ServiceOAuthClient("login.v1")
   192  
   193  		expectedScopes := [2]string{"app1.full_access", "app2.read_only"}
   194  
   195  		var foundScopes [2]string
   196  		copy(foundScopes[:], client.Scopes)
   197  
   198  		if foundScopes != expectedScopes || len(client.Scopes) != len(expectedScopes) {
   199  			t.Errorf("unexpected scopes %q; want %q", client.Scopes, expectedScopes)
   200  		}
   201  	}))
   202  
   203  	t.Run("TFE host without login support", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   204  		// Enter "yes" at the consent prompt, then paste a token with some
   205  		// accidental whitespace.
   206  		defer testInputMap(t, map[string]string{
   207  			"approve": "yes",
   208  			"token":   "  good-token ",
   209  		})()
   210  		status := c.Run([]string{"tfe.acme.com"})
   211  		if status != 0 {
   212  			t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
   213  		}
   214  
   215  		credsSrc := c.Services.CredentialsSource()
   216  		creds, err := credsSrc.ForHost(svchost.Hostname("tfe.acme.com"))
   217  		if err != nil {
   218  			t.Errorf("failed to retrieve credentials: %s", err)
   219  		}
   220  		if got, want := creds.Token(), "good-token"; got != want {
   221  			t.Errorf("wrong token %q; want %q", got, want)
   222  		}
   223  
   224  		if got, want := ui.OutputWriter.String(), "Logged in to Terraform Enterprise"; !strings.Contains(got, want) {
   225  			t.Errorf("expected output to contain %q, but was:\n%s", want, got)
   226  		}
   227  	}))
   228  
   229  	t.Run("TFE host without login support, incorrectly pasted token", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   230  		// Enter "yes" at the consent prompt, then paste an invalid token.
   231  		defer testInputMap(t, map[string]string{
   232  			"approve": "yes",
   233  			"token":   "good-tok",
   234  		})()
   235  		status := c.Run([]string{"tfe.acme.com"})
   236  		if status != 1 {
   237  			t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
   238  		}
   239  
   240  		credsSrc := c.Services.CredentialsSource()
   241  		creds, err := credsSrc.ForHost(svchost.Hostname("tfe.acme.com"))
   242  		if err != nil {
   243  			t.Errorf("failed to retrieve credentials: %s", err)
   244  		}
   245  		if creds != nil {
   246  			t.Errorf("wrong token %q; should have no token", creds.Token())
   247  		}
   248  	}))
   249  
   250  	t.Run("host without login or TFE API support", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   251  		status := c.Run([]string{"unsupported.example.net"})
   252  		if status == 0 {
   253  			t.Fatalf("successful exit; want error")
   254  		}
   255  
   256  		if got, want := ui.ErrorWriter.String(), "Error: Host does not support Terraform tokens API"; !strings.Contains(got, want) {
   257  			t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got)
   258  		}
   259  	}))
   260  
   261  	t.Run("answering no cancels", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   262  		// Enter "no" at the consent prompt
   263  		defer testInputMap(t, map[string]string{
   264  			"approve": "no",
   265  		})()
   266  		status := c.Run(nil)
   267  		if status != 1 {
   268  			t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
   269  		}
   270  
   271  		if got, want := ui.ErrorWriter.String(), "Login cancelled"; !strings.Contains(got, want) {
   272  			t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got)
   273  		}
   274  	}))
   275  
   276  	t.Run("answering y cancels", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
   277  		// Enter "y" at the consent prompt
   278  		defer testInputMap(t, map[string]string{
   279  			"approve": "y",
   280  		})()
   281  		status := c.Run(nil)
   282  		if status != 1 {
   283  			t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
   284  		}
   285  
   286  		if got, want := ui.ErrorWriter.String(), "Login cancelled"; !strings.Contains(got, want) {
   287  			t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got)
   288  		}
   289  	}))
   290  }