kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/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  	"kubeform.dev/terraform-backend-sdk/backend"
    17  	"kubeform.dev/terraform-backend-sdk/configs"
    18  	"kubeform.dev/terraform-backend-sdk/configs/configschema"
    19  	"kubeform.dev/terraform-backend-sdk/httpclient"
    20  	"kubeform.dev/terraform-backend-sdk/providers"
    21  	"kubeform.dev/terraform-backend-sdk/states/remote"
    22  	"kubeform.dev/terraform-backend-sdk/terraform"
    23  	"kubeform.dev/terraform-backend-sdk/tfdiags"
    24  	"kubeform.dev/terraform-backend-sdk/version"
    25  	"github.com/mitchellh/cli"
    26  	"github.com/zclconf/go-cty/cty"
    27  
    28  	backendLocal "kubeform.dev/terraform-backend-sdk/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  	// Set local to a local test backend.
   130  	b.local = testLocalBackend(t, b)
   131  
   132  	ctx := context.Background()
   133  
   134  	// Create the organization.
   135  	_, err := b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
   136  		Name: tfe.String(b.organization),
   137  	})
   138  	if err != nil {
   139  		t.Fatalf("error: %v", err)
   140  	}
   141  
   142  	// Create the default workspace if required.
   143  	if b.workspace != "" {
   144  		_, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{
   145  			Name: tfe.String(b.workspace),
   146  		})
   147  		if err != nil {
   148  			t.Fatalf("error: %v", err)
   149  		}
   150  	}
   151  
   152  	return b, s.Close
   153  }
   154  
   155  func testLocalBackend(t *testing.T, remote *Remote) backend.Enhanced {
   156  	b := backendLocal.NewWithBackend(remote)
   157  
   158  	// Add a test provider to the local backend.
   159  	p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{
   160  		ResourceTypes: map[string]*configschema.Block{
   161  			"null_resource": {
   162  				Attributes: map[string]*configschema.Attribute{
   163  					"id": {Type: cty.String, Computed: true},
   164  				},
   165  			},
   166  		},
   167  	})
   168  	p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
   169  		"id": cty.StringVal("yes"),
   170  	})}
   171  
   172  	return b
   173  }
   174  
   175  // testServer returns a *httptest.Server used for local testing.
   176  func testServer(t *testing.T) *httptest.Server {
   177  	mux := http.NewServeMux()
   178  
   179  	// Respond to service discovery calls.
   180  	mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
   181  		w.Header().Set("Content-Type", "application/json")
   182  		io.WriteString(w, `{
   183    "state.v2": "/api/v2/",
   184    "tfe.v2.1": "/api/v2/",
   185    "versions.v1": "/v1/versions/"
   186  }`)
   187  	})
   188  
   189  	// Respond to service version constraints calls.
   190  	mux.HandleFunc("/v1/versions/", func(w http.ResponseWriter, r *http.Request) {
   191  		w.Header().Set("Content-Type", "application/json")
   192  		io.WriteString(w, fmt.Sprintf(`{
   193    "service": "%s",
   194    "product": "terraform",
   195    "minimum": "0.1.0",
   196    "maximum": "10.0.0"
   197  }`, path.Base(r.URL.Path)))
   198  	})
   199  
   200  	// Respond to pings to get the API version header.
   201  	mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) {
   202  		w.Header().Set("Content-Type", "application/json")
   203  		w.Header().Set("TFP-API-Version", "2.4")
   204  	})
   205  
   206  	// Respond to the initial query to read the hashicorp org entitlements.
   207  	mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) {
   208  		w.Header().Set("Content-Type", "application/vnd.api+json")
   209  		io.WriteString(w, `{
   210    "data": {
   211      "id": "org-GExadygjSbKP8hsY",
   212      "type": "entitlement-sets",
   213      "attributes": {
   214        "operations": true,
   215        "private-module-registry": true,
   216        "sentinel": true,
   217        "state-storage": true,
   218        "teams": true,
   219        "vcs-integrations": true
   220      }
   221    }
   222  }`)
   223  	})
   224  
   225  	// Respond to the initial query to read the no-operations org entitlements.
   226  	mux.HandleFunc("/api/v2/organizations/no-operations/entitlement-set", func(w http.ResponseWriter, r *http.Request) {
   227  		w.Header().Set("Content-Type", "application/vnd.api+json")
   228  		io.WriteString(w, `{
   229    "data": {
   230      "id": "org-ufxa3y8jSbKP8hsT",
   231      "type": "entitlement-sets",
   232      "attributes": {
   233        "operations": false,
   234        "private-module-registry": true,
   235        "sentinel": true,
   236        "state-storage": true,
   237        "teams": true,
   238        "vcs-integrations": true
   239      }
   240    }
   241  }`)
   242  	})
   243  
   244  	// All tests that are assumed to pass will use the hashicorp organization,
   245  	// so for all other organization requests we will return a 404.
   246  	mux.HandleFunc("/api/v2/organizations/", func(w http.ResponseWriter, r *http.Request) {
   247  		w.WriteHeader(404)
   248  		io.WriteString(w, `{
   249    "errors": [
   250      {
   251        "status": "404",
   252        "title": "not found"
   253      }
   254    ]
   255  }`)
   256  	})
   257  
   258  	return httptest.NewServer(mux)
   259  }
   260  
   261  // testDisco returns a *disco.Disco mapping app.terraform.io and
   262  // localhost to a local test server.
   263  func testDisco(s *httptest.Server) *disco.Disco {
   264  	services := map[string]interface{}{
   265  		"state.v2":    fmt.Sprintf("%s/api/v2/", s.URL),
   266  		"tfe.v2.1":    fmt.Sprintf("%s/api/v2/", s.URL),
   267  		"versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL),
   268  	}
   269  	d := disco.NewWithCredentialsSource(credsSrc)
   270  	d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
   271  
   272  	d.ForceHostServices(svchost.Hostname(defaultHostname), services)
   273  	d.ForceHostServices(svchost.Hostname("localhost"), services)
   274  	return d
   275  }
   276  
   277  type unparsedVariableValue struct {
   278  	value  string
   279  	source terraform.ValueSourceType
   280  }
   281  
   282  func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
   283  	return &terraform.InputValue{
   284  		Value:      cty.StringVal(v.value),
   285  		SourceType: v.source,
   286  	}, tfdiags.Diagnostics{}
   287  }
   288  
   289  // testVariable returns a backend.UnparsedVariableValue used for testing.
   290  func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue {
   291  	vars := make(map[string]backend.UnparsedVariableValue, len(vs))
   292  	for _, v := range vs {
   293  		vars[v] = &unparsedVariableValue{
   294  			value:  v,
   295  			source: s,
   296  		}
   297  	}
   298  	return vars
   299  }