17944: Adds a case-insensitive check for key/value against labels.
[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         for _, tt := range tests {
173                 c.Log(c.TestName()+" ", tt.name)
174                 s.testVoc.StrictTags = tt.strictVoc
175
176                 var data map[string]interface{}
177                 err := json.Unmarshal([]byte(tt.props), &data)
178                 c.Assert(err, check.IsNil)
179                 err = s.testVoc.Check(data)
180                 if tt.expectSuccess {
181                         c.Assert(err, check.IsNil)
182                 } else {
183                         c.Assert(err, check.NotNil)
184                         c.Assert(err.Error(), check.Matches, tt.errMatches)
185                 }
186         }
187 }
188
189 func (s *VocabularySuite) TestNewVocabulary(c *check.C) {
190         tests := []struct {
191                 name       string
192                 data       string
193                 isValid    bool
194                 errMatches string
195                 expect     *Vocabulary
196         }{
197                 {"Empty data", "", true, "", &Vocabulary{}},
198                 {"Invalid JSON", "foo", false, "invalid JSON format.*", nil},
199                 {"Valid, empty JSON", "{}", false, ".*doesn't match Vocabulary format.*", nil},
200                 {"Valid JSON, wrong data", `{"foo":"bar"}`, false, ".*doesn't match Vocabulary format.*", nil},
201                 {
202                         "Simple valid example",
203                         `{"tags":{
204                                 "IDTAGANIMALS":{
205                                         "strict": false,
206                                         "labels": [{"label": "Animal"}, {"label": "Creature"}],
207                                         "values": {
208                                                 "IDVALANIMAL1":{"labels":[{"label":"Human"}, {"label":"Homo sapiens"}]},
209                                                 "IDVALANIMAL2":{"labels":[{"label":"Elephant"}, {"label":"Loxodonta"}]}
210                                         }
211                                 }
212                         }}`,
213                         true, "",
214                         &Vocabulary{
215                                 reservedTagKeys: map[string]bool{
216                                         "type":                  true,
217                                         "template_uuid":         true,
218                                         "groups":                true,
219                                         "username":              true,
220                                         "image_timestamp":       true,
221                                         "docker-image-repo-tag": true,
222                                         "filters":               true,
223                                         "container_request":     true,
224                                 },
225                                 StrictTags: false,
226                                 Tags: map[string]VocabularyTag{
227                                         "IDTAGANIMALS": {
228                                                 Strict: false,
229                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
230                                                 Values: map[string]VocabularyTagValue{
231                                                         "IDVALANIMAL1": {
232                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Homo sapiens"}},
233                                                         },
234                                                         "IDVALANIMAL2": {
235                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Loxodonta"}},
236                                                         },
237                                                 },
238                                         },
239                                 },
240                         },
241                 },
242                 {
243                         "Valid data, but uses reserved key",
244                         `{"tags":{
245                                 "type":{
246                                         "strict": false,
247                                         "labels": [{"label": "Type"}]
248                                 }
249                         }}`,
250                         false, "tag key.*is reserved", nil,
251                 },
252         }
253
254         for _, tt := range tests {
255                 c.Log(c.TestName()+" ", tt.name)
256                 voc, err := NewVocabulary([]byte(tt.data), []string{})
257                 if tt.isValid {
258                         c.Assert(err, check.IsNil)
259                 } else {
260                         c.Assert(err, check.NotNil)
261                         if tt.errMatches != "" {
262                                 c.Assert(err, check.ErrorMatches, tt.errMatches)
263                         }
264                 }
265                 c.Assert(voc, check.DeepEquals, tt.expect)
266         }
267 }
268
269 func (s *VocabularySuite) TestValidationErrors(c *check.C) {
270         tests := []struct {
271                 name       string
272                 voc        *Vocabulary
273                 errMatches string
274         }{
275                 {
276                         "Strict vocabulary, no keys",
277                         &Vocabulary{
278                                 StrictTags: true,
279                         },
280                         "vocabulary is strict but no tags are defined",
281                 },
282                 {
283                         "Collision between tag key and tag key label",
284                         &Vocabulary{
285                                 StrictTags: false,
286                                 Tags: map[string]VocabularyTag{
287                                         "IDTAGANIMALS": {
288                                                 Strict: false,
289                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
290                                         },
291                                         "IDTAGCOMMENT": {
292                                                 Strict: false,
293                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IDTAGANIMALS"}},
294                                         },
295                                 },
296                         },
297                         "", // Depending on how the map is sorted, this could be one of two errors
298                 },
299                 {
300                         "Collision between tag key and tag key label (case-insensitive)",
301                         &Vocabulary{
302                                 StrictTags: false,
303                                 Tags: map[string]VocabularyTag{
304                                         "IDTAGANIMALS": {
305                                                 Strict: false,
306                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
307                                         },
308                                         "IDTAGCOMMENT": {
309                                                 Strict: false,
310                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IdTagAnimals"}},
311                                         },
312                                 },
313                         },
314                         "", // Depending on how the map is sorted, this could be one of two errors
315                 },
316                 {
317                         "Collision between tag key labels",
318                         &Vocabulary{
319                                 StrictTags: false,
320                                 Tags: map[string]VocabularyTag{
321                                         "IDTAGANIMALS": {
322                                                 Strict: false,
323                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
324                                         },
325                                         "IDTAGCOMMENT": {
326                                                 Strict: false,
327                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "Animal"}},
328                                         },
329                                 },
330                         },
331                         "tag label.*for key.*already seen.*",
332                 },
333                 {
334                         "Collision between tag value and tag value label",
335                         &Vocabulary{
336                                 StrictTags: false,
337                                 Tags: map[string]VocabularyTag{
338                                         "IDTAGANIMALS": {
339                                                 Strict: false,
340                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
341                                                 Values: map[string]VocabularyTagValue{
342                                                         "IDVALANIMAL1": {
343                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
344                                                         },
345                                                         "IDVALANIMAL2": {
346                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDVALANIMAL1"}},
347                                                         },
348                                                 },
349                                         },
350                                 },
351                         },
352                         "", // Depending on how the map is sorted, this could be one of two errors
353                 },
354                 {
355                         "Collision between tag value and tag value label (case-insensitive)",
356                         &Vocabulary{
357                                 StrictTags: false,
358                                 Tags: map[string]VocabularyTag{
359                                         "IDTAGANIMALS": {
360                                                 Strict: false,
361                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
362                                                 Values: map[string]VocabularyTagValue{
363                                                         "IDVALANIMAL1": {
364                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
365                                                         },
366                                                         "IDVALANIMAL2": {
367                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDValAnimal1"}},
368                                                         },
369                                                 },
370                                         },
371                                 },
372                         },
373                         "", // Depending on how the map is sorted, this could be one of two errors
374                 },
375                 {
376                         "Collision between tag value labels",
377                         &Vocabulary{
378                                 StrictTags: false,
379                                 Tags: map[string]VocabularyTag{
380                                         "IDTAGANIMALS": {
381                                                 Strict: false,
382                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
383                                                 Values: map[string]VocabularyTagValue{
384                                                         "IDVALANIMAL1": {
385                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
386                                                         },
387                                                         "IDVALANIMAL2": {
388                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Mammal"}},
389                                                         },
390                                                 },
391                                         },
392                                 },
393                         },
394                         "tag value label.*for pair.*already seen.*",
395                 },
396                 {
397                         "Strict tag key, with no values",
398                         &Vocabulary{
399                                 StrictTags: false,
400                                 Tags: map[string]VocabularyTag{
401                                         "IDTAGANIMALS": {
402                                                 Strict: true,
403                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
404                                         },
405                                 },
406                         },
407                         "tag key.*is configured as strict but doesn't provide values",
408                 },
409         }
410         for _, tt := range tests {
411                 c.Log(c.TestName()+" ", tt.name)
412                 err := tt.voc.validate()
413                 c.Assert(err, check.NotNil)
414                 if tt.errMatches != "" {
415                         c.Assert(err, check.ErrorMatches, tt.errMatches)
416                 }
417         }
418 }