github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/cloud/testing.go (about) 1 package cloud 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "path" 10 "testing" 11 "time" 12 13 tfe "github.com/hashicorp/go-tfe" 14 svchost "github.com/hashicorp/terraform-svchost" 15 "github.com/hashicorp/terraform-svchost/auth" 16 "github.com/hashicorp/terraform-svchost/disco" 17 "github.com/cycloidio/terraform/backend" 18 "github.com/cycloidio/terraform/configs" 19 "github.com/cycloidio/terraform/configs/configschema" 20 "github.com/cycloidio/terraform/httpclient" 21 "github.com/cycloidio/terraform/providers" 22 "github.com/cycloidio/terraform/states/remote" 23 "github.com/cycloidio/terraform/terraform" 24 "github.com/cycloidio/terraform/tfdiags" 25 "github.com/cycloidio/terraform/version" 26 "github.com/mitchellh/cli" 27 "github.com/zclconf/go-cty/cty" 28 29 backendLocal "github.com/cycloidio/terraform/backend/local" 30 ) 31 32 const ( 33 testCred = "test-auth-token" 34 ) 35 36 var ( 37 tfeHost = svchost.Hostname(defaultHostname) 38 credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ 39 tfeHost: {"token": testCred}, 40 }) 41 testBackendSingleWorkspaceName = "app-prod" 42 ) 43 44 // mockInput is a mock implementation of terraform.UIInput. 45 type mockInput struct { 46 answers map[string]string 47 } 48 49 func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) { 50 v, ok := m.answers[opts.Id] 51 if !ok { 52 return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) 53 } 54 if v == "wait-for-external-update" { 55 select { 56 case <-ctx.Done(): 57 case <-time.After(time.Minute): 58 } 59 } 60 delete(m.answers, opts.Id) 61 return v, nil 62 } 63 64 func testInput(t *testing.T, answers map[string]string) *mockInput { 65 return &mockInput{answers: answers} 66 } 67 68 func testBackendWithName(t *testing.T) (*Cloud, func()) { 69 obj := cty.ObjectVal(map[string]cty.Value{ 70 "hostname": cty.NullVal(cty.String), 71 "organization": cty.StringVal("hashicorp"), 72 "token": cty.NullVal(cty.String), 73 "workspaces": cty.ObjectVal(map[string]cty.Value{ 74 "name": cty.StringVal(testBackendSingleWorkspaceName), 75 "tags": cty.NullVal(cty.Set(cty.String)), 76 }), 77 }) 78 return testBackend(t, obj) 79 } 80 81 func testBackendWithTags(t *testing.T) (*Cloud, func()) { 82 obj := cty.ObjectVal(map[string]cty.Value{ 83 "hostname": cty.NullVal(cty.String), 84 "organization": cty.StringVal("hashicorp"), 85 "token": cty.NullVal(cty.String), 86 "workspaces": cty.ObjectVal(map[string]cty.Value{ 87 "name": cty.NullVal(cty.String), 88 "tags": cty.SetVal( 89 []cty.Value{ 90 cty.StringVal("billing"), 91 }, 92 ), 93 }), 94 }) 95 return testBackend(t, obj) 96 } 97 98 func testBackendNoOperations(t *testing.T) (*Cloud, func()) { 99 obj := cty.ObjectVal(map[string]cty.Value{ 100 "hostname": cty.NullVal(cty.String), 101 "organization": cty.StringVal("no-operations"), 102 "token": cty.NullVal(cty.String), 103 "workspaces": cty.ObjectVal(map[string]cty.Value{ 104 "name": cty.StringVal(testBackendSingleWorkspaceName), 105 "tags": cty.NullVal(cty.Set(cty.String)), 106 }), 107 }) 108 return testBackend(t, obj) 109 } 110 111 func testRemoteClient(t *testing.T) remote.Client { 112 b, bCleanup := testBackendWithName(t) 113 defer bCleanup() 114 115 raw, err := b.StateMgr(testBackendSingleWorkspaceName) 116 if err != nil { 117 t.Fatalf("error: %v", err) 118 } 119 120 return raw.(*remote.State).Client 121 } 122 123 func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { 124 s := testServer(t) 125 b := New(testDisco(s)) 126 127 // Configure the backend so the client is created. 128 newObj, valDiags := b.PrepareConfig(obj) 129 if len(valDiags) != 0 { 130 t.Fatalf("testBackend: backend.PrepareConfig() failed: %s", valDiags.ErrWithWarnings()) 131 } 132 obj = newObj 133 134 confDiags := b.Configure(obj) 135 if len(confDiags) != 0 { 136 t.Fatalf("testBackend: backend.Configure() failed: %s", confDiags.ErrWithWarnings()) 137 } 138 139 // Get a new mock client. 140 mc := NewMockClient() 141 142 // Replace the services we use with our mock services. 143 b.CLI = cli.NewMockUi() 144 b.client.Applies = mc.Applies 145 b.client.ConfigurationVersions = mc.ConfigurationVersions 146 b.client.CostEstimates = mc.CostEstimates 147 b.client.Organizations = mc.Organizations 148 b.client.Plans = mc.Plans 149 b.client.PolicyChecks = mc.PolicyChecks 150 b.client.Runs = mc.Runs 151 b.client.StateVersions = mc.StateVersions 152 b.client.Variables = mc.Variables 153 b.client.Workspaces = mc.Workspaces 154 155 // Set local to a local test backend. 156 b.local = testLocalBackend(t, b) 157 158 ctx := context.Background() 159 160 // Create the organization. 161 _, err := b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{ 162 Name: tfe.String(b.organization), 163 }) 164 if err != nil { 165 t.Fatalf("error: %v", err) 166 } 167 168 // Create the default workspace if required. 169 if b.WorkspaceMapping.Name != "" { 170 _, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{ 171 Name: tfe.String(b.WorkspaceMapping.Name), 172 }) 173 if err != nil { 174 t.Fatalf("error: %v", err) 175 } 176 } 177 178 return b, s.Close 179 } 180 181 func testLocalBackend(t *testing.T, cloud *Cloud) backend.Enhanced { 182 b := backendLocal.NewWithBackend(cloud) 183 184 // Add a test provider to the local backend. 185 p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{ 186 ResourceTypes: map[string]*configschema.Block{ 187 "null_resource": { 188 Attributes: map[string]*configschema.Attribute{ 189 "id": {Type: cty.String, Computed: true}, 190 }, 191 }, 192 }, 193 }) 194 p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 195 "id": cty.StringVal("yes"), 196 })} 197 198 return b 199 } 200 201 // testServer returns a started *httptest.Server used for local testing with the default set of 202 // request handlers. 203 func testServer(t *testing.T) *httptest.Server { 204 return testServerWithHandlers(testDefaultRequestHandlers) 205 } 206 207 // testServerWithHandlers returns a started *httptest.Server with the given set of request handlers 208 // overriding any default request handlers (testDefaultRequestHandlers). 209 func testServerWithHandlers(handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server { 210 mux := http.NewServeMux() 211 for route, handler := range handlers { 212 mux.HandleFunc(route, handler) 213 } 214 for route, handler := range testDefaultRequestHandlers { 215 if handlers[route] == nil { 216 mux.HandleFunc(route, handler) 217 } 218 } 219 220 return httptest.NewServer(mux) 221 } 222 223 // testDefaultRequestHandlers is a map of request handlers intended to be used in a request 224 // multiplexer for a test server. A caller may use testServerWithHandlers to start a server with 225 // this base set of routes, and override a particular route for whatever edge case is being tested. 226 var testDefaultRequestHandlers = map[string]func(http.ResponseWriter, *http.Request){ 227 // Respond to service discovery calls. 228 "/well-known/terraform.json": func(w http.ResponseWriter, r *http.Request) { 229 w.Header().Set("Content-Type", "application/json") 230 io.WriteString(w, `{ 231 "tfe.v2": "/api/v2/", 232 }`) 233 }, 234 235 // Respond to service version constraints calls. 236 "/v1/versions/": func(w http.ResponseWriter, r *http.Request) { 237 w.Header().Set("Content-Type", "application/json") 238 io.WriteString(w, fmt.Sprintf(`{ 239 "service": "%s", 240 "product": "terraform", 241 "minimum": "0.1.0", 242 "maximum": "10.0.0" 243 }`, path.Base(r.URL.Path))) 244 }, 245 246 // Respond to pings to get the API version header. 247 "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { 248 w.Header().Set("Content-Type", "application/json") 249 w.Header().Set("TFP-API-Version", "2.5") 250 }, 251 252 // Respond to the initial query to read the hashicorp org entitlements. 253 "/api/v2/organizations/hashicorp/entitlement-set": func(w http.ResponseWriter, r *http.Request) { 254 w.Header().Set("Content-Type", "application/vnd.api+json") 255 io.WriteString(w, `{ 256 "data": { 257 "id": "org-GExadygjSbKP8hsY", 258 "type": "entitlement-sets", 259 "attributes": { 260 "operations": true, 261 "private-module-registry": true, 262 "sentinel": true, 263 "state-storage": true, 264 "teams": true, 265 "vcs-integrations": true 266 } 267 } 268 }`) 269 }, 270 271 // Respond to the initial query to read the no-operations org entitlements. 272 "/api/v2/organizations/no-operations/entitlement-set": func(w http.ResponseWriter, r *http.Request) { 273 w.Header().Set("Content-Type", "application/vnd.api+json") 274 io.WriteString(w, `{ 275 "data": { 276 "id": "org-ufxa3y8jSbKP8hsT", 277 "type": "entitlement-sets", 278 "attributes": { 279 "operations": false, 280 "private-module-registry": true, 281 "sentinel": true, 282 "state-storage": true, 283 "teams": true, 284 "vcs-integrations": true 285 } 286 } 287 }`) 288 }, 289 290 // All tests that are assumed to pass will use the hashicorp organization, 291 // so for all other organization requests we will return a 404. 292 "/api/v2/organizations/": func(w http.ResponseWriter, r *http.Request) { 293 w.WriteHeader(404) 294 io.WriteString(w, `{ 295 "errors": [ 296 { 297 "status": "404", 298 "title": "not found" 299 } 300 ] 301 }`) 302 }, 303 } 304 305 // testDisco returns a *disco.Disco mapping app.terraform.io and 306 // localhost to a local test server. 307 func testDisco(s *httptest.Server) *disco.Disco { 308 services := map[string]interface{}{ 309 "tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL), 310 } 311 d := disco.NewWithCredentialsSource(credsSrc) 312 d.SetUserAgent(httpclient.TerraformUserAgent(version.String())) 313 314 d.ForceHostServices(svchost.Hostname(defaultHostname), services) 315 d.ForceHostServices(svchost.Hostname("localhost"), services) 316 return d 317 } 318 319 type unparsedVariableValue struct { 320 value string 321 source terraform.ValueSourceType 322 } 323 324 func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 325 return &terraform.InputValue{ 326 Value: cty.StringVal(v.value), 327 SourceType: v.source, 328 }, tfdiags.Diagnostics{} 329 } 330 331 // testVariable returns a backend.UnparsedVariableValue used for testing. 332 func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue { 333 vars := make(map[string]backend.UnparsedVariableValue, len(vs)) 334 for _, v := range vs { 335 vars[v] = &unparsedVariableValue{ 336 value: v, 337 source: s, 338 } 339 } 340 return vars 341 }