// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package stackaddrs import ( "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/tfdiags" ) func TestParseRemovedFrom_Stacks(t *testing.T) { tcs := []struct { from string want StackInstance vars map[string]cty.Value parseDiags func() tfdiags.Diagnostics addrDiags func() tfdiags.Diagnostics }{ { from: "stack.stack_name", want: mustStackInstance(t, "stack.stack_name"), }, { from: "stack.parent.stack.child", want: mustStackInstance(t, "stack.parent.stack.child"), }, { from: "stack.parent[each.key].stack.child", want: mustStackInstance(t, "stack.parent[\"parent\"].stack.child"), vars: map[string]cty.Value{ "each": cty.MapVal(map[string]cty.Value{ "key": cty.StringVal("parent"), }), }, }, { from: "stack.parent.stack.child[each.key]", want: mustStackInstance(t, "stack.parent.stack.child[\"child\"]"), vars: map[string]cty.Value{ "each": cty.MapVal(map[string]cty.Value{ "key": cty.StringVal("child"), }), }, }, } for _, tc := range tcs { t.Run(tc.from, func(t *testing.T) { expr := mustExpr(t, tc.from) from, parseDiags := ParseRemovedFrom(expr) var wantParseDiags tfdiags.Diagnostics if tc.parseDiags != nil { wantParseDiags = tc.parseDiags() } tfdiags.AssertDiagnosticsMatch(t, parseDiags, wantParseDiags) if from.Component != nil { t.Fatal("from.Component should be empty") } configAddress := from.TargetStack() instanceAddress, addrDiags := from.TargetStackInstance(&hcl.EvalContext{ Variables: tc.vars, }, RootStackInstance) var wantAddrDiags tfdiags.Diagnostics if tc.addrDiags != nil { wantAddrDiags = tc.addrDiags() } tfdiags.AssertDiagnosticsMatch(t, addrDiags, wantAddrDiags) wantConfigAddress := tc.want.ConfigAddr() if diff := cmp.Diff(configAddress.String(), wantConfigAddress.String()); len(diff) > 0 { t.Errorf("wrong config address; %s", diff) } if diff := cmp.Diff(instanceAddress.String(), tc.want.String()); len(diff) > 0 { t.Errorf("wrong instance address: %s", diff) } }) } } func TestParseRemovedFrom_Components(t *testing.T) { tcs := []struct { from string want AbsComponentInstance vars map[string]cty.Value parseDiags func() tfdiags.Diagnostics addrDiags func() tfdiags.Diagnostics }{ { from: "component.component_name", want: mustAbsComponentInstance(t, "component.component_name"), }, { from: "component.component_name[0]", want: mustAbsComponentInstance(t, "component.component_name[0]"), }, { from: "component.component_name[\"key\"]", want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"), }, { from: "component.component_name[each.key]", want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "key": cty.StringVal("key"), }), }, }, { from: "component.component_name[each.value.attribute]", want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.ObjectVal(map[string]cty.Value{ "attribute": cty.StringVal("attribute"), }), }), }, }, { from: "component.component_name[each.value[\"key\"]]", want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.MapVal(map[string]cty.Value{ "key": cty.StringVal("key"), }), }), }, }, { from: "component.component_name[each.value[\"key\"].attribute]", want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.MapVal(map[string]cty.Value{ "key": cty.ObjectVal(map[string]cty.Value{ "attribute": cty.StringVal("attribute"), }), }), }), }, }, { from: "component.component_name[each.value[local.key]]", want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.MapVal(map[string]cty.Value{ "key": cty.StringVal("key"), }), }), "local": cty.ObjectVal(map[string]cty.Value{ "key": cty.StringVal("key"), }), }, }, { from: "component.component_name[each.value[local.key].attribute]", want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.MapVal(map[string]cty.Value{ "key": cty.ObjectVal(map[string]cty.Value{ "attribute": cty.StringVal("attribute"), }), }), }), "local": cty.ObjectVal(map[string]cty.Value{ "key": cty.StringVal("key"), }), }, }, { from: "stack.stack_name.component.component_name", want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name"), }, { from: "stack.parent.stack.child.component.component_name", want: mustAbsComponentInstance(t, "stack.parent.stack.child.component.component_name"), }, { from: "stack.stack_name[\"stack\"].component.component_name", want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name"), }, { from: "stack.stack_name.component.component_name[\"component\"]", want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name[\"component\"]"), }, { from: "stack.stack_name[\"stack\"].component.component_name[\"component\"]", want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"), }, { from: "stack.stack_name.component.component_name[each.value]", want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name[\"component\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.StringVal("component"), }), }, }, { from: "stack.stack_name[\"stack\"].component.component_name[each.value]", want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.StringVal("component"), }), }, }, { from: "stack.stack_name[each.value].component.component_name", want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.StringVal("stack"), }), }, }, { from: "stack.stack_name[each.value].component.component_name[\"component\"]", want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.StringVal("stack"), }), }, }, { from: "stack.stack_name[each.value[\"stack\"]].component.component_name[each.value[\"component\"]]", want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.ObjectVal(map[string]cty.Value{ "stack": cty.StringVal("stack"), "component": cty.StringVal("component"), }), }), }, }, { from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name", want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.ObjectVal(map[string]cty.Value{ "parent": cty.StringVal("parent"), "child": cty.StringVal("child"), "component": cty.StringVal("component"), }), }), }, }, { from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name[\"component\"]", want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name[\"component\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.ObjectVal(map[string]cty.Value{ "parent": cty.StringVal("parent"), "child": cty.StringVal("child"), }), }), }, }, { from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name[each.value[\"component\"]]", want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name[\"component\"]"), vars: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.ObjectVal(map[string]cty.Value{ "parent": cty.StringVal("parent"), "child": cty.StringVal("child"), "component": cty.StringVal("component"), }), }), }, }, { from: "component.component_name.attribute_key", parseDiags: func() tfdiags.Diagnostics { var diags tfdiags.Diagnostics diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid 'from' attribute", Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", Subject: &hcl.Range{ Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, End: hcl.Pos{Line: 1, Column: 39, Byte: 38}, }, }) return diags }, }, { from: "component.component_name[0].attribute_key", parseDiags: func() tfdiags.Diagnostics { var diags tfdiags.Diagnostics diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid 'from' attribute", Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", Subject: &hcl.Range{ Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, End: hcl.Pos{Line: 1, Column: 42, Byte: 41}, }, }) return diags }, }, { from: "component.component_name[\"key\"].attribute_key", parseDiags: func() tfdiags.Diagnostics { var diags tfdiags.Diagnostics diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid 'from' attribute", Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", Subject: &hcl.Range{ Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, End: hcl.Pos{Line: 1, Column: 46, Byte: 45}, }, }) return diags }, }, { from: "component.component_name[each.key].attribute_key", parseDiags: func() tfdiags.Diagnostics { var diags tfdiags.Diagnostics diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid 'from' attribute", Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", Subject: &hcl.Range{ Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, End: hcl.Pos{Line: 1, Column: 49, Byte: 48}, }, }) return diags }, }, { from: "component.component_name.attribute_key[0]", parseDiags: func() tfdiags.Diagnostics { var diags tfdiags.Diagnostics diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid 'from' attribute", Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", Subject: &hcl.Range{ Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, End: hcl.Pos{Line: 1, Column: 42, Byte: 41}, }, }) return diags }, }, { from: "component[0].component_name", parseDiags: func() tfdiags.Diagnostics { var diags tfdiags.Diagnostics diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid 'from' attribute", Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", Subject: &hcl.Range{ Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, }, }) return diags }, }, } for _, tc := range tcs { t.Run(tc.from, func(t *testing.T) { expr := mustExpr(t, tc.from) from, parseDiags := ParseRemovedFrom(expr) var wantParseDiags tfdiags.Diagnostics if tc.parseDiags != nil { wantParseDiags = tc.parseDiags() } tfdiags.AssertDiagnosticsMatch(t, parseDiags, wantParseDiags) if len(wantParseDiags) > 0 { return // don't do the rest of the test if we expected to fail here } if from.Component == nil { t.Fatal("from.Component should not be empty") } configAddress := from.TargetConfigComponent() instanceAddress, addrDiags := from.TargetAbsComponentInstance(&hcl.EvalContext{ Variables: tc.vars, }, RootStackInstance) var wantAddrDiags tfdiags.Diagnostics if tc.addrDiags != nil { wantAddrDiags = tc.addrDiags() } tfdiags.AssertDiagnosticsMatch(t, addrDiags, wantAddrDiags) wantConfigAddress := ConfigComponent{ Stack: tc.want.Stack.ConfigAddr(), Item: tc.want.Item.Component, } if diff := cmp.Diff(configAddress.String(), wantConfigAddress.String()); len(diff) > 0 { t.Errorf("wrong config address; %s", diff) } if diff := cmp.Diff(instanceAddress.String(), tc.want.String()); len(diff) > 0 { t.Errorf("wrong instance address: %s", diff) } }) } } func mustStackInstance(t *testing.T, str string) StackInstance { traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(str), "", hcl.InitialPos) if len(hclDiags) > 0 { t.Fatal(hclDiags.Error()) } inst, rest, diags := parseInStackInstancePrefix(traversal) if len(diags) > 0 { t.Fatal(diags.Err()) } if len(rest) > 0 { t.Fatal("invalid stack instance, has extra steps") } return inst } func mustAbsComponentInstance(t *testing.T, str string) AbsComponentInstance { inst, diags := ParseAbsComponentInstanceStr(str) if diags.HasErrors() { t.Fatal(diags.Err()) } return inst } func mustExpr(t *testing.T, expr string) hcl.Expression { ret, diags := hclsyntax.ParseExpression([]byte(expr), "", hcl.InitialPos) if diags.HasErrors() { t.Fatal(diags.Error()) } return ret }