github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/backend/remote/testing.go (about) 1 package remote 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/hashicorp/terraform/internal/backend" 18 "github.com/hashicorp/terraform/internal/cloud" 19 "github.com/hashicorp/terraform/internal/configs" 20 "github.com/hashicorp/terraform/internal/configs/configschema" 21 "github.com/hashicorp/terraform/internal/httpclient" 22 "github.com/hashicorp/terraform/internal/providers" 23 "github.com/hashicorp/terraform/internal/states/remote" 24 "github.com/hashicorp/terraform/internal/terraform" 25 "github.com/hashicorp/terraform/internal/tfdiags" 26 "github.com/hashicorp/terraform/version" 27 "github.com/mitchellh/cli" 28 "github.com/zclconf/go-cty/cty" 29 30 backendLocal "github.com/hashicorp/terraform/internal/backend/local" 31 ) 32 33 const ( 34 testCred = "test-auth-token" 35 ) 36 37 var ( 38 tfeHost = svchost.Hostname(defaultHostname) 39 credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ 40 tfeHost: {"token": testCred}, 41 }) 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 testBackendDefault(t *testing.T) (*Remote, 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("prod"), 75 "prefix": cty.NullVal(cty.String), 76 }), 77 }) 78 return testBackend(t, obj) 79 } 80 81 func testBackendNoDefault(t *testing.T) (*Remote, 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 "prefix": cty.StringVal("my-app-"), 89 }), 90 }) 91 return testBackend(t, obj) 92 } 93 94 func testBackendNoOperations(t *testing.T) (*Remote, func()) { 95 obj := cty.ObjectVal(map[string]cty.Value{ 96 "hostname": cty.NullVal(cty.String), 97 "organization": cty.StringVal("no-operations"), 98 "token": cty.NullVal(cty.String), 99 "workspaces": cty.ObjectVal(map[string]cty.Value{ 100 "name": cty.StringVal("prod"), 101 "prefix": cty.NullVal(cty.String), 102 }), 103 }) 104 return testBackend(t, obj) 105 } 106 107 func testRemoteClient(t *testing.T) remote.Client { 108 b, bCleanup := testBackendDefault(t) 109 defer bCleanup() 110 111 raw, err := b.StateMgr(backend.DefaultStateName) 112 if err != nil { 113 t.Fatalf("error: %v", err) 114 } 115 116 return raw.(*remote.State).Client 117 } 118 119 func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) { 120 s := testServer(t) 121 b := New(testDisco(s)) 122 123 // Configure the backend so the client is created. 124 newObj, valDiags := b.PrepareConfig(obj) 125 if len(valDiags) != 0 { 126 t.Fatal(valDiags.ErrWithWarnings()) 127 } 128 obj = newObj 129 130 confDiags := b.Configure(obj) 131 if len(confDiags) != 0 { 132 t.Fatal(confDiags.ErrWithWarnings()) 133 } 134 135 // Get a new mock client. 136 mc := cloud.NewMockClient() 137 138 // Replace the services we use with our mock services. 139 b.CLI = cli.NewMockUi() 140 b.client.Applies = mc.Applies 141 b.client.ConfigurationVersions = mc.ConfigurationVersions 142 b.client.CostEstimates = mc.CostEstimates 143 b.client.Organizations = mc.Organizations 144 b.client.Plans = mc.Plans 145 b.client.PolicyChecks = mc.PolicyChecks 146 b.client.Runs = mc.Runs 147 b.client.StateVersions = mc.StateVersions 148 b.client.Variables = mc.Variables 149 b.client.Workspaces = mc.Workspaces 150 151 // Set local to a local test backend. 152 b.local = testLocalBackend(t, b) 153 154 ctx := context.Background() 155 156 // Create the organization. 157 _, err := b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{ 158 Name: tfe.String(b.organization), 159 }) 160 if err != nil { 161 t.Fatalf("error: %v", err) 162 } 163 164 // Create the default workspace if required. 165 if b.workspace != "" { 166 _, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{ 167 Name: tfe.String(b.workspace), 168 }) 169 if err != nil { 170 t.Fatalf("error: %v", err) 171 } 172 } 173 174 return b, s.Close 175 } 176 177 func testLocalBackend(t *testing.T, remote *Remote) backend.Enhanced { 178 b := backendLocal.NewWithBackend(remote) 179 180 // Add a test provider to the local backend. 181 p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{ 182 ResourceTypes: map[string]*configschema.Block{ 183 "null_resource": { 184 Attributes: map[string]*configschema.Attribute{ 185 "id": {Type: cty.String, Computed: true}, 186 }, 187 }, 188 }, 189 }) 190 p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 191 "id": cty.StringVal("yes"), 192 })} 193 194 return b 195 } 196 197 // testServer returns a *httptest.Server used for local testing. 198 func testServer(t *testing.T) *httptest.Server { 199 mux := http.NewServeMux() 200 201 // Respond to service discovery calls. 202 mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) { 203 w.Header().Set("Content-Type", "application/json") 204 io.WriteString(w, `{ 205 "state.v2": "/api/v2/", 206 "tfe.v2.1": "/api/v2/", 207 "versions.v1": "/v1/versions/" 208 }`) 209 }) 210 211 // Respond to service version constraints calls. 212 mux.HandleFunc("/v1/versions/", func(w http.ResponseWriter, r *http.Request) { 213 w.Header().Set("Content-Type", "application/json") 214 io.WriteString(w, fmt.Sprintf(`{ 215 "service": "%s", 216 "product": "terraform", 217 "minimum": "0.1.0", 218 "maximum": "10.0.0" 219 }`, path.Base(r.URL.Path))) 220 }) 221 222 // Respond to pings to get the API version header. 223 mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) { 224 w.Header().Set("Content-Type", "application/json") 225 w.Header().Set("TFP-API-Version", "2.4") 226 }) 227 228 // Respond to the initial query to read the hashicorp org entitlements. 229 mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) { 230 w.Header().Set("Content-Type", "application/vnd.api+json") 231 io.WriteString(w, `{ 232 "data": { 233 "id": "org-GExadygjSbKP8hsY", 234 "type": "entitlement-sets", 235 "attributes": { 236 "operations": true, 237 "private-module-registry": true, 238 "sentinel": true, 239 "state-storage": true, 240 "teams": true, 241 "vcs-integrations": true 242 } 243 } 244 }`) 245 }) 246 247 // Respond to the initial query to read the no-operations org entitlements. 248 mux.HandleFunc("/api/v2/organizations/no-operations/entitlement-set", func(w http.ResponseWriter, r *http.Request) { 249 w.Header().Set("Content-Type", "application/vnd.api+json") 250 io.WriteString(w, `{ 251 "data": { 252 "id": "org-ufxa3y8jSbKP8hsT", 253 "type": "entitlement-sets", 254 "attributes": { 255 "operations": false, 256 "private-module-registry": true, 257 "sentinel": true, 258 "state-storage": true, 259 "teams": true, 260 "vcs-integrations": true 261 } 262 } 263 }`) 264 }) 265 266 // All tests that are assumed to pass will use the hashicorp organization, 267 // so for all other organization requests we will return a 404. 268 mux.HandleFunc("/api/v2/organizations/", func(w http.ResponseWriter, r *http.Request) { 269 w.WriteHeader(404) 270 io.WriteString(w, `{ 271 "errors": [ 272 { 273 "status": "404", 274 "title": "not found" 275 } 276 ] 277 }`) 278 }) 279 280 return httptest.NewServer(mux) 281 } 282 283 // testDisco returns a *disco.Disco mapping app.terraform.io and 284 // localhost to a local test server. 285 func testDisco(s *httptest.Server) *disco.Disco { 286 services := map[string]interface{}{ 287 "state.v2": fmt.Sprintf("%s/api/v2/", s.URL), 288 "tfe.v2.1": fmt.Sprintf("%s/api/v2/", s.URL), 289 "versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL), 290 } 291 d := disco.NewWithCredentialsSource(credsSrc) 292 d.SetUserAgent(httpclient.TerraformUserAgent(version.String())) 293 294 d.ForceHostServices(svchost.Hostname(defaultHostname), services) 295 d.ForceHostServices(svchost.Hostname("localhost"), services) 296 return d 297 } 298 299 type unparsedVariableValue struct { 300 value string 301 source terraform.ValueSourceType 302 } 303 304 func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 305 return &terraform.InputValue{ 306 Value: cty.StringVal(v.value), 307 SourceType: v.source, 308 }, tfdiags.Diagnostics{} 309 } 310 311 // testVariable returns a backend.UnparsedVariableValue used for testing. 312 func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue { 313 vars := make(map[string]backend.UnparsedVariableValue, len(vs)) 314 for _, v := range vs { 315 vars[v] = &unparsedVariableValue{ 316 value: v, 317 source: s, 318 } 319 } 320 return vars 321 }