@@ -302,6 +302,158 @@ func TestWriteStateForMigration(t *testing.T) {
302302 },
303303 }
304304
305+ testCases := []migrationTestCase {
306+ // Refreshing state before we run the test loop causes a GET
307+ {
308+ name : "refresh state" ,
309+ stateFile : func (mgr * State ) * statefile.File {
310+ return mgr .StateForMigration ()
311+ },
312+ expectedRequest : mockClientRequest {
313+ Method : "Get" ,
314+ Content : map [string ]interface {}{
315+ "version" : 4.0 ,
316+ "lineage" : "mock-lineage" ,
317+ "serial" : 3.0 ,
318+ "terraform_version" : "0.0.0" ,
319+ "outputs" : map [string ]interface {}{"foo" : map [string ]interface {}{"type" : string ("string" ), "value" : string ("bar" )}},
320+ "resources" : []interface {}{},
321+ },
322+ },
323+ },
324+ {
325+ name : "cannot import lesser serial without force" ,
326+ stateFile : func (mgr * State ) * statefile.File {
327+ return statefile .New (mgr .state , mgr .lineage , 1 )
328+ },
329+ expectedError : "cannot import state with serial 1 over newer state with serial 3" ,
330+ },
331+ {
332+ name : "cannot import differing lineage without force" ,
333+ stateFile : func (mgr * State ) * statefile.File {
334+ return statefile .New (mgr .state , "different-lineage" , mgr .serial )
335+ },
336+ expectedError : `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"` ,
337+ },
338+ {
339+ name : "can import lesser serial with force" ,
340+ stateFile : func (mgr * State ) * statefile.File {
341+ return statefile .New (mgr .state , mgr .lineage , 1 )
342+ },
343+ expectedRequest : mockClientRequest {
344+ Method : "Put" ,
345+ Content : map [string ]interface {}{
346+ "version" : 4.0 ,
347+ "lineage" : "mock-lineage" ,
348+ "serial" : 2.0 ,
349+ "terraform_version" : version .Version ,
350+ "outputs" : map [string ]interface {}{"foo" : map [string ]interface {}{"type" : string ("string" ), "value" : string ("bar" )}},
351+ "resources" : []interface {}{},
352+ },
353+ },
354+ force : true ,
355+ },
356+ {
357+ name : "cannot import differing lineage without force" ,
358+ stateFile : func (mgr * State ) * statefile.File {
359+ return statefile .New (mgr .state , "different-lineage" , mgr .serial )
360+ },
361+ expectedRequest : mockClientRequest {
362+ Method : "Put" ,
363+ Content : map [string ]interface {}{
364+ "version" : 4.0 ,
365+ "lineage" : "different-lineage" ,
366+ "serial" : 3.0 ,
367+ "terraform_version" : version .Version ,
368+ "outputs" : map [string ]interface {}{"foo" : map [string ]interface {}{"type" : string ("string" ), "value" : string ("bar" )}},
369+ "resources" : []interface {}{},
370+ },
371+ },
372+ force : true ,
373+ },
374+ }
375+
376+ // In normal use (during a Terraform operation) we always refresh and read
377+ // before any writes would happen, so we'll mimic that here for realism.
378+ // NB This causes a GET to be logged so the first item in the test cases
379+ // must account for this
380+ if err := mgr .RefreshState (); err != nil {
381+ t .Fatalf ("failed to RefreshState: %s" , err )
382+ }
383+
384+ if err := mgr .WriteState (mgr .State ()); err != nil {
385+ t .Fatalf ("failed to write initial state: %s" , err )
386+ }
387+
388+ // Our client is a mockClient which has a log we
389+ // use to check that operations generate expected requests
390+ mockClient := mgr .Client .(* mockClient )
391+
392+ // logIdx tracks the current index of the log separate from
393+ // the loop iteration so we can check operations that don't
394+ // cause any requests to be generated
395+ logIdx := 0
396+
397+ for _ , tc := range testCases {
398+ sf := tc .stateFile (mgr )
399+ err := mgr .WriteStateForMigration (sf , tc .force )
400+ shouldError := tc .expectedError != ""
401+
402+ // If we are expecting and error check it and move on
403+ if shouldError {
404+ if err == nil {
405+ t .Fatalf ("test case %q should have failed with error %q" , tc .name , tc .expectedError )
406+ } else if err .Error () != tc .expectedError {
407+ t .Fatalf ("test case %q expected error %q but got %q" , tc .name , tc .expectedError , err )
408+ }
409+ continue
410+ }
411+
412+ if err != nil {
413+ t .Fatalf ("test case %q failed: %v" , tc .name , err )
414+ }
415+
416+ // At this point we should just do a normal write and persist
417+ // as would happen from the CLI
418+ mgr .WriteState (mgr .State ())
419+ mgr .PersistState ()
420+
421+ if logIdx >= len (mockClient .log ) {
422+ t .Fatalf ("request lock and index are out of sync on %q: idx=%d len=%d" , tc .name , logIdx , len (mockClient .log ))
423+ }
424+ loggedRequest := mockClient .log [logIdx ]
425+ logIdx ++
426+ if diff := cmp .Diff (tc .expectedRequest , loggedRequest ); len (diff ) > 0 {
427+ t .Fatalf ("incorrect client requests for %q:\n %s" , tc .name , diff )
428+ }
429+ }
430+
431+ logCnt := len (mockClient .log )
432+ if logIdx != logCnt {
433+ log .Fatalf ("not all requests were read. Expected logIdx to be %d but got %d" , logCnt , logIdx )
434+ }
435+ }
436+
437+ // This test runs the same test cases as above, but with
438+ // a client that implements EnableForcePush -- this allows
439+ // us to test that -force continues to work for backends without
440+ // this interface, but that this interface works for those that do.
441+ func TestWriteStateForMigrationWithForcePushClient (t * testing.T ) {
442+ mgr := & State {
443+ Client : & mockRemoteClient {
444+ current : []byte (`
445+ {
446+ "version": 4,
447+ "lineage": "mock-lineage",
448+ "serial": 3,
449+ "terraform_version":"0.0.0",
450+ "outputs": {"foo": {"value":"bar", "type": "string"}},
451+ "resources": []
452+ }
453+ ` ),
454+ },
455+ }
456+
305457 testCases := []migrationTestCase {
306458 // Refreshing state before we run the test loop causes a GET
307459 {
@@ -387,7 +539,7 @@ func TestWriteStateForMigration(t *testing.T) {
387539
388540 // Our client is a mockClient which has a log we
389541 // use to check that operations generate expected requests
390- mockClient := mgr .Client .(* mockClient )
542+ mockClient := mgr .Client .(* mockRemoteClient )
391543
392544 if mockClient .force {
393545 t .Fatalf ("client should not default to force" )
0 commit comments