Merge branch '17583-federated-token-reqs'. Closes #17583
[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                                         "type":                  true,
234                                         "template_uuid":         true,
235                                         "groups":                true,
236                                         "username":              true,
237                                         "image_timestamp":       true,
238                                         "docker-image-repo-tag": true,
239                                         "filters":               true,
240                                         "container_request":     true,
241                                 },
242                                 StrictTags: false,
243                                 Tags: map[string]VocabularyTag{
244                                         "IDTAGANIMALS": {
245                                                 Strict: false,
246                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
247                                                 Values: map[string]VocabularyTagValue{
248                                                         "IDVALANIMAL1": {
249                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Homo sapiens"}},
250                                                         },
251                                                         "IDVALANIMAL2": {
252                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Loxodonta"}},
253                                                         },
254                                                         "DOG": {
255                                                                 Labels: []VocabularyLabel{{Label: "Dog"}, {Label: "Canis lupus familiaris"}, {Label: "dOg"}},
256                                                         },
257                                                 },
258                                         },
259                                 },
260                         },
261                 },
262                 {
263                         "Invalid JSON error with line & column numbers",
264                         `{"tags":{
265                                 "aKey":{
266                                         "labels": [,{"label": "A label"}]
267                                 }
268                         }}`,
269                         false, `invalid JSON format:.*\(line \d+, column \d+\)`, nil,
270                 },
271                 {
272                         "Invalid JSON with duplicate & reserved keys",
273                         `{"tags":{
274                                 "type":{
275                                         "strict": false,
276                                         "labels": [{"label": "Class", "label": "Type"}]
277                                 },
278                                 "type":{
279                                         "labels": []
280                                 }
281                         }}`,
282                         false, "(?s).*duplicate JSON key \"tags.type.labels.0.label\"\nduplicate JSON key \"tags.type\"\ntag key \"type\" is reserved", nil,
283                 },
284         }
285
286         for _, tt := range tests {
287                 c.Log(c.TestName()+" ", tt.name)
288                 voc, err := NewVocabulary([]byte(tt.data), []string{})
289                 if tt.isValid {
290                         c.Assert(err, check.IsNil)
291                 } else {
292                         c.Assert(err, check.NotNil)
293                         if tt.errMatches != "" {
294                                 c.Assert(err, check.ErrorMatches, tt.errMatches)
295                         }
296                 }
297                 c.Assert(voc, check.DeepEquals, tt.expect)
298         }
299 }
300
301 func (s *VocabularySuite) TestValidationErrors(c *check.C) {
302         tests := []struct {
303                 name       string
304                 voc        *Vocabulary
305                 errMatches []string
306         }{
307                 {
308                         "Strict vocabulary, no keys",
309                         &Vocabulary{
310                                 StrictTags: true,
311                         },
312                         []string{"vocabulary is strict but no tags are defined"},
313                 },
314                 {
315                         "Collision between tag key and tag key label",
316                         &Vocabulary{
317                                 StrictTags: false,
318                                 Tags: map[string]VocabularyTag{
319                                         "IDTAGANIMALS": {
320                                                 Strict: false,
321                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
322                                         },
323                                         "IDTAGCOMMENT": {
324                                                 Strict: false,
325                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IDTAGANIMALS"}},
326                                         },
327                                 },
328                         },
329                         nil, // Depending on how the map is sorted, this could be one of two errors
330                 },
331                 {
332                         "Collision between tag key and tag key label (case-insensitive)",
333                         &Vocabulary{
334                                 StrictTags: false,
335                                 Tags: map[string]VocabularyTag{
336                                         "IDTAGANIMALS": {
337                                                 Strict: false,
338                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
339                                         },
340                                         "IDTAGCOMMENT": {
341                                                 Strict: false,
342                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IdTagAnimals"}},
343                                         },
344                                 },
345                         },
346                         nil, // Depending on how the map is sorted, this could be one of two errors
347                 },
348                 {
349                         "Collision between tag key labels",
350                         &Vocabulary{
351                                 StrictTags: false,
352                                 Tags: map[string]VocabularyTag{
353                                         "IDTAGANIMALS": {
354                                                 Strict: false,
355                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
356                                         },
357                                         "IDTAGCOMMENT": {
358                                                 Strict: false,
359                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "Animal"}},
360                                         },
361                                 },
362                         },
363                         []string{"(?s).*tag label.*for key.*already seen.*"},
364                 },
365                 {
366                         "Collision between tag value and tag value label",
367                         &Vocabulary{
368                                 StrictTags: false,
369                                 Tags: map[string]VocabularyTag{
370                                         "IDTAGANIMALS": {
371                                                 Strict: false,
372                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
373                                                 Values: map[string]VocabularyTagValue{
374                                                         "IDVALANIMAL1": {
375                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
376                                                         },
377                                                         "IDVALANIMAL2": {
378                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDVALANIMAL1"}},
379                                                         },
380                                                 },
381                                         },
382                                 },
383                         },
384                         nil, // Depending on how the map is sorted, this could be one of two errors
385                 },
386                 {
387                         "Collision between tag value and tag value label (case-insensitive)",
388                         &Vocabulary{
389                                 StrictTags: false,
390                                 Tags: map[string]VocabularyTag{
391                                         "IDTAGANIMALS": {
392                                                 Strict: false,
393                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
394                                                 Values: map[string]VocabularyTagValue{
395                                                         "IDVALANIMAL1": {
396                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
397                                                         },
398                                                         "IDVALANIMAL2": {
399                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDValAnimal1"}},
400                                                         },
401                                                 },
402                                         },
403                                 },
404                         },
405                         nil, // Depending on how the map is sorted, this could be one of two errors
406                 },
407                 {
408                         "Collision between tag value labels",
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: "Mammal"}},
421                                                         },
422                                                 },
423                                         },
424                                 },
425                         },
426                         []string{"(?s).*tag value label.*for pair.*already seen.*on value.*"},
427                 },
428                 {
429                         "Collision between tag value labels (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: "mAMMAL"}},
442                                                         },
443                                                 },
444                                         },
445                                 },
446                         },
447                         []string{"(?s).*tag value label.*for pair.*already seen.*on value.*"},
448                 },
449                 {
450                         "Strict tag key, with no values",
451                         &Vocabulary{
452                                 StrictTags: false,
453                                 Tags: map[string]VocabularyTag{
454                                         "IDTAGANIMALS": {
455                                                 Strict: true,
456                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
457                                         },
458                                 },
459                         },
460                         []string{"(?s).*tag key.*is configured as strict but doesn't provide values"},
461                 },
462                 {
463                         "Multiple errors reported",
464                         &Vocabulary{
465                                 StrictTags: false,
466                                 Tags: map[string]VocabularyTag{
467                                         "IDTAGANIMALS": {
468                                                 Strict: true,
469                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
470                                         },
471                                         "IDTAGSIZES": {
472                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Size"}},
473                                         },
474                                 },
475                         },
476                         []string{
477                                 "(?s).*tag key.*is configured as strict but doesn't provide values.*",
478                                 "(?s).*tag label.*for key.*already seen.*",
479                         },
480                 },
481         }
482         for _, tt := range tests {
483                 c.Log(c.TestName()+" ", tt.name)
484                 validationErrs, err := tt.voc.validate()
485                 c.Assert(err, check.NotNil)
486                 for _, errMatch := range tt.errMatches {
487                         seen := false
488                         for _, validationErr := range validationErrs {
489                                 if regexp.MustCompile(errMatch).MatchString(validationErr) {
490                                         seen = true
491                                         break
492                                 }
493                         }
494                         if len(validationErrs) == 0 {
495                                 c.Assert(err, check.ErrorMatches, errMatch)
496                         } else {
497                                 c.Assert(seen, check.Equals, true,
498                                         check.Commentf("Expected to see error matching %q:\n%s",
499                                                 errMatch, strings.Join(validationErrs, "\n")))
500                         }
501                 }
502         }
503 }