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