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