kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/command/login_test.go (about)

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