17944: Improves error reporting on invalid value types.
[arvados.git] / sdk / go / arvados / vocabulary_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package arvados
6
7 import (
8         "encoding/json"
9
10         check "gopkg.in/check.v1"
11 )
12
13 type VocabularySuite struct {
14         testVoc *Vocabulary
15 }
16
17 var _ = check.Suite(&VocabularySuite{})
18
19 func (s *VocabularySuite) SetUpTest(c *check.C) {
20         s.testVoc = &Vocabulary{
21                 reservedTagKeys: map[string]bool{
22                         "reservedKey": true,
23                 },
24                 StrictTags: false,
25                 Tags: map[string]VocabularyTag{
26                         "IDTAGANIMALS": {
27                                 Strict: false,
28                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
29                                 Values: map[string]VocabularyTagValue{
30                                         "IDVALANIMAL1": {
31                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Homo sapiens"}},
32                                         },
33                                         "IDVALANIMAL2": {
34                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Loxodonta"}},
35                                         },
36                                 },
37                         },
38                         "IDTAGIMPORTANCE": {
39                                 Strict: true,
40                                 Labels: []VocabularyLabel{{Label: "Importance"}, {Label: "Priority"}},
41                                 Values: map[string]VocabularyTagValue{
42                                         "IDVAL3": {
43                                                 Labels: []VocabularyLabel{{Label: "Low"}, {Label: "Low priority"}},
44                                         },
45                                         "IDVAL2": {
46                                                 Labels: []VocabularyLabel{{Label: "Medium"}, {Label: "Medium priority"}},
47                                         },
48                                         "IDVAL1": {
49                                                 Labels: []VocabularyLabel{{Label: "High"}, {Label: "High priority"}},
50                                         },
51                                 },
52                         },
53                         "IDTAGCOMMENT": {
54                                 Strict: false,
55                                 Labels: []VocabularyLabel{{Label: "Comment"}},
56                         },
57                 },
58         }
59         err := s.testVoc.validate()
60         c.Assert(err, check.IsNil)
61 }
62
63 func (s *VocabularySuite) TestCheck(c *check.C) {
64         tests := []struct {
65                 name          string
66                 strictVoc     bool
67                 props         string
68                 expectSuccess bool
69                 errMatches    string
70         }{
71                 // Check succeeds
72                 {
73                         "Known key, known value",
74                         false,
75                         `{"IDTAGANIMALS":"IDVALANIMAL1"}`,
76                         true,
77                         "",
78                 },
79                 {
80                         "Unknown non-alias key on non-strict vocabulary",
81                         false,
82                         `{"foo":"bar"}`,
83                         true,
84                         "",
85                 },
86                 {
87                         "Known non-strict key, unknown non-alias value",
88                         false,
89                         `{"IDTAGANIMALS":"IDVALANIMAL3"}`,
90                         true,
91                         "",
92                 },
93                 {
94                         "Undefined but reserved key on strict vocabulary",
95                         true,
96                         `{"reservedKey":"bar"}`,
97                         true,
98                         "",
99                 },
100                 {
101                         "Known key, list of known values",
102                         false,
103                         `{"IDTAGANIMALS":["IDVALANIMAL1","IDVALANIMAL2"]}`,
104                         true,
105                         "",
106                 },
107                 {
108                         "Known non-strict key, list of unknown non-alias values",
109                         false,
110                         `{"IDTAGCOMMENT":["hello world","lorem ipsum"]}`,
111                         true,
112                         "",
113                 },
114                 // Check fails
115                 {
116                         "Known first key & value; known 2nd key, unknown 2nd value",
117                         false,
118                         `{"IDTAGANIMALS":"IDVALANIMAL1", "IDTAGIMPORTANCE": "blah blah"}`,
119                         false,
120                         "tag value.*is not valid for key.*",
121                 },
122                 {
123                         "Unknown non-alias key on strict vocabulary",
124                         true,
125                         `{"foo":"bar"}`,
126                         false,
127                         "tag key.*is not defined in the vocabulary",
128                 },
129                 {
130                         "Known non-strict key, known value alias",
131                         false,
132                         `{"IDTAGANIMALS":"Loxodonta"}`,
133                         false,
134                         "tag value.*for key.* is an alias, must be provided as.*",
135                 },
136                 {
137                         "Known strict key, unknown non-alias value",
138                         false,
139                         `{"IDTAGIMPORTANCE":"Unimportant"}`,
140                         false,
141                         "tag value.*is not valid for key.*",
142                 },
143                 {
144                         "Known strict key, lowercase value regarded as alias",
145                         false,
146                         `{"IDTAGIMPORTANCE":"idval1"}`,
147                         false,
148                         "tag value.*for key.* is an alias, must be provided as.*",
149                 },
150                 {
151                         "Known strict key, known value alias",
152                         false,
153                         `{"IDTAGIMPORTANCE":"High"}`,
154                         false,
155                         "tag value.* for key.*is an alias, must be provided as.*",
156                 },
157                 {
158                         "Known strict key, list of known alias values",
159                         false,
160                         `{"IDTAGIMPORTANCE":["High", "Low"]}`,
161                         false,
162                         "tag value.*for key.*is an alias, must be provided as.*",
163                 },
164                 {
165                         "Known strict key, list of unknown non-alias values",
166                         false,
167                         `{"IDTAGIMPORTANCE":["foo","bar"]}`,
168                         false,
169                         "tag value.*is not valid for key.*",
170                 },
171                 {
172                         "Invalid value type",
173                         false,
174                         `{"IDTAGANIMALS":1}`,
175                         false,
176                         "value type for tag key.* was.*, but expected a string or list of strings",
177                 },
178                 {
179                         "Value list of invalid type",
180                         false,
181                         `{"IDTAGANIMALS":[1]}`,
182                         false,
183                         "value list element type for tag key.* was.*, but expected a string",
184                 },
185         }
186         for _, tt := range tests {
187                 c.Log(c.TestName()+" ", tt.name)
188                 s.testVoc.StrictTags = tt.strictVoc
189
190                 var data map[string]interface{}
191                 err := json.Unmarshal([]byte(tt.props), &data)
192                 c.Assert(err, check.IsNil)
193                 err = s.testVoc.Check(data)
194                 if tt.expectSuccess {
195                         c.Assert(err, check.IsNil)
196                 } else {
197                         c.Assert(err, check.NotNil)
198                         c.Assert(err.Error(), check.Matches, tt.errMatches)
199                 }
200         }
201 }
202
203 func (s *VocabularySuite) TestNewVocabulary(c *check.C) {
204         tests := []struct {
205                 name       string
206                 data       string
207                 isValid    bool
208                 errMatches string
209                 expect     *Vocabulary
210         }{
211                 {"Empty data", "", true, "", &Vocabulary{}},
212                 {"Invalid JSON", "foo", false, "invalid JSON format.*", nil},
213                 {"Valid, empty JSON", "{}", false, ".*doesn't match Vocabulary format.*", nil},
214                 {"Valid JSON, wrong data", `{"foo":"bar"}`, false, ".*doesn't match Vocabulary format.*", nil},
215                 {
216                         "Simple valid example",
217                         `{"tags":{
218                                 "IDTAGANIMALS":{
219                                         "strict": false,
220                                         "labels": [{"label": "Animal"}, {"label": "Creature"}],
221                                         "values": {
222                                                 "IDVALANIMAL1":{"labels":[{"label":"Human"}, {"label":"Homo sapiens"}]},
223                                                 "IDVALANIMAL2":{"labels":[{"label":"Elephant"}, {"label":"Loxodonta"}]}
224                                         }
225                                 }
226                         }}`,
227                         true, "",
228                         &Vocabulary{
229                                 reservedTagKeys: map[string]bool{
230                                         "type":                  true,
231                                         "template_uuid":         true,
232                                         "groups":                true,
233                                         "username":              true,
234                                         "image_timestamp":       true,
235                                         "docker-image-repo-tag": true,
236                                         "filters":               true,
237                                         "container_request":     true,
238                                 },
239                                 StrictTags: false,
240                                 Tags: map[string]VocabularyTag{
241                                         "IDTAGANIMALS": {
242                                                 Strict: false,
243                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
244                                                 Values: map[string]VocabularyTagValue{
245                                                         "IDVALANIMAL1": {
246                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Homo sapiens"}},
247                                                         },
248                                                         "IDVALANIMAL2": {
249                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Loxodonta"}},
250                                                         },
251                                                 },
252                                         },
253                                 },
254                         },
255                 },
256                 {
257                         "Valid data, but uses reserved key",
258                         `{"tags":{
259                                 "type":{
260                                         "strict": false,
261                                         "labels": [{"label": "Type"}]
262                                 }
263                         }}`,
264                         false, "tag key.*is reserved", nil,
265                 },
266         }
267
268         for _, tt := range tests {
269                 c.Log(c.TestName()+" ", tt.name)
270                 voc, err := NewVocabulary([]byte(tt.data), []string{})
271                 if tt.isValid {
272                         c.Assert(err, check.IsNil)
273                 } else {
274                         c.Assert(err, check.NotNil)
275                         if tt.errMatches != "" {
276                                 c.Assert(err, check.ErrorMatches, tt.errMatches)
277                         }
278                 }
279                 c.Assert(voc, check.DeepEquals, tt.expect)
280         }
281 }
282
283 func (s *VocabularySuite) TestValidationErrors(c *check.C) {
284         tests := []struct {
285                 name       string
286                 voc        *Vocabulary
287                 errMatches string
288         }{
289                 {
290                         "Strict vocabulary, no keys",
291                         &Vocabulary{
292                                 StrictTags: true,
293                         },
294                         "vocabulary is strict but no tags are defined",
295                 },
296                 {
297                         "Collision between tag key and tag key label",
298                         &Vocabulary{
299                                 StrictTags: false,
300                                 Tags: map[string]VocabularyTag{
301                                         "IDTAGANIMALS": {
302                                                 Strict: false,
303                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
304                                         },
305                                         "IDTAGCOMMENT": {
306                                                 Strict: false,
307                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IDTAGANIMALS"}},
308                                         },
309                                 },
310                         },
311                         "", // Depending on how the map is sorted, this could be one of two errors
312                 },
313                 {
314                         "Collision between tag key and tag key label (case-insensitive)",
315                         &Vocabulary{
316                                 StrictTags: false,
317                                 Tags: map[string]VocabularyTag{
318                                         "IDTAGANIMALS": {
319                                                 Strict: false,
320                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
321                                         },
322                                         "IDTAGCOMMENT": {
323                                                 Strict: false,
324                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IdTagAnimals"}},
325                                         },
326                                 },
327                         },
328                         "", // Depending on how the map is sorted, this could be one of two errors
329                 },
330                 {
331                         "Collision between tag key labels",
332                         &Vocabulary{
333                                 StrictTags: false,
334                                 Tags: map[string]VocabularyTag{
335                                         "IDTAGANIMALS": {
336                                                 Strict: false,
337                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
338                                         },
339                                         "IDTAGCOMMENT": {
340                                                 Strict: false,
341                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "Animal"}},
342                                         },
343                                 },
344                         },
345                         "tag label.*for key.*already seen.*",
346                 },
347                 {
348                         "Collision between tag value and tag value label",
349                         &Vocabulary{
350                                 StrictTags: false,
351                                 Tags: map[string]VocabularyTag{
352                                         "IDTAGANIMALS": {
353                                                 Strict: false,
354                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
355                                                 Values: map[string]VocabularyTagValue{
356                                                         "IDVALANIMAL1": {
357                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
358                                                         },
359                                                         "IDVALANIMAL2": {
360                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDVALANIMAL1"}},
361                                                         },
362                                                 },
363                                         },
364                                 },
365                         },
366                         "", // Depending on how the map is sorted, this could be one of two errors
367                 },
368                 {
369                         "Collision between tag value and tag value label (case-insensitive)",
370                         &Vocabulary{
371                                 StrictTags: false,
372                                 Tags: map[string]VocabularyTag{
373                                         "IDTAGANIMALS": {
374                                                 Strict: false,
375                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
376                                                 Values: map[string]VocabularyTagValue{
377                                                         "IDVALANIMAL1": {
378                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
379                                                         },
380                                                         "IDVALANIMAL2": {
381                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDValAnimal1"}},
382                                                         },
383                                                 },
384                                         },
385                                 },
386                         },
387                         "", // Depending on how the map is sorted, this could be one of two errors
388                 },
389                 {
390                         "Collision between tag value labels",
391                         &Vocabulary{
392                                 StrictTags: false,
393                                 Tags: map[string]VocabularyTag{
394                                         "IDTAGANIMALS": {
395                                                 Strict: false,
396                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
397                                                 Values: map[string]VocabularyTagValue{
398                                                         "IDVALANIMAL1": {
399                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
400                                                         },
401                                                         "IDVALANIMAL2": {
402                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Mammal"}},
403                                                         },
404                                                 },
405                                         },
406                                 },
407                         },
408                         "tag value label.*for pair.*already seen.*",
409                 },
410                 {
411                         "Strict tag key, with no values",
412                         &Vocabulary{
413                                 StrictTags: false,
414                                 Tags: map[string]VocabularyTag{
415                                         "IDTAGANIMALS": {
416                                                 Strict: true,
417                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
418                                         },
419                                 },
420                         },
421                         "tag key.*is configured as strict but doesn't provide values",
422                 },
423         }
424         for _, tt := range tests {
425                 c.Log(c.TestName()+" ", tt.name)
426                 err := tt.voc.validate()
427                 c.Assert(err, check.NotNil)
428                 if tt.errMatches != "" {
429                         c.Assert(err, check.ErrorMatches, tt.errMatches)
430                 }
431         }
432 }