1package configschema
2
3import (
4	"testing"
5
6	"github.com/hashicorp/hcl/v2"
7	"github.com/hashicorp/hcl/v2/hclsyntax"
8	"github.com/zclconf/go-cty/cty"
9)
10
11func TestStaticValidateTraversal(t *testing.T) {
12	attrs := map[string]*Attribute{
13		"str":  {Type: cty.String, Optional: true},
14		"list": {Type: cty.List(cty.String), Optional: true},
15		"dyn":  {Type: cty.DynamicPseudoType, Optional: true},
16	}
17	schema := &Block{
18		Attributes: attrs,
19		BlockTypes: map[string]*NestedBlock{
20			"single_block": {
21				Nesting: NestingSingle,
22				Block: Block{
23					Attributes: attrs,
24				},
25			},
26			"list_block": {
27				Nesting: NestingList,
28				Block: Block{
29					Attributes: attrs,
30				},
31			},
32			"set_block": {
33				Nesting: NestingSet,
34				Block: Block{
35					Attributes: attrs,
36				},
37			},
38			"map_block": {
39				Nesting: NestingMap,
40				Block: Block{
41					Attributes: attrs,
42				},
43			},
44		},
45	}
46
47	tests := []struct {
48		Traversal string
49		WantError string
50	}{
51		{
52			`obj`,
53			``,
54		},
55		{
56			`obj.str`,
57			``,
58		},
59		{
60			`obj.str.nonexist`,
61			`Unsupported attribute: Can't access attributes on a primitive-typed value (string).`,
62		},
63		{
64			`obj.list`,
65			``,
66		},
67		{
68			`obj.list[0]`,
69			``,
70		},
71		{
72			`obj.list.nonexist`,
73			`Unsupported attribute: This value does not have any attributes.`,
74		},
75		{
76			`obj.dyn`,
77			``,
78		},
79		{
80			`obj.dyn.anything_goes`,
81			``,
82		},
83		{
84			`obj.dyn[0]`,
85			``,
86		},
87		{
88			`obj.nonexist`,
89			`Unsupported attribute: This object has no argument, nested block, or exported attribute named "nonexist".`,
90		},
91		{
92			`obj[1]`,
93			`Invalid index operation: Only attribute access is allowed here, using the dot operator.`,
94		},
95		{
96			`obj["str"]`, // we require attribute access for the first step to avoid ambiguity with resource instance indices
97			`Invalid index operation: Only attribute access is allowed here. Did you mean to access attribute "str" using the dot operator?`,
98		},
99		{
100			`obj.atr`,
101			`Unsupported attribute: This object has no argument, nested block, or exported attribute named "atr". Did you mean "str"?`,
102		},
103		{
104			`obj.single_block`,
105			``,
106		},
107		{
108			`obj.single_block.str`,
109			``,
110		},
111		{
112			`obj.single_block.nonexist`,
113			`Unsupported attribute: This object has no argument, nested block, or exported attribute named "nonexist".`,
114		},
115		{
116			`obj.list_block`,
117			``,
118		},
119		{
120			`obj.list_block[0]`,
121			``,
122		},
123		{
124			`obj.list_block[0].str`,
125			``,
126		},
127		{
128			`obj.list_block[0].nonexist`,
129			`Unsupported attribute: This object has no argument, nested block, or exported attribute named "nonexist".`,
130		},
131		{
132			`obj.list_block.str`,
133			`Invalid operation: Block type "list_block" is represented by a list of objects, so it must be indexed using a numeric key, like .list_block[0].`,
134		},
135		{
136			`obj.set_block`,
137			``,
138		},
139		{
140			`obj.set_block[0]`,
141			`Cannot index a set value: Block type "set_block" is represented by a set of objects, and set elements do not have addressable keys. To find elements matching specific criteria, use a "for" expression with an "if" clause.`,
142		},
143		{
144			`obj.set_block.str`,
145			`Cannot index a set value: Block type "set_block" is represented by a set of objects, and set elements do not have addressable keys. To find elements matching specific criteria, use a "for" expression with an "if" clause.`,
146		},
147		{
148			`obj.map_block`,
149			``,
150		},
151		{
152			`obj.map_block.anything`,
153			``,
154		},
155		{
156			`obj.map_block["anything"]`,
157			``,
158		},
159		{
160			`obj.map_block.anything.str`,
161			``,
162		},
163		{
164			`obj.map_block["anything"].str`,
165			``,
166		},
167		{
168			`obj.map_block.anything.nonexist`,
169			`Unsupported attribute: This object has no argument, nested block, or exported attribute named "nonexist".`,
170		},
171	}
172
173	for _, test := range tests {
174		t.Run(test.Traversal, func(t *testing.T) {
175			traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(test.Traversal), "", hcl.Pos{Line: 1, Column: 1})
176			for _, diag := range parseDiags {
177				t.Error(diag.Error())
178			}
179
180			// We trim the "obj." portion from the front since StaticValidateTraversal
181			// only works with relative traversals.
182			traversal = traversal[1:]
183
184			diags := schema.StaticValidateTraversal(traversal)
185			if test.WantError == "" {
186				if diags.HasErrors() {
187					t.Errorf("unexpected error: %s", diags.Err().Error())
188				}
189			} else {
190				if diags.HasErrors() {
191					if got := diags.Err().Error(); got != test.WantError {
192						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, test.WantError)
193					}
194				} else {
195					t.Errorf("wrong error\ngot:  <no error>\nwant: %s", test.WantError)
196				}
197			}
198		})
199	}
200}
201