github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/hashicorp/terraform/internal/backend"
    14  	"github.com/hashicorp/terraform/internal/tfdiags"
    15  	tfversion "github.com/hashicorp/terraform/version"
    16  	"github.com/zclconf/go-cty/cty"
    17  
    18  	backendLocal "github.com/hashicorp/terraform/internal/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, true); 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", true); 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 or missing required argument: "organization" must be set in the cloud configuration or as an environment variable: TF_CLOUD_ORGANIZATION.`,
    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_PrepareConfigWithEnvVars(t *testing.T) {
   150  	cases := map[string]struct {
   151  		config      cty.Value
   152  		vars        map[string]string
   153  		expectedErr string
   154  	}{
   155  		"with no organization": {
   156  			config: cty.ObjectVal(map[string]cty.Value{
   157  				"organization": cty.NullVal(cty.String),
   158  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   159  					"name": cty.StringVal("prod"),
   160  					"tags": cty.NullVal(cty.Set(cty.String)),
   161  				}),
   162  			}),
   163  			vars: map[string]string{
   164  				"TF_CLOUD_ORGANIZATION": "example-org",
   165  			},
   166  		},
   167  		"with no organization attribute or env var": {
   168  			config: cty.ObjectVal(map[string]cty.Value{
   169  				"organization": cty.NullVal(cty.String),
   170  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   171  					"name": cty.StringVal("prod"),
   172  					"tags": cty.NullVal(cty.Set(cty.String)),
   173  				}),
   174  			}),
   175  			vars:        map[string]string{},
   176  			expectedErr: `Invalid or missing required argument: "organization" must be set in the cloud configuration or as an environment variable: TF_CLOUD_ORGANIZATION.`,
   177  		},
   178  		"null workspace": {
   179  			config: cty.ObjectVal(map[string]cty.Value{
   180  				"organization": cty.StringVal("hashicorp"),
   181  				"workspaces":   cty.NullVal(cty.String),
   182  			}),
   183  			vars: map[string]string{
   184  				"TF_WORKSPACE": "my-workspace",
   185  			},
   186  		},
   187  		"organization and workspace env var": {
   188  			config: cty.ObjectVal(map[string]cty.Value{
   189  				"organization": cty.NullVal(cty.String),
   190  				"workspaces":   cty.NullVal(cty.String),
   191  			}),
   192  			vars: map[string]string{
   193  				"TF_CLOUD_ORGANIZATION": "hashicorp",
   194  				"TF_WORKSPACE":          "my-workspace",
   195  			},
   196  		},
   197  	}
   198  
   199  	for name, tc := range cases {
   200  		t.Run(name, func(t *testing.T) {
   201  			s := testServer(t)
   202  			b := New(testDisco(s))
   203  
   204  			for k, v := range tc.vars {
   205  				os.Setenv(k, v)
   206  			}
   207  			t.Cleanup(func() {
   208  				for k := range tc.vars {
   209  					os.Unsetenv(k)
   210  				}
   211  			})
   212  
   213  			_, valDiags := b.PrepareConfig(tc.config)
   214  			if valDiags.Err() != nil && tc.expectedErr != "" {
   215  				actualErr := valDiags.Err().Error()
   216  				if !strings.Contains(actualErr, tc.expectedErr) {
   217  					t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
   218  				}
   219  			}
   220  		})
   221  	}
   222  }
   223  
   224  func TestCloud_configWithEnvVars(t *testing.T) {
   225  	cases := map[string]struct {
   226  		setup                 func(b *Cloud)
   227  		config                cty.Value
   228  		vars                  map[string]string
   229  		expectedOrganization  string
   230  		expectedHostname      string
   231  		expectedWorkspaceName string
   232  		expectedErr           string
   233  	}{
   234  		"with no organization specified": {
   235  			config: cty.ObjectVal(map[string]cty.Value{
   236  				"hostname":     cty.NullVal(cty.String),
   237  				"token":        cty.NullVal(cty.String),
   238  				"organization": cty.NullVal(cty.String),
   239  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   240  					"name": cty.StringVal("prod"),
   241  					"tags": cty.NullVal(cty.Set(cty.String)),
   242  				}),
   243  			}),
   244  			vars: map[string]string{
   245  				"TF_CLOUD_ORGANIZATION": "hashicorp",
   246  			},
   247  			expectedOrganization: "hashicorp",
   248  		},
   249  		"with both organization and env var specified": {
   250  			config: cty.ObjectVal(map[string]cty.Value{
   251  				"hostname":     cty.NullVal(cty.String),
   252  				"token":        cty.NullVal(cty.String),
   253  				"organization": cty.StringVal("hashicorp"),
   254  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   255  					"name": cty.StringVal("prod"),
   256  					"tags": cty.NullVal(cty.Set(cty.String)),
   257  				}),
   258  			}),
   259  			vars: map[string]string{
   260  				"TF_CLOUD_ORGANIZATION": "we-should-not-see-this",
   261  			},
   262  			expectedOrganization: "hashicorp",
   263  		},
   264  		"with no hostname specified": {
   265  			config: cty.ObjectVal(map[string]cty.Value{
   266  				"hostname":     cty.NullVal(cty.String),
   267  				"token":        cty.NullVal(cty.String),
   268  				"organization": cty.StringVal("hashicorp"),
   269  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   270  					"name": cty.StringVal("prod"),
   271  					"tags": cty.NullVal(cty.Set(cty.String)),
   272  				}),
   273  			}),
   274  			vars: map[string]string{
   275  				"TF_CLOUD_HOSTNAME": "private.hashicorp.engineering",
   276  			},
   277  			expectedHostname: "private.hashicorp.engineering",
   278  		},
   279  		"with hostname and env var specified": {
   280  			config: cty.ObjectVal(map[string]cty.Value{
   281  				"hostname":     cty.StringVal("private.hashicorp.engineering"),
   282  				"token":        cty.NullVal(cty.String),
   283  				"organization": cty.StringVal("hashicorp"),
   284  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   285  					"name": cty.StringVal("prod"),
   286  					"tags": cty.NullVal(cty.Set(cty.String)),
   287  				}),
   288  			}),
   289  			vars: map[string]string{
   290  				"TF_CLOUD_HOSTNAME": "mycool.tfe-host.io",
   291  			},
   292  			expectedHostname: "private.hashicorp.engineering",
   293  		},
   294  		"an invalid workspace env var": {
   295  			config: cty.ObjectVal(map[string]cty.Value{
   296  				"hostname":     cty.NullVal(cty.String),
   297  				"token":        cty.NullVal(cty.String),
   298  				"organization": cty.StringVal("hashicorp"),
   299  				"workspaces": cty.NullVal(cty.Object(map[string]cty.Type{
   300  					"name": cty.String,
   301  					"tags": cty.Set(cty.String),
   302  				})),
   303  			}),
   304  			vars: map[string]string{
   305  				"TF_WORKSPACE": "i-dont-exist-in-org",
   306  			},
   307  			expectedErr: `Invalid workspace selection: Terraform failed to find workspace "i-dont-exist-in-org" in organization hashicorp`,
   308  		},
   309  		"workspaces and env var specified": {
   310  			config: cty.ObjectVal(map[string]cty.Value{
   311  				"hostname":     cty.NullVal(cty.String),
   312  				"token":        cty.NullVal(cty.String),
   313  				"organization": cty.StringVal("mordor"),
   314  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   315  					"name": cty.StringVal("mt-doom"),
   316  					"tags": cty.NullVal(cty.Set(cty.String)),
   317  				}),
   318  			}),
   319  			vars: map[string]string{
   320  				"TF_WORKSPACE": "shire",
   321  			},
   322  			expectedWorkspaceName: "mt-doom",
   323  		},
   324  		"env var workspace does not have specified tag": {
   325  			setup: func(b *Cloud) {
   326  				b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{
   327  					Name: tfe.String("mordor"),
   328  				})
   329  
   330  				b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{
   331  					Name: tfe.String("shire"),
   332  				})
   333  			},
   334  			config: cty.ObjectVal(map[string]cty.Value{
   335  				"hostname":     cty.NullVal(cty.String),
   336  				"token":        cty.NullVal(cty.String),
   337  				"organization": cty.StringVal("mordor"),
   338  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   339  					"name": cty.NullVal(cty.String),
   340  					"tags": cty.SetVal([]cty.Value{
   341  						cty.StringVal("cloud"),
   342  					}),
   343  				}),
   344  			}),
   345  			vars: map[string]string{
   346  				"TF_WORKSPACE": "shire",
   347  			},
   348  			expectedErr: "Terraform failed to find workspace \"shire\" with the tags specified in your configuration:\n[cloud]",
   349  		},
   350  		"env var workspace has specified tag": {
   351  			setup: func(b *Cloud) {
   352  				b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{
   353  					Name: tfe.String("mordor"),
   354  				})
   355  
   356  				b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{
   357  					Name: tfe.String("shire"),
   358  					Tags: []*tfe.Tag{
   359  						{
   360  							Name: "hobbity",
   361  						},
   362  					},
   363  				})
   364  			},
   365  			config: cty.ObjectVal(map[string]cty.Value{
   366  				"hostname":     cty.NullVal(cty.String),
   367  				"token":        cty.NullVal(cty.String),
   368  				"organization": cty.StringVal("mordor"),
   369  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   370  					"name": cty.NullVal(cty.String),
   371  					"tags": cty.SetVal([]cty.Value{
   372  						cty.StringVal("hobbity"),
   373  					}),
   374  				}),
   375  			}),
   376  			vars: map[string]string{
   377  				"TF_WORKSPACE": "shire",
   378  			},
   379  			expectedWorkspaceName: "", // No error is raised, but workspace is not set
   380  		},
   381  		"with everything set as env vars": {
   382  			config: cty.ObjectVal(map[string]cty.Value{
   383  				"hostname":     cty.NullVal(cty.String),
   384  				"token":        cty.NullVal(cty.String),
   385  				"organization": cty.NullVal(cty.String),
   386  				"workspaces":   cty.NullVal(cty.String),
   387  			}),
   388  			vars: map[string]string{
   389  				"TF_CLOUD_ORGANIZATION": "mordor",
   390  				"TF_WORKSPACE":          "mt-doom",
   391  				"TF_CLOUD_HOSTNAME":     "mycool.tfe-host.io",
   392  			},
   393  			expectedOrganization:  "mordor",
   394  			expectedWorkspaceName: "mt-doom",
   395  			expectedHostname:      "mycool.tfe-host.io",
   396  		},
   397  	}
   398  
   399  	for name, tc := range cases {
   400  		t.Run(name, func(t *testing.T) {
   401  			b, cleanup := testUnconfiguredBackend(t)
   402  			t.Cleanup(cleanup)
   403  
   404  			for k, v := range tc.vars {
   405  				os.Setenv(k, v)
   406  			}
   407  
   408  			t.Cleanup(func() {
   409  				for k := range tc.vars {
   410  					os.Unsetenv(k)
   411  				}
   412  			})
   413  
   414  			_, valDiags := b.PrepareConfig(tc.config)
   415  			if valDiags.Err() != nil {
   416  				t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
   417  			}
   418  
   419  			if tc.setup != nil {
   420  				tc.setup(b)
   421  			}
   422  
   423  			diags := b.Configure(tc.config)
   424  			if (diags.Err() != nil || tc.expectedErr != "") &&
   425  				(diags.Err() == nil || !strings.Contains(diags.Err().Error(), tc.expectedErr)) {
   426  				t.Fatalf("%s: unexpected configure result: %v", name, diags.Err())
   427  			}
   428  
   429  			if tc.expectedOrganization != "" && tc.expectedOrganization != b.organization {
   430  				t.Fatalf("%s: organization not valid: %s, expected: %s", name, b.organization, tc.expectedOrganization)
   431  			}
   432  
   433  			if tc.expectedHostname != "" && tc.expectedHostname != b.hostname {
   434  				t.Fatalf("%s: hostname not valid: %s, expected: %s", name, b.hostname, tc.expectedHostname)
   435  			}
   436  
   437  			if tc.expectedWorkspaceName != "" && tc.expectedWorkspaceName != b.WorkspaceMapping.Name {
   438  				t.Fatalf("%s: workspace name not valid: %s, expected: %s", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName)
   439  			}
   440  		})
   441  	}
   442  }
   443  
   444  func TestCloud_config(t *testing.T) {
   445  	cases := map[string]struct {
   446  		config  cty.Value
   447  		confErr string
   448  		valErr  string
   449  	}{
   450  		"with_a_non_tfe_host": {
   451  			config: cty.ObjectVal(map[string]cty.Value{
   452  				"hostname":     cty.StringVal("nontfe.local"),
   453  				"organization": cty.StringVal("hashicorp"),
   454  				"token":        cty.NullVal(cty.String),
   455  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   456  					"name": cty.StringVal("prod"),
   457  					"tags": cty.NullVal(cty.Set(cty.String)),
   458  				}),
   459  			}),
   460  			confErr: "Host nontfe.local does not provide a tfe service",
   461  		},
   462  		// localhost advertises TFE services, but has no token in the credentials
   463  		"without_a_token": {
   464  			config: cty.ObjectVal(map[string]cty.Value{
   465  				"hostname":     cty.StringVal("localhost"),
   466  				"organization": cty.StringVal("hashicorp"),
   467  				"token":        cty.NullVal(cty.String),
   468  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   469  					"name": cty.StringVal("prod"),
   470  					"tags": cty.NullVal(cty.Set(cty.String)),
   471  				}),
   472  			}),
   473  			confErr: "terraform login localhost",
   474  		},
   475  		"with_tags": {
   476  			config: cty.ObjectVal(map[string]cty.Value{
   477  				"hostname":     cty.NullVal(cty.String),
   478  				"organization": cty.StringVal("hashicorp"),
   479  				"token":        cty.NullVal(cty.String),
   480  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   481  					"name": cty.NullVal(cty.String),
   482  					"tags": cty.SetVal(
   483  						[]cty.Value{
   484  							cty.StringVal("billing"),
   485  						},
   486  					),
   487  				}),
   488  			}),
   489  		},
   490  		"with_a_name": {
   491  			config: cty.ObjectVal(map[string]cty.Value{
   492  				"hostname":     cty.NullVal(cty.String),
   493  				"organization": cty.StringVal("hashicorp"),
   494  				"token":        cty.NullVal(cty.String),
   495  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   496  					"name": cty.StringVal("prod"),
   497  					"tags": cty.NullVal(cty.Set(cty.String)),
   498  				}),
   499  			}),
   500  		},
   501  		"without_a_name_tags": {
   502  			config: cty.ObjectVal(map[string]cty.Value{
   503  				"hostname":     cty.NullVal(cty.String),
   504  				"organization": cty.StringVal("hashicorp"),
   505  				"token":        cty.NullVal(cty.String),
   506  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   507  					"name": cty.NullVal(cty.String),
   508  					"tags": cty.NullVal(cty.Set(cty.String)),
   509  				}),
   510  			}),
   511  			valErr: `Missing workspace mapping strategy.`,
   512  		},
   513  		"with_both_a_name_and_tags": {
   514  			config: cty.ObjectVal(map[string]cty.Value{
   515  				"hostname":     cty.NullVal(cty.String),
   516  				"organization": cty.StringVal("hashicorp"),
   517  				"token":        cty.NullVal(cty.String),
   518  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   519  					"name": cty.StringVal("prod"),
   520  					"tags": cty.SetVal(
   521  						[]cty.Value{
   522  							cty.StringVal("billing"),
   523  						},
   524  					),
   525  				}),
   526  			}),
   527  			valErr: `Only one of workspace "tags" or "name" is allowed.`,
   528  		},
   529  		"null config": {
   530  			config: cty.NullVal(cty.EmptyObject),
   531  		},
   532  	}
   533  
   534  	for name, tc := range cases {
   535  		t.Run(name, func(t *testing.T) {
   536  			b, cleanup := testUnconfiguredBackend(t)
   537  			t.Cleanup(cleanup)
   538  
   539  			// Validate
   540  			_, valDiags := b.PrepareConfig(tc.config)
   541  			if (valDiags.Err() != nil || tc.valErr != "") &&
   542  				(valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) {
   543  				t.Fatalf("unexpected validation result: %v", valDiags.Err())
   544  			}
   545  
   546  			// Configure
   547  			confDiags := b.Configure(tc.config)
   548  			if (confDiags.Err() != nil || tc.confErr != "") &&
   549  				(confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) {
   550  				t.Fatalf("unexpected configure result: %v", confDiags.Err())
   551  			}
   552  		})
   553  	}
   554  }
   555  
   556  func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) {
   557  	config := cty.ObjectVal(map[string]cty.Value{
   558  		"hostname":     cty.NullVal(cty.String),
   559  		"organization": cty.StringVal("hashicorp"),
   560  		"token":        cty.NullVal(cty.String),
   561  		"workspaces": cty.ObjectVal(map[string]cty.Value{
   562  			"name": cty.NullVal(cty.String),
   563  			"tags": cty.SetVal(
   564  				[]cty.Value{
   565  					cty.StringVal("billing"),
   566  				},
   567  			),
   568  		}),
   569  	})
   570  
   571  	handlers := map[string]func(http.ResponseWriter, *http.Request){
   572  		"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
   573  			w.Header().Set("Content-Type", "application/json")
   574  			w.Header().Set("TFP-API-Version", "2.4")
   575  		},
   576  	}
   577  	s := testServerWithHandlers(handlers)
   578  
   579  	b := New(testDisco(s))
   580  
   581  	confDiags := b.Configure(config)
   582  	if confDiags.Err() == nil {
   583  		t.Fatalf("expected configure to error")
   584  	}
   585  
   586  	expected := `The 'cloud' option is not supported with this version of Terraform Enterprise.`
   587  	if !strings.Contains(confDiags.Err().Error(), expected) {
   588  		t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error())
   589  	}
   590  }
   591  
   592  func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) {
   593  	config := cty.ObjectVal(map[string]cty.Value{
   594  		"hostname":     cty.NullVal(cty.String),
   595  		"organization": cty.StringVal("hashicorp"),
   596  		"token":        cty.NullVal(cty.String),
   597  		"workspaces": cty.ObjectVal(map[string]cty.Value{
   598  			"name": cty.NullVal(cty.String),
   599  			"tags": cty.SetVal(
   600  				[]cty.Value{
   601  					cty.StringVal("billing"),
   602  				},
   603  			),
   604  		}),
   605  	})
   606  
   607  	handlers := map[string]func(http.ResponseWriter, *http.Request){
   608  		"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
   609  			w.Header().Set("Content-Type", "application/json")
   610  			w.Header().Set("TFP-API-Version", "2.4")
   611  		},
   612  	}
   613  	s := testServerWithHandlers(handlers)
   614  
   615  	b := New(testDisco(s))
   616  	b.runningInAutomation = true
   617  
   618  	confDiags := b.Configure(config)
   619  	if confDiags.Err() == nil {
   620  		t.Fatalf("expected configure to error")
   621  	}
   622  
   623  	expected := `This version of Terraform Cloud/Enterprise does not support the state mechanism
   624  attempting to be used by the platform. This should never happen.`
   625  	if !strings.Contains(confDiags.Err().Error(), expected) {
   626  		t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error())
   627  	}
   628  }
   629  
   630  func TestCloud_setUnavailableTerraformVersion(t *testing.T) {
   631  	// go-tfe returns an error IRL if you try to set a Terraform version that's
   632  	// not available in your TFC instance. To test this, tfe_client_mock errors if
   633  	// you try to set any Terraform version for this specific workspace name.
   634  	workspaceName := "unavailable-terraform-version"
   635  
   636  	config := cty.ObjectVal(map[string]cty.Value{
   637  		"hostname":     cty.NullVal(cty.String),
   638  		"organization": cty.StringVal("hashicorp"),
   639  		"token":        cty.NullVal(cty.String),
   640  		"workspaces": cty.ObjectVal(map[string]cty.Value{
   641  			"name": cty.NullVal(cty.String),
   642  			"tags": cty.SetVal(
   643  				[]cty.Value{
   644  					cty.StringVal("sometag"),
   645  				},
   646  			),
   647  		}),
   648  	})
   649  
   650  	b, bCleanup := testBackend(t, config)
   651  	defer bCleanup()
   652  
   653  	// Make sure the workspace doesn't exist yet -- otherwise, we can't test what
   654  	// happens when a workspace gets created. This is why we can't use "name" in
   655  	// the backend config above, btw: if you do, testBackend() creates the default
   656  	// workspace before we get a chance to do anything.
   657  	_, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName)
   658  	if err != tfe.ErrResourceNotFound {
   659  		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)
   660  	}
   661  
   662  	_, err = b.StateMgr(workspaceName)
   663  	if err != nil {
   664  		t.Fatalf("expected no error from StateMgr, despite not being able to set remote Terraform version: %#v", err)
   665  	}
   666  	// Make sure the workspace was created:
   667  	workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName)
   668  	if err != nil {
   669  		t.Fatalf("b.StateMgr() didn't actually create the desired workspace")
   670  	}
   671  	// Make sure our mocks still error as expected, using the same update function b.StateMgr() would call:
   672  	_, err = b.client.Workspaces.UpdateByID(
   673  		context.Background(),
   674  		workspace.ID,
   675  		tfe.WorkspaceUpdateOptions{TerraformVersion: tfe.String("1.1.0")},
   676  	)
   677  	if err == nil {
   678  		t.Fatalf("the mocks aren't emulating a nonexistent remote Terraform version correctly, so this test isn't trustworthy anymore")
   679  	}
   680  }
   681  
   682  func TestCloud_setConfigurationFields(t *testing.T) {
   683  	originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND")
   684  
   685  	cases := map[string]struct {
   686  		obj                   cty.Value
   687  		expectedHostname      string
   688  		expectedOrganziation  string
   689  		expectedWorkspaceName string
   690  		expectedWorkspaceTags []string
   691  		expectedForceLocal    bool
   692  		setEnv                func()
   693  		resetEnv              func()
   694  		expectedErr           string
   695  	}{
   696  		"with hostname set": {
   697  			obj: cty.ObjectVal(map[string]cty.Value{
   698  				"organization": cty.StringVal("hashicorp"),
   699  				"hostname":     cty.StringVal("hashicorp.com"),
   700  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   701  					"name": cty.StringVal("prod"),
   702  					"tags": cty.NullVal(cty.Set(cty.String)),
   703  				}),
   704  			}),
   705  			expectedHostname:     "hashicorp.com",
   706  			expectedOrganziation: "hashicorp",
   707  		},
   708  		"with hostname not set, set to default hostname": {
   709  			obj: cty.ObjectVal(map[string]cty.Value{
   710  				"organization": cty.StringVal("hashicorp"),
   711  				"hostname":     cty.NullVal(cty.String),
   712  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   713  					"name": cty.StringVal("prod"),
   714  					"tags": cty.NullVal(cty.Set(cty.String)),
   715  				}),
   716  			}),
   717  			expectedHostname:     defaultHostname,
   718  			expectedOrganziation: "hashicorp",
   719  		},
   720  		"with workspace name set": {
   721  			obj: cty.ObjectVal(map[string]cty.Value{
   722  				"organization": cty.StringVal("hashicorp"),
   723  				"hostname":     cty.StringVal("hashicorp.com"),
   724  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   725  					"name": cty.StringVal("prod"),
   726  					"tags": cty.NullVal(cty.Set(cty.String)),
   727  				}),
   728  			}),
   729  			expectedHostname:      "hashicorp.com",
   730  			expectedOrganziation:  "hashicorp",
   731  			expectedWorkspaceName: "prod",
   732  		},
   733  		"with workspace tags set": {
   734  			obj: cty.ObjectVal(map[string]cty.Value{
   735  				"organization": cty.StringVal("hashicorp"),
   736  				"hostname":     cty.StringVal("hashicorp.com"),
   737  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   738  					"name": cty.NullVal(cty.String),
   739  					"tags": cty.SetVal(
   740  						[]cty.Value{
   741  							cty.StringVal("billing"),
   742  						},
   743  					),
   744  				}),
   745  			}),
   746  			expectedHostname:      "hashicorp.com",
   747  			expectedOrganziation:  "hashicorp",
   748  			expectedWorkspaceTags: []string{"billing"},
   749  		},
   750  		"with force local set": {
   751  			obj: cty.ObjectVal(map[string]cty.Value{
   752  				"organization": cty.StringVal("hashicorp"),
   753  				"hostname":     cty.StringVal("hashicorp.com"),
   754  				"workspaces": cty.ObjectVal(map[string]cty.Value{
   755  					"name": cty.NullVal(cty.String),
   756  					"tags": cty.NullVal(cty.Set(cty.String)),
   757  				}),
   758  			}),
   759  			expectedHostname:     "hashicorp.com",
   760  			expectedOrganziation: "hashicorp",
   761  			setEnv: func() {
   762  				os.Setenv("TF_FORCE_LOCAL_BACKEND", "1")
   763  			},
   764  			resetEnv: func() {
   765  				os.Setenv("TF_FORCE_LOCAL_BACKEND", originalForceBackendEnv)
   766  			},
   767  			expectedForceLocal: true,
   768  		},
   769  	}
   770  
   771  	for name, tc := range cases {
   772  		b := &Cloud{}
   773  
   774  		// if `setEnv` is set, then we expect `resetEnv` to also be set
   775  		if tc.setEnv != nil {
   776  			tc.setEnv()
   777  			defer tc.resetEnv()
   778  		}
   779  
   780  		errDiags := b.setConfigurationFields(tc.obj)
   781  		if errDiags.HasErrors() || tc.expectedErr != "" {
   782  			actualErr := errDiags.Err().Error()
   783  			if !strings.Contains(actualErr, tc.expectedErr) {
   784  				t.Fatalf("%s: unexpected validation result: %v", name, errDiags.Err())
   785  			}
   786  		}
   787  
   788  		if tc.expectedHostname != "" && b.hostname != tc.expectedHostname {
   789  			t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname)
   790  		}
   791  		if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation {
   792  			t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganziation)
   793  		}
   794  		if tc.expectedWorkspaceName != "" && b.WorkspaceMapping.Name != tc.expectedWorkspaceName {
   795  			t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName)
   796  		}
   797  		if len(tc.expectedWorkspaceTags) > 0 {
   798  			presentSet := make(map[string]struct{})
   799  			for _, tag := range b.WorkspaceMapping.Tags {
   800  				presentSet[tag] = struct{}{}
   801  			}
   802  
   803  			expectedSet := make(map[string]struct{})
   804  			for _, tag := range tc.expectedWorkspaceTags {
   805  				expectedSet[tag] = struct{}{}
   806  			}
   807  
   808  			var missing []string
   809  			var unexpected []string
   810  
   811  			for _, expected := range tc.expectedWorkspaceTags {
   812  				if _, ok := presentSet[expected]; !ok {
   813  					missing = append(missing, expected)
   814  				}
   815  			}
   816  
   817  			for _, actual := range b.WorkspaceMapping.Tags {
   818  				if _, ok := expectedSet[actual]; !ok {
   819  					unexpected = append(unexpected, actual)
   820  				}
   821  			}
   822  
   823  			if len(missing) > 0 {
   824  				t.Fatalf("%s: expected workspace tag mapping (%s) to contain the following tags: %s", name, b.WorkspaceMapping.Tags, missing)
   825  			}
   826  
   827  			if len(unexpected) > 0 {
   828  				t.Fatalf("%s: expected workspace tag mapping (%s) to NOT contain the following tags: %s", name, b.WorkspaceMapping.Tags, unexpected)
   829  			}
   830  
   831  		}
   832  		if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal {
   833  			t.Fatalf("%s: expected force local backend to be set ", name)
   834  		}
   835  	}
   836  }
   837  
   838  func TestCloud_localBackend(t *testing.T) {
   839  	b, bCleanup := testBackendWithName(t)
   840  	defer bCleanup()
   841  
   842  	local, ok := b.local.(*backendLocal.Local)
   843  	if !ok {
   844  		t.Fatalf("expected b.local to be \"*local.Local\", got: %T", b.local)
   845  	}
   846  
   847  	cloud, ok := local.Backend.(*Cloud)
   848  	if !ok {
   849  		t.Fatalf("expected local.Backend to be *cloud.Cloud, got: %T", cloud)
   850  	}
   851  }
   852  
   853  func TestCloud_addAndRemoveWorkspacesDefault(t *testing.T) {
   854  	b, bCleanup := testBackendWithName(t)
   855  	defer bCleanup()
   856  
   857  	if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil {
   858  		t.Fatalf("expected no error, got %v", err)
   859  	}
   860  
   861  	if err := b.DeleteWorkspace(testBackendSingleWorkspaceName, true); err != backend.ErrWorkspacesNotSupported {
   862  		t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err)
   863  	}
   864  }
   865  
   866  func TestCloud_StateMgr_versionCheck(t *testing.T) {
   867  	b, bCleanup := testBackendWithName(t)
   868  	defer bCleanup()
   869  
   870  	// Some fixed versions for testing with. This logic is a simple string
   871  	// comparison, so we don't need many test cases.
   872  	v0135 := version.Must(version.NewSemver("0.13.5"))
   873  	v0140 := version.Must(version.NewSemver("0.14.0"))
   874  
   875  	// Save original local version state and restore afterwards
   876  	p := tfversion.Prerelease
   877  	v := tfversion.Version
   878  	s := tfversion.SemVer
   879  	defer func() {
   880  		tfversion.Prerelease = p
   881  		tfversion.Version = v
   882  		tfversion.SemVer = s
   883  	}()
   884  
   885  	// For this test, the local Terraform version is set to 0.14.0
   886  	tfversion.Prerelease = ""
   887  	tfversion.Version = v0140.String()
   888  	tfversion.SemVer = v0140
   889  
   890  	// Update the mock remote workspace Terraform version to match the local
   891  	// Terraform version
   892  	if _, err := b.client.Workspaces.Update(
   893  		context.Background(),
   894  		b.organization,
   895  		b.WorkspaceMapping.Name,
   896  		tfe.WorkspaceUpdateOptions{
   897  			TerraformVersion: tfe.String(v0140.String()),
   898  		},
   899  	); err != nil {
   900  		t.Fatalf("error: %v", err)
   901  	}
   902  
   903  	// This should succeed
   904  	if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil {
   905  		t.Fatalf("expected no error, got %v", err)
   906  	}
   907  
   908  	// Now change the remote workspace to a different Terraform version
   909  	if _, err := b.client.Workspaces.Update(
   910  		context.Background(),
   911  		b.organization,
   912  		b.WorkspaceMapping.Name,
   913  		tfe.WorkspaceUpdateOptions{
   914  			TerraformVersion: tfe.String(v0135.String()),
   915  		},
   916  	); err != nil {
   917  		t.Fatalf("error: %v", err)
   918  	}
   919  
   920  	// This should fail
   921  	want := `Remote workspace Terraform version "0.13.5" does not match local Terraform version "0.14.0"`
   922  	if _, err := b.StateMgr(testBackendSingleWorkspaceName); err.Error() != want {
   923  		t.Fatalf("wrong error\n got: %v\nwant: %v", err.Error(), want)
   924  	}
   925  }
   926  
   927  func TestCloud_StateMgr_versionCheckLatest(t *testing.T) {
   928  	b, bCleanup := testBackendWithName(t)
   929  	defer bCleanup()
   930  
   931  	v0140 := version.Must(version.NewSemver("0.14.0"))
   932  
   933  	// Save original local version state and restore afterwards
   934  	p := tfversion.Prerelease
   935  	v := tfversion.Version
   936  	s := tfversion.SemVer
   937  	defer func() {
   938  		tfversion.Prerelease = p
   939  		tfversion.Version = v
   940  		tfversion.SemVer = s
   941  	}()
   942  
   943  	// For this test, the local Terraform version is set to 0.14.0
   944  	tfversion.Prerelease = ""
   945  	tfversion.Version = v0140.String()
   946  	tfversion.SemVer = v0140
   947  
   948  	// Update the remote workspace to the pseudo-version "latest"
   949  	if _, err := b.client.Workspaces.Update(
   950  		context.Background(),
   951  		b.organization,
   952  		b.WorkspaceMapping.Name,
   953  		tfe.WorkspaceUpdateOptions{
   954  			TerraformVersion: tfe.String("latest"),
   955  		},
   956  	); err != nil {
   957  		t.Fatalf("error: %v", err)
   958  	}
   959  
   960  	// This should succeed despite not being a string match
   961  	if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil {
   962  		t.Fatalf("expected no error, got %v", err)
   963  	}
   964  }
   965  
   966  func TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) {
   967  	testCases := []struct {
   968  		local         string
   969  		remote        string
   970  		executionMode string
   971  		wantErr       bool
   972  	}{
   973  		{"0.13.5", "0.13.5", "agent", false},
   974  		{"0.14.0", "0.13.5", "remote", true},
   975  		{"0.14.0", "0.13.5", "local", false},
   976  		{"0.14.0", "0.14.1", "remote", false},
   977  		{"0.14.0", "1.0.99", "remote", false},
   978  		{"0.14.0", "1.1.0", "remote", false},
   979  		{"0.14.0", "1.3.0", "remote", true},
   980  		{"1.2.0", "1.2.99", "remote", false},
   981  		{"1.2.0", "1.3.0", "remote", true},
   982  		{"0.15.0", "latest", "remote", false},
   983  		{"1.1.5", "~> 1.1.1", "remote", false},
   984  		{"1.1.5", "> 1.1.0, < 1.3.0", "remote", false},
   985  		{"1.1.5", "~> 1.0.1", "remote", true},
   986  		// pre-release versions are comparable within their pre-release stage (dev,
   987  		// alpha, beta), but not comparable to different stages and not comparable
   988  		// to final releases.
   989  		{"1.1.0-beta1", "1.1.0-beta1", "remote", false},
   990  		{"1.1.0-beta1", "~> 1.1.0-beta", "remote", false},
   991  		{"1.1.0", "~> 1.1.0-beta", "remote", true},
   992  		{"1.1.0-beta1", "~> 1.1.0-dev", "remote", true},
   993  	}
   994  	for _, tc := range testCases {
   995  		t.Run(fmt.Sprintf("local %s, remote %s", tc.local, tc.remote), func(t *testing.T) {
   996  			b, bCleanup := testBackendWithName(t)
   997  			defer bCleanup()
   998  
   999  			local := version.Must(version.NewSemver(tc.local))
  1000  
  1001  			// Save original local version state and restore afterwards
  1002  			p := tfversion.Prerelease
  1003  			v := tfversion.Version
  1004  			s := tfversion.SemVer
  1005  			defer func() {
  1006  				tfversion.Prerelease = p
  1007  				tfversion.Version = v
  1008  				tfversion.SemVer = s
  1009  			}()
  1010  
  1011  			// Override local version as specified
  1012  			tfversion.Prerelease = ""
  1013  			tfversion.Version = local.String()
  1014  			tfversion.SemVer = local
  1015  
  1016  			// Update the mock remote workspace Terraform version to the
  1017  			// specified remote version
  1018  			if _, err := b.client.Workspaces.Update(
  1019  				context.Background(),
  1020  				b.organization,
  1021  				b.WorkspaceMapping.Name,
  1022  				tfe.WorkspaceUpdateOptions{
  1023  					ExecutionMode:    &tc.executionMode,
  1024  					TerraformVersion: tfe.String(tc.remote),
  1025  				},
  1026  			); err != nil {
  1027  				t.Fatalf("error: %v", err)
  1028  			}
  1029  
  1030  			diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName)
  1031  			if tc.wantErr {
  1032  				if len(diags) != 1 {
  1033  					t.Fatal("expected diag, but none returned")
  1034  				}
  1035  				if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version") {
  1036  					t.Fatalf("unexpected error: %s", got)
  1037  				}
  1038  			} else {
  1039  				if len(diags) != 0 {
  1040  					t.Fatalf("unexpected diags: %s", diags.Err())
  1041  				}
  1042  			}
  1043  		})
  1044  	}
  1045  }
  1046  
  1047  func TestCloud_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) {
  1048  	b, bCleanup := testBackendWithName(t)
  1049  	defer bCleanup()
  1050  
  1051  	// Attempting to check the version against a workspace which doesn't exist
  1052  	// should result in no errors
  1053  	diags := b.VerifyWorkspaceTerraformVersion("invalid-workspace")
  1054  	if len(diags) != 0 {
  1055  		t.Fatalf("unexpected error: %s", diags.Err())
  1056  	}
  1057  
  1058  	// Use a special workspace ID to trigger a 500 error, which should result
  1059  	// in a failed check
  1060  	diags = b.VerifyWorkspaceTerraformVersion("network-error")
  1061  	if len(diags) != 1 {
  1062  		t.Fatal("expected diag, but none returned")
  1063  	}
  1064  	if got := diags.Err().Error(); !strings.Contains(got, "Error looking up workspace: Workspace read failed") {
  1065  		t.Fatalf("unexpected error: %s", got)
  1066  	}
  1067  
  1068  	// Update the mock remote workspace Terraform version to an invalid version
  1069  	if _, err := b.client.Workspaces.Update(
  1070  		context.Background(),
  1071  		b.organization,
  1072  		b.WorkspaceMapping.Name,
  1073  		tfe.WorkspaceUpdateOptions{
  1074  			TerraformVersion: tfe.String("1.0.cheetarah"),
  1075  		},
  1076  	); err != nil {
  1077  		t.Fatalf("error: %v", err)
  1078  	}
  1079  	diags = b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName)
  1080  
  1081  	if len(diags) != 1 {
  1082  		t.Fatal("expected diag, but none returned")
  1083  	}
  1084  	if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version: The remote workspace specified") {
  1085  		t.Fatalf("unexpected error: %s", got)
  1086  	}
  1087  }
  1088  
  1089  func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) {
  1090  	b, bCleanup := testBackendWithName(t)
  1091  	defer bCleanup()
  1092  
  1093  	// If the ignore flag is set, the behaviour changes
  1094  	b.IgnoreVersionConflict()
  1095  
  1096  	// Different local & remote versions to cause an error
  1097  	local := version.Must(version.NewSemver("0.14.0"))
  1098  	remote := version.Must(version.NewSemver("0.13.5"))
  1099  
  1100  	// Save original local version state and restore afterwards
  1101  	p := tfversion.Prerelease
  1102  	v := tfversion.Version
  1103  	s := tfversion.SemVer
  1104  	defer func() {
  1105  		tfversion.Prerelease = p
  1106  		tfversion.Version = v
  1107  		tfversion.SemVer = s
  1108  	}()
  1109  
  1110  	// Override local version as specified
  1111  	tfversion.Prerelease = ""
  1112  	tfversion.Version = local.String()
  1113  	tfversion.SemVer = local
  1114  
  1115  	// Update the mock remote workspace Terraform version to the
  1116  	// specified remote version
  1117  	if _, err := b.client.Workspaces.Update(
  1118  		context.Background(),
  1119  		b.organization,
  1120  		b.WorkspaceMapping.Name,
  1121  		tfe.WorkspaceUpdateOptions{
  1122  			TerraformVersion: tfe.String(remote.String()),
  1123  		},
  1124  	); err != nil {
  1125  		t.Fatalf("error: %v", err)
  1126  	}
  1127  
  1128  	diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName)
  1129  	if len(diags) != 1 {
  1130  		t.Fatal("expected diag, but none returned")
  1131  	}
  1132  
  1133  	if got, want := diags[0].Severity(), tfdiags.Warning; got != want {
  1134  		t.Errorf("wrong severity: got %#v, want %#v", got, want)
  1135  	}
  1136  	if got, want := diags[0].Description().Summary, "Incompatible Terraform version"; got != want {
  1137  		t.Errorf("wrong summary: got %s, want %s", got, want)
  1138  	}
  1139  	wantDetail := "The local Terraform version (0.14.0) does not meet the version requirements for remote workspace hashicorp/app-prod (0.13.5)."
  1140  	if got := diags[0].Description().Detail; got != wantDetail {
  1141  		t.Errorf("wrong summary: got %s, want %s", got, wantDetail)
  1142  	}
  1143  }
  1144  
  1145  func TestClodBackend_DeleteWorkspace_SafeAndForce(t *testing.T) {
  1146  	b, bCleanup := testBackendWithTags(t)
  1147  	defer bCleanup()
  1148  	safeDeleteWorkspaceName := "safe-delete-workspace"
  1149  	forceDeleteWorkspaceName := "force-delete-workspace"
  1150  
  1151  	_, err := b.StateMgr(safeDeleteWorkspaceName)
  1152  	if err != nil {
  1153  		t.Fatalf("error: %s", err)
  1154  	}
  1155  
  1156  	_, err = b.StateMgr(forceDeleteWorkspaceName)
  1157  	if err != nil {
  1158  		t.Fatalf("error: %s", err)
  1159  	}
  1160  
  1161  	// sanity check that the mock now contains two workspaces
  1162  	wl, err := b.Workspaces()
  1163  	if err != nil {
  1164  		t.Fatalf("error fetching workspace names: %v", err)
  1165  	}
  1166  	if len(wl) != 2 {
  1167  		t.Fatalf("expected 2 workspaced but got %d", len(wl))
  1168  	}
  1169  
  1170  	c := context.Background()
  1171  	safeDeleteWorkspace, err := b.client.Workspaces.Read(c, b.organization, safeDeleteWorkspaceName)
  1172  	if err != nil {
  1173  		t.Fatalf("error fetching workspace: %v", err)
  1174  	}
  1175  
  1176  	// Lock a workspace so that it should fail to be safe deleted
  1177  	_, err = b.client.Workspaces.Lock(context.Background(), safeDeleteWorkspace.ID, tfe.WorkspaceLockOptions{Reason: tfe.String("test")})
  1178  	if err != nil {
  1179  		t.Fatalf("error locking workspace: %v", err)
  1180  	}
  1181  	err = b.DeleteWorkspace(safeDeleteWorkspaceName, false)
  1182  	if err == nil {
  1183  		t.Fatalf("workspace should have failed to safe delete")
  1184  	}
  1185  
  1186  	// unlock the workspace and confirm that safe-delete now works
  1187  	_, err = b.client.Workspaces.Unlock(context.Background(), safeDeleteWorkspace.ID)
  1188  	if err != nil {
  1189  		t.Fatalf("error unlocking workspace: %v", err)
  1190  	}
  1191  	err = b.DeleteWorkspace(safeDeleteWorkspaceName, false)
  1192  	if err != nil {
  1193  		t.Fatalf("error safe deleting workspace: %v", err)
  1194  	}
  1195  
  1196  	// lock a workspace and then confirm that force deleting it works
  1197  	forceDeleteWorkspace, err := b.client.Workspaces.Read(c, b.organization, forceDeleteWorkspaceName)
  1198  	if err != nil {
  1199  		t.Fatalf("error fetching workspace: %v", err)
  1200  	}
  1201  	_, err = b.client.Workspaces.Lock(context.Background(), forceDeleteWorkspace.ID, tfe.WorkspaceLockOptions{Reason: tfe.String("test")})
  1202  	if err != nil {
  1203  		t.Fatalf("error locking workspace: %v", err)
  1204  	}
  1205  	err = b.DeleteWorkspace(forceDeleteWorkspaceName, true)
  1206  	if err != nil {
  1207  		t.Fatalf("error force deleting workspace: %v", err)
  1208  	}
  1209  }
  1210  
  1211  func TestClodBackend_DeleteWorkspace_DoesNotExist(t *testing.T) {
  1212  	b, bCleanup := testBackendWithTags(t)
  1213  	defer bCleanup()
  1214  
  1215  	err := b.DeleteWorkspace("non-existent-workspace", false)
  1216  	if err != nil {
  1217  		t.Fatalf("expected deleting a workspace which does not exist to succeed")
  1218  	}
  1219  }