| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackstate | |
| import ( | |
| "encoding/json" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/zclconf/go-cty/cty" | |
| ctymsgpack "github.com/zclconf/go-cty/cty/msgpack" | |
| "google.golang.org/protobuf/proto" | |
| "google.golang.org/protobuf/testing/protocmp" | |
| "google.golang.org/protobuf/types/known/anypb" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/plans/planproto" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" | |
| "github.com/hashicorp/terraform/internal/stacks/stackaddrs" | |
| "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" | |
| "github.com/hashicorp/terraform/internal/states" | |
| ) | |
| func TestAppliedChangeAsProto(t *testing.T) { | |
| tests := map[string]struct { | |
| Receiver AppliedChange | |
| Want *stacks.AppliedChange | |
| }{ | |
| "resource instance": { | |
| Receiver: &AppliedChangeResourceInstanceObject{ | |
| ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ | |
| Component: stackaddrs.AbsComponentInstance{ | |
| Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), | |
| Item: stackaddrs.ComponentInstance{ | |
| Component: stackaddrs.Component{Name: "foo"}, | |
| Key: addrs.StringKey("beep"), | |
| }, | |
| }, | |
| Item: addrs.AbsResourceInstanceObject{ | |
| ResourceInstance: addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "thingy", | |
| Name: "thingamajig", | |
| }.Instance(addrs.IntKey(1)).Absolute( | |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), | |
| ), | |
| }, | |
| }, | |
| ProviderConfigAddr: addrs.AbsProviderConfig{ | |
| Module: addrs.RootModule, | |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), | |
| }, | |
| Schema: providers.Schema{ | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": { | |
| Type: cty.String, | |
| Required: true, | |
| }, | |
| "secret": { | |
| Type: cty.String, | |
| Sensitive: true, | |
| }, | |
| }, | |
| }, | |
| }, | |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: []byte(`{"id":"bar","secret":"top"}`), | |
| AttrSensitivePaths: []cty.Path{ | |
| cty.GetAttrPath("secret"), | |
| }, | |
| }, | |
| }, | |
| Want: &stacks.AppliedChange{ | |
| Raw: []*stacks.AppliedChange_RawChange{ | |
| { | |
| Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, | |
| Value: mustMarshalAnyPb(t, &tfstackdata1.StateResourceInstanceObjectV1{ | |
| ValueJson: json.RawMessage(`{"id":"bar","secret":"top"}`), | |
| SensitivePaths: []*planproto.Path{ | |
| { | |
| Steps: []*planproto.Path_Step{{ | |
| Selector: &planproto.Path_Step_AttributeName{AttributeName: "secret"}}}, | |
| }, | |
| }, | |
| ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, | |
| Status: tfstackdata1.StateResourceInstanceObjectV1_READY, | |
| }), | |
| }, | |
| }, | |
| Descriptions: []*stacks.AppliedChange_ChangeDescription{ | |
| { | |
| Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, | |
| Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{ | |
| ResourceInstance: &stacks.AppliedChange_ResourceInstance{ | |
| Addr: &stacks.ResourceInstanceObjectInStackAddr{ | |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, | |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.thingamajig[1]`, | |
| }, | |
| NewValue: &stacks.DynamicValue{ | |
| Msgpack: mustMsgpack(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("bar"), | |
| "secret": cty.StringVal("top"), | |
| }), cty.Object(map[string]cty.Type{"id": cty.String, "secret": cty.String})), | |
| Sensitive: []*stacks.AttributePath{{ | |
| Steps: []*stacks.AttributePath_Step{{ | |
| Selector: &stacks.AttributePath_Step_AttributeName{AttributeName: "secret"}, | |
| }}}, | |
| }, | |
| }, | |
| ResourceMode: stacks.ResourceMode_MANAGED, | |
| ResourceType: "thingy", | |
| ProviderAddr: "example.com/thingers/thingy", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| "moved_resource instance": { | |
| Receiver: &AppliedChangeResourceInstanceObject{ | |
| ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ | |
| Component: stackaddrs.AbsComponentInstance{ | |
| Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), | |
| Item: stackaddrs.ComponentInstance{ | |
| Component: stackaddrs.Component{Name: "foo"}, | |
| Key: addrs.StringKey("beep"), | |
| }, | |
| }, | |
| Item: addrs.AbsResourceInstanceObject{ | |
| ResourceInstance: addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "thingy", | |
| Name: "thingamajig", | |
| }.Instance(addrs.IntKey(1)).Absolute( | |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), | |
| ), | |
| }, | |
| }, | |
| PreviousResourceInstanceObjectAddr: &stackaddrs.AbsResourceInstanceObject{ | |
| Component: stackaddrs.AbsComponentInstance{ | |
| Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), | |
| Item: stackaddrs.ComponentInstance{ | |
| Component: stackaddrs.Component{Name: "foo"}, | |
| Key: addrs.StringKey("beep"), | |
| }, | |
| }, | |
| Item: addrs.AbsResourceInstanceObject{ | |
| ResourceInstance: addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "thingy", | |
| Name: "previous_thingamajig", | |
| }.Instance(addrs.IntKey(1)).Absolute( | |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), | |
| ), | |
| }, | |
| }, | |
| ProviderConfigAddr: addrs.AbsProviderConfig{ | |
| Module: addrs.RootModule, | |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), | |
| }, | |
| Schema: providers.Schema{ | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": { | |
| Type: cty.String, | |
| Required: true, | |
| }, | |
| "secret": { | |
| Type: cty.String, | |
| Sensitive: true, | |
| }, | |
| }, | |
| }, | |
| }, | |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: []byte(`{"id":"bar","secret":"top"}`), | |
| AttrSensitivePaths: []cty.Path{ | |
| cty.GetAttrPath("secret"), | |
| }, | |
| }, | |
| }, | |
| Want: &stacks.AppliedChange{ | |
| Raw: []*stacks.AppliedChange_RawChange{ | |
| { | |
| Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.previous_thingamajig[1],cur`, | |
| Value: nil, | |
| }, | |
| { | |
| Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, | |
| Value: mustMarshalAnyPb(t, &tfstackdata1.StateResourceInstanceObjectV1{ | |
| ValueJson: json.RawMessage(`{"id":"bar","secret":"top"}`), | |
| SensitivePaths: []*planproto.Path{ | |
| { | |
| Steps: []*planproto.Path_Step{{ | |
| Selector: &planproto.Path_Step_AttributeName{AttributeName: "secret"}}}, | |
| }, | |
| }, | |
| ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, | |
| Status: tfstackdata1.StateResourceInstanceObjectV1_READY, | |
| }), | |
| }, | |
| }, | |
| Descriptions: []*stacks.AppliedChange_ChangeDescription{ | |
| { | |
| Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.previous_thingamajig[1],cur`, | |
| Description: &stacks.AppliedChange_ChangeDescription_Moved{ | |
| Moved: &stacks.AppliedChange_Nothing{}, | |
| }, | |
| }, | |
| { | |
| Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, | |
| Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{ | |
| ResourceInstance: &stacks.AppliedChange_ResourceInstance{ | |
| Addr: &stacks.ResourceInstanceObjectInStackAddr{ | |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, | |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.thingamajig[1]`, | |
| }, | |
| NewValue: &stacks.DynamicValue{ | |
| Msgpack: mustMsgpack(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("bar"), | |
| "secret": cty.StringVal("top"), | |
| }), cty.Object(map[string]cty.Type{"id": cty.String, "secret": cty.String})), | |
| Sensitive: []*stacks.AttributePath{{ | |
| Steps: []*stacks.AttributePath_Step{{ | |
| Selector: &stacks.AttributePath_Step_AttributeName{AttributeName: "secret"}, | |
| }}}, | |
| }, | |
| }, | |
| ResourceMode: stacks.ResourceMode_MANAGED, | |
| ResourceType: "thingy", | |
| ProviderAddr: "example.com/thingers/thingy", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| got, err := test.Receiver.AppliedChangeProto() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if diff := cmp.Diff(test.Want, got, protocmp.Transform()); diff != "" { | |
| t.Errorf("wrong result\n%s", diff) | |
| } | |
| }) | |
| } | |
| } | |
| func mustMarshalAnyPb(t *testing.T, msg proto.Message) *anypb.Any { | |
| var ret anypb.Any | |
| err := anypb.MarshalFrom(&ret, msg, proto.MarshalOptions{}) | |
| if err != nil { | |
| t.Fatalf("error marshalling anypb: %q", err) | |
| } | |
| return &ret | |
| } | |
| func mustMsgpack(t *testing.T, v cty.Value, ty cty.Type) []byte { | |
| t.Helper() | |
| ret, err := ctymsgpack.Marshal(v, ty) | |
| if err != nil { | |
| t.Fatalf("error marshalling %#v: %s", v, err) | |
| } | |
| return ret | |
| } |