21137: Support RP-initiated logout with OIDC
[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                                         "cwl_input":             true,
236                                         "cwl_output":            true,
237                                         "docker-image-repo-tag": true,
238                                         "filters":               true,
239                                         "groups":                true,
240                                         "image_timestamp":       true,
241                                         "template_uuid":         true,
242                                         "type":                  true,
243                                         "username":              true,
244                                 },
245                                 StrictTags: false,
246                                 Tags: map[string]VocabularyTag{
247                                         "IDTAGANIMALS": {
248                                                 Strict: false,
249                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
250                                                 Values: map[string]VocabularyTagValue{
251                                                         "IDVALANIMAL1": {
252                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Homo sapiens"}},
253                                                         },
254                                                         "IDVALANIMAL2": {
255                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Loxodonta"}},
256                                                         },
257                                                         "DOG": {
258                                                                 Labels: []VocabularyLabel{{Label: "Dog"}, {Label: "Canis lupus familiaris"}, {Label: "dOg"}},
259                                                         },
260                                                 },
261                                         },
262                                 },
263                         },
264                 },
265                 {
266                         "Invalid JSON error with line & column numbers",
267                         `{"tags":{
268                                 "aKey":{
269                                         "labels": [,{"label": "A label"}]
270                                 }
271                         }}`,
272                         false, `invalid JSON format:.*\(line \d+, column \d+\)`, nil,
273                 },
274                 {
275                         "Invalid JSON with duplicate & reserved keys",
276                         `{"tags":{
277                                 "type":{
278                                         "strict": false,
279                                         "labels": [{"label": "Class", "label": "Type"}]
280                                 },
281                                 "type":{
282                                         "labels": []
283                                 }
284                         }}`,
285                         false, "(?s).*duplicate JSON key \"tags.type.labels.0.label\"\nduplicate JSON key \"tags.type\"\ntag key \"type\" is reserved", nil,
286                 },
287         }
288
289         for _, tt := range tests {
290                 c.Log(c.TestName()+" ", tt.name)
291                 voc, err := NewVocabulary([]byte(tt.data), []string{})
292                 if tt.isValid {
293                         c.Assert(err, check.IsNil)
294                 } else {
295                         c.Assert(err, check.NotNil)
296                         if tt.errMatches != "" {
297                                 c.Assert(err, check.ErrorMatches, tt.errMatches)
298                         }
299                 }
300                 c.Assert(voc, check.DeepEquals, tt.expect)
301         }
302 }
303
304 func (s *VocabularySuite) TestValidSystemProperties(c *check.C) {
305         s.testVoc.StrictTags = true
306         properties := map[string]interface{}{
307                 "arv:gitBranch": "main",
308                 "arv:OK":        true,
309                 "arv:cost":      123,
310         }
311         c.Check(s.testVoc.Check(properties), check.IsNil)
312 }
313
314 func (s *VocabularySuite) TestSystemPropertiesPrefixTypo(c *check.C) {
315         s.testVoc.StrictTags = true
316         for _, key := range []string{
317                 // Extra characters in prefix
318                 "arv :foo",
319                 " arv:foo",
320                 // Wrong punctuation
321                 "arv.foo",
322                 "arv-foo",
323                 "arv_foo",
324                 // Wrong case
325                 "Arv:foo",
326                 // Wrong word
327                 "arvados",
328                 "arvados:foo",
329         } {
330                 properties := map[string]interface{}{key: "value"}
331                 c.Check(s.testVoc.Check(properties), check.NotNil)
332         }
333 }
334
335 func (s *VocabularySuite) TestValidationErrors(c *check.C) {
336         tests := []struct {
337                 name       string
338                 voc        *Vocabulary
339                 errMatches []string
340         }{
341                 {
342                         "Strict vocabulary, no keys",
343                         &Vocabulary{
344                                 StrictTags: true,
345                         },
346                         []string{"vocabulary is strict but no tags are defined"},
347                 },
348                 {
349                         "Collision between tag key and tag key label",
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: "IDTAGANIMALS"}},
360                                         },
361                                 },
362                         },
363                         nil, // Depending on how the map is sorted, this could be one of two errors
364                 },
365                 {
366                         "Collision between tag key and tag key label (case-insensitive)",
367                         &Vocabulary{
368                                 StrictTags: false,
369                                 Tags: map[string]VocabularyTag{
370                                         "IDTAGANIMALS": {
371                                                 Strict: false,
372                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
373                                         },
374                                         "IDTAGCOMMENT": {
375                                                 Strict: false,
376                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IdTagAnimals"}},
377                                         },
378                                 },
379                         },
380                         nil, // Depending on how the map is sorted, this could be one of two errors
381                 },
382                 {
383                         "Collision between tag key labels",
384                         &Vocabulary{
385                                 StrictTags: false,
386                                 Tags: map[string]VocabularyTag{
387                                         "IDTAGANIMALS": {
388                                                 Strict: false,
389                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
390                                         },
391                                         "IDTAGCOMMENT": {
392                                                 Strict: false,
393                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "Animal"}},
394                                         },
395                                 },
396                         },
397                         []string{"(?s).*tag label.*for key.*already seen.*"},
398                 },
399                 {
400                         "Collision between tag value and tag value label",
401                         &Vocabulary{
402                                 StrictTags: false,
403                                 Tags: map[string]VocabularyTag{
404                                         "IDTAGANIMALS": {
405                                                 Strict: false,
406                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
407                                                 Values: map[string]VocabularyTagValue{
408                                                         "IDVALANIMAL1": {
409                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
410                                                         },
411                                                         "IDVALANIMAL2": {
412                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDVALANIMAL1"}},
413                                                         },
414                                                 },
415                                         },
416                                 },
417                         },
418                         nil, // Depending on how the map is sorted, this could be one of two errors
419                 },
420                 {
421                         "Collision between tag value and tag value label (case-insensitive)",
422                         &Vocabulary{
423                                 StrictTags: false,
424                                 Tags: map[string]VocabularyTag{
425                                         "IDTAGANIMALS": {
426                                                 Strict: false,
427                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
428                                                 Values: map[string]VocabularyTagValue{
429                                                         "IDVALANIMAL1": {
430                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
431                                                         },
432                                                         "IDVALANIMAL2": {
433                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDValAnimal1"}},
434                                                         },
435                                                 },
436                                         },
437                                 },
438                         },
439                         nil, // Depending on how the map is sorted, this could be one of two errors
440                 },
441                 {
442                         "Collision between tag value labels",
443                         &Vocabulary{
444                                 StrictTags: false,
445                                 Tags: map[string]VocabularyTag{
446                                         "IDTAGANIMALS": {
447                                                 Strict: false,
448                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
449                                                 Values: map[string]VocabularyTagValue{
450                                                         "IDVALANIMAL1": {
451                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
452                                                         },
453                                                         "IDVALANIMAL2": {
454                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Mammal"}},
455                                                         },
456                                                 },
457                                         },
458                                 },
459                         },
460                         []string{"(?s).*tag value label.*for pair.*already seen.*on value.*"},
461                 },
462                 {
463                         "Collision between tag value labels (case-insensitive)",
464                         &Vocabulary{
465                                 StrictTags: false,
466                                 Tags: map[string]VocabularyTag{
467                                         "IDTAGANIMALS": {
468                                                 Strict: false,
469                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
470                                                 Values: map[string]VocabularyTagValue{
471                                                         "IDVALANIMAL1": {
472                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
473                                                         },
474                                                         "IDVALANIMAL2": {
475                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "mAMMAL"}},
476                                                         },
477                                                 },
478                                         },
479                                 },
480                         },
481                         []string{"(?s).*tag value label.*for pair.*already seen.*on value.*"},
482                 },
483                 {
484                         "Strict tag key, with no values",
485                         &Vocabulary{
486                                 StrictTags: false,
487                                 Tags: map[string]VocabularyTag{
488                                         "IDTAGANIMALS": {
489                                                 Strict: true,
490                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
491                                         },
492                                 },
493                         },
494                         []string{"(?s).*tag key.*is configured as strict but doesn't provide values"},
495                 },
496                 {
497                         "Multiple errors reported",
498                         &Vocabulary{
499                                 StrictTags: false,
500                                 Tags: map[string]VocabularyTag{
501                                         "IDTAGANIMALS": {
502                                                 Strict: true,
503                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
504                                         },
505                                         "IDTAGSIZES": {
506                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Size"}},
507                                         },
508                                 },
509                         },
510                         []string{
511                                 "(?s).*tag key.*is configured as strict but doesn't provide values.*",
512                                 "(?s).*tag label.*for key.*already seen.*",
513                         },
514                 },
515         }
516         for _, tt := range tests {
517                 c.Log(c.TestName()+" ", tt.name)
518                 validationErrs, err := tt.voc.validate()
519                 c.Assert(err, check.NotNil)
520                 for _, errMatch := range tt.errMatches {
521                         seen := false
522                         for _, validationErr := range validationErrs {
523                                 if regexp.MustCompile(errMatch).MatchString(validationErr) {
524                                         seen = true
525                                         break
526                                 }
527                         }
528                         if len(validationErrs) == 0 {
529                                 c.Assert(err, check.ErrorMatches, errMatch)
530                         } else {
531                                 c.Assert(seen, check.Equals, true,
532                                         check.Commentf("Expected to see error matching %q:\n%s",
533                                                 errMatch, strings.Join(validationErrs, "\n")))
534                         }
535                 }
536         }
537 }