github.com/opentofu/opentofu@v1.7.1/internal/states/remote/state_test.go (about)

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