github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/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/iaas-resource-provision/iaas-rpc-svchost" 15 "github.com/iaas-resource-provision/iaas-rpc-svchost/disco" 16 "github.com/iaas-resource-provision/iaas-rpc/internal/command/cliconfig" 17 oauthserver "github.com/iaas-resource-provision/iaas-rpc/internal/command/testdata/login-oauth-server" 18 tfeserver "github.com/iaas-resource-provision/iaas-rpc/internal/command/testdata/login-tfe-server" 19 "github.com/iaas-resource-provision/iaas-rpc/internal/command/webbrowser" 20 "github.com/iaas-resource-provision/iaas-rpc/internal/httpclient" 21 "github.com/iaas-resource-provision/iaas-rpc/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 }