17944: Improves keys & value collision validation against aliases. Adds tests.
[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         }{
70                 // Check succeeds
71                 {"Known key, known value", false, `{"IDTAGANIMALS":"IDVALANIMAL1"}`, true},
72                 {"Unknown non-alias key on non-strict vocabulary", false, `{"foo":"bar"}`, true},
73                 {"Known non-strict key, unknown non-alias value", false, `{"IDTAGANIMALS":"IDVALANIMAL3"}`, true},
74                 {"Undefined but reserved key on strict vocabulary", true, `{"reservedKey":"bar"}`, true},
75                 {"Known key, list of known values", false, `{"IDTAGANIMALS":["IDVALANIMAL1","IDVALANIMAL2"]}`, true},
76                 {"Known non-strict key, list of unknown non-alias values", false, `{"IDTAGCOMMENT":["hello world","lorem ipsum"]}`, true},
77                 // Check fails
78                 {"Known first key & value; known 2nd key, unknown 2nd value", false, `{"IDTAGANIMALS":"IDVALANIMAL1", "IDTAGIMPORTANCE": "blah blah"}`, false},
79                 {"Unknown non-alias key on strict vocabulary", true, `{"foo":"bar"}`, false},
80                 {"Known non-strict key, known value alias", false, `{"IDTAGANIMALS":"Loxodonta"}`, false},
81                 {"Known strict key, unknown non-alias value", false, `{"IDTAGIMPORTANCE":"Unimportant"}`, false},
82                 {"Known strict key, known value alias", false, `{"IDTAGIMPORTANCE":"High"}`, false},
83                 {"Known strict key, list of known alias values", false, `{"IDTAGIMPORTANCE":["Unimportant","High"]}`, false},
84                 {"Known strict key, list of unknown non-alias values", false, `{"IDTAGIMPORTANCE":["foo","bar"]}`, false},
85         }
86         for _, tt := range tests {
87                 c.Log(c.TestName()+" ", tt.name)
88                 s.testVoc.StrictTags = tt.strictVoc
89
90                 var data map[string]interface{}
91                 err := json.Unmarshal([]byte(tt.props), &data)
92                 c.Assert(err, check.IsNil)
93                 err = s.testVoc.Check(data)
94                 if tt.expectSuccess {
95                         c.Assert(err, check.IsNil)
96                 } else {
97                         c.Assert(err, check.NotNil)
98                 }
99         }
100 }
101
102 func (s *VocabularySuite) TestNewVocabulary(c *check.C) {
103         tests := []struct {
104                 name       string
105                 data       string
106                 isValid    bool
107                 errMatches string
108                 expect     *Vocabulary
109         }{
110                 {"Empty data", "", true, "", &Vocabulary{}},
111                 {"Invalid JSON", "foo", false, "invalid JSON format.*", nil},
112                 {"Valid, empty JSON", "{}", false, ".*doesn't match Vocabulary format.*", nil},
113                 {"Valid JSON, wrong data", `{"foo":"bar"}`, false, ".*doesn't match Vocabulary format.*", nil},
114                 {
115                         "Simple valid example",
116                         `{"tags":{
117                                 "IDTAGANIMALS":{
118                                         "strict": false,
119                                         "labels": [{"label": "Animal"}, {"label": "Creature"}],
120                                         "values": {
121                                                 "IDVALANIMAL1":{"labels":[{"label":"Human"}, {"label":"Homo sapiens"}]},
122                                                 "IDVALANIMAL2":{"labels":[{"label":"Elephant"}, {"label":"Loxodonta"}]}
123                                         }
124                                 }
125                         }}`,
126                         true, "",
127                         &Vocabulary{
128                                 reservedTagKeys: map[string]bool{
129                                         "type":                  true,
130                                         "template_uuid":         true,
131                                         "groups":                true,
132                                         "username":              true,
133                                         "image_timestamp":       true,
134                                         "docker-image-repo-tag": true,
135                                         "filters":               true,
136                                         "container_request":     true,
137                                 },
138                                 StrictTags: false,
139                                 Tags: map[string]VocabularyTag{
140                                         "IDTAGANIMALS": {
141                                                 Strict: false,
142                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
143                                                 Values: map[string]VocabularyTagValue{
144                                                         "IDVALANIMAL1": {
145                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Homo sapiens"}},
146                                                         },
147                                                         "IDVALANIMAL2": {
148                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Loxodonta"}},
149                                                         },
150                                                 },
151                                         },
152                                 },
153                         },
154                 },
155                 {
156                         "Valid data, but uses reserved key",
157                         `{"tags":{
158                                 "type":{
159                                         "strict": false,
160                                         "labels": [{"label": "Type"}]
161                                 }
162                         }}`,
163                         false, "tag key.*is reserved", nil,
164                 },
165         }
166
167         for _, tt := range tests {
168                 c.Log(c.TestName()+" ", tt.name)
169                 voc, err := NewVocabulary([]byte(tt.data), []string{})
170                 if tt.isValid {
171                         c.Assert(err, check.IsNil)
172                 } else {
173                         c.Assert(err, check.NotNil)
174                         if tt.errMatches != "" {
175                                 c.Assert(err, check.ErrorMatches, tt.errMatches)
176                         }
177                 }
178                 c.Assert(voc, check.DeepEquals, tt.expect)
179         }
180 }
181
182 func (s *VocabularySuite) TestValidationErrors(c *check.C) {
183         tests := []struct {
184                 name       string
185                 voc        *Vocabulary
186                 errMatches string
187         }{
188                 {
189                         "Strict vocabulary, no keys",
190                         &Vocabulary{
191                                 StrictTags: true,
192                         },
193                         "vocabulary is strict but no tags are defined",
194                 },
195                 {
196                         "Collision between tag key and tag key label",
197                         &Vocabulary{
198                                 StrictTags: false,
199                                 Tags: map[string]VocabularyTag{
200                                         "IDTAGANIMALS": {
201                                                 Strict: false,
202                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
203                                         },
204                                         "IDTAGCOMMENT": {
205                                                 Strict: false,
206                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IDTAGANIMALS"}},
207                                         },
208                                 },
209                         },
210                         "", // Depending on how the map is sorted, this could be one of two errors
211                 },
212                 {
213                         "Collision between tag key and tag key label (case-insensitive)",
214                         &Vocabulary{
215                                 StrictTags: false,
216                                 Tags: map[string]VocabularyTag{
217                                         "IDTAGANIMALS": {
218                                                 Strict: false,
219                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
220                                         },
221                                         "IDTAGCOMMENT": {
222                                                 Strict: false,
223                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "IdTagAnimals"}},
224                                         },
225                                 },
226                         },
227                         "", // Depending on how the map is sorted, this could be one of two errors
228                 },
229                 {
230                         "Collision between tag key labels",
231                         &Vocabulary{
232                                 StrictTags: false,
233                                 Tags: map[string]VocabularyTag{
234                                         "IDTAGANIMALS": {
235                                                 Strict: false,
236                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
237                                         },
238                                         "IDTAGCOMMENT": {
239                                                 Strict: false,
240                                                 Labels: []VocabularyLabel{{Label: "Comment"}, {Label: "Animal"}},
241                                         },
242                                 },
243                         },
244                         "tag label.*for key.*already seen.*",
245                 },
246                 {
247                         "Collision between tag value and tag value label",
248                         &Vocabulary{
249                                 StrictTags: false,
250                                 Tags: map[string]VocabularyTag{
251                                         "IDTAGANIMALS": {
252                                                 Strict: false,
253                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
254                                                 Values: map[string]VocabularyTagValue{
255                                                         "IDVALANIMAL1": {
256                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
257                                                         },
258                                                         "IDVALANIMAL2": {
259                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDVALANIMAL1"}},
260                                                         },
261                                                 },
262                                         },
263                                 },
264                         },
265                         "", // Depending on how the map is sorted, this could be one of two errors
266                 },
267                 {
268                         "Collision between tag value and tag value label (case-insensitive)",
269                         &Vocabulary{
270                                 StrictTags: false,
271                                 Tags: map[string]VocabularyTag{
272                                         "IDTAGANIMALS": {
273                                                 Strict: false,
274                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
275                                                 Values: map[string]VocabularyTagValue{
276                                                         "IDVALANIMAL1": {
277                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
278                                                         },
279                                                         "IDVALANIMAL2": {
280                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "IDValAnimal1"}},
281                                                         },
282                                                 },
283                                         },
284                                 },
285                         },
286                         "", // Depending on how the map is sorted, this could be one of two errors
287                 },
288                 {
289                         "Collision between tag value labels",
290                         &Vocabulary{
291                                 StrictTags: false,
292                                 Tags: map[string]VocabularyTag{
293                                         "IDTAGANIMALS": {
294                                                 Strict: false,
295                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
296                                                 Values: map[string]VocabularyTagValue{
297                                                         "IDVALANIMAL1": {
298                                                                 Labels: []VocabularyLabel{{Label: "Human"}, {Label: "Mammal"}},
299                                                         },
300                                                         "IDVALANIMAL2": {
301                                                                 Labels: []VocabularyLabel{{Label: "Elephant"}, {Label: "Mammal"}},
302                                                         },
303                                                 },
304                                         },
305                                 },
306                         },
307                         "tag value label.*for pair.*already seen.*",
308                 },
309                 {
310                         "Strict tag key, with no values",
311                         &Vocabulary{
312                                 StrictTags: false,
313                                 Tags: map[string]VocabularyTag{
314                                         "IDTAGANIMALS": {
315                                                 Strict: true,
316                                                 Labels: []VocabularyLabel{{Label: "Animal"}, {Label: "Creature"}},
317                                         },
318                                 },
319                         },
320                         "tag key.*is configured as strict but doesn't provide values",
321                 },
322         }
323         for _, tt := range tests {
324                 c.Log(c.TestName()+" ", tt.name)
325                 err := tt.voc.validate()
326                 c.Assert(err, check.NotNil)
327                 if tt.errMatches != "" {
328                         c.Assert(err, check.ErrorMatches, tt.errMatches)
329                 }
330         }
331 }