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  }