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