github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/states/remote/state_test.go (about)

     1  package remote
     2  
     3  import (
     4  	"log"
     5  	"sync"
     6  	"testing"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	"github.com/google/go-cmp/cmp/cmpopts"
    10  	"github.com/zclconf/go-cty/cty"
    11  
    12  	tfaddr "github.com/hashicorp/terraform-registry-address"
    13  	"github.com/hashicorp/terraform/internal/addrs"
    14  	"github.com/hashicorp/terraform/internal/states"
    15  	"github.com/hashicorp/terraform/internal/states/statefile"
    16  	"github.com/hashicorp/terraform/internal/states/statemgr"
    17  	"github.com/hashicorp/terraform/version"
    18  )
    19  
    20  func TestState_impl(t *testing.T) {
    21  	var _ statemgr.Reader = new(State)
    22  	var _ statemgr.Writer = new(State)
    23  	var _ statemgr.Persister = new(State)
    24  	var _ statemgr.Refresher = new(State)
    25  	var _ statemgr.OutputReader = new(State)
    26  	var _ statemgr.Locker = new(State)
    27  }
    28  
    29  func TestStateRace(t *testing.T) {
    30  	s := &State{
    31  		Client: nilClient{},
    32  	}
    33  
    34  	current := states.NewState()
    35  
    36  	var wg sync.WaitGroup
    37  
    38  	for i := 0; i < 100; i++ {
    39  		wg.Add(1)
    40  		go func() {
    41  			defer wg.Done()
    42  			s.WriteState(current)
    43  			s.PersistState(nil)
    44  			s.RefreshState()
    45  		}()
    46  	}
    47  	wg.Wait()
    48  }
    49  
    50  // testCase encapsulates a test state test
    51  type testCase struct {
    52  	name string
    53  	// A function to mutate state and return a cleanup function
    54  	mutationFunc func(*State) (*states.State, func())
    55  	// The expected requests to have taken place
    56  	expectedRequests []mockClientRequest
    57  	// Mark this case as not having a request
    58  	noRequest bool
    59  }
    60  
    61  // isRequested ensures a test that is specified as not having
    62  // a request doesn't have one by checking if a method exists
    63  // on the expectedRequest.
    64  func (tc testCase) isRequested(t *testing.T) bool {
    65  	for _, expectedMethod := range tc.expectedRequests {
    66  		hasMethod := expectedMethod.Method != ""
    67  		if tc.noRequest && hasMethod {
    68  			t.Fatalf("expected no content for %q but got: %v", tc.name, expectedMethod)
    69  		}
    70  	}
    71  	return !tc.noRequest
    72  }
    73  
    74  func TestStatePersist(t *testing.T) {
    75  	testCases := []testCase{
    76  		{
    77  			name: "first state persistence",
    78  			mutationFunc: func(mgr *State) (*states.State, func()) {
    79  				mgr.state = &states.State{
    80  					Modules: map[string]*states.Module{"": {}},
    81  				}
    82  				s := mgr.State()
    83  				s.RootModule().SetResourceInstanceCurrent(
    84  					addrs.Resource{
    85  						Mode: addrs.ManagedResourceMode,
    86  						Name: "myfile",
    87  						Type: "local_file",
    88  					}.Instance(addrs.NoKey),
    89  					&states.ResourceInstanceObjectSrc{
    90  						AttrsFlat: map[string]string{
    91  							"filename": "file.txt",
    92  						},
    93  						Status: states.ObjectReady,
    94  					},
    95  					addrs.AbsProviderConfig{
    96  						Provider: tfaddr.Provider{Namespace: "local"},
    97  					},
    98  				)
    99  				return s, func() {}
   100  			},
   101  			expectedRequests: []mockClientRequest{
   102  				// Expect an initial refresh, which returns nothing since there is no remote state.
   103  				{
   104  					Method:  "Get",
   105  					Content: nil,
   106  				},
   107  				// Expect a second refresh, since the read state is nil
   108  				{
   109  					Method:  "Get",
   110  					Content: nil,
   111  				},
   112  				// Expect an initial push with values and a serial of 1
   113  				{
   114  					Method: "Put",
   115  					Content: map[string]interface{}{
   116  						"version":           4.0, // encoding/json decodes this as float64 by default
   117  						"lineage":           "some meaningless value",
   118  						"serial":            1.0, // encoding/json decodes this as float64 by default
   119  						"terraform_version": version.Version,
   120  						"outputs":           map[string]interface{}{},
   121  						"resources": []interface{}{
   122  							map[string]interface{}{
   123  								"instances": []interface{}{
   124  									map[string]interface{}{
   125  										"attributes_flat": map[string]interface{}{
   126  											"filename": "file.txt",
   127  										},
   128  										"schema_version":       0.0,
   129  										"sensitive_attributes": []interface{}{},
   130  									},
   131  								},
   132  								"mode":     "managed",
   133  								"name":     "myfile",
   134  								"provider": `provider["/local/"]`,
   135  								"type":     "local_file",
   136  							},
   137  						},
   138  						"check_results": nil,
   139  					},
   140  				},
   141  			},
   142  		},
   143  		// If lineage changes, expect the serial to increment
   144  		{
   145  			name: "change lineage",
   146  			mutationFunc: func(mgr *State) (*states.State, func()) {
   147  				mgr.lineage = "mock-lineage"
   148  				return mgr.State(), func() {}
   149  			},
   150  			expectedRequests: []mockClientRequest{
   151  				{
   152  					Method: "Put",
   153  					Content: map[string]interface{}{
   154  						"version":           4.0, // encoding/json decodes this as float64 by default
   155  						"lineage":           "mock-lineage",
   156  						"serial":            2.0, // encoding/json decodes this as float64 by default
   157  						"terraform_version": version.Version,
   158  						"outputs":           map[string]interface{}{},
   159  						"resources": []interface{}{
   160  							map[string]interface{}{
   161  								"instances": []interface{}{
   162  									map[string]interface{}{
   163  										"attributes_flat": map[string]interface{}{
   164  											"filename": "file.txt",
   165  										},
   166  										"schema_version":       0.0,
   167  										"sensitive_attributes": []interface{}{},
   168  									},
   169  								},
   170  								"mode":     "managed",
   171  								"name":     "myfile",
   172  								"provider": `provider["/local/"]`,
   173  								"type":     "local_file",
   174  							},
   175  						},
   176  						"check_results": nil,
   177  					},
   178  				},
   179  			},
   180  		},
   181  		// removing resources should increment the serial
   182  		{
   183  			name: "remove resources",
   184  			mutationFunc: func(mgr *State) (*states.State, func()) {
   185  				mgr.state.RootModule().Resources = map[string]*states.Resource{}
   186  				return mgr.State(), func() {}
   187  			},
   188  			expectedRequests: []mockClientRequest{
   189  				{
   190  					Method: "Put",
   191  					Content: map[string]interface{}{
   192  						"version":           4.0, // encoding/json decodes this as float64 by default
   193  						"lineage":           "mock-lineage",
   194  						"serial":            3.0, // encoding/json decodes this as float64 by default
   195  						"terraform_version": version.Version,
   196  						"outputs":           map[string]interface{}{},
   197  						"resources":         []interface{}{},
   198  						"check_results":     nil,
   199  					},
   200  				},
   201  			},
   202  		},
   203  		// If the remote serial is incremented, then we increment it once more.
   204  		{
   205  			name: "change serial",
   206  			mutationFunc: func(mgr *State) (*states.State, func()) {
   207  				originalSerial := mgr.serial
   208  				mgr.serial++
   209  				return mgr.State(), func() {
   210  					mgr.serial = originalSerial
   211  				}
   212  			},
   213  			expectedRequests: []mockClientRequest{
   214  				{
   215  					Method: "Put",
   216  					Content: map[string]interface{}{
   217  						"version":           4.0, // encoding/json decodes this as float64 by default
   218  						"lineage":           "mock-lineage",
   219  						"serial":            5.0, // encoding/json decodes this as float64 by default
   220  						"terraform_version": version.Version,
   221  						"outputs":           map[string]interface{}{},
   222  						"resources":         []interface{}{},
   223  						"check_results":     nil,
   224  					},
   225  				},
   226  			},
   227  		},
   228  		// Adding an output should cause the serial to increment as well.
   229  		{
   230  			name: "add output to state",
   231  			mutationFunc: func(mgr *State) (*states.State, func()) {
   232  				s := mgr.State()
   233  				s.RootModule().SetOutputValue("foo", cty.StringVal("bar"), false)
   234  				return s, func() {}
   235  			},
   236  			expectedRequests: []mockClientRequest{
   237  				{
   238  					Method: "Put",
   239  					Content: map[string]interface{}{
   240  						"version":           4.0, // encoding/json decodes this as float64 by default
   241  						"lineage":           "mock-lineage",
   242  						"serial":            4.0, // encoding/json decodes this as float64 by default
   243  						"terraform_version": version.Version,
   244  						"outputs": map[string]interface{}{
   245  							"foo": map[string]interface{}{
   246  								"type":  "string",
   247  								"value": "bar",
   248  							},
   249  						},
   250  						"resources":     []interface{}{},
   251  						"check_results": nil,
   252  					},
   253  				},
   254  			},
   255  		},
   256  		// ...as should changing an output
   257  		{
   258  			name: "mutate state bar -> baz",
   259  			mutationFunc: func(mgr *State) (*states.State, func()) {
   260  				s := mgr.State()
   261  				s.RootModule().SetOutputValue("foo", cty.StringVal("baz"), false)
   262  				return s, func() {}
   263  			},
   264  			expectedRequests: []mockClientRequest{
   265  				{
   266  					Method: "Put",
   267  					Content: map[string]interface{}{
   268  						"version":           4.0, // encoding/json decodes this as float64 by default
   269  						"lineage":           "mock-lineage",
   270  						"serial":            5.0, // encoding/json decodes this as float64 by default
   271  						"terraform_version": version.Version,
   272  						"outputs": map[string]interface{}{
   273  							"foo": map[string]interface{}{
   274  								"type":  "string",
   275  								"value": "baz",
   276  							},
   277  						},
   278  						"resources":     []interface{}{},
   279  						"check_results": nil,
   280  					},
   281  				},
   282  			},
   283  		},
   284  		{
   285  			name: "nothing changed",
   286  			mutationFunc: func(mgr *State) (*states.State, func()) {
   287  				s := mgr.State()
   288  				return s, func() {}
   289  			},
   290  			noRequest: true,
   291  		},
   292  		// If the remote state's serial is less (force push), then we
   293  		// increment it once from there.
   294  		{
   295  			name: "reset serial (force push style)",
   296  			mutationFunc: func(mgr *State) (*states.State, func()) {
   297  				mgr.serial = 2
   298  				return mgr.State(), func() {}
   299  			},
   300  			expectedRequests: []mockClientRequest{
   301  				{
   302  					Method: "Put",
   303  					Content: map[string]interface{}{
   304  						"version":           4.0, // encoding/json decodes this as float64 by default
   305  						"lineage":           "mock-lineage",
   306  						"serial":            3.0, // encoding/json decodes this as float64 by default
   307  						"terraform_version": version.Version,
   308  						"outputs": map[string]interface{}{
   309  							"foo": map[string]interface{}{
   310  								"type":  "string",
   311  								"value": "baz",
   312  							},
   313  						},
   314  						"resources":     []interface{}{},
   315  						"check_results": nil,
   316  					},
   317  				},
   318  			},
   319  		},
   320  	}
   321  
   322  	// Initial setup of state just to give us a fixed starting point for our
   323  	// test assertions below, or else we'd need to deal with
   324  	// random lineage.
   325  	mgr := &State{
   326  		Client: &mockClient{},
   327  	}
   328  
   329  	// In normal use (during a Terraform operation) we always refresh and read
   330  	// before any writes would happen, so we'll mimic that here for realism.
   331  	// NB This causes a GET to be logged so the first item in the test cases
   332  	// must account for this
   333  	if err := mgr.RefreshState(); err != nil {
   334  		t.Fatalf("failed to RefreshState: %s", err)
   335  	}
   336  
   337  	// Our client is a mockClient which has a log we
   338  	// use to check that operations generate expected requests
   339  	mockClient := mgr.Client.(*mockClient)
   340  
   341  	// logIdx tracks the current index of the log separate from
   342  	// the loop iteration so we can check operations that don't
   343  	// cause any requests to be generated
   344  	logIdx := 0
   345  
   346  	// Run tests in order.
   347  	for _, tc := range testCases {
   348  		t.Run(tc.name, func(t *testing.T) {
   349  			s, cleanup := tc.mutationFunc(mgr)
   350  
   351  			if err := mgr.WriteState(s); err != nil {
   352  				t.Fatalf("failed to WriteState for %q: %s", tc.name, err)
   353  			}
   354  			if err := mgr.PersistState(nil); err != nil {
   355  				t.Fatalf("failed to PersistState for %q: %s", tc.name, err)
   356  			}
   357  
   358  			if tc.isRequested(t) {
   359  				// Get captured request from the mock client log
   360  				// based on the index of the current test
   361  				if logIdx >= len(mockClient.log) {
   362  					t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log))
   363  				}
   364  				for expectedRequestIdx := 0; expectedRequestIdx < len(tc.expectedRequests); expectedRequestIdx++ {
   365  					loggedRequest := mockClient.log[logIdx]
   366  					logIdx++
   367  					if diff := cmp.Diff(tc.expectedRequests[expectedRequestIdx], loggedRequest, cmpopts.IgnoreMapEntries(func(key string, value interface{}) bool {
   368  						// This is required since the initial state creation causes the lineage to be a UUID that is not known at test time.
   369  						return tc.name == "first state persistence" && key == "lineage"
   370  					})); len(diff) > 0 {
   371  						t.Logf("incorrect client requests for %q:\n%s", tc.name, diff)
   372  						t.Fail()
   373  					}
   374  				}
   375  			}
   376  			cleanup()
   377  		})
   378  	}
   379  	logCnt := len(mockClient.log)
   380  	if logIdx != logCnt {
   381  		t.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx)
   382  	}
   383  }
   384  
   385  func TestState_GetRootOutputValues(t *testing.T) {
   386  	// Initial setup of state with outputs already defined
   387  	mgr := &State{
   388  		Client: &mockClient{
   389  			current: []byte(`
   390  				{
   391  					"version": 4,
   392  					"lineage": "mock-lineage",
   393  					"serial": 1,
   394  					"terraform_version":"0.0.0",
   395  					"outputs": {"foo": {"value":"bar", "type": "string"}},
   396  					"resources": []
   397  				}
   398  			`),
   399  		},
   400  	}
   401  
   402  	outputs, err := mgr.GetRootOutputValues()
   403  	if err != nil {
   404  		t.Errorf("Expected GetRootOutputValues to not return an error, but it returned %v", err)
   405  	}
   406  
   407  	if len(outputs) != 1 {
   408  		t.Errorf("Expected %d outputs, but received %d", 1, len(outputs))
   409  	}
   410  }
   411  
   412  type migrationTestCase struct {
   413  	name string
   414  	// A function to generate a statefile
   415  	stateFile func(*State) *statefile.File
   416  	// The expected request to have taken place
   417  	expectedRequest mockClientRequest
   418  	// Mark this case as not having a request
   419  	expectedError string
   420  	// force flag passed to client
   421  	force bool
   422  }
   423  
   424  func TestWriteStateForMigration(t *testing.T) {
   425  	mgr := &State{
   426  		Client: &mockClient{
   427  			current: []byte(`
   428  				{
   429  					"version": 4,
   430  					"lineage": "mock-lineage",
   431  					"serial": 3,
   432  					"terraform_version":"0.0.0",
   433  					"outputs": {"foo": {"value":"bar", "type": "string"}},
   434  					"resources": []
   435  				}
   436  			`),
   437  		},
   438  	}
   439  
   440  	testCases := []migrationTestCase{
   441  		// Refreshing state before we run the test loop causes a GET
   442  		{
   443  			name: "refresh state",
   444  			stateFile: func(mgr *State) *statefile.File {
   445  				return mgr.StateForMigration()
   446  			},
   447  			expectedRequest: mockClientRequest{
   448  				Method: "Get",
   449  				Content: map[string]interface{}{
   450  					"version":           4.0,
   451  					"lineage":           "mock-lineage",
   452  					"serial":            3.0,
   453  					"terraform_version": "0.0.0",
   454  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   455  					"resources":         []interface{}{},
   456  				},
   457  			},
   458  		},
   459  		{
   460  			name: "cannot import lesser serial without force",
   461  			stateFile: func(mgr *State) *statefile.File {
   462  				return statefile.New(mgr.state, mgr.lineage, 1)
   463  			},
   464  			expectedError: "cannot import state with serial 1 over newer state with serial 3",
   465  		},
   466  		{
   467  			name: "cannot import differing lineage without force",
   468  			stateFile: func(mgr *State) *statefile.File {
   469  				return statefile.New(mgr.state, "different-lineage", mgr.serial)
   470  			},
   471  			expectedError: `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"`,
   472  		},
   473  		{
   474  			name: "can import lesser serial with force",
   475  			stateFile: func(mgr *State) *statefile.File {
   476  				return statefile.New(mgr.state, mgr.lineage, 1)
   477  			},
   478  			expectedRequest: mockClientRequest{
   479  				Method: "Put",
   480  				Content: map[string]interface{}{
   481  					"version":           4.0,
   482  					"lineage":           "mock-lineage",
   483  					"serial":            2.0,
   484  					"terraform_version": version.Version,
   485  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   486  					"resources":         []interface{}{},
   487  					"check_results":     nil,
   488  				},
   489  			},
   490  			force: true,
   491  		},
   492  		{
   493  			name: "cannot import differing lineage without force",
   494  			stateFile: func(mgr *State) *statefile.File {
   495  				return statefile.New(mgr.state, "different-lineage", mgr.serial)
   496  			},
   497  			expectedRequest: mockClientRequest{
   498  				Method: "Put",
   499  				Content: map[string]interface{}{
   500  					"version":           4.0,
   501  					"lineage":           "different-lineage",
   502  					"serial":            3.0,
   503  					"terraform_version": version.Version,
   504  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   505  					"resources":         []interface{}{},
   506  					"check_results":     nil,
   507  				},
   508  			},
   509  			force: true,
   510  		},
   511  	}
   512  
   513  	// In normal use (during a Terraform operation) we always refresh and read
   514  	// before any writes would happen, so we'll mimic that here for realism.
   515  	// NB This causes a GET to be logged so the first item in the test cases
   516  	// must account for this
   517  	if err := mgr.RefreshState(); err != nil {
   518  		t.Fatalf("failed to RefreshState: %s", err)
   519  	}
   520  
   521  	if err := mgr.WriteState(mgr.State()); err != nil {
   522  		t.Fatalf("failed to write initial state: %s", err)
   523  	}
   524  
   525  	// Our client is a mockClient which has a log we
   526  	// use to check that operations generate expected requests
   527  	mockClient := mgr.Client.(*mockClient)
   528  
   529  	// logIdx tracks the current index of the log separate from
   530  	// the loop iteration so we can check operations that don't
   531  	// cause any requests to be generated
   532  	logIdx := 0
   533  
   534  	for _, tc := range testCases {
   535  		t.Run(tc.name, func(t *testing.T) {
   536  			sf := tc.stateFile(mgr)
   537  			err := mgr.WriteStateForMigration(sf, tc.force)
   538  			shouldError := tc.expectedError != ""
   539  
   540  			// If we are expecting and error check it and move on
   541  			if shouldError {
   542  				if err == nil {
   543  					t.Fatalf("test case %q should have failed with error %q", tc.name, tc.expectedError)
   544  				} else if err.Error() != tc.expectedError {
   545  					t.Fatalf("test case %q expected error %q but got %q", tc.name, tc.expectedError, err)
   546  				}
   547  				return
   548  			}
   549  
   550  			if err != nil {
   551  				t.Fatalf("test case %q failed: %v", tc.name, err)
   552  			}
   553  
   554  			// At this point we should just do a normal write and persist
   555  			// as would happen from the CLI
   556  			mgr.WriteState(mgr.State())
   557  			mgr.PersistState(nil)
   558  
   559  			if logIdx >= len(mockClient.log) {
   560  				t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log))
   561  			}
   562  			loggedRequest := mockClient.log[logIdx]
   563  			logIdx++
   564  			if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 {
   565  				t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff)
   566  			}
   567  		})
   568  	}
   569  
   570  	logCnt := len(mockClient.log)
   571  	if logIdx != logCnt {
   572  		log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx)
   573  	}
   574  }
   575  
   576  // This test runs the same test cases as above, but with
   577  // a client that implements EnableForcePush -- this allows
   578  // us to test that -force continues to work for backends without
   579  // this interface, but that this interface works for those that do.
   580  func TestWriteStateForMigrationWithForcePushClient(t *testing.T) {
   581  	mgr := &State{
   582  		Client: &mockClientForcePusher{
   583  			current: []byte(`
   584  				{
   585  					"version": 4,
   586  					"lineage": "mock-lineage",
   587  					"serial": 3,
   588  					"terraform_version":"0.0.0",
   589  					"outputs": {"foo": {"value":"bar", "type": "string"}},
   590  					"resources": []
   591  				}
   592  			`),
   593  		},
   594  	}
   595  
   596  	testCases := []migrationTestCase{
   597  		// Refreshing state before we run the test loop causes a GET
   598  		{
   599  			name: "refresh state",
   600  			stateFile: func(mgr *State) *statefile.File {
   601  				return mgr.StateForMigration()
   602  			},
   603  			expectedRequest: mockClientRequest{
   604  				Method: "Get",
   605  				Content: map[string]interface{}{
   606  					"version":           4.0,
   607  					"lineage":           "mock-lineage",
   608  					"serial":            3.0,
   609  					"terraform_version": "0.0.0",
   610  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   611  					"resources":         []interface{}{},
   612  				},
   613  			},
   614  		},
   615  		{
   616  			name: "cannot import lesser serial without force",
   617  			stateFile: func(mgr *State) *statefile.File {
   618  				return statefile.New(mgr.state, mgr.lineage, 1)
   619  			},
   620  			expectedError: "cannot import state with serial 1 over newer state with serial 3",
   621  		},
   622  		{
   623  			name: "cannot import differing lineage without force",
   624  			stateFile: func(mgr *State) *statefile.File {
   625  				return statefile.New(mgr.state, "different-lineage", mgr.serial)
   626  			},
   627  			expectedError: `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"`,
   628  		},
   629  		{
   630  			name: "can import lesser serial with force",
   631  			stateFile: func(mgr *State) *statefile.File {
   632  				return statefile.New(mgr.state, mgr.lineage, 1)
   633  			},
   634  			expectedRequest: mockClientRequest{
   635  				Method: "Force Put",
   636  				Content: map[string]interface{}{
   637  					"version":           4.0,
   638  					"lineage":           "mock-lineage",
   639  					"serial":            2.0,
   640  					"terraform_version": version.Version,
   641  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   642  					"resources":         []interface{}{},
   643  					"check_results":     nil,
   644  				},
   645  			},
   646  			force: true,
   647  		},
   648  		{
   649  			name: "cannot import differing lineage without force",
   650  			stateFile: func(mgr *State) *statefile.File {
   651  				return statefile.New(mgr.state, "different-lineage", mgr.serial)
   652  			},
   653  			expectedRequest: mockClientRequest{
   654  				Method: "Force Put",
   655  				Content: map[string]interface{}{
   656  					"version":           4.0,
   657  					"lineage":           "different-lineage",
   658  					"serial":            3.0,
   659  					"terraform_version": version.Version,
   660  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   661  					"resources":         []interface{}{},
   662  					"check_results":     nil,
   663  				},
   664  			},
   665  			force: true,
   666  		},
   667  	}
   668  
   669  	// In normal use (during a Terraform operation) we always refresh and read
   670  	// before any writes would happen, so we'll mimic that here for realism.
   671  	// NB This causes a GET to be logged so the first item in the test cases
   672  	// must account for this
   673  	if err := mgr.RefreshState(); err != nil {
   674  		t.Fatalf("failed to RefreshState: %s", err)
   675  	}
   676  
   677  	if err := mgr.WriteState(mgr.State()); err != nil {
   678  		t.Fatalf("failed to write initial state: %s", err)
   679  	}
   680  
   681  	// Our client is a mockClientForcePusher which has a log we
   682  	// use to check that operations generate expected requests
   683  	mockClient := mgr.Client.(*mockClientForcePusher)
   684  
   685  	if mockClient.force {
   686  		t.Fatalf("client should not default to force")
   687  	}
   688  
   689  	// logIdx tracks the current index of the log separate from
   690  	// the loop iteration so we can check operations that don't
   691  	// cause any requests to be generated
   692  	logIdx := 0
   693  
   694  	for _, tc := range testCases {
   695  		t.Run(tc.name, func(t *testing.T) {
   696  			// Always reset client to not be force pushing
   697  			mockClient.force = false
   698  			sf := tc.stateFile(mgr)
   699  			err := mgr.WriteStateForMigration(sf, tc.force)
   700  			shouldError := tc.expectedError != ""
   701  
   702  			// If we are expecting and error check it and move on
   703  			if shouldError {
   704  				if err == nil {
   705  					t.Fatalf("test case %q should have failed with error %q", tc.name, tc.expectedError)
   706  				} else if err.Error() != tc.expectedError {
   707  					t.Fatalf("test case %q expected error %q but got %q", tc.name, tc.expectedError, err)
   708  				}
   709  				return
   710  			}
   711  
   712  			if err != nil {
   713  				t.Fatalf("test case %q failed: %v", tc.name, err)
   714  			}
   715  
   716  			if tc.force && !mockClient.force {
   717  				t.Fatalf("test case %q should have enabled force push", tc.name)
   718  			}
   719  
   720  			// At this point we should just do a normal write and persist
   721  			// as would happen from the CLI
   722  			mgr.WriteState(mgr.State())
   723  			mgr.PersistState(nil)
   724  
   725  			if logIdx >= len(mockClient.log) {
   726  				t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log))
   727  			}
   728  			loggedRequest := mockClient.log[logIdx]
   729  			logIdx++
   730  			if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 {
   731  				t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff)
   732  			}
   733  		})
   734  	}
   735  
   736  	logCnt := len(mockClient.log)
   737  	if logIdx != logCnt {
   738  		log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx)
   739  	}
   740  }