github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/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/eliastor/durgaform/internal/command/cliconfig" 15 oauthserver "github.com/eliastor/durgaform/internal/command/testdata/login-oauth-server" 16 tfeserver "github.com/eliastor/durgaform/internal/command/testdata/login-tfe-server" 17 "github.com/eliastor/durgaform/internal/command/webbrowser" 18 "github.com/eliastor/durgaform/internal/httpclient" 19 "github.com/eliastor/durgaform/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.DurgaformUserAgent(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.durgaform.io"), map[string]interface{}{ 74 // This represents Durgaform 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/durgaform/motd", 80 }) 81 svcs.ForceHostServices(svchost.Hostname("tfe.acme.com"), map[string]interface{}{ 82 // This represents a Durgaform 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.durgaform.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.durgaform.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.durgaform.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 Durgaform 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(), "Durgaform 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(), "Durgaform 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 Durgaform 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 Durgaform 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 }