toscaparser: Support deriving from capability types of no property
[parser.git] / tosca2heat / tosca-parser / toscaparser / tests / test_datatypes.py
1 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
2 #    not use this file except in compliance with the License. You may obtain
3 #    a copy of the License at
4 #
5 #         http://www.apache.org/licenses/LICENSE-2.0
6 #
7 #    Unless required by applicable law or agreed to in writing, software
8 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 #    License for the specific language governing permissions and limitations
11 #    under the License.
12
13 import os
14
15 from testtools.testcase import skip
16 from toscaparser.common import exception
17 from toscaparser.dataentity import DataEntity
18 from toscaparser.elements.datatype import DataType
19 from toscaparser.parameters import Input
20 from toscaparser.tests.base import TestCase
21 from toscaparser.tosca_template import ToscaTemplate
22 from toscaparser.utils.gettextutils import _
23 from toscaparser.utils import yamlparser
24
25
26 class DataTypeTest(TestCase):
27
28     custom_type_schema = '''
29     tosca.my.datatypes.PeopleBase:
30       properties:
31         name:
32           type: string
33           required: true
34           constraints:
35             - min_length: 2
36         gender:
37           type: string
38           default: unknown
39
40     tosca.my.datatypes.People:
41       derived_from: tosca.my.datatypes.PeopleBase
42       properties:
43         addresses:
44           type: map
45           required: false
46           entry_schema:
47             type: string
48         contacts:
49           type: list
50           required: false
51           entry_schema:
52             type: tosca.my.datatypes.ContactInfo
53
54     tosca.my.datatypes.ContactInfo:
55       description: simple contact information
56       properties:
57         contact_name:
58           type: string
59           required: true
60           constraints:
61             - min_length: 2
62         contact_email:
63           type: string
64         contact_phone:
65           type: string
66
67     tosca.my.datatypes.TestLab:
68       properties:
69         humidity:
70           type: range
71           required: false
72           constraints:
73             - in_range: [-256, INFINITY]
74         temperature1:
75           type: range
76           required: false
77           constraints:
78             - in_range: [-256, UNBOUNDED]
79         temperature2:
80           type: range
81           required: false
82           constraints:
83             - in_range: [UNBOUNDED, 256]
84     '''
85     custom_type_def = yamlparser.simple_parse(custom_type_schema)
86
87     def test_empty_template(self):
88         value_snippet = ''
89         value = yamlparser.simple_parse(value_snippet)
90         self.assertEqual(value, {})
91
92     def test_built_in_datatype(self):
93         value_snippet = '''
94         private_network:
95           network_name: private
96           network_id: 3e54214f-5c09-1bc9-9999-44100326da1b
97           addresses: [ 10.111.128.10 ]
98         '''
99         value = yamlparser.simple_parse(value_snippet)
100         data = DataEntity('tosca.datatypes.network.NetworkInfo',
101                           value.get('private_network'))
102         self.assertIsNotNone(data.validate())
103
104         value_snippet = '''
105         portspec_valid:
106           protocol: tcp
107         '''
108         value = yamlparser.simple_parse(value_snippet)
109         data = DataEntity('tosca.datatypes.network.PortSpec',
110                           value.get('portspec_valid'))
111         self.assertIsNotNone(data.validate())
112
113         value_snippet = '''
114         portspec_invalid:
115           protocol: xyz
116         '''
117         value = yamlparser.simple_parse(value_snippet)
118         data = DataEntity('tosca.datatypes.network.PortSpec',
119                           value.get('portspec_invalid'))
120         err = self.assertRaises(exception.ValidationError, data.validate)
121         self.assertEqual(_('The value "xyz" of property "protocol" is not '
122                            'valid. Expected a value from "[udp, tcp, igmp]".'
123                            ),
124                          err.__str__())
125
126     def test_built_in_datatype_with_short_name(self):
127         value_snippet = '''
128         ethernet_port:
129           port_name: port1
130           port_id: 2c0c7a37-691a-23a6-7709-2d10ad041467
131           network_id: 3e54214f-5c09-1bc9-9999-44100326da1b
132           mac_address: f1:18:3b:41:92:1e
133           addresses: [ 172.24.9.102 ]
134         '''
135         value = yamlparser.simple_parse(value_snippet)
136         data = DataEntity('PortInfo', value.get('ethernet_port'))
137         self.assertIsNotNone(data.validate())
138
139     # Test normative PortSpec datatype's additional requirements
140     # TODO(Matt) - opened as bug 1555300
141     # Need a test for PortSpec normative data type
142     # that tests the spec. requirement: "A valid PortSpec
143     # must have at least one of the following properties:
144     # target, target_range, source or source_range."
145     # TODO(Matt) - opened as bug 1555310
146     # test PortSpec value for source and target
147     # against the source_range and target_range
148     # when specified.
149     def test_port_spec_addl_reqs(self):
150         value_snippet = '''
151         test_port:
152           protocol: tcp
153           target: 65535
154           target_range: [ 1, 65535 ]
155           source: 1
156           source_range: [ 1, 65535 ]
157
158         '''
159         value = yamlparser.simple_parse(value_snippet)
160         data = DataEntity('tosca.datatypes.network.PortSpec',
161                           value.get('test_port'))
162         self.assertIsNotNone(data.validate())
163
164     def test_built_in_datatype_without_properties(self):
165         value_snippet = '''
166         2
167         '''
168         value = yamlparser.simple_parse(value_snippet)
169         datatype = DataType('PortDef')
170         self.assertEqual('integer', datatype.value_type)
171         data = DataEntity('PortDef', value)
172         self.assertIsNotNone(data.validate())
173
174     @skip('The example in TOSCA spec may have some problem.')
175     def test_built_in_nested_datatype(self):
176         value_snippet = '''
177         user_port:
178           protocol: tcp
179           target: [50000]
180           source: [9000]
181         '''
182         value = yamlparser.simple_parse(value_snippet)
183         data = DataEntity('PortSpec', value.get('user_port'))
184         self.assertIsNotNone(data.validate())
185
186     def test_built_in_nested_datatype_portdef(self):
187         tpl_snippet = '''
188         inputs:
189           db_port:
190             type: PortDef
191             description: Port for the MySQL database
192         '''
193         inputs = yamlparser.simple_parse(tpl_snippet)['inputs']
194         name, attrs = list(inputs.items())[0]
195         input = Input(name, attrs)
196         self.assertIsNone(input.validate(3360))
197         err = self.assertRaises(exception.ValidationError, input.validate,
198                                 336000)
199         self.assertEqual(_('The value "336000" of property "None" is out of '
200                            'range "(min:1, max:65535)".'),
201                          err.__str__())
202
203     def test_custom_datatype(self):
204         value_snippet = '''
205         name: Mike
206         gender: male
207         '''
208         value = yamlparser.simple_parse(value_snippet)
209         data = DataEntity('tosca.my.datatypes.PeopleBase', value,
210                           DataTypeTest.custom_type_def)
211         self.assertIsNotNone(data.validate())
212
213     def test_custom_datatype_with_parent(self):
214         value_snippet = '''
215         name: Mike
216         gender: male
217         contacts:
218           - {contact_name: Tom,
219             contact_email: tom@email.com,
220             contact_phone: '123456789'}
221           - {contact_name: Jerry,
222             contact_email: jerry@email.com,
223             contact_phone: '321654987'}
224         '''
225         value = yamlparser.simple_parse(value_snippet)
226         data = DataEntity('tosca.my.datatypes.People', value,
227                           DataTypeTest.custom_type_def)
228         self.assertIsNotNone(data.validate())
229
230     # [Tom, Jerry] is not a dict, it can't be a value of datatype PeopleBase
231     def test_non_dict_value_for_datatype(self):
232         value_snippet = '''
233         [Tom, Jerry]
234         '''
235         value = yamlparser.simple_parse(value_snippet)
236         data = DataEntity('tosca.my.datatypes.PeopleBase', value,
237                           DataTypeTest.custom_type_def)
238         error = self.assertRaises(exception.TypeMismatchError, data.validate)
239         self.assertEqual(_('[\'Tom\', \'Jerry\'] must be of type '
240                            '"tosca.my.datatypes.PeopleBase".'),
241                          error.__str__())
242
243     # 'nema' is an invalid field name
244     def test_field_error_in_dataentity(self):
245         value_snippet = '''
246         nema: Mike
247         gender: male
248         '''
249         value = yamlparser.simple_parse(value_snippet)
250         data = DataEntity('tosca.my.datatypes.PeopleBase', value,
251                           DataTypeTest.custom_type_def)
252         error = self.assertRaises(exception.UnknownFieldError, data.validate)
253         self.assertEqual(_('Data value of type '
254                            '"tosca.my.datatypes.PeopleBase" contains unknown '
255                            'field "nema". Refer to the definition to verify '
256                            'valid values.'),
257                          error.__str__())
258
259     def test_default_field_in_dataentity(self):
260         value_snippet = '''
261         name: Mike
262         '''
263         value = yamlparser.simple_parse(value_snippet)
264         data = DataEntity('tosca.my.datatypes.PeopleBase', value,
265                           DataTypeTest.custom_type_def)
266         data = data.validate()
267         self.assertEqual('unknown', data.get('gender'))
268
269     # required field 'name' is missing
270     def test_missing_field_in_dataentity(self):
271         value_snippet = '''
272         gender: male
273         '''
274         value = yamlparser.simple_parse(value_snippet)
275         data = DataEntity('tosca.my.datatypes.PeopleBase', value,
276                           DataTypeTest.custom_type_def)
277         error = self.assertRaises(exception.MissingRequiredFieldError,
278                                   data.validate)
279         self.assertEqual(_('Data value of type '
280                            '"tosca.my.datatypes.PeopleBase" is missing '
281                            'required field "[\'name\']".'),
282                          error.__str__())
283
284     # the value of name field is not a string
285     def test_type_error_in_dataentity(self):
286         value_snippet = '''
287         name: 123
288         gender: male
289         '''
290         value = yamlparser.simple_parse(value_snippet)
291         data = DataEntity('tosca.my.datatypes.PeopleBase', value,
292                           DataTypeTest.custom_type_def)
293         error = self.assertRaises(ValueError, data.validate)
294         self.assertEqual(_('"123" is not a string.'), error.__str__())
295
296     # the value of name doesn't meet the defined constraint
297     def test_value_error_in_dataentity(self):
298         value_snippet = '''
299         name: M
300         gender: male
301         '''
302         value = yamlparser.simple_parse(value_snippet)
303         data = DataEntity('tosca.my.datatypes.PeopleBase', value,
304                           DataTypeTest.custom_type_def)
305         error = self.assertRaises(exception.ValidationError, data.validate)
306         self.assertEqual(_('Length of value "M" of property "name" must be '
307                            'at least "2".'), error.__str__())
308
309     # value of addresses doesn't fit the entry_schema
310     def test_validation_in_collection_entry(self):
311         value_snippet = '''
312         name: Mike
313         gender: male
314         addresses: {Home: 1, Office: 9 bar avenue}
315         '''
316         value = yamlparser.simple_parse(value_snippet)
317         data = DataEntity('tosca.my.datatypes.People', value,
318                           DataTypeTest.custom_type_def)
319         error = self.assertRaises(ValueError, data.validate)
320         self.assertEqual(_('"1" is not a string.'), error.__str__())
321
322     # 'contact_pone' is an invalid attribute name in nested datatype below
323     def test_validation_in_nested_datatype(self):
324         value_snippet = '''
325         name: Mike
326         gender: male
327         contacts:
328           - {contact_name: Tom,
329             contact_email: tom@email.com,
330             contact_pone: '123456789'}
331           - {contact_name: Jerry,
332             contact_email: jerry@email.com,
333             contact_phone: '321654987'}
334         '''
335         value = yamlparser.simple_parse(value_snippet)
336         data = DataEntity('tosca.my.datatypes.People', value,
337                           DataTypeTest.custom_type_def)
338         error = self.assertRaises(exception.UnknownFieldError, data.validate)
339         self.assertEqual(_('Data value of type '
340                            '"tosca.my.datatypes.ContactInfo" contains unknown '
341                            'field "contact_pone". Refer to the definition to '
342                            'verify valid values.'),
343                          error.__str__())
344
345     def test_datatype_in_current_template(self):
346         tpl_path = os.path.join(
347             os.path.dirname(os.path.abspath(__file__)),
348             "data/datatypes/test_custom_datatypes_in_current_template.yaml")
349         self.assertIsNotNone(ToscaTemplate(tpl_path))
350
351     def test_datatype_in_template_positive(self):
352         tpl_path = os.path.join(
353             os.path.dirname(os.path.abspath(__file__)),
354             "data/datatypes/test_custom_datatypes_positive.yaml")
355         self.assertIsNotNone(ToscaTemplate(tpl_path))
356
357     def test_datatype_in_template_invalid_value(self):
358         tpl_path = os.path.join(
359             os.path.dirname(os.path.abspath(__file__)),
360             "data/datatypes/test_custom_datatypes_value_error.yaml")
361         self.assertRaises(exception.ValidationError, ToscaTemplate, tpl_path)
362         exception.ExceptionCollector.assertExceptionMessage(
363             ValueError,
364             _('"[\'1 foo street\', \'9 bar avenue\']" is not a map.'))
365
366     def test_datatype_in_template_nested_datatype_error(self):
367         tpl_path = os.path.join(
368             os.path.dirname(os.path.abspath(__file__)),
369             "data/datatypes/test_custom_datatypes_nested_datatype_error.yaml")
370         self.assertRaises(exception.ValidationError, ToscaTemplate, tpl_path)
371         exception.ExceptionCollector.assertExceptionMessage(
372             ValueError, _('"123456789" is not a string.'))
373
374     def test_valid_range_type(self):
375         value_snippet = '''
376         user_port:
377           protocol: tcp
378           target_range:  [20000, 60000]
379           source_range:  [1000, 3000]
380         '''
381         value = yamlparser.simple_parse(value_snippet)
382         data = DataEntity('PortSpec', value.get('user_port'))
383         self.assertIsNotNone(data.validate())
384
385     def test_invalid_range_datatype(self):
386         value_snippet = '''
387         user_port:
388           protocol: tcp
389           target: 1
390           target_range: [20000]
391         '''
392         value = yamlparser.simple_parse(value_snippet)
393         data = DataEntity('PortSpec', value.get('user_port'))
394         err = self.assertRaises(ValueError, data.validate)
395         self.assertEqual(_('"[20000]" is not a valid range.'
396                            ),
397                          err.__str__())
398
399         value_snippet = '''
400         user_port:
401           protocol: tcp
402           target: 1
403           target_range: [20000, 3000]
404         '''
405         value = yamlparser.simple_parse(value_snippet)
406         data = DataEntity('PortSpec', value.get('user_port'))
407         err = self.assertRaises(ValueError, data.validate)
408         self.assertEqual(_('"[20000, 3000]" is not a valid range.'
409                            ),
410                          err.__str__())
411
412         value_snippet = '''
413         humidity: [-100, 100]
414         '''
415         value = yamlparser.simple_parse(value_snippet)
416         data = DataEntity('tosca.my.datatypes.TestLab',
417                           value, DataTypeTest.custom_type_def)
418         err = self.assertRaises(exception.InvalidSchemaError,
419                                 lambda: data.validate())
420         self.assertEqual(_('The property "in_range" expects comparable values.'
421                            ),
422                          err.__str__())
423
424     def test_range_unbounded(self):
425         value_snippet = '''
426         humidity: [-100, 100]
427         '''
428         value = yamlparser.simple_parse(value_snippet)
429         data = DataEntity('tosca.my.datatypes.TestLab',
430                           value, DataTypeTest.custom_type_def)
431         err = self.assertRaises(exception.InvalidSchemaError,
432                                 lambda: data.validate())
433         self.assertEqual(_('The property "in_range" expects comparable values.'
434                            ),
435                          err.__str__())
436
437     def test_invalid_ranges_against_constraints(self):
438         # The TestLab range type has min=-256, max=UNBOUNDED
439         value_snippet = '''
440         temperature1: [-257, 999999]
441         '''
442         value = yamlparser.simple_parse(value_snippet)
443         data = DataEntity('tosca.my.datatypes.TestLab', value,
444                           DataTypeTest.custom_type_def)
445         err = self.assertRaises(exception.ValidationError, data.validate)
446         self.assertEqual(_('The value "-257" of property "temperature1" is '
447                            'out of range "(min:-256, max:UNBOUNDED)".'),
448                          err.__str__())
449
450         value_snippet = '''
451         temperature2: [-999999, 257]
452         '''
453         value = yamlparser.simple_parse(value_snippet)
454         data = DataEntity('tosca.my.datatypes.TestLab', value,
455                           DataTypeTest.custom_type_def)
456         err = self.assertRaises(exception.ValidationError, data.validate)
457         self.assertEqual(_('The value "257" of property "temperature2" is '
458                            'out of range "(min:UNBOUNDED, max:256)".'),
459                          err.__str__())
460
461     def test_valid_ranges_against_constraints(self):
462
463         # The TestLab range type has max=UNBOUNDED
464         value_snippet = '''
465         temperature1: [-255, 999999]
466         '''
467         value = yamlparser.simple_parse(value_snippet)
468         data = DataEntity('tosca.my.datatypes.TestLab', value,
469                           DataTypeTest.custom_type_def)
470         self.assertIsNotNone(data.validate())
471
472         # The TestLab range type has min=UNBOUNDED
473         value_snippet = '''
474         temperature2: [-999999, 255]
475         '''
476         value = yamlparser.simple_parse(value_snippet)
477         data = DataEntity('tosca.my.datatypes.TestLab', value,
478                           DataTypeTest.custom_type_def)
479         self.assertIsNotNone(data.validate())
480
481     def test_incorrect_field_in_datatype(self):
482         tpl_snippet = '''
483         tosca_definitions_version: tosca_simple_yaml_1_0
484         topology_template:
485           node_templates:
486             server:
487               type: tosca.nodes.Compute
488
489             webserver:
490               type: tosca.nodes.WebServer
491               properties:
492                 admin_credential:
493                   user: username
494                   token: some_pass
495                   some_field: value
496               requirements:
497                 - host: server
498         '''
499         tpl = yamlparser.simple_parse(tpl_snippet)
500         err = self.assertRaises(exception.ValidationError, ToscaTemplate,
501                                 None, None, None, tpl)
502         self.assertIn(_('The pre-parsed input failed validation with the '
503                         'following error(s): \n\n\tUnknownFieldError: Data '
504                         'value of type "tosca.datatypes.Credential" contains'
505                         ' unknown field "some_field". Refer to the definition'
506                         ' to verify valid values'), err.__str__())
507
508     def test_functions_datatype(self):
509         value_snippet = '''
510         admin_credential:
511           user: username
512           token: { get_input: password }
513         '''
514         value = yamlparser.simple_parse(value_snippet)
515         data = DataEntity('tosca.datatypes.Credential',
516                           value.get('admin_credential'))
517         self.assertIsNotNone(data.validate())