@@ -14,6 +14,7 @@ import (
1414 testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
1515 "github.com/hashicorp/terraform/internal/states"
1616 "github.com/hashicorp/terraform/internal/states/statefile"
17+ "github.com/hashicorp/terraform/internal/states/statemgr"
1718 "github.com/hashicorp/terraform/internal/tfdiags"
1819 "github.com/zclconf/go-cty/cty"
1920)
@@ -271,3 +272,244 @@ func Test_grpcClient_Delete(t *testing.T) {
271272 t .Fatal ("expected Delete method to call DeleteState method on underlying provider, but it has not been called" )
272273 }
273274}
275+
276+ // Testing grpcClient's Lock method.
277+ // The Lock method on a state manager calls the Lock method of the underlying client.
278+ func Test_grpcClient_Lock (t * testing.T ) {
279+ typeName := "foo_bar" // state store 'bar' in provider 'foo'
280+ stateId := "production"
281+ operation := "apply"
282+ lockInfo := & statemgr.LockInfo {
283+ Operation : operation ,
284+ // This is sufficient when locking via PSS
285+ }
286+
287+ t .Run ("state manager made using grpcClient sends expected values to Lock method" , func (t * testing.T ) {
288+ expectedLockId := "id-from-mock"
289+ provider := testing_provider.MockProvider {
290+ // Mock a provider and internal state store that
291+ // have both been configured
292+ ConfigureProviderCalled : true ,
293+ ConfigureStateStoreCalled : true ,
294+
295+ // Check values received by the provider from the Lock method.
296+ LockStateFn : func (req providers.LockStateRequest ) providers.LockStateResponse {
297+ if req .TypeName != typeName || req .StateId != stateId || req .Operation != operation {
298+ t .Fatalf ("expected provider ReadStateBytes method to receive TypeName %q, StateId %q, and Operation %q, instead got TypeName %q, StateId %q, and Operation %q" ,
299+ typeName ,
300+ stateId ,
301+ operation ,
302+ req .TypeName ,
303+ req .StateId ,
304+ req .Operation ,
305+ )
306+ }
307+ return providers.LockStateResponse {
308+ LockId : expectedLockId ,
309+ }
310+ },
311+ }
312+
313+ // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
314+ // and invoke the method on that interface that uses Lock.
315+ c := NewRemoteGRPC (& provider , typeName , stateId )
316+
317+ lockId , err := c .Lock (lockInfo )
318+ if err != nil {
319+ t .Fatalf ("unexpected error: %s" , err )
320+ }
321+
322+ if ! provider .LockStateCalled {
323+ t .Fatal ("expected remote grpc state manager's Lock method to call Lock method on underlying provider, but it has not been called" )
324+ }
325+ if lockId != expectedLockId {
326+ t .Fatalf ("unexpected lock id returned, wanted %q, got %q" , expectedLockId , lockId )
327+ }
328+ })
329+
330+ t .Run ("state manager made using grpcClient returns expected error from Lock method's error diagnostic" , func (t * testing.T ) {
331+ var diags tfdiags.Diagnostics
332+ diags = diags .Append (& hcl.Diagnostic {
333+ Severity : hcl .DiagError ,
334+ Summary : "error forced from test" ,
335+ Detail : "error forced from test" ,
336+ })
337+
338+ provider := testing_provider.MockProvider {
339+ // Mock a provider and internal state store that
340+ // have both been configured
341+ ConfigureProviderCalled : true ,
342+ ConfigureStateStoreCalled : true ,
343+
344+ // Force return of an error.
345+ LockStateResponse : providers.LockStateResponse {
346+ Diagnostics : diags ,
347+ },
348+ }
349+
350+ // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
351+ // and invoke the method on that interface that uses Lock.
352+ c := NewRemoteGRPC (& provider , typeName , stateId )
353+
354+ _ , err := c .Lock (lockInfo )
355+ if ! provider .LockStateCalled {
356+ t .Fatal ("expected remote grpc state manager's Lock method to call Lock method on underlying provider, but it has not been called" )
357+ }
358+ if err == nil {
359+ t .Fatal ("expected error but got none" )
360+ }
361+ expectedMsg := "error forced from test"
362+ if ! strings .Contains (err .Error (), expectedMsg ) {
363+ t .Fatalf ("expected error to include %q but got: %s" , expectedMsg , err )
364+ }
365+ })
366+
367+ t .Run ("state manager made using grpcClient currently swallows warning diagnostics returned from the Lock method" , func (t * testing.T ) {
368+ var diags tfdiags.Diagnostics
369+ diags = diags .Append (& hcl.Diagnostic {
370+ Severity : hcl .DiagWarning ,
371+ Summary : "warning forced from test" ,
372+ Detail : "warning forced from test" ,
373+ })
374+
375+ provider := testing_provider.MockProvider {
376+ // Mock a provider and internal state store that
377+ // have both been configured
378+ ConfigureProviderCalled : true ,
379+ ConfigureStateStoreCalled : true ,
380+
381+ // Force return of a warning.
382+ LockStateResponse : providers.LockStateResponse {
383+ Diagnostics : diags ,
384+ },
385+ }
386+
387+ c := NewRemoteGRPC (& provider , typeName , stateId )
388+
389+ _ , err := c .Lock (lockInfo )
390+ if err != nil {
391+ t .Fatalf ("unexpected error: %s" , err )
392+ }
393+
394+ // The warning is swallowed by the Lock method.
395+ // The Locker interface should be updated to allow use of diagnostics instead of errors,
396+ // and this test should be updated.
397+ })
398+ }
399+
400+ // Testing grpcClient's Unlock method.
401+ // The Unlock method on a state manager calls the Unlock method of the underlying client.
402+ func Test_grpcClient_Unlock (t * testing.T ) {
403+ typeName := "foo_bar" // state store 'bar' in provider 'foo'
404+ stateId := "production"
405+
406+ t .Run ("state manager made using grpcClient sends expected values to Unlock method" , func (t * testing.T ) {
407+ expectedLockId := "id-from-mock"
408+ provider := testing_provider.MockProvider {
409+ // Mock a provider and internal state store that
410+ // have both been configured
411+ ConfigureProviderCalled : true ,
412+ ConfigureStateStoreCalled : true ,
413+
414+ // Check values received by the provider from the Lock method.
415+ UnlockStateFn : func (req providers.UnlockStateRequest ) providers.UnlockStateResponse {
416+ if req .TypeName != typeName || req .StateId != stateId || req .LockId != expectedLockId {
417+ t .Fatalf ("expected provider ReadStateBytes method to receive TypeName %q, StateId %q, and LockId %q, instead got TypeName %q, StateId %q, and LockId %q" ,
418+ typeName ,
419+ stateId ,
420+ expectedLockId ,
421+ req .TypeName ,
422+ req .StateId ,
423+ req .LockId ,
424+ )
425+ }
426+ return providers.UnlockStateResponse {}
427+ },
428+ }
429+
430+ // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
431+ // and invoke the method on that interface that uses Unlock.
432+ c := NewRemoteGRPC (& provider , typeName , stateId )
433+
434+ err := c .Unlock (expectedLockId )
435+ if err != nil {
436+ t .Fatalf ("unexpected error: %s" , err )
437+ }
438+ if ! provider .UnlockStateCalled {
439+ t .Fatal ("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called" )
440+ }
441+
442+ })
443+
444+ t .Run ("state manager made using grpcClient returns expected error from Unlock method's error diagnostic" , func (t * testing.T ) {
445+ var diags tfdiags.Diagnostics
446+ diags = diags .Append (& hcl.Diagnostic {
447+ Severity : hcl .DiagError ,
448+ Summary : "error forced from test" ,
449+ Detail : "error forced from test" ,
450+ })
451+
452+ provider := testing_provider.MockProvider {
453+ // Mock a provider and internal state store that
454+ // have both been configured
455+ ConfigureProviderCalled : true ,
456+ ConfigureStateStoreCalled : true ,
457+
458+ // Force return of an error.
459+ UnlockStateResponse : providers.UnlockStateResponse {
460+ Diagnostics : diags ,
461+ },
462+ }
463+
464+ // This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
465+ // and invoke the method on that interface that uses Unlock.
466+ c := NewRemoteGRPC (& provider , typeName , stateId )
467+
468+ err := c .Unlock ("foobar" ) // argument used here isn't important in this test
469+ if ! provider .UnlockStateCalled {
470+ t .Fatal ("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called" )
471+ }
472+ if err == nil {
473+ t .Fatal ("expected error but got none" )
474+ }
475+ expectedMsg := "error forced from test"
476+ if ! strings .Contains (err .Error (), expectedMsg ) {
477+ t .Fatalf ("expected error to include %q but got: %s" , expectedMsg , err )
478+ }
479+ })
480+
481+ t .Run ("state manager made using grpcClient currently swallows warning diagnostics returned from the Unlock method" , func (t * testing.T ) {
482+ var diags tfdiags.Diagnostics
483+ diags = diags .Append (& hcl.Diagnostic {
484+ Severity : hcl .DiagWarning ,
485+ Summary : "warning forced from test" ,
486+ Detail : "warning forced from test" ,
487+ })
488+
489+ provider := testing_provider.MockProvider {
490+ // Mock a provider and internal state store that
491+ // have both been configured
492+ ConfigureProviderCalled : true ,
493+ ConfigureStateStoreCalled : true ,
494+
495+ // Force return of a warning.
496+ UnlockStateResponse : providers.UnlockStateResponse {
497+ Diagnostics : diags ,
498+ },
499+ }
500+
501+ c := NewRemoteGRPC (& provider , typeName , stateId )
502+
503+ err := c .Unlock ("foobar" ) // argument used here isn't important in this test
504+ if ! provider .UnlockStateCalled {
505+ t .Fatal ("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called" )
506+ }
507+ if err != nil {
508+ t .Fatalf ("unexpected error: %s" , err )
509+ }
510+
511+ // The warning is swallowed by the Unlock method.
512+ // The Locker interface should be updated to allow use of diagnostics instead of errors,
513+ // and this test should be updated.
514+ })
515+ }
0 commit comments