github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/cloud/testing.go (about)

     1  package cloud
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"net/url"
    11  	"path"
    12  	"testing"
    13  	"time"
    14  
    15  	tfe "github.com/hashicorp/go-tfe"
    16  	svchost "github.com/hashicorp/terraform-svchost"
    17  	"github.com/hashicorp/terraform-svchost/auth"
    18  	"github.com/hashicorp/terraform-svchost/disco"
    19  	"github.com/mitchellh/cli"
    20  	"github.com/mitchellh/colorstring"
    21  	"github.com/zclconf/go-cty/cty"
    22  
    23  	"github.com/hashicorp/terraform/internal/backend"
    24  	"github.com/hashicorp/terraform/internal/command/jsonformat"
    25  	"github.com/hashicorp/terraform/internal/configs"
    26  	"github.com/hashicorp/terraform/internal/configs/configschema"
    27  	"github.com/hashicorp/terraform/internal/httpclient"
    28  	"github.com/hashicorp/terraform/internal/providers"
    29  	"github.com/hashicorp/terraform/internal/terraform"
    30  	"github.com/hashicorp/terraform/internal/tfdiags"
    31  	"github.com/hashicorp/terraform/version"
    32  
    33  	backendLocal "github.com/hashicorp/terraform/internal/backend/local"
    34  )
    35  
    36  const (
    37  	testCred = "test-auth-token"
    38  )
    39  
    40  var (
    41  	tfeHost  = svchost.Hostname(defaultHostname)
    42  	credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
    43  		tfeHost: {"token": testCred},
    44  	})
    45  	testBackendSingleWorkspaceName = "app-prod"
    46  )
    47  
    48  // mockInput is a mock implementation of terraform.UIInput.
    49  type mockInput struct {
    50  	answers map[string]string
    51  }
    52  
    53  func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
    54  	v, ok := m.answers[opts.Id]
    55  	if !ok {
    56  		return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
    57  	}
    58  	if v == "wait-for-external-update" {
    59  		select {
    60  		case <-ctx.Done():
    61  		case <-time.After(time.Minute):
    62  		}
    63  	}
    64  	delete(m.answers, opts.Id)
    65  	return v, nil
    66  }
    67  
    68  func testInput(t *testing.T, answers map[string]string) *mockInput {
    69  	return &mockInput{answers: answers}
    70  }
    71  
    72  func testBackendWithName(t *testing.T) (*Cloud, func()) {
    73  	obj := cty.ObjectVal(map[string]cty.Value{
    74  		"hostname":     cty.NullVal(cty.String),
    75  		"organization": cty.StringVal("hashicorp"),
    76  		"token":        cty.NullVal(cty.String),
    77  		"workspaces": cty.ObjectVal(map[string]cty.Value{
    78  			"name": cty.StringVal(testBackendSingleWorkspaceName),
    79  			"tags": cty.NullVal(cty.Set(cty.String)),
    80  		}),
    81  	})
    82  	return testBackend(t, obj)
    83  }
    84  
    85  func testBackendWithTags(t *testing.T) (*Cloud, func()) {
    86  	obj := cty.ObjectVal(map[string]cty.Value{
    87  		"hostname":     cty.NullVal(cty.String),
    88  		"organization": cty.StringVal("hashicorp"),
    89  		"token":        cty.NullVal(cty.String),
    90  		"workspaces": cty.ObjectVal(map[string]cty.Value{
    91  			"name": cty.NullVal(cty.String),
    92  			"tags": cty.SetVal(
    93  				[]cty.Value{
    94  					cty.StringVal("billing"),
    95  				},
    96  			),
    97  		}),
    98  	})
    99  	return testBackend(t, obj)
   100  }
   101  
   102  func testBackendNoOperations(t *testing.T) (*Cloud, func()) {
   103  	obj := cty.ObjectVal(map[string]cty.Value{
   104  		"hostname":     cty.NullVal(cty.String),
   105  		"organization": cty.StringVal("no-operations"),
   106  		"token":        cty.NullVal(cty.String),
   107  		"workspaces": cty.ObjectVal(map[string]cty.Value{
   108  			"name": cty.StringVal(testBackendSingleWorkspaceName),
   109  			"tags": cty.NullVal(cty.Set(cty.String)),
   110  		}),
   111  	})
   112  	return testBackend(t, obj)
   113  }
   114  
   115  func testCloudState(t *testing.T) *State {
   116  	b, bCleanup := testBackendWithName(t)
   117  	defer bCleanup()
   118  
   119  	raw, err := b.StateMgr(testBackendSingleWorkspaceName)
   120  	if err != nil {
   121  		t.Fatalf("error: %v", err)
   122  	}
   123  
   124  	return raw.(*State)
   125  }
   126  
   127  func testBackendWithOutputs(t *testing.T) (*Cloud, func()) {
   128  	b, cleanup := testBackendWithName(t)
   129  
   130  	// Get a new mock client to use for adding outputs
   131  	mc := NewMockClient()
   132  
   133  	mc.StateVersionOutputs.create("svo-abcd", &tfe.StateVersionOutput{
   134  		ID:           "svo-abcd",
   135  		Value:        "foobar",
   136  		Sensitive:    true,
   137  		Type:         "string",
   138  		Name:         "sensitive_output",
   139  		DetailedType: "string",
   140  	})
   141  
   142  	mc.StateVersionOutputs.create("svo-zyxw", &tfe.StateVersionOutput{
   143  		ID:           "svo-zyxw",
   144  		Value:        "bazqux",
   145  		Type:         "string",
   146  		Name:         "nonsensitive_output",
   147  		DetailedType: "string",
   148  	})
   149  
   150  	var dt interface{}
   151  	var val interface{}
   152  	err := json.Unmarshal([]byte(`["object", {"foo":"string"}]`), &dt)
   153  	if err != nil {
   154  		t.Fatalf("could not unmarshal detailed type: %s", err)
   155  	}
   156  	err = json.Unmarshal([]byte(`{"foo":"bar"}`), &val)
   157  	if err != nil {
   158  		t.Fatalf("could not unmarshal value: %s", err)
   159  	}
   160  	mc.StateVersionOutputs.create("svo-efgh", &tfe.StateVersionOutput{
   161  		ID:           "svo-efgh",
   162  		Value:        val,
   163  		Type:         "object",
   164  		Name:         "object_output",
   165  		DetailedType: dt,
   166  	})
   167  
   168  	err = json.Unmarshal([]byte(`["list", "bool"]`), &dt)
   169  	if err != nil {
   170  		t.Fatalf("could not unmarshal detailed type: %s", err)
   171  	}
   172  	err = json.Unmarshal([]byte(`[true, false, true, true]`), &val)
   173  	if err != nil {
   174  		t.Fatalf("could not unmarshal value: %s", err)
   175  	}
   176  	mc.StateVersionOutputs.create("svo-ijkl", &tfe.StateVersionOutput{
   177  		ID:           "svo-ijkl",
   178  		Value:        val,
   179  		Type:         "array",
   180  		Name:         "list_output",
   181  		DetailedType: dt,
   182  	})
   183  
   184  	b.client.StateVersionOutputs = mc.StateVersionOutputs
   185  
   186  	return b, cleanup
   187  }
   188  
   189  func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) {
   190  	s := testServer(t)
   191  	b := New(testDisco(s))
   192  
   193  	// Configure the backend so the client is created.
   194  	newObj, valDiags := b.PrepareConfig(obj)
   195  	if len(valDiags) != 0 {
   196  		t.Fatalf("testBackend: backend.PrepareConfig() failed: %s", valDiags.ErrWithWarnings())
   197  	}
   198  	obj = newObj
   199  
   200  	confDiags := b.Configure(obj)
   201  	if len(confDiags) != 0 {
   202  		t.Fatalf("testBackend: backend.Configure() failed: %s", confDiags.ErrWithWarnings())
   203  	}
   204  
   205  	// Get a new mock client.
   206  	mc := NewMockClient()
   207  
   208  	// Replace the services we use with our mock services.
   209  	b.CLI = cli.NewMockUi()
   210  	b.client.Applies = mc.Applies
   211  	b.client.ConfigurationVersions = mc.ConfigurationVersions
   212  	b.client.CostEstimates = mc.CostEstimates
   213  	b.client.Organizations = mc.Organizations
   214  	b.client.Plans = mc.Plans
   215  	b.client.TaskStages = mc.TaskStages
   216  	b.client.PolicySetOutcomes = mc.PolicySetOutcomes
   217  	b.client.PolicyChecks = mc.PolicyChecks
   218  	b.client.Runs = mc.Runs
   219  	b.client.StateVersions = mc.StateVersions
   220  	b.client.StateVersionOutputs = mc.StateVersionOutputs
   221  	b.client.Variables = mc.Variables
   222  	b.client.Workspaces = mc.Workspaces
   223  
   224  	// Set local to a local test backend.
   225  	b.local = testLocalBackend(t, b)
   226  	b.input = true
   227  
   228  	baseURL, err := url.Parse("https://app.terraform.io")
   229  	if err != nil {
   230  		t.Fatalf("testBackend: failed to parse base URL for client")
   231  	}
   232  	baseURL.Path = "/api/v2/"
   233  
   234  	readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) (*jsonformat.Plan, error) {
   235  		return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID)
   236  	}
   237  
   238  	ctx := context.Background()
   239  
   240  	// Create the organization.
   241  	_, err = b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
   242  		Name: tfe.String(b.organization),
   243  	})
   244  	if err != nil {
   245  		t.Fatalf("error: %v", err)
   246  	}
   247  
   248  	// Create the default workspace if required.
   249  	if b.WorkspaceMapping.Name != "" {
   250  		_, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{
   251  			Name: tfe.String(b.WorkspaceMapping.Name),
   252  		})
   253  		if err != nil {
   254  			t.Fatalf("error: %v", err)
   255  		}
   256  	}
   257  
   258  	return b, s.Close
   259  }
   260  
   261  // testUnconfiguredBackend is used for testing the configuration of the backend
   262  // with the mock client
   263  func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) {
   264  	s := testServer(t)
   265  	b := New(testDisco(s))
   266  
   267  	// Normally, the client is created during configuration, but the configuration uses the
   268  	// client to read entitlements.
   269  	var err error
   270  	b.client, err = tfe.NewClient(&tfe.Config{
   271  		Token: "fake-token",
   272  	})
   273  	if err != nil {
   274  		t.Fatal(err)
   275  	}
   276  
   277  	// Get a new mock client.
   278  	mc := NewMockClient()
   279  
   280  	// Replace the services we use with our mock services.
   281  	b.CLI = cli.NewMockUi()
   282  	b.client.Applies = mc.Applies
   283  	b.client.ConfigurationVersions = mc.ConfigurationVersions
   284  	b.client.CostEstimates = mc.CostEstimates
   285  	b.client.Organizations = mc.Organizations
   286  	b.client.Plans = mc.Plans
   287  	b.client.PolicySetOutcomes = mc.PolicySetOutcomes
   288  	b.client.PolicyChecks = mc.PolicyChecks
   289  	b.client.Runs = mc.Runs
   290  	b.client.StateVersions = mc.StateVersions
   291  	b.client.Variables = mc.Variables
   292  	b.client.Workspaces = mc.Workspaces
   293  
   294  	baseURL, err := url.Parse("https://app.terraform.io")
   295  	if err != nil {
   296  		t.Fatalf("testBackend: failed to parse base URL for client")
   297  	}
   298  	baseURL.Path = "/api/v2/"
   299  
   300  	readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) (*jsonformat.Plan, error) {
   301  		return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID)
   302  	}
   303  
   304  	// Set local to a local test backend.
   305  	b.local = testLocalBackend(t, b)
   306  
   307  	return b, s.Close
   308  }
   309  
   310  func testLocalBackend(t *testing.T, cloud *Cloud) backend.Enhanced {
   311  	b := backendLocal.NewWithBackend(cloud)
   312  
   313  	// Add a test provider to the local backend.
   314  	p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{
   315  		ResourceTypes: map[string]*configschema.Block{
   316  			"null_resource": {
   317  				Attributes: map[string]*configschema.Attribute{
   318  					"id": {Type: cty.String, Computed: true},
   319  				},
   320  			},
   321  		},
   322  	})
   323  	p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
   324  		"id": cty.StringVal("yes"),
   325  	})}
   326  
   327  	return b
   328  }
   329  
   330  // testServer returns a started *httptest.Server used for local testing with the default set of
   331  // request handlers.
   332  func testServer(t *testing.T) *httptest.Server {
   333  	return testServerWithHandlers(testDefaultRequestHandlers)
   334  }
   335  
   336  // testServerWithHandlers returns a started *httptest.Server with the given set of request handlers
   337  // overriding any default request handlers (testDefaultRequestHandlers).
   338  func testServerWithHandlers(handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server {
   339  	mux := http.NewServeMux()
   340  	for route, handler := range handlers {
   341  		mux.HandleFunc(route, handler)
   342  	}
   343  	for route, handler := range testDefaultRequestHandlers {
   344  		if handlers[route] == nil {
   345  			mux.HandleFunc(route, handler)
   346  		}
   347  	}
   348  
   349  	return httptest.NewServer(mux)
   350  }
   351  
   352  // testDefaultRequestHandlers is a map of request handlers intended to be used in a request
   353  // multiplexer for a test server. A caller may use testServerWithHandlers to start a server with
   354  // this base set of routes, and override a particular route for whatever edge case is being tested.
   355  var testDefaultRequestHandlers = map[string]func(http.ResponseWriter, *http.Request){
   356  	// Respond to service discovery calls.
   357  	"/well-known/terraform.json": func(w http.ResponseWriter, r *http.Request) {
   358  		w.Header().Set("Content-Type", "application/json")
   359  		io.WriteString(w, `{
   360    "tfe.v2": "/api/v2/",
   361  }`)
   362  	},
   363  
   364  	// Respond to service version constraints calls.
   365  	"/v1/versions/": func(w http.ResponseWriter, r *http.Request) {
   366  		w.Header().Set("Content-Type", "application/json")
   367  		io.WriteString(w, fmt.Sprintf(`{
   368    "service": "%s",
   369    "product": "terraform",
   370    "minimum": "0.1.0",
   371    "maximum": "10.0.0"
   372  }`, path.Base(r.URL.Path)))
   373  	},
   374  
   375  	// Respond to pings to get the API version header.
   376  	"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
   377  		w.Header().Set("Content-Type", "application/json")
   378  		w.Header().Set("TFP-API-Version", "2.5")
   379  	},
   380  
   381  	// Respond to the initial query to read the hashicorp org entitlements.
   382  	"/api/v2/organizations/hashicorp/entitlement-set": func(w http.ResponseWriter, r *http.Request) {
   383  		w.Header().Set("Content-Type", "application/vnd.api+json")
   384  		io.WriteString(w, `{
   385    "data": {
   386      "id": "org-GExadygjSbKP8hsY",
   387      "type": "entitlement-sets",
   388      "attributes": {
   389        "operations": true,
   390        "private-module-registry": true,
   391        "sentinel": true,
   392        "state-storage": true,
   393        "teams": true,
   394        "vcs-integrations": true
   395      }
   396    }
   397  }`)
   398  	},
   399  
   400  	// Respond to the initial query to read the no-operations org entitlements.
   401  	"/api/v2/organizations/no-operations/entitlement-set": func(w http.ResponseWriter, r *http.Request) {
   402  		w.Header().Set("Content-Type", "application/vnd.api+json")
   403  		io.WriteString(w, `{
   404    "data": {
   405      "id": "org-ufxa3y8jSbKP8hsT",
   406      "type": "entitlement-sets",
   407      "attributes": {
   408        "operations": false,
   409        "private-module-registry": true,
   410        "sentinel": true,
   411        "state-storage": true,
   412        "teams": true,
   413        "vcs-integrations": true
   414      }
   415    }
   416  }`)
   417  	},
   418  
   419  	// All tests that are assumed to pass will use the hashicorp organization,
   420  	// so for all other organization requests we will return a 404.
   421  	"/api/v2/organizations/": func(w http.ResponseWriter, r *http.Request) {
   422  		w.WriteHeader(404)
   423  		io.WriteString(w, `{
   424    "errors": [
   425      {
   426        "status": "404",
   427        "title": "not found"
   428      }
   429    ]
   430  }`)
   431  	},
   432  }
   433  
   434  func mockColorize() *colorstring.Colorize {
   435  	colors := make(map[string]string)
   436  	for k, v := range colorstring.DefaultColors {
   437  		colors[k] = v
   438  	}
   439  	colors["purple"] = "38;5;57"
   440  
   441  	return &colorstring.Colorize{
   442  		Colors:  colors,
   443  		Disable: false,
   444  		Reset:   true,
   445  	}
   446  }
   447  
   448  func mockSROWorkspace(t *testing.T, b *Cloud, workspaceName string) {
   449  	_, err := b.client.Workspaces.Update(context.Background(), "hashicorp", workspaceName, tfe.WorkspaceUpdateOptions{
   450  		StructuredRunOutputEnabled: tfe.Bool(true),
   451  		TerraformVersion:           tfe.String("1.4.0"),
   452  	})
   453  	if err != nil {
   454  		t.Fatalf("Error enabling SRO on workspace %s: %v", workspaceName, err)
   455  	}
   456  }
   457  
   458  // testDisco returns a *disco.Disco mapping app.terraform.io and
   459  // localhost to a local test server.
   460  func testDisco(s *httptest.Server) *disco.Disco {
   461  	services := map[string]interface{}{
   462  		"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
   463  	}
   464  	d := disco.NewWithCredentialsSource(credsSrc)
   465  	d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
   466  
   467  	d.ForceHostServices(svchost.Hostname(defaultHostname), services)
   468  	d.ForceHostServices(svchost.Hostname("localhost"), services)
   469  	d.ForceHostServices(svchost.Hostname("nontfe.local"), nil)
   470  	return d
   471  }
   472  
   473  type unparsedVariableValue struct {
   474  	value  string
   475  	source terraform.ValueSourceType
   476  }
   477  
   478  func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
   479  	return &terraform.InputValue{
   480  		Value:      cty.StringVal(v.value),
   481  		SourceType: v.source,
   482  	}, tfdiags.Diagnostics{}
   483  }
   484  
   485  // testVariable returns a backend.UnparsedVariableValue used for testing.
   486  func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue {
   487  	vars := make(map[string]backend.UnparsedVariableValue, len(vs))
   488  	for _, v := range vs {
   489  		vars[v] = &unparsedVariableValue{
   490  			value:  v,
   491  			source: s,
   492  		}
   493  	}
   494  	return vars
   495  }