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

     1  package cloud
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"os"
     8  	"strings"
     9  	"testing"
    10  
    11  	tfe "github.com/hashicorp/go-tfe"
    12  	version "github.com/hashicorp/go-version"
    13  	"github.com/cycloidio/terraform/backend"
    14  	"github.com/cycloidio/terraform/tfdiags"
    15  	tfversion "github.com/cycloidio/terraform/version"
    16  	"github.com/zclconf/go-cty/cty"
    17  
    18  	backendLocal "github.com/cycloidio/terraform/backend/local"
    19  )
    20  
    21  func TestCloud(t *testing.T) {
    22  	var _ backend.Enhanced = New(nil)
    23  	var _ backend.CLI = New(nil)
    24  }
    25  
    26  func TestCloud_backendWithName(t *testing.T) {
    27  	b, bCleanup := testBackendWithName(t)
    28  	defer bCleanup()
    29  
    30  	workspaces, err := b.Workspaces()
    31  	if err != nil {
    32  		t.Fatalf("error: %v", err)
    33  	}
    34  
    35  	if len(workspaces) != 1 || workspaces[0] != testBackendSingleWorkspaceName {
    36  		t.Fatalf("should only have a single configured workspace matching the configured 'name' strategy, but got: %#v", workspaces)
    37  	}
    38  
    39  	if _, err := b.StateMgr("foo"); err != backend.ErrWorkspacesNotSupported {
    40  		t.Fatalf("expected fetching a state which is NOT the single configured workspace to have an ErrWorkspacesNotSupported error, but got: %v", err)
    41  	}
    42  
    43  	if err := b.DeleteWorkspace(testBackendSingleWorkspaceName); err != backend.ErrWorkspacesNotSupported {
    44  		t.Fatalf("expected deleting the single configured workspace name to result in an error, but got: %v", err)
    45  	}
    46  
    47  	if err := b.DeleteWorkspace("foo"); err != backend.ErrWorkspacesNotSupported {
    48  		t.Fatalf("expected deleting a workspace which is NOT the configured workspace name to result in an error, but got: %v", err)
    49  	}
    50  }
    51  
    52  func TestCloud_backendWithTags(t *testing.T) {
    53  	b, bCleanup := testBackendWithTags(t)
    54  	defer bCleanup()
    55  
    56  	backend.TestBackendStates(t, b)
    57  
    58  	// Test pagination works
    59  	for i := 0; i < 25; i++ {
    60  		_, err := b.StateMgr(fmt.Sprintf("foo-%d", i+1))
    61  		if err != nil {
    62  			t.Fatalf("error: %s", err)
    63  		}
    64  	}
    65  
    66  	workspaces, err := b.Workspaces()
    67  	if err != nil {
    68  		t.Fatalf("error: %s", err)
    69  	}
    70  	actual := len(workspaces)
    71  	if actual != 26 {
    72  		t.Errorf("expected 26 workspaces (over one standard paginated response), got %d", actual)
    73  	}
    74  }
    75  
    76  func TestCloud_PrepareConfig(t *testing.T) {
    77  	cases := map[string]struct {
    78  		config      cty.Value
    79  		expectedErr string
    80  	}{
    81  		"null organization": {
    82  			config: cty.ObjectVal(map[string]cty.Value{
    83  				"organization": cty.NullVal(cty.String),
    84  				"workspaces": cty.ObjectVal(map[string]cty.Value{
    85  					"name": cty.StringVal("prod"),
    86  					"tags": cty.NullVal(cty.Set(cty.String)),
    87  				}),
    88  			}),
    89  			expectedErr: `Invalid organization value: The "organization" attribute value must not be empty.`,
    90  		},
    91  		"null workspace": {
    92  			config: cty.ObjectVal(map[string]cty.Value{
    93  				"organization": cty.StringVal("org"),
    94  				"workspaces":   cty.NullVal(cty.String),
    95  			}),
    96  			expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`,
    97  		},
    98  		"workspace: empty tags, name": {
    99  			config: cty.ObjectVal(map[string]cty.Value{
   100  				"organization": cty.StringVal("org"),
   101  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   102  					"name": cty.NullVal(cty.String),
   103  					"tags": cty.NullVal(cty.Set(cty.String)),
   104  				}),
   105  			}),
   106  			expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`,
   107  		},
   108  		"workspace: name present": {
   109  			config: cty.ObjectVal(map[string]cty.Value{
   110  				"organization": cty.StringVal("org"),
   111  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   112  					"name": cty.StringVal("prod"),
   113  					"tags": cty.NullVal(cty.Set(cty.String)),
   114  				}),
   115  			}),
   116  			expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`,
   117  		},
   118  		"workspace: name and tags present": {
   119  			config: cty.ObjectVal(map[string]cty.Value{
   120  				"organization": cty.StringVal("org"),
   121  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   122  					"name": cty.StringVal("prod"),
   123  					"tags": cty.SetVal(
   124  						[]cty.Value{
   125  							cty.StringVal("billing"),
   126  						},
   127  					),
   128  				}),
   129  			}),
   130  			expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`,
   131  		},
   132  	}
   133  
   134  	for name, tc := range cases {
   135  		s := testServer(t)
   136  		b := New(testDisco(s))
   137  
   138  		// Validate
   139  		_, valDiags := b.PrepareConfig(tc.config)
   140  		if valDiags.Err() != nil && tc.expectedErr != "" {
   141  			actualErr := valDiags.Err().Error()
   142  			if !strings.Contains(actualErr, tc.expectedErr) {
   143  				t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
   144  			}
   145  		}
   146  	}
   147  }
   148  
   149  func TestCloud_config(t *testing.T) {
   150  	cases := map[string]struct {
   151  		config  cty.Value
   152  		confErr string
   153  		valErr  string
   154  	}{
   155  		"with_a_nonexisting_organization": {
   156  			config: cty.ObjectVal(map[string]cty.Value{
   157  				"hostname":     cty.NullVal(cty.String),
   158  				"organization": cty.StringVal("nonexisting"),
   159  				"token":        cty.NullVal(cty.String),
   160  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   161  					"name": cty.StringVal("prod"),
   162  					"tags": cty.NullVal(cty.Set(cty.String)),
   163  				}),
   164  			}),
   165  			confErr: "organization \"nonexisting\" at host app.terraform.io not found",
   166  		},
   167  		"with_an_unknown_host": {
   168  			config: cty.ObjectVal(map[string]cty.Value{
   169  				"hostname":     cty.StringVal("nonexisting.local"),
   170  				"organization": cty.StringVal("hashicorp"),
   171  				"token":        cty.NullVal(cty.String),
   172  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   173  					"name": cty.StringVal("prod"),
   174  					"tags": cty.NullVal(cty.Set(cty.String)),
   175  				}),
   176  			}),
   177  			confErr: "Failed to request discovery document",
   178  		},
   179  		// localhost advertises TFE services, but has no token in the credentials
   180  		"without_a_token": {
   181  			config: cty.ObjectVal(map[string]cty.Value{
   182  				"hostname":     cty.StringVal("localhost"),
   183  				"organization": cty.StringVal("hashicorp"),
   184  				"token":        cty.NullVal(cty.String),
   185  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   186  					"name": cty.StringVal("prod"),
   187  					"tags": cty.NullVal(cty.Set(cty.String)),
   188  				}),
   189  			}),
   190  			confErr: "terraform login localhost",
   191  		},
   192  		"with_tags": {
   193  			config: cty.ObjectVal(map[string]cty.Value{
   194  				"hostname":     cty.NullVal(cty.String),
   195  				"organization": cty.StringVal("hashicorp"),
   196  				"token":        cty.NullVal(cty.String),
   197  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   198  					"name": cty.NullVal(cty.String),
   199  					"tags": cty.SetVal(
   200  						[]cty.Value{
   201  							cty.StringVal("billing"),
   202  						},
   203  					),
   204  				}),
   205  			}),
   206  		},
   207  		"with_a_name": {
   208  			config: cty.ObjectVal(map[string]cty.Value{
   209  				"hostname":     cty.NullVal(cty.String),
   210  				"organization": cty.StringVal("hashicorp"),
   211  				"token":        cty.NullVal(cty.String),
   212  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   213  					"name": cty.StringVal("prod"),
   214  					"tags": cty.NullVal(cty.Set(cty.String)),
   215  				}),
   216  			}),
   217  		},
   218  		"without_a_name_tags": {
   219  			config: cty.ObjectVal(map[string]cty.Value{
   220  				"hostname":     cty.NullVal(cty.String),
   221  				"organization": cty.StringVal("hashicorp"),
   222  				"token":        cty.NullVal(cty.String),
   223  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   224  					"name": cty.NullVal(cty.String),
   225  					"tags": cty.NullVal(cty.Set(cty.String)),
   226  				}),
   227  			}),
   228  			valErr: `Missing workspace mapping strategy.`,
   229  		},
   230  		"with_both_a_name_and_tags": {
   231  			config: cty.ObjectVal(map[string]cty.Value{
   232  				"hostname":     cty.NullVal(cty.String),
   233  				"organization": cty.StringVal("hashicorp"),
   234  				"token":        cty.NullVal(cty.String),
   235  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   236  					"name": cty.StringVal("prod"),
   237  					"tags": cty.SetVal(
   238  						[]cty.Value{
   239  							cty.StringVal("billing"),
   240  						},
   241  					),
   242  				}),
   243  			}),
   244  			valErr: `Only one of workspace "tags" or "name" is allowed.`,
   245  		},
   246  		"null config": {
   247  			config: cty.NullVal(cty.EmptyObject),
   248  		},
   249  	}
   250  
   251  	for name, tc := range cases {
   252  		s := testServer(t)
   253  		b := New(testDisco(s))
   254  
   255  		// Validate
   256  		_, valDiags := b.PrepareConfig(tc.config)
   257  		if (valDiags.Err() != nil || tc.valErr != "") &&
   258  			(valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) {
   259  			t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
   260  		}
   261  
   262  		// Configure
   263  		confDiags := b.Configure(tc.config)
   264  		if (confDiags.Err() != nil || tc.confErr != "") &&
   265  			(confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) {
   266  			t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err())
   267  		}
   268  	}
   269  }
   270  
   271  func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) {
   272  	config := cty.ObjectVal(map[string]cty.Value{
   273  		"hostname":     cty.NullVal(cty.String),
   274  		"organization": cty.StringVal("hashicorp"),
   275  		"token":        cty.NullVal(cty.String),
   276  		"workspaces": cty.ObjectVal(map[string]cty.Value{
   277  			"name": cty.NullVal(cty.String),
   278  			"tags": cty.SetVal(
   279  				[]cty.Value{
   280  					cty.StringVal("billing"),
   281  				},
   282  			),
   283  		}),
   284  	})
   285  
   286  	handlers := map[string]func(http.ResponseWriter, *http.Request){
   287  		"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
   288  			w.Header().Set("Content-Type", "application/json")
   289  			w.Header().Set("TFP-API-Version", "2.4")
   290  		},
   291  	}
   292  	s := testServerWithHandlers(handlers)
   293  
   294  	b := New(testDisco(s))
   295  
   296  	confDiags := b.Configure(config)
   297  	if confDiags.Err() == nil {
   298  		t.Fatalf("expected configure to error")
   299  	}
   300  
   301  	expected := `The 'cloud' option is not supported with this version of Terraform Enterprise.`
   302  	if !strings.Contains(confDiags.Err().Error(), expected) {
   303  		t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error())
   304  	}
   305  }
   306  
   307  func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) {
   308  	config := cty.ObjectVal(map[string]cty.Value{
   309  		"hostname":     cty.NullVal(cty.String),
   310  		"organization": cty.StringVal("hashicorp"),
   311  		"token":        cty.NullVal(cty.String),
   312  		"workspaces": cty.ObjectVal(map[string]cty.Value{
   313  			"name": cty.NullVal(cty.String),
   314  			"tags": cty.SetVal(
   315  				[]cty.Value{
   316  					cty.StringVal("billing"),
   317  				},
   318  			),
   319  		}),
   320  	})
   321  
   322  	handlers := map[string]func(http.ResponseWriter, *http.Request){
   323  		"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
   324  			w.Header().Set("Content-Type", "application/json")
   325  			w.Header().Set("TFP-API-Version", "2.4")
   326  		},
   327  	}
   328  	s := testServerWithHandlers(handlers)
   329  
   330  	b := New(testDisco(s))
   331  	b.runningInAutomation = true
   332  
   333  	confDiags := b.Configure(config)
   334  	if confDiags.Err() == nil {
   335  		t.Fatalf("expected configure to error")
   336  	}
   337  
   338  	expected := `This version of Terraform Cloud/Enterprise does not support the state mechanism
   339  attempting to be used by the platform. This should never happen.`
   340  	if !strings.Contains(confDiags.Err().Error(), expected) {
   341  		t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error())
   342  	}
   343  }
   344  
   345  func TestCloud_setUnavailableTerraformVersion(t *testing.T) {
   346  	// go-tfe returns an error IRL if you try to set a Terraform version that's
   347  	// not available in your TFC instance. To test this, tfe_client_mock errors if
   348  	// you try to set any Terraform version for this specific workspace name.
   349  	workspaceName := "unavailable-terraform-version"
   350  
   351  	config := cty.ObjectVal(map[string]cty.Value{
   352  		"hostname":     cty.NullVal(cty.String),
   353  		"organization": cty.StringVal("hashicorp"),
   354  		"token":        cty.NullVal(cty.String),
   355  		"workspaces": cty.ObjectVal(map[string]cty.Value{
   356  			"name": cty.NullVal(cty.String),
   357  			"tags": cty.SetVal(
   358  				[]cty.Value{
   359  					cty.StringVal("sometag"),
   360  				},
   361  			),
   362  		}),
   363  	})
   364  
   365  	b, bCleanup := testBackend(t, config)
   366  	defer bCleanup()
   367  
   368  	// Make sure the workspace doesn't exist yet -- otherwise, we can't test what
   369  	// happens when a workspace gets created. This is why we can't use "name" in
   370  	// the backend config above, btw: if you do, testBackend() creates the default
   371  	// workspace before we get a chance to do anything.
   372  	_, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName)
   373  	if err != tfe.ErrResourceNotFound {
   374  		t.Fatalf("the workspace we were about to try and create (%s/%s) already exists in the mocks somehow, so this test isn't trustworthy anymore", b.organization, workspaceName)
   375  	}
   376  
   377  	_, err = b.StateMgr(workspaceName)
   378  	if err != nil {
   379  		t.Fatalf("expected no error from StateMgr, despite not being able to set remote Terraform version: %#v", err)
   380  	}
   381  	// Make sure the workspace was created:
   382  	workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName)
   383  	if err != nil {
   384  		t.Fatalf("b.StateMgr() didn't actually create the desired workspace")
   385  	}
   386  	// Make sure our mocks still error as expected, using the same update function b.StateMgr() would call:
   387  	_, err = b.client.Workspaces.UpdateByID(
   388  		context.Background(),
   389  		workspace.ID,
   390  		tfe.WorkspaceUpdateOptions{TerraformVersion: tfe.String("1.1.0")},
   391  	)
   392  	if err == nil {
   393  		t.Fatalf("the mocks aren't emulating a nonexistent remote Terraform version correctly, so this test isn't trustworthy anymore")
   394  	}
   395  }
   396  
   397  func TestCloud_setConfigurationFields(t *testing.T) {
   398  	originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND")
   399  
   400  	cases := map[string]struct {
   401  		obj                   cty.Value
   402  		expectedHostname      string
   403  		expectedOrganziation  string
   404  		expectedWorkspaceName string
   405  		expectedWorkspaceTags []string
   406  		expectedForceLocal    bool
   407  		setEnv                func()
   408  		resetEnv              func()
   409  		expectedErr           string
   410  	}{
   411  		"with hostname set": {
   412  			obj: cty.ObjectVal(map[string]cty.Value{
   413  				"organization": cty.StringVal("hashicorp"),
   414  				"hostname":     cty.StringVal("hashicorp.com"),
   415  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   416  					"name": cty.StringVal("prod"),
   417  					"tags": cty.NullVal(cty.Set(cty.String)),
   418  				}),
   419  			}),
   420  			expectedHostname:     "hashicorp.com",
   421  			expectedOrganziation: "hashicorp",
   422  		},
   423  		"with hostname not set, set to default hostname": {
   424  			obj: cty.ObjectVal(map[string]cty.Value{
   425  				"organization": cty.StringVal("hashicorp"),
   426  				"hostname":     cty.NullVal(cty.String),
   427  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   428  					"name": cty.StringVal("prod"),
   429  					"tags": cty.NullVal(cty.Set(cty.String)),
   430  				}),
   431  			}),
   432  			expectedHostname:     defaultHostname,
   433  			expectedOrganziation: "hashicorp",
   434  		},
   435  		"with workspace name set": {
   436  			obj: cty.ObjectVal(map[string]cty.Value{
   437  				"organization": cty.StringVal("hashicorp"),
   438  				"hostname":     cty.StringVal("hashicorp.com"),
   439  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   440  					"name": cty.StringVal("prod"),
   441  					"tags": cty.NullVal(cty.Set(cty.String)),
   442  				}),
   443  			}),
   444  			expectedHostname:      "hashicorp.com",
   445  			expectedOrganziation:  "hashicorp",
   446  			expectedWorkspaceName: "prod",
   447  		},
   448  		"with workspace tags set": {
   449  			obj: cty.ObjectVal(map[string]cty.Value{
   450  				"organization": cty.StringVal("hashicorp"),
   451  				"hostname":     cty.StringVal("hashicorp.com"),
   452  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   453  					"name": cty.NullVal(cty.String),
   454  					"tags": cty.SetVal(
   455  						[]cty.Value{
   456  							cty.StringVal("billing"),
   457  						},
   458  					),
   459  				}),
   460  			}),
   461  			expectedHostname:      "hashicorp.com",
   462  			expectedOrganziation:  "hashicorp",
   463  			expectedWorkspaceTags: []string{"billing"},
   464  		},
   465  		"with force local set": {
   466  			obj: cty.ObjectVal(map[string]cty.Value{
   467  				"organization": cty.StringVal("hashicorp"),
   468  				"hostname":     cty.StringVal("hashicorp.com"),
   469  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   470  					"name": cty.NullVal(cty.String),
   471  					"tags": cty.NullVal(cty.Set(cty.String)),
   472  				}),
   473  			}),
   474  			expectedHostname:     "hashicorp.com",
   475  			expectedOrganziation: "hashicorp",
   476  			setEnv: func() {
   477  				os.Setenv("TF_FORCE_LOCAL_BACKEND", "1")
   478  			},
   479  			resetEnv: func() {
   480  				os.Setenv("TF_FORCE_LOCAL_BACKEND", originalForceBackendEnv)
   481  			},
   482  			expectedForceLocal: true,
   483  		},
   484  	}
   485  
   486  	for name, tc := range cases {
   487  		b := &Cloud{}
   488  
   489  		// if `setEnv` is set, then we expect `resetEnv` to also be set
   490  		if tc.setEnv != nil {
   491  			tc.setEnv()
   492  			defer tc.resetEnv()
   493  		}
   494  
   495  		errDiags := b.setConfigurationFields(tc.obj)
   496  		if errDiags.HasErrors() || tc.expectedErr != "" {
   497  			actualErr := errDiags.Err().Error()
   498  			if !strings.Contains(actualErr, tc.expectedErr) {
   499  				t.Fatalf("%s: unexpected validation result: %v", name, errDiags.Err())
   500  			}
   501  		}
   502  
   503  		if tc.expectedHostname != "" && b.hostname != tc.expectedHostname {
   504  			t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname)
   505  		}
   506  		if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation {
   507  			t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganziation)
   508  		}
   509  		if tc.expectedWorkspaceName != "" && b.WorkspaceMapping.Name != tc.expectedWorkspaceName {
   510  			t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName)
   511  		}
   512  		if len(tc.expectedWorkspaceTags) > 0 {
   513  			presentSet := make(map[string]struct{})
   514  			for _, tag := range b.WorkspaceMapping.Tags {
   515  				presentSet[tag] = struct{}{}
   516  			}
   517  
   518  			expectedSet := make(map[string]struct{})
   519  			for _, tag := range tc.expectedWorkspaceTags {
   520  				expectedSet[tag] = struct{}{}
   521  			}
   522  
   523  			var missing []string
   524  			var unexpected []string
   525  
   526  			for _, expected := range tc.expectedWorkspaceTags {
   527  				if _, ok := presentSet[expected]; !ok {
   528  					missing = append(missing, expected)
   529  				}
   530  			}
   531  
   532  			for _, actual := range b.WorkspaceMapping.Tags {
   533  				if _, ok := expectedSet[actual]; !ok {
   534  					unexpected = append(missing, actual)
   535  				}
   536  			}
   537  
   538  			if len(missing) > 0 {
   539  				t.Fatalf("%s: expected workspace tag mapping (%s) to contain the following tags: %s", name, b.WorkspaceMapping.Tags, missing)
   540  			}
   541  
   542  			if len(unexpected) > 0 {
   543  				t.Fatalf("%s: expected workspace tag mapping (%s) to NOT contain the following tags: %s", name, b.WorkspaceMapping.Tags, unexpected)
   544  			}
   545  
   546  		}
   547  		if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal {
   548  			t.Fatalf("%s: expected force local backend to be set ", name)
   549  		}
   550  	}
   551  }
   552  
   553  func TestCloud_localBackend(t *testing.T) {
   554  	b, bCleanup := testBackendWithName(t)
   555  	defer bCleanup()
   556  
   557  	local, ok := b.local.(*backendLocal.Local)
   558  	if !ok {
   559  		t.Fatalf("expected b.local to be \"*local.Local\", got: %T", b.local)
   560  	}
   561  
   562  	cloud, ok := local.Backend.(*Cloud)
   563  	if !ok {
   564  		t.Fatalf("expected local.Backend to be *cloud.Cloud, got: %T", cloud)
   565  	}
   566  }
   567  
   568  func TestCloud_addAndRemoveWorkspacesDefault(t *testing.T) {
   569  	b, bCleanup := testBackendWithName(t)
   570  	defer bCleanup()
   571  
   572  	if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil {
   573  		t.Fatalf("expected no error, got %v", err)
   574  	}
   575  
   576  	if err := b.DeleteWorkspace(testBackendSingleWorkspaceName); err != backend.ErrWorkspacesNotSupported {
   577  		t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err)
   578  	}
   579  }
   580  
   581  func TestCloud_StateMgr_versionCheck(t *testing.T) {
   582  	b, bCleanup := testBackendWithName(t)
   583  	defer bCleanup()
   584  
   585  	// Some fixed versions for testing with. This logic is a simple string
   586  	// comparison, so we don't need many test cases.
   587  	v0135 := version.Must(version.NewSemver("0.13.5"))
   588  	v0140 := version.Must(version.NewSemver("0.14.0"))
   589  
   590  	// Save original local version state and restore afterwards
   591  	p := tfversion.Prerelease
   592  	v := tfversion.Version
   593  	s := tfversion.SemVer
   594  	defer func() {
   595  		tfversion.Prerelease = p
   596  		tfversion.Version = v
   597  		tfversion.SemVer = s
   598  	}()
   599  
   600  	// For this test, the local Terraform version is set to 0.14.0
   601  	tfversion.Prerelease = ""
   602  	tfversion.Version = v0140.String()
   603  	tfversion.SemVer = v0140
   604  
   605  	// Update the mock remote workspace Terraform version to match the local
   606  	// Terraform version
   607  	if _, err := b.client.Workspaces.Update(
   608  		context.Background(),
   609  		b.organization,
   610  		b.WorkspaceMapping.Name,
   611  		tfe.WorkspaceUpdateOptions{
   612  			TerraformVersion: tfe.String(v0140.String()),
   613  		},
   614  	); err != nil {
   615  		t.Fatalf("error: %v", err)
   616  	}
   617  
   618  	// This should succeed
   619  	if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil {
   620  		t.Fatalf("expected no error, got %v", err)
   621  	}
   622  
   623  	// Now change the remote workspace to a different Terraform version
   624  	if _, err := b.client.Workspaces.Update(
   625  		context.Background(),
   626  		b.organization,
   627  		b.WorkspaceMapping.Name,
   628  		tfe.WorkspaceUpdateOptions{
   629  			TerraformVersion: tfe.String(v0135.String()),
   630  		},
   631  	); err != nil {
   632  		t.Fatalf("error: %v", err)
   633  	}
   634  
   635  	// This should fail
   636  	want := `Remote workspace Terraform version "0.13.5" does not match local Terraform version "0.14.0"`
   637  	if _, err := b.StateMgr(testBackendSingleWorkspaceName); err.Error() != want {
   638  		t.Fatalf("wrong error\n got: %v\nwant: %v", err.Error(), want)
   639  	}
   640  }
   641  
   642  func TestCloud_StateMgr_versionCheckLatest(t *testing.T) {
   643  	b, bCleanup := testBackendWithName(t)
   644  	defer bCleanup()
   645  
   646  	v0140 := version.Must(version.NewSemver("0.14.0"))
   647  
   648  	// Save original local version state and restore afterwards
   649  	p := tfversion.Prerelease
   650  	v := tfversion.Version
   651  	s := tfversion.SemVer
   652  	defer func() {
   653  		tfversion.Prerelease = p
   654  		tfversion.Version = v
   655  		tfversion.SemVer = s
   656  	}()
   657  
   658  	// For this test, the local Terraform version is set to 0.14.0
   659  	tfversion.Prerelease = ""
   660  	tfversion.Version = v0140.String()
   661  	tfversion.SemVer = v0140
   662  
   663  	// Update the remote workspace to the pseudo-version "latest"
   664  	if _, err := b.client.Workspaces.Update(
   665  		context.Background(),
   666  		b.organization,
   667  		b.WorkspaceMapping.Name,
   668  		tfe.WorkspaceUpdateOptions{
   669  			TerraformVersion: tfe.String("latest"),
   670  		},
   671  	); err != nil {
   672  		t.Fatalf("error: %v", err)
   673  	}
   674  
   675  	// This should succeed despite not being a string match
   676  	if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil {
   677  		t.Fatalf("expected no error, got %v", err)
   678  	}
   679  }
   680  
   681  func TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) {
   682  	testCases := []struct {
   683  		local         string
   684  		remote        string
   685  		executionMode string
   686  		wantErr       bool
   687  	}{
   688  		{"0.13.5", "0.13.5", "remote", false},
   689  		{"0.14.0", "0.13.5", "remote", true},
   690  		{"0.14.0", "0.13.5", "local", false},
   691  		{"0.14.0", "0.14.1", "remote", false},
   692  		{"0.14.0", "1.0.99", "remote", false},
   693  		{"0.14.0", "1.1.0", "remote", false},
   694  		{"0.14.0", "1.2.0", "remote", true},
   695  		{"1.2.0", "1.2.99", "remote", false},
   696  		{"1.2.0", "1.3.0", "remote", true},
   697  		{"0.15.0", "latest", "remote", false},
   698  		{"1.1.5", "~> 1.1.1", "remote", false},
   699  		{"1.1.5", "> 1.1.0, < 1.3.0", "remote", false},
   700  		{"1.1.5", "~> 1.0.1", "remote", true},
   701  		// pre-release versions are comparable within their pre-release stage (dev,
   702  		// alpha, beta), but not comparable to different stages and not comparable
   703  		// to final releases.
   704  		{"1.1.0-beta1", "1.1.0-beta1", "remote", false},
   705  		{"1.1.0-beta1", "~> 1.1.0-beta", "remote", false},
   706  		{"1.1.0", "~> 1.1.0-beta", "remote", true},
   707  		{"1.1.0-beta1", "~> 1.1.0-dev", "remote", true},
   708  	}
   709  	for _, tc := range testCases {
   710  		t.Run(fmt.Sprintf("local %s, remote %s", tc.local, tc.remote), func(t *testing.T) {
   711  			b, bCleanup := testBackendWithName(t)
   712  			defer bCleanup()
   713  
   714  			local := version.Must(version.NewSemver(tc.local))
   715  
   716  			// Save original local version state and restore afterwards
   717  			p := tfversion.Prerelease
   718  			v := tfversion.Version
   719  			s := tfversion.SemVer
   720  			defer func() {
   721  				tfversion.Prerelease = p
   722  				tfversion.Version = v
   723  				tfversion.SemVer = s
   724  			}()
   725  
   726  			// Override local version as specified
   727  			tfversion.Prerelease = ""
   728  			tfversion.Version = local.String()
   729  			tfversion.SemVer = local
   730  
   731  			// Update the mock remote workspace Terraform version to the
   732  			// specified remote version
   733  			if _, err := b.client.Workspaces.Update(
   734  				context.Background(),
   735  				b.organization,
   736  				b.WorkspaceMapping.Name,
   737  				tfe.WorkspaceUpdateOptions{
   738  					ExecutionMode:    &tc.executionMode,
   739  					TerraformVersion: tfe.String(tc.remote),
   740  				},
   741  			); err != nil {
   742  				t.Fatalf("error: %v", err)
   743  			}
   744  
   745  			diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName)
   746  			if tc.wantErr {
   747  				if len(diags) != 1 {
   748  					t.Fatal("expected diag, but none returned")
   749  				}
   750  				if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version") {
   751  					t.Fatalf("unexpected error: %s", got)
   752  				}
   753  			} else {
   754  				if len(diags) != 0 {
   755  					t.Fatalf("unexpected diags: %s", diags.Err())
   756  				}
   757  			}
   758  		})
   759  	}
   760  }
   761  
   762  func TestCloud_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) {
   763  	b, bCleanup := testBackendWithName(t)
   764  	defer bCleanup()
   765  
   766  	// Attempting to check the version against a workspace which doesn't exist
   767  	// should result in no errors
   768  	diags := b.VerifyWorkspaceTerraformVersion("invalid-workspace")
   769  	if len(diags) != 0 {
   770  		t.Fatalf("unexpected error: %s", diags.Err())
   771  	}
   772  
   773  	// Use a special workspace ID to trigger a 500 error, which should result
   774  	// in a failed check
   775  	diags = b.VerifyWorkspaceTerraformVersion("network-error")
   776  	if len(diags) != 1 {
   777  		t.Fatal("expected diag, but none returned")
   778  	}
   779  	if got := diags.Err().Error(); !strings.Contains(got, "Error looking up workspace: Workspace read failed") {
   780  		t.Fatalf("unexpected error: %s", got)
   781  	}
   782  
   783  	// Update the mock remote workspace Terraform version to an invalid version
   784  	if _, err := b.client.Workspaces.Update(
   785  		context.Background(),
   786  		b.organization,
   787  		b.WorkspaceMapping.Name,
   788  		tfe.WorkspaceUpdateOptions{
   789  			TerraformVersion: tfe.String("1.0.cheetarah"),
   790  		},
   791  	); err != nil {
   792  		t.Fatalf("error: %v", err)
   793  	}
   794  	diags = b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName)
   795  
   796  	if len(diags) != 1 {
   797  		t.Fatal("expected diag, but none returned")
   798  	}
   799  	if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version: The remote workspace specified") {
   800  		t.Fatalf("unexpected error: %s", got)
   801  	}
   802  }
   803  
   804  func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) {
   805  	b, bCleanup := testBackendWithName(t)
   806  	defer bCleanup()
   807  
   808  	// If the ignore flag is set, the behaviour changes
   809  	b.IgnoreVersionConflict()
   810  
   811  	// Different local & remote versions to cause an error
   812  	local := version.Must(version.NewSemver("0.14.0"))
   813  	remote := version.Must(version.NewSemver("0.13.5"))
   814  
   815  	// Save original local version state and restore afterwards
   816  	p := tfversion.Prerelease
   817  	v := tfversion.Version
   818  	s := tfversion.SemVer
   819  	defer func() {
   820  		tfversion.Prerelease = p
   821  		tfversion.Version = v
   822  		tfversion.SemVer = s
   823  	}()
   824  
   825  	// Override local version as specified
   826  	tfversion.Prerelease = ""
   827  	tfversion.Version = local.String()
   828  	tfversion.SemVer = local
   829  
   830  	// Update the mock remote workspace Terraform version to the
   831  	// specified remote version
   832  	if _, err := b.client.Workspaces.Update(
   833  		context.Background(),
   834  		b.organization,
   835  		b.WorkspaceMapping.Name,
   836  		tfe.WorkspaceUpdateOptions{
   837  			TerraformVersion: tfe.String(remote.String()),
   838  		},
   839  	); err != nil {
   840  		t.Fatalf("error: %v", err)
   841  	}
   842  
   843  	diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName)
   844  	if len(diags) != 1 {
   845  		t.Fatal("expected diag, but none returned")
   846  	}
   847  
   848  	if got, want := diags[0].Severity(), tfdiags.Warning; got != want {
   849  		t.Errorf("wrong severity: got %#v, want %#v", got, want)
   850  	}
   851  	if got, want := diags[0].Description().Summary, "Incompatible Terraform version"; got != want {
   852  		t.Errorf("wrong summary: got %s, want %s", got, want)
   853  	}
   854  	wantDetail := "The local Terraform version (0.14.0) does not meet the version requirements for remote workspace hashicorp/app-prod (0.13.5)."
   855  	if got := diags[0].Description().Detail; got != wantDetail {
   856  		t.Errorf("wrong summary: got %s, want %s", got, wantDetail)
   857  	}
   858  }