github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/cloud/testing.go (about) 1 package cloud 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/http/httptest" 10 "net/url" 11 "path" 12 "testing" 13 "time" 14 15 tfe "github.com/hashicorp/go-tfe" 16 svchost "github.com/hashicorp/terraform-svchost" 17 "github.com/hashicorp/terraform-svchost/auth" 18 "github.com/hashicorp/terraform-svchost/disco" 19 "github.com/mitchellh/cli" 20 "github.com/mitchellh/colorstring" 21 "github.com/zclconf/go-cty/cty" 22 23 "github.com/hashicorp/terraform/internal/backend" 24 "github.com/hashicorp/terraform/internal/command/jsonformat" 25 "github.com/hashicorp/terraform/internal/configs" 26 "github.com/hashicorp/terraform/internal/configs/configschema" 27 "github.com/hashicorp/terraform/internal/httpclient" 28 "github.com/hashicorp/terraform/internal/providers" 29 "github.com/hashicorp/terraform/internal/terraform" 30 "github.com/hashicorp/terraform/internal/tfdiags" 31 "github.com/hashicorp/terraform/version" 32 33 backendLocal "github.com/hashicorp/terraform/internal/backend/local" 34 ) 35 36 const ( 37 testCred = "test-auth-token" 38 ) 39 40 var ( 41 tfeHost = svchost.Hostname(defaultHostname) 42 credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ 43 tfeHost: {"token": testCred}, 44 }) 45 testBackendSingleWorkspaceName = "app-prod" 46 ) 47 48 // mockInput is a mock implementation of terraform.UIInput. 49 type mockInput struct { 50 answers map[string]string 51 } 52 53 func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) { 54 v, ok := m.answers[opts.Id] 55 if !ok { 56 return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) 57 } 58 if v == "wait-for-external-update" { 59 select { 60 case <-ctx.Done(): 61 case <-time.After(time.Minute): 62 } 63 } 64 delete(m.answers, opts.Id) 65 return v, nil 66 } 67 68 func testInput(t *testing.T, answers map[string]string) *mockInput { 69 return &mockInput{answers: answers} 70 } 71 72 func testBackendWithName(t *testing.T) (*Cloud, func()) { 73 obj := cty.ObjectVal(map[string]cty.Value{ 74 "hostname": cty.NullVal(cty.String), 75 "organization": cty.StringVal("hashicorp"), 76 "token": cty.NullVal(cty.String), 77 "workspaces": cty.ObjectVal(map[string]cty.Value{ 78 "name": cty.StringVal(testBackendSingleWorkspaceName), 79 "tags": cty.NullVal(cty.Set(cty.String)), 80 }), 81 }) 82 return testBackend(t, obj) 83 } 84 85 func testBackendWithTags(t *testing.T) (*Cloud, func()) { 86 obj := cty.ObjectVal(map[string]cty.Value{ 87 "hostname": cty.NullVal(cty.String), 88 "organization": cty.StringVal("hashicorp"), 89 "token": cty.NullVal(cty.String), 90 "workspaces": cty.ObjectVal(map[string]cty.Value{ 91 "name": cty.NullVal(cty.String), 92 "tags": cty.SetVal( 93 []cty.Value{ 94 cty.StringVal("billing"), 95 }, 96 ), 97 }), 98 }) 99 return testBackend(t, obj) 100 } 101 102 func testBackendNoOperations(t *testing.T) (*Cloud, func()) { 103 obj := cty.ObjectVal(map[string]cty.Value{ 104 "hostname": cty.NullVal(cty.String), 105 "organization": cty.StringVal("no-operations"), 106 "token": cty.NullVal(cty.String), 107 "workspaces": cty.ObjectVal(map[string]cty.Value{ 108 "name": cty.StringVal(testBackendSingleWorkspaceName), 109 "tags": cty.NullVal(cty.Set(cty.String)), 110 }), 111 }) 112 return testBackend(t, obj) 113 } 114 115 func testCloudState(t *testing.T) *State { 116 b, bCleanup := testBackendWithName(t) 117 defer bCleanup() 118 119 raw, err := b.StateMgr(testBackendSingleWorkspaceName) 120 if err != nil { 121 t.Fatalf("error: %v", err) 122 } 123 124 return raw.(*State) 125 } 126 127 func testBackendWithOutputs(t *testing.T) (*Cloud, func()) { 128 b, cleanup := testBackendWithName(t) 129 130 // Get a new mock client to use for adding outputs 131 mc := NewMockClient() 132 133 mc.StateVersionOutputs.create("svo-abcd", &tfe.StateVersionOutput{ 134 ID: "svo-abcd", 135 Value: "foobar", 136 Sensitive: true, 137 Type: "string", 138 Name: "sensitive_output", 139 DetailedType: "string", 140 }) 141 142 mc.StateVersionOutputs.create("svo-zyxw", &tfe.StateVersionOutput{ 143 ID: "svo-zyxw", 144 Value: "bazqux", 145 Type: "string", 146 Name: "nonsensitive_output", 147 DetailedType: "string", 148 }) 149 150 var dt interface{} 151 var val interface{} 152 err := json.Unmarshal([]byte(`["object", {"foo":"string"}]`), &dt) 153 if err != nil { 154 t.Fatalf("could not unmarshal detailed type: %s", err) 155 } 156 err = json.Unmarshal([]byte(`{"foo":"bar"}`), &val) 157 if err != nil { 158 t.Fatalf("could not unmarshal value: %s", err) 159 } 160 mc.StateVersionOutputs.create("svo-efgh", &tfe.StateVersionOutput{ 161 ID: "svo-efgh", 162 Value: val, 163 Type: "object", 164 Name: "object_output", 165 DetailedType: dt, 166 }) 167 168 err = json.Unmarshal([]byte(`["list", "bool"]`), &dt) 169 if err != nil { 170 t.Fatalf("could not unmarshal detailed type: %s", err) 171 } 172 err = json.Unmarshal([]byte(`[true, false, true, true]`), &val) 173 if err != nil { 174 t.Fatalf("could not unmarshal value: %s", err) 175 } 176 mc.StateVersionOutputs.create("svo-ijkl", &tfe.StateVersionOutput{ 177 ID: "svo-ijkl", 178 Value: val, 179 Type: "array", 180 Name: "list_output", 181 DetailedType: dt, 182 }) 183 184 b.client.StateVersionOutputs = mc.StateVersionOutputs 185 186 return b, cleanup 187 } 188 189 func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { 190 s := testServer(t) 191 b := New(testDisco(s)) 192 193 // Configure the backend so the client is created. 194 newObj, valDiags := b.PrepareConfig(obj) 195 if len(valDiags) != 0 { 196 t.Fatalf("testBackend: backend.PrepareConfig() failed: %s", valDiags.ErrWithWarnings()) 197 } 198 obj = newObj 199 200 confDiags := b.Configure(obj) 201 if len(confDiags) != 0 { 202 t.Fatalf("testBackend: backend.Configure() failed: %s", confDiags.ErrWithWarnings()) 203 } 204 205 // Get a new mock client. 206 mc := NewMockClient() 207 208 // Replace the services we use with our mock services. 209 b.CLI = cli.NewMockUi() 210 b.client.Applies = mc.Applies 211 b.client.ConfigurationVersions = mc.ConfigurationVersions 212 b.client.CostEstimates = mc.CostEstimates 213 b.client.Organizations = mc.Organizations 214 b.client.Plans = mc.Plans 215 b.client.TaskStages = mc.TaskStages 216 b.client.PolicySetOutcomes = mc.PolicySetOutcomes 217 b.client.PolicyChecks = mc.PolicyChecks 218 b.client.Runs = mc.Runs 219 b.client.StateVersions = mc.StateVersions 220 b.client.StateVersionOutputs = mc.StateVersionOutputs 221 b.client.Variables = mc.Variables 222 b.client.Workspaces = mc.Workspaces 223 224 // Set local to a local test backend. 225 b.local = testLocalBackend(t, b) 226 b.input = true 227 228 baseURL, err := url.Parse("https://app.terraform.io") 229 if err != nil { 230 t.Fatalf("testBackend: failed to parse base URL for client") 231 } 232 baseURL.Path = "/api/v2/" 233 234 readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) (*jsonformat.Plan, error) { 235 return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID) 236 } 237 238 ctx := context.Background() 239 240 // Create the organization. 241 _, err = b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{ 242 Name: tfe.String(b.organization), 243 }) 244 if err != nil { 245 t.Fatalf("error: %v", err) 246 } 247 248 // Create the default workspace if required. 249 if b.WorkspaceMapping.Name != "" { 250 _, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{ 251 Name: tfe.String(b.WorkspaceMapping.Name), 252 }) 253 if err != nil { 254 t.Fatalf("error: %v", err) 255 } 256 } 257 258 return b, s.Close 259 } 260 261 // testUnconfiguredBackend is used for testing the configuration of the backend 262 // with the mock client 263 func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) { 264 s := testServer(t) 265 b := New(testDisco(s)) 266 267 // Normally, the client is created during configuration, but the configuration uses the 268 // client to read entitlements. 269 var err error 270 b.client, err = tfe.NewClient(&tfe.Config{ 271 Token: "fake-token", 272 }) 273 if err != nil { 274 t.Fatal(err) 275 } 276 277 // Get a new mock client. 278 mc := NewMockClient() 279 280 // Replace the services we use with our mock services. 281 b.CLI = cli.NewMockUi() 282 b.client.Applies = mc.Applies 283 b.client.ConfigurationVersions = mc.ConfigurationVersions 284 b.client.CostEstimates = mc.CostEstimates 285 b.client.Organizations = mc.Organizations 286 b.client.Plans = mc.Plans 287 b.client.PolicySetOutcomes = mc.PolicySetOutcomes 288 b.client.PolicyChecks = mc.PolicyChecks 289 b.client.Runs = mc.Runs 290 b.client.StateVersions = mc.StateVersions 291 b.client.Variables = mc.Variables 292 b.client.Workspaces = mc.Workspaces 293 294 baseURL, err := url.Parse("https://app.terraform.io") 295 if err != nil { 296 t.Fatalf("testBackend: failed to parse base URL for client") 297 } 298 baseURL.Path = "/api/v2/" 299 300 readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) (*jsonformat.Plan, error) { 301 return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID) 302 } 303 304 // Set local to a local test backend. 305 b.local = testLocalBackend(t, b) 306 307 return b, s.Close 308 } 309 310 func testLocalBackend(t *testing.T, cloud *Cloud) backend.Enhanced { 311 b := backendLocal.NewWithBackend(cloud) 312 313 // Add a test provider to the local backend. 314 p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{ 315 ResourceTypes: map[string]*configschema.Block{ 316 "null_resource": { 317 Attributes: map[string]*configschema.Attribute{ 318 "id": {Type: cty.String, Computed: true}, 319 }, 320 }, 321 }, 322 }) 323 p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 324 "id": cty.StringVal("yes"), 325 })} 326 327 return b 328 } 329 330 // testServer returns a started *httptest.Server used for local testing with the default set of 331 // request handlers. 332 func testServer(t *testing.T) *httptest.Server { 333 return testServerWithHandlers(testDefaultRequestHandlers) 334 } 335 336 // testServerWithHandlers returns a started *httptest.Server with the given set of request handlers 337 // overriding any default request handlers (testDefaultRequestHandlers). 338 func testServerWithHandlers(handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server { 339 mux := http.NewServeMux() 340 for route, handler := range handlers { 341 mux.HandleFunc(route, handler) 342 } 343 for route, handler := range testDefaultRequestHandlers { 344 if handlers[route] == nil { 345 mux.HandleFunc(route, handler) 346 } 347 } 348 349 return httptest.NewServer(mux) 350 } 351 352 // testDefaultRequestHandlers is a map of request handlers intended to be used in a request 353 // multiplexer for a test server. A caller may use testServerWithHandlers to start a server with 354 // this base set of routes, and override a particular route for whatever edge case is being tested. 355 var testDefaultRequestHandlers = map[string]func(http.ResponseWriter, *http.Request){ 356 // Respond to service discovery calls. 357 "/well-known/terraform.json": func(w http.ResponseWriter, r *http.Request) { 358 w.Header().Set("Content-Type", "application/json") 359 io.WriteString(w, `{ 360 "tfe.v2": "/api/v2/", 361 }`) 362 }, 363 364 // Respond to service version constraints calls. 365 "/v1/versions/": func(w http.ResponseWriter, r *http.Request) { 366 w.Header().Set("Content-Type", "application/json") 367 io.WriteString(w, fmt.Sprintf(`{ 368 "service": "%s", 369 "product": "terraform", 370 "minimum": "0.1.0", 371 "maximum": "10.0.0" 372 }`, path.Base(r.URL.Path))) 373 }, 374 375 // Respond to pings to get the API version header. 376 "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { 377 w.Header().Set("Content-Type", "application/json") 378 w.Header().Set("TFP-API-Version", "2.5") 379 }, 380 381 // Respond to the initial query to read the hashicorp org entitlements. 382 "/api/v2/organizations/hashicorp/entitlement-set": func(w http.ResponseWriter, r *http.Request) { 383 w.Header().Set("Content-Type", "application/vnd.api+json") 384 io.WriteString(w, `{ 385 "data": { 386 "id": "org-GExadygjSbKP8hsY", 387 "type": "entitlement-sets", 388 "attributes": { 389 "operations": true, 390 "private-module-registry": true, 391 "sentinel": true, 392 "state-storage": true, 393 "teams": true, 394 "vcs-integrations": true 395 } 396 } 397 }`) 398 }, 399 400 // Respond to the initial query to read the no-operations org entitlements. 401 "/api/v2/organizations/no-operations/entitlement-set": func(w http.ResponseWriter, r *http.Request) { 402 w.Header().Set("Content-Type", "application/vnd.api+json") 403 io.WriteString(w, `{ 404 "data": { 405 "id": "org-ufxa3y8jSbKP8hsT", 406 "type": "entitlement-sets", 407 "attributes": { 408 "operations": false, 409 "private-module-registry": true, 410 "sentinel": true, 411 "state-storage": true, 412 "teams": true, 413 "vcs-integrations": true 414 } 415 } 416 }`) 417 }, 418 419 // All tests that are assumed to pass will use the hashicorp organization, 420 // so for all other organization requests we will return a 404. 421 "/api/v2/organizations/": func(w http.ResponseWriter, r *http.Request) { 422 w.WriteHeader(404) 423 io.WriteString(w, `{ 424 "errors": [ 425 { 426 "status": "404", 427 "title": "not found" 428 } 429 ] 430 }`) 431 }, 432 } 433 434 func mockColorize() *colorstring.Colorize { 435 colors := make(map[string]string) 436 for k, v := range colorstring.DefaultColors { 437 colors[k] = v 438 } 439 colors["purple"] = "38;5;57" 440 441 return &colorstring.Colorize{ 442 Colors: colors, 443 Disable: false, 444 Reset: true, 445 } 446 } 447 448 func mockSROWorkspace(t *testing.T, b *Cloud, workspaceName string) { 449 _, err := b.client.Workspaces.Update(context.Background(), "hashicorp", workspaceName, tfe.WorkspaceUpdateOptions{ 450 StructuredRunOutputEnabled: tfe.Bool(true), 451 TerraformVersion: tfe.String("1.4.0"), 452 }) 453 if err != nil { 454 t.Fatalf("Error enabling SRO on workspace %s: %v", workspaceName, err) 455 } 456 } 457 458 // testDisco returns a *disco.Disco mapping app.terraform.io and 459 // localhost to a local test server. 460 func testDisco(s *httptest.Server) *disco.Disco { 461 services := map[string]interface{}{ 462 "tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL), 463 } 464 d := disco.NewWithCredentialsSource(credsSrc) 465 d.SetUserAgent(httpclient.TerraformUserAgent(version.String())) 466 467 d.ForceHostServices(svchost.Hostname(defaultHostname), services) 468 d.ForceHostServices(svchost.Hostname("localhost"), services) 469 d.ForceHostServices(svchost.Hostname("nontfe.local"), nil) 470 return d 471 } 472 473 type unparsedVariableValue struct { 474 value string 475 source terraform.ValueSourceType 476 } 477 478 func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 479 return &terraform.InputValue{ 480 Value: cty.StringVal(v.value), 481 SourceType: v.source, 482 }, tfdiags.Diagnostics{} 483 } 484 485 // testVariable returns a backend.UnparsedVariableValue used for testing. 486 func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue { 487 vars := make(map[string]backend.UnparsedVariableValue, len(vs)) 488 for _, v := range vs { 489 vars[v] = &unparsedVariableValue{ 490 value: v, 491 source: s, 492 } 493 } 494 return vars 495 }