| // Copyright IBM Corp. 2014, 2026 |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package remote |
| |
| import ( |
| "bytes" |
| "strings" |
| "testing" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/providers" |
| testing_provider "github.com/hashicorp/terraform/internal/providers/testing" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/states/statefile" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // Testing grpcClient's Get method, via the state manager made using a grpcClient. |
| // The RefreshState method on a state manager calls the Get method of the underlying client. |
| func Test_grpcClient_Get(t *testing.T) { |
| typeName := "foo_bar" // state store 'bar' in provider 'foo' |
| stateId := "production" |
| stateString := `{ |
| "version": 4, |
| "terraform_version": "0.13.0", |
| "serial": 0, |
| "lineage": "", |
| "outputs": { |
| "foo": { |
| "value": "bar", |
| "type": "string" |
| } |
| } |
| }` |
| |
| t.Run("state manager made using grpcClient returns expected state", func(t *testing.T) { |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Check values received by the provider from the Get method. |
| ReadStateBytesFn: func(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse { |
| if req.TypeName != typeName || req.StateId != stateId { |
| t.Fatalf("expected provider ReadStateBytes method to receive TypeName %q and StateId %q, instead got TypeName %q and StateId %q", |
| typeName, |
| stateId, |
| req.TypeName, |
| req.StateId) |
| } |
| return providers.ReadStateBytesResponse{ |
| Bytes: []byte(stateString), |
| // no diags |
| } |
| }, |
| } |
| |
| // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC |
| // and invoke the method on that interface that uses Get. |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| err := c.RefreshState() // Calls Get |
| if err != nil { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| |
| if !provider.ReadStateBytesCalled { |
| t.Fatal("expected remote grpc state manager's RefreshState method to, via Get, call ReadStateBytes method on underlying provider, but it has not been called") |
| } |
| s := c.State() |
| v, ok := s.RootOutputValues["foo"] |
| if !ok { |
| t.Fatal("state manager doesn't contain the state returned by the mock") |
| } |
| if v.Value.AsString() != "bar" { |
| t.Fatal("state manager doesn't contain the correct output value in the state") |
| } |
| }) |
| |
| t.Run("state manager made using grpcClient returns expected error from error diagnostic", func(t *testing.T) { |
| var diags tfdiags.Diagnostics |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "error forced from test", |
| Detail: "error forced from test", |
| }) |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Force an error diagnostic |
| ReadStateBytesFn: func(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse { |
| return providers.ReadStateBytesResponse{ |
| // we don't expect state to accompany an error, but this test shows that |
| // if an error us present amy state returned is ignored. |
| Bytes: []byte(stateString), |
| Diagnostics: diags, |
| } |
| }, |
| } |
| |
| // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC |
| // and invoke the method on that interface that uses Get. |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| err := c.RefreshState() // Calls Get |
| if err == nil { |
| t.Fatal("expected an error but got none") |
| } |
| |
| if !provider.ReadStateBytesCalled { |
| t.Fatal("expected remote grpc state manager's RefreshState method to, via Get, call ReadStateBytes method on underlying provider, but it has not been called") |
| } |
| s := c.State() |
| if s != nil { |
| t.Fatalf("expected refresh to fail due to error diagnostic, but state has been refreshed: %s", s.String()) |
| } |
| }) |
| } |
| |
| // Testing grpcClient's Put method, via the state manager made using a grpcClient. |
| // The PersistState method on a state manager calls the Put method of the underlying client. |
| func Test_grpcClient_Put(t *testing.T) { |
| typeName := "foo_bar" // state store 'bar' in provider 'foo' |
| stateId := "production" |
| |
| // State with 1 output |
| s := states.NewState() |
| s.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "foo"}, |
| }, cty.StringVal("bar"), false) |
| |
| t.Run("state manager made using grpcClient writes the expected state", func(t *testing.T) { |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Check values received by the provider from the Put method. |
| WriteStateBytesFn: func(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse { |
| if req.TypeName != typeName || req.StateId != stateId { |
| t.Fatalf("expected provider WriteStateBytes method to receive TypeName %q and StateId %q, instead got TypeName %q and StateId %q", |
| typeName, |
| stateId, |
| req.TypeName, |
| req.StateId) |
| } |
| |
| r := bytes.NewReader(req.Bytes) |
| reqState, err := statefile.Read(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if reqState.State.String() != s.String() { |
| t.Fatalf("wanted state %s got %s", s.String(), reqState.State.String()) |
| } |
| return providers.WriteStateBytesResponse{ |
| // no diags |
| } |
| }, |
| } |
| |
| // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC |
| // and invoke the method on that interface that uses Put. |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| // Set internal state value that will be persisted. |
| c.WriteState(s) |
| |
| // Test PersistState, which uses Put. |
| err := c.PersistState(nil) |
| if err != nil { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| }) |
| |
| t.Run("state manager made using grpcClient returns expected error from error diagnostic", func(t *testing.T) { |
| expectedErr := "error forced from test" |
| var diags tfdiags.Diagnostics |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: expectedErr, |
| Detail: expectedErr, |
| }) |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Force an error diagnostic |
| WriteStateBytesFn: func(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse { |
| return providers.WriteStateBytesResponse{ |
| Diagnostics: diags, |
| } |
| }, |
| } |
| |
| // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC |
| // and invoke the method on that interface that uses Get. |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| // Set internal state value that will be persisted. |
| c.WriteState(s) |
| |
| // Test PersistState, which uses Put. |
| err := c.PersistState(nil) |
| if err == nil { |
| t.Fatalf("expected error but got none") |
| } |
| if !strings.Contains(err.Error(), expectedErr) { |
| t.Fatalf("expected error to contain %q, but got: %s", expectedErr, err.Error()) |
| } |
| }) |
| |
| t.Run("grpcClient refuses zero-byte writes", func(t *testing.T) { |
| provider := testing_provider.MockProvider{ |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| WriteStateBytesFn: func(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse { |
| t.Fatal("expected WriteStateBytes not to be called for zero-byte payload") |
| return providers.WriteStateBytesResponse{} |
| }, |
| } |
| |
| client := &grpcClient{ |
| provider: &provider, |
| typeName: typeName, |
| stateId: stateId, |
| } |
| |
| diags := client.Put(nil) |
| if !diags.HasErrors() { |
| t.Fatalf("expected diagnostics when attempting to write zero bytes") |
| } |
| if provider.WriteStateBytesCalled { |
| t.Fatalf("provider WriteStateBytes should not be called") |
| } |
| }) |
| } |
| |
| // Testing grpcClient's Delete method. |
| // This method is needed to implement the remote.Client interface, but |
| // this is not invoked by the remote state manager (remote.State) that |
| // will contain the client. |
| // |
| // In future we should remove the need for a Delete method in |
| // remote.Client, but for now it is implemented and tested. |
| func Test_grpcClient_Delete(t *testing.T) { |
| typeName := "foo_bar" // state store 'bar' in provider 'foo' |
| stateId := "production" |
| |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Check values received by the provider from the Delete method. |
| DeleteStateFn: func(req providers.DeleteStateRequest) providers.DeleteStateResponse { |
| if req.TypeName != typeName || req.StateId != stateId { |
| t.Fatalf("expected provider DeleteState method to receive TypeName %q and StateId %q, instead got TypeName %q and StateId %q", |
| typeName, |
| stateId, |
| req.TypeName, |
| req.StateId) |
| } |
| return providers.DeleteStateResponse{ |
| // no diags |
| } |
| }, |
| } |
| |
| // Delete isn't accessible via a statemgr.Full, so we don't use NewRemoteGRPC. |
| // See comment above test for more information. |
| c := grpcClient{ |
| provider: &provider, |
| typeName: typeName, |
| stateId: stateId, |
| } |
| |
| diags := c.Delete() |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error: %s", diags.Err()) |
| } |
| |
| if !provider.DeleteStateCalled { |
| t.Fatal("expected Delete method to call DeleteState method on underlying provider, but it has not been called") |
| } |
| } |
| |
| // Testing grpcClient's Lock method. |
| // The Lock method on a state manager calls the Lock method of the underlying client. |
| func Test_grpcClient_Lock(t *testing.T) { |
| typeName := "foo_bar" // state store 'bar' in provider 'foo' |
| stateId := "production" |
| operation := "apply" |
| lockInfo := &statemgr.LockInfo{ |
| Operation: operation, |
| // This is sufficient when locking via PSS |
| } |
| |
| t.Run("state manager made using grpcClient sends expected values to Lock method", func(t *testing.T) { |
| expectedLockId := "id-from-mock" |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Check values received by the provider from the Lock method. |
| LockStateFn: func(req providers.LockStateRequest) providers.LockStateResponse { |
| if req.TypeName != typeName || req.StateId != stateId || req.Operation != operation { |
| t.Fatalf("expected provider ReadStateBytes method to receive TypeName %q, StateId %q, and Operation %q, instead got TypeName %q, StateId %q, and Operation %q", |
| typeName, |
| stateId, |
| operation, |
| req.TypeName, |
| req.StateId, |
| req.Operation, |
| ) |
| } |
| return providers.LockStateResponse{ |
| LockId: expectedLockId, |
| } |
| }, |
| } |
| |
| // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC |
| // and invoke the method on that interface that uses Lock. |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| lockId, err := c.Lock(lockInfo) |
| if err != nil { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| |
| if !provider.LockStateCalled { |
| t.Fatal("expected remote grpc state manager's Lock method to call Lock method on underlying provider, but it has not been called") |
| } |
| if lockId != expectedLockId { |
| t.Fatalf("unexpected lock id returned, wanted %q, got %q", expectedLockId, lockId) |
| } |
| }) |
| |
| t.Run("state manager made using grpcClient returns expected error from Lock method's error diagnostic", func(t *testing.T) { |
| var diags tfdiags.Diagnostics |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "error forced from test", |
| Detail: "error forced from test", |
| }) |
| |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Force return of an error. |
| LockStateResponse: providers.LockStateResponse{ |
| Diagnostics: diags, |
| }, |
| } |
| |
| // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC |
| // and invoke the method on that interface that uses Lock. |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| _, err := c.Lock(lockInfo) |
| if !provider.LockStateCalled { |
| t.Fatal("expected remote grpc state manager's Lock method to call Lock method on underlying provider, but it has not been called") |
| } |
| if err == nil { |
| t.Fatal("expected error but got none") |
| } |
| expectedMsg := "error forced from test" |
| if !strings.Contains(err.Error(), expectedMsg) { |
| t.Fatalf("expected error to include %q but got: %s", expectedMsg, err) |
| } |
| }) |
| |
| t.Run("state manager made using grpcClient currently swallows warning diagnostics returned from the Lock method", func(t *testing.T) { |
| var diags tfdiags.Diagnostics |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagWarning, |
| Summary: "warning forced from test", |
| Detail: "warning forced from test", |
| }) |
| |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Force return of a warning. |
| LockStateResponse: providers.LockStateResponse{ |
| Diagnostics: diags, |
| }, |
| } |
| |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| _, err := c.Lock(lockInfo) |
| if err != nil { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| |
| // The warning is swallowed by the Lock method. |
| // The Locker interface should be updated to allow use of diagnostics instead of errors, |
| // and this test should be updated. |
| }) |
| } |
| |
| // Testing grpcClient's Unlock method. |
| // The Unlock method on a state manager calls the Unlock method of the underlying client. |
| func Test_grpcClient_Unlock(t *testing.T) { |
| typeName := "foo_bar" // state store 'bar' in provider 'foo' |
| stateId := "production" |
| |
| t.Run("state manager made using grpcClient sends expected values to Unlock method", func(t *testing.T) { |
| expectedLockId := "id-from-mock" |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Check values received by the provider from the Lock method. |
| UnlockStateFn: func(req providers.UnlockStateRequest) providers.UnlockStateResponse { |
| if req.TypeName != typeName || req.StateId != stateId || req.LockId != expectedLockId { |
| t.Fatalf("expected provider ReadStateBytes method to receive TypeName %q, StateId %q, and LockId %q, instead got TypeName %q, StateId %q, and LockId %q", |
| typeName, |
| stateId, |
| expectedLockId, |
| req.TypeName, |
| req.StateId, |
| req.LockId, |
| ) |
| } |
| return providers.UnlockStateResponse{} |
| }, |
| } |
| |
| // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC |
| // and invoke the method on that interface that uses Unlock. |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| err := c.Unlock(expectedLockId) |
| if err != nil { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| if !provider.UnlockStateCalled { |
| t.Fatal("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called") |
| } |
| |
| }) |
| |
| t.Run("state manager made using grpcClient returns expected error from Unlock method's error diagnostic", func(t *testing.T) { |
| var diags tfdiags.Diagnostics |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "error forced from test", |
| Detail: "error forced from test", |
| }) |
| |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Force return of an error. |
| UnlockStateResponse: providers.UnlockStateResponse{ |
| Diagnostics: diags, |
| }, |
| } |
| |
| // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC |
| // and invoke the method on that interface that uses Unlock. |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| err := c.Unlock("foobar") // argument used here isn't important in this test |
| if !provider.UnlockStateCalled { |
| t.Fatal("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called") |
| } |
| if err == nil { |
| t.Fatal("expected error but got none") |
| } |
| expectedMsg := "error forced from test" |
| if !strings.Contains(err.Error(), expectedMsg) { |
| t.Fatalf("expected error to include %q but got: %s", expectedMsg, err) |
| } |
| }) |
| |
| t.Run("state manager made using grpcClient currently swallows warning diagnostics returned from the Unlock method", func(t *testing.T) { |
| var diags tfdiags.Diagnostics |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagWarning, |
| Summary: "warning forced from test", |
| Detail: "warning forced from test", |
| }) |
| |
| provider := testing_provider.MockProvider{ |
| // Mock a provider and internal state store that |
| // have both been configured |
| ConfigureProviderCalled: true, |
| ConfigureStateStoreCalled: true, |
| |
| // Force return of a warning. |
| UnlockStateResponse: providers.UnlockStateResponse{ |
| Diagnostics: diags, |
| }, |
| } |
| |
| c := NewRemoteGRPC(&provider, typeName, stateId) |
| |
| err := c.Unlock("foobar") // argument used here isn't important in this test |
| if !provider.UnlockStateCalled { |
| t.Fatal("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called") |
| } |
| if err != nil { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| |
| // The warning is swallowed by the Unlock method. |
| // The Locker interface should be updated to allow use of diagnostics instead of errors, |
| // and this test should be updated. |
| }) |
| } |