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