// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package dynblock import ( "strings" "testing" "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/hcl/v2/hcltest" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" ) func TestExpand(t *testing.T) { srcBody := hcltest.MockBody(&hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "a", Labels: []string{"static0"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprLiteral(cty.StringVal("static a 0")), }), }), }, { Type: "b", Body: hcltest.MockBody(&hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "c", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val0": hcltest.MockExprLiteral(cty.StringVal("static c 0")), }), }), }, { Type: "dynamic", Labels: []string{"c"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ cty.StringVal("dynamic c 0"), cty.StringVal("dynamic c 1"), })), "iterator": hcltest.MockExprVariable("dyn_c"), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val0": hcltest.MockExprTraversalSrc("dyn_c.value"), }), }), }, }, }), }, }, }), }, { Type: "dynamic", Labels: []string{"a"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ cty.StringVal("dynamic a 0"), cty.StringVal("dynamic a 1"), cty.StringVal("dynamic a 2"), })), "labels": hcltest.MockExprList([]hcl.Expression{ hcltest.MockExprTraversalSrc("a.key"), }), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprTraversalSrc("a.value"), }), }), }, }, }), }, { Type: "dynamic", Labels: []string{"b"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ cty.StringVal("dynamic b 0"), cty.StringVal("dynamic b 1"), })), "iterator": hcltest.MockExprVariable("dyn_b"), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "c", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")), "val1": hcltest.MockExprTraversalSrc("dyn_b.value"), }), }), }, { Type: "dynamic", Labels: []string{"c"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ cty.StringVal("dynamic c 2"), cty.StringVal("dynamic c 3"), })), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val0": hcltest.MockExprTraversalSrc("c.value"), "val1": hcltest.MockExprTraversalSrc("dyn_b.value"), }), }), }, }, }), }, }, }), }, }, }), }, { Type: "dynamic", Labels: []string{"b"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.StringVal("dynamic c nested 0"), cty.StringVal("dynamic c nested 1"), }), })), "iterator": hcltest.MockExprVariable("dyn_b"), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "dynamic", Labels: []string{"c"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprTraversalSrc("dyn_b.value"), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val0": hcltest.MockExprTraversalSrc("c.value"), "val1": hcltest.MockExprTraversalSrc("dyn_b.key"), }), }), }, }, }), }, }, }), }, }, }), }, { Type: "a", Labels: []string{"static1"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprLiteral(cty.StringVal("static a 1")), }), }), }, }, }) dynBody := Expand(srcBody, nil) var remain hcl.Body t.Run("PartialDecode", func(t *testing.T) { decSpec := &hcldec.BlockMapSpec{ TypeName: "a", LabelNames: []string{"key"}, Nested: &hcldec.AttrSpec{ Name: "val", Type: cty.String, Required: true, }, } var got cty.Value var diags hcl.Diagnostics got, remain, diags = hcldec.PartialDecode(dynBody, decSpec, nil) if len(diags) != 0 { t.Errorf("unexpected diagnostics") for _, diag := range diags { t.Logf("- %s", diag) } return } want := cty.MapVal(map[string]cty.Value{ "static0": cty.StringVal("static a 0"), "static1": cty.StringVal("static a 1"), "0": cty.StringVal("dynamic a 0"), "1": cty.StringVal("dynamic a 1"), "2": cty.StringVal("dynamic a 2"), }) if !got.RawEquals(want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("Decode", func(t *testing.T) { decSpec := &hcldec.BlockListSpec{ TypeName: "b", Nested: &hcldec.BlockListSpec{ TypeName: "c", Nested: &hcldec.ObjectSpec{ "val0": &hcldec.AttrSpec{ Name: "val0", Type: cty.String, }, "val1": &hcldec.AttrSpec{ Name: "val1", Type: cty.String, }, }, }, } var got cty.Value var diags hcl.Diagnostics got, diags = hcldec.Decode(remain, decSpec, nil) if len(diags) != 0 { t.Errorf("unexpected diagnostics") for _, diag := range diags { t.Logf("- %s", diag) } return } want := cty.ListVal([]cty.Value{ cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("static c 0"), "val1": cty.NullVal(cty.String), }), cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("dynamic c 0"), "val1": cty.NullVal(cty.String), }), cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("dynamic c 1"), "val1": cty.NullVal(cty.String), }), }), cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("static c 1"), "val1": cty.StringVal("dynamic b 0"), }), cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("dynamic c 2"), "val1": cty.StringVal("dynamic b 0"), }), cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("dynamic c 3"), "val1": cty.StringVal("dynamic b 0"), }), }), cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("static c 1"), "val1": cty.StringVal("dynamic b 1"), }), cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("dynamic c 2"), "val1": cty.StringVal("dynamic b 1"), }), cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("dynamic c 3"), "val1": cty.StringVal("dynamic b 1"), }), }), cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("dynamic c nested 0"), "val1": cty.StringVal("foo"), }), cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("dynamic c nested 1"), "val1": cty.StringVal("foo"), }), }), }) if !got.RawEquals(want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) } func TestExpandWithForEachCheck(t *testing.T) { forEachExpr := hcltest.MockExprLiteral(cty.MapValEmpty(cty.String).Mark("boop")) evalCtx := &hcl.EvalContext{} srcContent := &hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "dynamic", Labels: []string{"foo"}, LabelRanges: []hcl.Range{{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": forEachExpr, }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{}), }, }, }), }, }, } srcBody := hcltest.MockBody(srcContent) hookCalled := false var gotV cty.Value var gotEvalCtx *hcl.EvalContext expBody := Expand( srcBody, evalCtx, OptCheckForEach(func(v cty.Value, e hcl.Expression, ec *hcl.EvalContext) hcl.Diagnostics { hookCalled = true gotV = v gotEvalCtx = ec return hcl.Diagnostics{ &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Bad for_each", Detail: "I don't like it.", Expression: e, EvalContext: ec, Extra: "diagnostic extra", }, } }), ) _, diags := expBody.Content(&hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ { Type: "foo", }, }, }) if !diags.HasErrors() { t.Fatal("succeeded; want an error") } if len(diags) != 1 { t.Fatalf("wrong number of diagnostics; want only one\n%s", spew.Sdump(diags)) } if got, want := diags[0].Summary, "Bad for_each"; got != want { t.Fatalf("wrong error\ngot: %s\nwant: %s\n\n%s", got, want, spew.Sdump(diags[0])) } if got, want := diags[0].Extra, "diagnostic extra"; got != want { // This is important to allow the application which provided the // hook to pass application-specific extra values through this // API in case the hook's diagnostics need some sort of special // treatment. t.Fatalf("diagnostic didn't preserve 'extra' field\ngot: %s\nwant: %s\n\n%s", got, want, spew.Sdump(diags[0])) } if !hookCalled { t.Fatal("check hook wasn't called") } if !gotV.HasMark("boop") { t.Errorf("wrong value passed to check hook; want the value marked \"boop\"\n%s", ctydebug.ValueString(gotV)) } if gotEvalCtx != evalCtx { t.Error("wrong EvalContext passed to check hook; want the one passed to Expand") } } func TestExpandUnknownBodies(t *testing.T) { srcContent := &hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "dynamic", Labels: []string{"list"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprTraversalSrc("each.value"), }), }), }, }, }), }, { Type: "dynamic", Labels: []string{"tuple"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprTraversalSrc("each.value"), }), }), }, }, }), }, { Type: "dynamic", Labels: []string{"set"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprTraversalSrc("each.value"), }), }), }, }, }), }, { Type: "dynamic", Labels: []string{"map"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))), "labels": hcltest.MockExprList([]hcl.Expression{ hcltest.MockExprLiteral(cty.StringVal("static")), }), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprTraversalSrc("each.value"), }), }), }, }, }), }, { Type: "dynamic", Labels: []string{"object"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))), "labels": hcltest.MockExprList([]hcl.Expression{ hcltest.MockExprLiteral(cty.StringVal("static")), }), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprTraversalSrc("each.value"), }), }), }, }, }), }, { Type: "dynamic", Labels: []string{"invalid_list"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val": hcltest.MockExprTraversalSrc("each.value"), // unexpected attributes should still produce an error "invalid": hcltest.MockExprLiteral(cty.StringVal("static")), }), }), }, }, }), }, }, } srcBody := hcltest.MockBody(srcContent) dynBody := Expand(srcBody, nil) t.Run("DecodeList", func(t *testing.T) { decSpec := &hcldec.BlockListSpec{ TypeName: "list", Nested: &hcldec.ObjectSpec{ "val": &hcldec.AttrSpec{ Name: "val", Type: cty.String, }, }, } var got cty.Value var diags hcl.Diagnostics got, _, diags = hcldec.PartialDecode(dynBody, decSpec, nil) if len(diags) != 0 { t.Errorf("unexpected diagnostics") for _, diag := range diags { t.Logf("- %s", diag) } return } want := cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ "val": cty.String, }))) if !got.RawEquals(want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("DecodeTuple", func(t *testing.T) { decSpec := &hcldec.BlockTupleSpec{ TypeName: "tuple", Nested: &hcldec.ObjectSpec{ "val": &hcldec.AttrSpec{ Name: "val", Type: cty.String, }, }, } var got cty.Value var diags hcl.Diagnostics got, _, diags = hcldec.PartialDecode(dynBody, decSpec, nil) if len(diags) != 0 { t.Errorf("unexpected diagnostics") for _, diag := range diags { t.Logf("- %s", diag) } return } want := cty.DynamicVal if !got.RawEquals(want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("DecodeSet", func(t *testing.T) { decSpec := &hcldec.BlockSetSpec{ TypeName: "tuple", Nested: &hcldec.ObjectSpec{ "val": &hcldec.AttrSpec{ Name: "val", Type: cty.String, }, }, } var got cty.Value var diags hcl.Diagnostics got, _, diags = hcldec.PartialDecode(dynBody, decSpec, nil) if len(diags) != 0 { t.Errorf("unexpected diagnostics") for _, diag := range diags { t.Logf("- %s", diag) } return } want := cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ "val": cty.String, }))) if !got.RawEquals(want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("DecodeMap", func(t *testing.T) { decSpec := &hcldec.BlockMapSpec{ TypeName: "map", LabelNames: []string{"key"}, Nested: &hcldec.ObjectSpec{ "val": &hcldec.AttrSpec{ Name: "val", Type: cty.String, }, }, } var got cty.Value var diags hcl.Diagnostics got, _, diags = hcldec.PartialDecode(dynBody, decSpec, nil) if len(diags) != 0 { t.Errorf("unexpected diagnostics") for _, diag := range diags { t.Logf("- %s", diag) } return } want := cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ "val": cty.String, }))) if !got.RawEquals(want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("DecodeInvalidList", func(t *testing.T) { decSpec := &hcldec.BlockListSpec{ TypeName: "invalid_list", Nested: &hcldec.ObjectSpec{ "val": &hcldec.AttrSpec{ Name: "val", Type: cty.String, }, }, } _, _, diags := hcldec.PartialDecode(dynBody, decSpec, nil) if len(diags) != 1 { t.Error("expected 1 extraneous argument") } want := `Mock body has extraneous argument "invalid"` if !strings.Contains(diags.Error(), want) { t.Errorf("unexpected diagnostics: %v", diags) } }) } func TestExpandMarkedForEach(t *testing.T) { srcBody := hcltest.MockBody(&hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "dynamic", Labels: []string{"b"}, LabelRanges: []hcl.Range{{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{ cty.StringVal("hey"), }).Mark("boop")), "iterator": hcltest.MockExprTraversalSrc("dyn_b"), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")), "val1": hcltest.MockExprTraversalSrc("dyn_b.value"), }), }), }, }, }), }, }, }) dynBody := Expand(srcBody, nil) t.Run("Decode", func(t *testing.T) { decSpec := &hcldec.BlockListSpec{ TypeName: "b", Nested: &hcldec.ObjectSpec{ "val0": &hcldec.AttrSpec{ Name: "val0", Type: cty.String, }, "val1": &hcldec.AttrSpec{ Name: "val1", Type: cty.String, }, }, } want := cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "val0": cty.StringVal("static c 1").Mark("boop"), "val1": cty.StringVal("hey").Mark("boop"), }).Mark("boop"), }) got, diags := hcldec.Decode(dynBody, decSpec, nil) if diags.HasErrors() { t.Fatalf("unexpected errors\n%s", diags.Error()) } if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { t.Errorf("wrong result\n%s", diff) } }) } func TestExpandInvalidIteratorError(t *testing.T) { srcBody := hcltest.MockBody(&hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "dynamic", Labels: []string{"b"}, LabelRanges: []hcl.Range{hcl.Range{}}, Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ cty.StringVal("dynamic b 0"), cty.StringVal("dynamic b 1"), })), "iterator": hcltest.MockExprLiteral(cty.StringVal("dyn_b")), }), Blocks: hcl.Blocks{ { Type: "content", Body: hcltest.MockBody(&hcl.BodyContent{ Blocks: hcl.Blocks{ { Type: "c", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ "val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")), "val1": hcltest.MockExprTraversalSrc("dyn_b.value"), }), }), }, }, }), }, }, }), }, }, }) dynBody := Expand(srcBody, nil) t.Run("Decode", func(t *testing.T) { decSpec := &hcldec.BlockListSpec{ TypeName: "b", Nested: &hcldec.BlockListSpec{ TypeName: "c", Nested: &hcldec.ObjectSpec{ "val0": &hcldec.AttrSpec{ Name: "val0", Type: cty.String, }, "val1": &hcldec.AttrSpec{ Name: "val1", Type: cty.String, }, }, }, } var diags hcl.Diagnostics _, diags = hcldec.Decode(dynBody, decSpec, nil) if len(diags) < 1 { t.Errorf("Expected diagnostics, got none") } if len(diags) > 1 { t.Errorf("Expected one diagnostic message, got %d", len(diags)) for _, diag := range diags { t.Logf("- %s", diag) } } if diags[0].Summary != "Invalid expression" { t.Errorf("Expected error subject to be invalid expression, instead it was %q", diags[0].Summary) } }) }