19980: Recognize the arv: prefix for all system properties
[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         "fmt"
10         "regexp"
11         "strings"
12
13         check "gopkg.in/check.v1"
14 )
15
16 type VocabularySuite struct {
17         testVoc *Vocabulary
18 }
19
20 var _ = check.Suite(&VocabularySuite{})
21
22 func (s *VocabularySuite) SetUpTest(c *check.C) {
23         s.testVoc = &Vocabulary{
24                 reservedTagKeys: map[string]bool{
25                         "reservedKey": true,
26                 },
27                 StrictTags: false,
28                 Tags: map[string]VocabularyTag{
29                         "IDTAGANIMALS": {
30                                 Strict: false,
31                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
32                                 Values: map[string]VocabularyTagValue{
33                                         "IDVALANIMAL1": {
34                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Homo sapiens"}},
35                                         },
36                                         "IDVALANIMAL2": {
37                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Loxodonta"}},
38                                         },
39                                 },
40                         },
41                         "IDTAGIMPORTANCE": {
42                                 Strict: true,
43                                 Labels: []VocabularyLabel{{Label: "Importance"}, {Label: "Priority"}},
44                                 Values: map[string]VocabularyTagValue{
45                                         "IDVAL3": {
46                                                 Labels: []VocabularyLabel{{Label: "Low"}, {Label: "Low priority"}},
47                                         },
48                                         "IDVAL2": {
49                                                 Labels: []VocabularyLabel{{Label: "Medium"}, {Label: "Medium priority"}},
50                                         },
51                                         "IDVAL1": {
52                                                 Labels: []VocabularyLabel{{Label: "High"}, {Label: "High priority"}},
53                                         },
54                                 },
55                         },
56                         "IDTAGCOMMENT": {
57                                 Strict: false,
58                                 Labels: []VocabularyLabel{{Label: "Comment"}},
59                         },
60                 },
61         }
62         _, err := s.testVoc.validate()
63         c.Assert(err, check.IsNil)
64 }
65
66 func (s *VocabularySuite) TestCheck(c *check.C) {
67         tests := []struct {
68                 name          string
69                 strictVoc     bool
70                 props         string
71                 expectSuccess bool
72                 errMatches    string
73         }{
74                 // Check succeeds
75                 {
76                         "Known key, known value",
77                         false,
78                         `{"IDTAGANIMALS":"IDVALANIMAL1"}`,
79                         true,
80                         "",
81                 },
82                 {
83                         "Unknown non-alias key on non-strict vocabulary",
84                         false,
85                         `{"foo":"bar"}`,
86                         true,
87                         "",
88                 },
89                 {
90                         "Known non-strict key, unknown non-alias value",
91                         false,
92                         `{"IDTAGANIMALS":"IDVALANIMAL3"}`,
93                         true,
94                         "",
95                 },
96                 {
97                         "Undefined but reserved key on strict vocabulary",
98                         true,
99                         `{"reservedKey":"bar"}`,
100                         true,
101                         "",
102                 },
103                 {
104                         "Known key, list of known values",
105                         false,
106                         `{"IDTAGANIMALS":["IDVALANIMAL1","IDVALANIMAL2"]}`,
107                         true,
108                         "",
109                 },
110                 {
111                         "Known non-strict key, list of unknown non-alias values",
112                         false,
113                         `{"IDTAGCOMMENT":["hello world","lorem ipsum"]}`,
114                         true,
115                         "",
116                 },
117                 // Check fails
118                 {
119                         "Known first key & value; known 2nd key, unknown 2nd value",
120                         false,
121                         `{"IDTAGANIMALS":"IDVALANIMAL1", "IDTAGIMPORTANCE": "blah blah"}`,
122                         false,
123                         "tag value.*is not valid for key.*",
124                 },
125                 {
126                         "Unknown non-alias key on strict vocabulary",
127                         true,
128                         `{"foo":"bar"}`,
129                         false,
130                         "tag key.*is not defined in the vocabulary",
131                 },
132                 {
133                         "Known non-strict key, known value alias",
134                         false,
135                         `{"IDTAGANIMALS":"Loxodonta"}`,
136                         false,
137                         "tag value.*for key.* is an alias, must be provided as.*",
138                 },
139                 {
140                         "Known strict key, unknown non-alias value",
141                         false,
142                         `{"IDTAGIMPORTANCE":"Unimportant"}`,
143                         false,
144                         "tag value.*is not valid for key.*",
145                 },
146                 {
147                         "Known strict key, lowercase value regarded as alias",
148                         false,
149                         `{"IDTAGIMPORTANCE":"idval1"}`,
150                         false,
151                         "tag value.*for key.* is an alias, must be provided as.*",
152                 },
153                 {
154                         "Known strict key, known value alias",
155                         false,
156                         `{"IDTAGIMPORTANCE":"High"}`,
157                         false,
158                         "tag value.* for key.*is an alias, must be provided as.*",
159                 },
160                 {
161                         "Known strict key, list of known alias values",
162                         false,
163                         `{"IDTAGIMPORTANCE":["High", "Low"]}`,
164                         false,
165                         "tag value.*for key.*is an alias, must be provided as.*",
166                 },
167                 {
168                         "Known strict key, list of unknown non-alias values",
169                         false,
170                         `{"IDTAGIMPORTANCE":["foo","bar"]}`,
171                         false,
172                         "tag value.*is not valid for key.*",
173                 },
174                 {
175                         "Invalid value type",
176                         false,
177                         `{"IDTAGANIMALS":1}`,
178                         false,
179                         "value type for tag key.* was.*, but expected a string or list of strings",
180                 },
181                 {
182                         "Value list of invalid type",
183                         false,
184                         `{"IDTAGANIMALS":[1]}`,
185                         false,
186                         "value list element type for tag key.* was.*, but expected a string",
187                 },
188         }
189         for _, tt := range tests {
190                 c.Log(c.TestName()+" ", tt.name)
191                 s.testVoc.StrictTags = tt.strictVoc
192
193                 var data map[string]interface{}
194                 err := json.Unmarshal([]byte(tt.props), &data)
195                 c.Assert(err, check.IsNil)
196                 err = s.testVoc.Check(data)
197                 if tt.expectSuccess {
198                         c.Assert(err, check.IsNil)
199                 } else {
200                         c.Assert(err, check.NotNil)
201                         c.Assert(err.Error(), check.Matches, tt.errMatches)
202                 }
203         }
204 }
205
206 func (s *VocabularySuite) TestNewVocabulary(c *check.C) {
207         tests := []struct {
208                 name       string
209                 data       string
210                 isValid    bool
211                 errMatches string
212                 expect     *Vocabulary
213         }{
214                 {"Empty data", "", true, "", &Vocabulary{}},
215                 {"Invalid JSON", "foo", false, "invalid JSON format.*", nil},
216                 {"Valid, empty JSON", "{}", false, ".*doesn't match Vocabulary format.*", nil},
217                 {"Valid JSON, wrong data", `{"foo":"bar"}`, false, ".*doesn't match Vocabulary format.*", nil},
218                 {
219                         "Simple valid example",
220                         `{"tags":{
221                                 "IDTAGANIMALS":{
222                                         "strict": false,
223                                         "labels": [{"label": "Animal"}, {"label": "Creature"}],
224                                         "values": {
225                                                 "IDVALANIMAL1":{"labels":[{"label":"Human"}, {"label":"Homo sapiens"}]},
226                                                 "IDVALANIMAL2":{"labels":[{"label":"Elephant"}, {"label":"Loxodonta"}]},
227                                                 "DOG":{"labels":[{"label":"Dog"}, {"label":"Canis lupus familiaris"}, {"label":"dOg"}]}
228                                         }
229                                 }
230                         }}`,
231                         true, "",
232                         &Vocabulary{
233                                 reservedTagKeys: map[string]bool{
234                                         "container_request":     true,
235                                         "container_uuid":        true,
236                                         "cwl_input":             true,
237                                         "cwl_output":            true,
238                                         "docker-image-repo-tag": true,
239                                         "filters":               true,
240                                         "groups":                true,
241                                         "image_timestamp":       true,
242                                         "template_uuid":         true,
243                                         "type":                  true,
244                                         "username":              true,
245                                 },
246                                 StrictTags: false,
247                                 Tags: map[string]VocabularyTag{
248                                         "IDTAGANIMALS": {
249                                                 Strict: false,
250                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
251                                                 Values: map[string]VocabularyTagValue{
252                                                         "IDVALANIMAL1": {
253                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Homo sapiens"}},
254                                                         },
255                                                         "IDVALANIMAL2": {
256                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Loxodonta"}},
257                                                         },
258                                                         "DOG": {
259                                                                 Labels: []VocabularyLabel{{Label: "Dog"}, {Label: "Canis lupus familiaris"}, {Label: "dOg"}},
260                                                         },
261                                                 },
262                                         },
263                                 },
264                         },
265                 },
266                 {
267                         "Invalid JSON error with line & column numbers",
268                         `{"tags":{
269                                 "aKey":{
270                                         "labels": [,{"label": "A label"}]
271                                 }
272                         }}`,
273                         false, `invalid JSON format:.*\(line \d+, column \d+\)`, nil,
274                 },
275                 {
276                         "Invalid JSON with duplicate & reserved keys",
277                         `{"tags":{
278                                 "type":{
279                                         "strict": false,
280                                         "labels": [{"label": "Class", "label": "Type"}]
281                                 },
282                                 "type":{
283                                         "labels": []
284                                 }
285                         }}`,
286                         false, "(?s).*duplicate JSON key \"tags.type.labels.0.label\"\nduplicate JSON key \"tags.type\"\ntag key \"type\" is reserved", nil,
287                 },
288         }
289
290         for _, tt := range tests {
291                 c.Log(c.TestName()+" ", tt.name)
292                 voc, err := NewVocabulary([]byte(tt.data), []string{})
293                 if tt.isValid {
294                         c.Assert(err, check.IsNil)
295                 } else {
296                         c.Assert(err, check.NotNil)
297                         if tt.errMatches != "" {
298                                 c.Assert(err, check.ErrorMatches, tt.errMatches)
299                         }
300                 }
301                 c.Assert(voc, check.DeepEquals, tt.expect)
302         }
303 }
304
305 func (s *VocabularySuite) TestValidSystemProperties(c *check.C) {
306         s.testVoc.StrictTags = true
307         properties := map[string]interface{}{
308                 "arv:gitBranch": "main",
309                 "arv:OK":        true,
310                 "arv:cost":      123,
311         }
312         c.Check(s.testVoc.Check(properties), check.IsNil)
313 }
314
315 func (s *VocabularySuite) TestSystemPropertiesFirstCharacterAlphabetic(c *check.C) {
316         s.testVoc.StrictTags = true
317         properties := map[string]interface{}{"arv:": "value"}
318         c.Check(s.testVoc.Check(properties), check.NotNil)
319         // If we expand the list of allowed characters in the future, these lists
320         // may need adjustment to match.
321         for _, prefix := range []string{" ", ".", "_", "-", "1"} {
322                 for _, suffix := range []string{"", "invalid"} {
323                         key := fmt.Sprintf("arv:%s%s", prefix, suffix)
324                         properties := map[string]interface{}{key: "value"}
325                         c.Check(s.testVoc.Check(properties), check.NotNil)
326                 }
327         }
328 }
329
330 func (s *VocabularySuite) TestSystemPropertiesPrefixTypo(c *check.C) {
331         s.testVoc.StrictTags = true
332         for _, key := range []string{
333                 "arv :foo",
334                 "arvados",
335                 "arvados:foo",
336                 "Arv:foo",
337         } {
338                 properties := map[string]interface{}{key: "value"}
339                 c.Check(s.testVoc.Check(properties), check.NotNil)
340         }
341 }
342
343 func (s *VocabularySuite) TestValidationErrors(c *check.C) {
344         tests := []struct {
345                 name       string
346                 voc        *Vocabulary
347                 errMatches []string
348         }{
349                 {
350                         "Strict vocabulary, no keys",
351                         &Vocabulary{
352                                 StrictTags: true,
353                         },
354                         []string{"vocabulary is strict but no tags are defined"},
355                 },
356                 {
357                         "Collision between tag key and tag key label",
358                         &Vocabulary{
359                                 StrictTags: false,
360                                 Tags: map[string]VocabularyTag{
361                                         "IDTAGANIMALS": {
362                                                 Strict: false,
363                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
364                                         },
365                                         "IDTAGCOMMENT": {
366                                                 Strict: false,
367                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IDTAGANIMALS"}},
368                                         },
369                                 },
370                         },
371                         nil, // Depending on how the map is sorted, this could be one of two errors
372                 },
373                 {
374                         "Collision between tag key and tag key label (case-insensitive)",
375                         &Vocabulary{
376                                 StrictTags: false,
377                                 Tags: map[string]VocabularyTag{
378                                         "IDTAGANIMALS": {
379                                                 Strict: false,
380                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
381                                         },
382                                         "IDTAGCOMMENT": {
383                                                 Strict: false,
384                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IdTagAnimals"}},
385                                         },
386                                 },
387                         },
388                         nil, // Depending on how the map is sorted, this could be one of two errors
389                 },
390                 {
391                         "Collision between tag key labels",
392                         &Vocabulary{
393                                 StrictTags: false,
394                                 Tags: map[string]VocabularyTag{
395                                         "IDTAGANIMALS": {
396                                                 Strict: false,
397                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
398                                         },
399                                         "IDTAGCOMMENT": {
400                                                 Strict: false,
401                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "Animal"}},
402                                         },
403                                 },
404                         },
405                         []string{"(?s).*tag label.*for key.*already seen.*"},
406                 },
407                 {
408                         "Collision between tag value and tag value label",
409                         &Vocabulary{
410                                 StrictTags: false,
411                                 Tags: map[string]VocabularyTag{
412                                         "IDTAGANIMALS": {
413                                                 Strict: false,
414                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
415                                                 Values: map[string]VocabularyTagValue{
416                                                         "IDVALANIMAL1": {
417                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
418                                                         },
419                                                         "IDVALANIMAL2": {
420                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDVALANIMAL1"}},
421                                                         },
422                                                 },
423                                         },
424                                 },
425                         },
426                         nil, // Depending on how the map is sorted, this could be one of two errors
427                 },
428                 {
429                         "Collision between tag value and tag value label (case-insensitive)",
430                         &Vocabulary{
431                                 StrictTags: false,
432                                 Tags: map[string]VocabularyTag{
433                                         "IDTAGANIMALS": {
434                                                 Strict: false,
435                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
436                                                 Values: map[string]VocabularyTagValue{
437                                                         "IDVALANIMAL1": {
438                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
439                                                         },
440                                                         "IDVALANIMAL2": {
441                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDValAnimal1"}},
442                                                         },
443                                                 },
444                                         },
445                                 },
446                         },
447                         nil, // Depending on how the map is sorted, this could be one of two errors
448                 },
449                 {
450                         "Collision between tag value labels",
451                         &Vocabulary{
452                                 StrictTags: false,
453                                 Tags: map[string]VocabularyTag{
454                                         "IDTAGANIMALS": {
455                                                 Strict: false,
456                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
457                                                 Values: map[string]VocabularyTagValue{
458                                                         "IDVALANIMAL1": {
459                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
460                                                         },
461                                                         "IDVALANIMAL2": {
462                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Mammal"}},
463                                                         },
464                                                 },
465                                         },
466                                 },
467                         },
468                         []string{"(?s).*tag value label.*for pair.*already seen.*on value.*"},
469                 },
470                 {
471                         "Collision between tag value labels (case-insensitive)",
472                         &Vocabulary{
473                                 StrictTags: false,
474                                 Tags: map[string]VocabularyTag{
475                                         "IDTAGANIMALS": {
476                                                 Strict: false,
477                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
478                                                 Values: map[string]VocabularyTagValue{
479                                                         "IDVALANIMAL1": {
480                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
481                                                         },
482                                                         "IDVALANIMAL2": {
483                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "mAMMAL"}},
484                                                         },
485                                                 },
486                                         },
487                                 },
488                         },
489                         []string{"(?s).*tag value label.*for pair.*already seen.*on value.*"},
490                 },
491                 {
492                         "Strict tag key, with no values",
493                         &Vocabulary{
494                                 StrictTags: false,
495                                 Tags: map[string]VocabularyTag{
496                                         "IDTAGANIMALS": {
497                                                 Strict: true,
498                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
499                                         },
500                                 },
501                         },
502                         []string{"(?s).*tag key.*is configured as strict but doesn't provide values"},
503                 },
504                 {
505                         "Multiple errors reported",
506                         &Vocabulary{
507                                 StrictTags: false,
508                                 Tags: map[string]VocabularyTag{
509                                         "IDTAGANIMALS": {
510                                                 Strict: true,
511                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
512                                         },
513                                         "IDTAGSIZES": {
514                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Size"}},
515                                         },
516                                 },
517                         },
518                         []string{
519                                 "(?s).*tag key.*is configured as strict but doesn't provide values.*",
520                                 "(?s).*tag label.*for key.*already seen.*",
521                         },
522                 },
523         }
524         for _, tt := range tests {
525                 c.Log(c.TestName()+" ", tt.name)
526                 validationErrs, err := tt.voc.validate()
527                 c.Assert(err, check.NotNil)
528                 for _, errMatch := range tt.errMatches {
529                         seen := false
530                         for _, validationErr := range validationErrs {
531                                 if regexp.MustCompile(errMatch).MatchString(validationErr) {
532                                         seen = true
533                                         break
534                                 }
535                         }
536                         if len(validationErrs) == 0 {
537                                 c.Assert(err, check.ErrorMatches, errMatch)
538                         } else {
539                                 c.Assert(seen, check.Equals, true,
540                                         check.Commentf("Expected to see error matching %q:\n%s",
541                                                 errMatch, strings.Join(validationErrs, "\n")))
542                         }
543                 }
544         }
545 }