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  }