// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package states import ( "fmt" "sync" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" "github.com/zclconf/go-cty/cty" ) func TestResourceInstanceObject_encode(t *testing.T) { value := cty.ObjectVal(map[string]cty.Value{ "foo": cty.True, "obj": cty.ObjectVal(map[string]cty.Value{ "sensitive": cty.StringVal("secret").Mark(marks.Sensitive), }), "sensitive_a": cty.StringVal("secret").Mark(marks.Sensitive), "sensitive_b": cty.StringVal("secret").Mark(marks.Sensitive), }) schema := providers.Schema{ Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.Bool, }, "obj": { Type: cty.Object(map[string]cty.Type{ "sensitive": cty.String, }), }, "sensitive_a": { Type: cty.String, }, "sensitive_b": { Type: cty.String, }, }, }, Version: 0, } // The in-memory order of resource dependencies is random, since they're an // unordered set. depsOne := []addrs.ConfigResource{ addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "honk"), addrs.RootModule.Child("child").Resource(addrs.ManagedResourceMode, "test", "flub"), addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "boop"), } depsTwo := []addrs.ConfigResource{ addrs.RootModule.Child("child").Resource(addrs.ManagedResourceMode, "test", "flub"), addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "boop"), addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "honk"), } // multiple instances may have been assigned the same deps slice objs := []*ResourceInstanceObject{ { Value: value, Status: ObjectPlanned, Dependencies: depsOne, }, { Value: value, Status: ObjectPlanned, Dependencies: depsTwo, }, { Value: value, Status: ObjectPlanned, Dependencies: depsOne, }, { Value: value, Status: ObjectPlanned, Dependencies: depsOne, }, } var encoded []*ResourceInstanceObjectSrc // Encoding can happen concurrently, so we need to make sure the shared // Dependencies are safely handled var wg sync.WaitGroup var mu sync.Mutex for _, obj := range objs { wg.Go(func() { rios, err := obj.Encode(schema) if err != nil { t.Errorf("unexpected error: %s", err) } mu.Lock() encoded = append(encoded, rios) mu.Unlock() }) } wg.Wait() // However, identical sets of dependencies should always be written to state // in an identical order, so we don't do meaningless state updates on refresh. for i := 0; i < len(encoded)-1; i++ { if diff := cmp.Diff(encoded[i].Dependencies, encoded[i+1].Dependencies); diff != "" { t.Errorf("identical dependencies got encoded in different orders:\n%s", diff) } } // sensitive paths must also be consistent got comparison for i := 0; i < len(encoded)-1; i++ { a, b := fmt.Sprintf("%#v", encoded[i].AttrSensitivePaths), fmt.Sprintf("%#v", encoded[i+1].AttrSensitivePaths) if diff := cmp.Diff(a, b); diff != "" { t.Errorf("sensitive paths got encoded in different orders:\n%s", diff) } } } func TestResourceInstanceObject_encodeInvalidMarks(t *testing.T) { value := cty.ObjectVal(map[string]cty.Value{ // State only supports a subset of marks that we know how to persist // between plan/apply rounds. All values with other marks must be // replaced with unmarked placeholders before attempting to store the // value in the state. "foo": cty.True.Mark("unsupported"), }) schema := providers.Schema{ Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.Bool, }, }, }, Version: 0, } obj := &ResourceInstanceObject{ Value: value, Status: ObjectReady, } _, err := obj.Encode(schema) if err == nil { t.Fatalf("unexpected success; want error") } got := err.Error() want := `.foo: cannot serialize value marked as cty.NewValueMarks("unsupported") for inclusion in a state snapshot (this is a bug in Terraform)` if got != want { t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) } }