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