github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/cloud/testing.go (about)

     1  package cloud
     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/cycloidio/terraform/backend"
    18  	"github.com/cycloidio/terraform/configs"
    19  	"github.com/cycloidio/terraform/configs/configschema"
    20  	"github.com/cycloidio/terraform/httpclient"
    21  	"github.com/cycloidio/terraform/providers"
    22  	"github.com/cycloidio/terraform/states/remote"
    23  	"github.com/cycloidio/terraform/terraform"
    24  	"github.com/cycloidio/terraform/tfdiags"
    25  	"github.com/cycloidio/terraform/version"
    26  	"github.com/mitchellh/cli"
    27  	"github.com/zclconf/go-cty/cty"
    28  
    29  	backendLocal "github.com/cycloidio/terraform/backend/local"
    30  )
    31  
    32  const (
    33  	testCred = "test-auth-token"
    34  )
    35  
    36  var (
    37  	tfeHost  = svchost.Hostname(defaultHostname)
    38  	credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
    39  		tfeHost: {"token": testCred},
    40  	})
    41  	testBackendSingleWorkspaceName = "app-prod"
    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 testBackendWithName(t *testing.T) (*Cloud, 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(testBackendSingleWorkspaceName),
    75  			"tags": cty.NullVal(cty.Set(cty.String)),
    76  		}),
    77  	})
    78  	return testBackend(t, obj)
    79  }
    80  
    81  func testBackendWithTags(t *testing.T) (*Cloud, 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  			"tags": cty.SetVal(
    89  				[]cty.Value{
    90  					cty.StringVal("billing"),
    91  				},
    92  			),
    93  		}),
    94  	})
    95  	return testBackend(t, obj)
    96  }
    97  
    98  func testBackendNoOperations(t *testing.T) (*Cloud, func()) {
    99  	obj := cty.ObjectVal(map[string]cty.Value{
   100  		"hostname":     cty.NullVal(cty.String),
   101  		"organization": cty.StringVal("no-operations"),
   102  		"token":        cty.NullVal(cty.String),
   103  		"workspaces": cty.ObjectVal(map[string]cty.Value{
   104  			"name": cty.StringVal(testBackendSingleWorkspaceName),
   105  			"tags": cty.NullVal(cty.Set(cty.String)),
   106  		}),
   107  	})
   108  	return testBackend(t, obj)
   109  }
   110  
   111  func testRemoteClient(t *testing.T) remote.Client {
   112  	b, bCleanup := testBackendWithName(t)
   113  	defer bCleanup()
   114  
   115  	raw, err := b.StateMgr(testBackendSingleWorkspaceName)
   116  	if err != nil {
   117  		t.Fatalf("error: %v", err)
   118  	}
   119  
   120  	return raw.(*remote.State).Client
   121  }
   122  
   123  func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) {
   124  	s := testServer(t)
   125  	b := New(testDisco(s))
   126  
   127  	// Configure the backend so the client is created.
   128  	newObj, valDiags := b.PrepareConfig(obj)
   129  	if len(valDiags) != 0 {
   130  		t.Fatalf("testBackend: backend.PrepareConfig() failed: %s", valDiags.ErrWithWarnings())
   131  	}
   132  	obj = newObj
   133  
   134  	confDiags := b.Configure(obj)
   135  	if len(confDiags) != 0 {
   136  		t.Fatalf("testBackend: backend.Configure() failed: %s", confDiags.ErrWithWarnings())
   137  	}
   138  
   139  	// Get a new mock client.
   140  	mc := NewMockClient()
   141  
   142  	// Replace the services we use with our mock services.
   143  	b.CLI = cli.NewMockUi()
   144  	b.client.Applies = mc.Applies
   145  	b.client.ConfigurationVersions = mc.ConfigurationVersions
   146  	b.client.CostEstimates = mc.CostEstimates
   147  	b.client.Organizations = mc.Organizations
   148  	b.client.Plans = mc.Plans
   149  	b.client.PolicyChecks = mc.PolicyChecks
   150  	b.client.Runs = mc.Runs
   151  	b.client.StateVersions = mc.StateVersions
   152  	b.client.Variables = mc.Variables
   153  	b.client.Workspaces = mc.Workspaces
   154  
   155  	// Set local to a local test backend.
   156  	b.local = testLocalBackend(t, b)
   157  
   158  	ctx := context.Background()
   159  
   160  	// Create the organization.
   161  	_, err := b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
   162  		Name: tfe.String(b.organization),
   163  	})
   164  	if err != nil {
   165  		t.Fatalf("error: %v", err)
   166  	}
   167  
   168  	// Create the default workspace if required.
   169  	if b.WorkspaceMapping.Name != "" {
   170  		_, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{
   171  			Name: tfe.String(b.WorkspaceMapping.Name),
   172  		})
   173  		if err != nil {
   174  			t.Fatalf("error: %v", err)
   175  		}
   176  	}
   177  
   178  	return b, s.Close
   179  }
   180  
   181  func testLocalBackend(t *testing.T, cloud *Cloud) backend.Enhanced {
   182  	b := backendLocal.NewWithBackend(cloud)
   183  
   184  	// Add a test provider to the local backend.
   185  	p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{
   186  		ResourceTypes: map[string]*configschema.Block{
   187  			"null_resource": {
   188  				Attributes: map[string]*configschema.Attribute{
   189  					"id": {Type: cty.String, Computed: true},
   190  				},
   191  			},
   192  		},
   193  	})
   194  	p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
   195  		"id": cty.StringVal("yes"),
   196  	})}
   197  
   198  	return b
   199  }
   200  
   201  // testServer returns a started *httptest.Server used for local testing with the default set of
   202  // request handlers.
   203  func testServer(t *testing.T) *httptest.Server {
   204  	return testServerWithHandlers(testDefaultRequestHandlers)
   205  }
   206  
   207  // testServerWithHandlers returns a started *httptest.Server with the given set of request handlers
   208  // overriding any default request handlers (testDefaultRequestHandlers).
   209  func testServerWithHandlers(handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server {
   210  	mux := http.NewServeMux()
   211  	for route, handler := range handlers {
   212  		mux.HandleFunc(route, handler)
   213  	}
   214  	for route, handler := range testDefaultRequestHandlers {
   215  		if handlers[route] == nil {
   216  			mux.HandleFunc(route, handler)
   217  		}
   218  	}
   219  
   220  	return httptest.NewServer(mux)
   221  }
   222  
   223  // testDefaultRequestHandlers is a map of request handlers intended to be used in a request
   224  // multiplexer for a test server. A caller may use testServerWithHandlers to start a server with
   225  // this base set of routes, and override a particular route for whatever edge case is being tested.
   226  var testDefaultRequestHandlers = map[string]func(http.ResponseWriter, *http.Request){
   227  	// Respond to service discovery calls.
   228  	"/well-known/terraform.json": func(w http.ResponseWriter, r *http.Request) {
   229  		w.Header().Set("Content-Type", "application/json")
   230  		io.WriteString(w, `{
   231    "tfe.v2": "/api/v2/",
   232  }`)
   233  	},
   234  
   235  	// Respond to service version constraints calls.
   236  	"/v1/versions/": func(w http.ResponseWriter, r *http.Request) {
   237  		w.Header().Set("Content-Type", "application/json")
   238  		io.WriteString(w, fmt.Sprintf(`{
   239    "service": "%s",
   240    "product": "terraform",
   241    "minimum": "0.1.0",
   242    "maximum": "10.0.0"
   243  }`, path.Base(r.URL.Path)))
   244  	},
   245  
   246  	// Respond to pings to get the API version header.
   247  	"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
   248  		w.Header().Set("Content-Type", "application/json")
   249  		w.Header().Set("TFP-API-Version", "2.5")
   250  	},
   251  
   252  	// Respond to the initial query to read the hashicorp org entitlements.
   253  	"/api/v2/organizations/hashicorp/entitlement-set": func(w http.ResponseWriter, r *http.Request) {
   254  		w.Header().Set("Content-Type", "application/vnd.api+json")
   255  		io.WriteString(w, `{
   256    "data": {
   257      "id": "org-GExadygjSbKP8hsY",
   258      "type": "entitlement-sets",
   259      "attributes": {
   260        "operations": true,
   261        "private-module-registry": true,
   262        "sentinel": true,
   263        "state-storage": true,
   264        "teams": true,
   265        "vcs-integrations": true
   266      }
   267    }
   268  }`)
   269  	},
   270  
   271  	// Respond to the initial query to read the no-operations org entitlements.
   272  	"/api/v2/organizations/no-operations/entitlement-set": func(w http.ResponseWriter, r *http.Request) {
   273  		w.Header().Set("Content-Type", "application/vnd.api+json")
   274  		io.WriteString(w, `{
   275    "data": {
   276      "id": "org-ufxa3y8jSbKP8hsT",
   277      "type": "entitlement-sets",
   278      "attributes": {
   279        "operations": false,
   280        "private-module-registry": true,
   281        "sentinel": true,
   282        "state-storage": true,
   283        "teams": true,
   284        "vcs-integrations": true
   285      }
   286    }
   287  }`)
   288  	},
   289  
   290  	// All tests that are assumed to pass will use the hashicorp organization,
   291  	// so for all other organization requests we will return a 404.
   292  	"/api/v2/organizations/": func(w http.ResponseWriter, r *http.Request) {
   293  		w.WriteHeader(404)
   294  		io.WriteString(w, `{
   295    "errors": [
   296      {
   297        "status": "404",
   298        "title": "not found"
   299      }
   300    ]
   301  }`)
   302  	},
   303  }
   304  
   305  // testDisco returns a *disco.Disco mapping app.terraform.io and
   306  // localhost to a local test server.
   307  func testDisco(s *httptest.Server) *disco.Disco {
   308  	services := map[string]interface{}{
   309  		"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
   310  	}
   311  	d := disco.NewWithCredentialsSource(credsSrc)
   312  	d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
   313  
   314  	d.ForceHostServices(svchost.Hostname(defaultHostname), services)
   315  	d.ForceHostServices(svchost.Hostname("localhost"), services)
   316  	return d
   317  }
   318  
   319  type unparsedVariableValue struct {
   320  	value  string
   321  	source terraform.ValueSourceType
   322  }
   323  
   324  func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
   325  	return &terraform.InputValue{
   326  		Value:      cty.StringVal(v.value),
   327  		SourceType: v.source,
   328  	}, tfdiags.Diagnostics{}
   329  }
   330  
   331  // testVariable returns a backend.UnparsedVariableValue used for testing.
   332  func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue {
   333  	vars := make(map[string]backend.UnparsedVariableValue, len(vs))
   334  	for _, v := range vs {
   335  		vars[v] = &unparsedVariableValue{
   336  			value:  v,
   337  			source: s,
   338  		}
   339  	}
   340  	return vars
   341  }