diff --git a/swh/model/fields/compound.py b/swh/model/fields/compound.py index f73117e8e71cc7c9f24078a9162575311c8f53c9..412ebdb1f8bacd0a11c9f14319470d9756f74406 100644 --- a/swh/model/fields/compound.py +++ b/swh/model/fields/compound.py @@ -26,11 +26,12 @@ def validate_against_schema(model, schema, value): if not isinstance(value, dict): raise ValidationError( - 'Unexpected type %(type)s for swh object, expected dict', + 'Unexpected type %(type)s for %(model)s, expected dict', params={ - 'type': value.__class__.__name__ + 'model': model, + 'type': value.__class__.__name__, }, - code='swh-unexpected-type', + code='model-unexpected-type', ) errors = defaultdict(list) @@ -55,7 +56,7 @@ def validate_against_schema(model, schema, value): ValidationError( 'Field %(field)s is mandatory', params={'field': key}, - code='swh-field-mandatory', + code='model-field-mandatory', ) ) diff --git a/swh/model/tests/fields/test_compound.py b/swh/model/tests/fields/test_compound.py new file mode 100644 index 0000000000000000000000000000000000000000..db840a919832082b2af173c56d8e13cc3f3129b2 --- /dev/null +++ b/swh/model/tests/fields/test_compound.py @@ -0,0 +1,232 @@ +# Copyright (C) 2015 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +import datetime +import unittest + +from nose.tools import istest + +from swh.model.exceptions import ValidationError, NON_FIELD_ERRORS +from swh.model.fields import compound, simple + + +class ValidateCompound(unittest.TestCase): + def setUp(self): + def validate_always(model): + return True + + def validate_never(model): + return False + + self.test_model = 'test model' + self.test_schema = { + 'int': (True, simple.validate_int), + 'str': (True, simple.validate_str), + 'str2': (True, simple.validate_str), + 'datetime': (False, simple.validate_datetime), + NON_FIELD_ERRORS: validate_always, + } + + self.test_schema_shortcut = self.test_schema.copy() + self.test_schema_shortcut[NON_FIELD_ERRORS] = validate_never + + self.test_schema_field_failed = self.test_schema.copy() + self.test_schema_field_failed['int'] = (True, [simple.validate_int, + validate_never]) + + self.test_value = { + 'str': 'value1', + 'str2': 'value2', + 'int': 42, + 'datetime': datetime.datetime(1990, 1, 1, 12, 0, 0, + tzinfo=datetime.timezone.utc), + } + + self.test_value_missing = { + 'str': 'value1', + } + + self.test_value_str_error = { + 'str': 1984, + 'str2': 'value2', + 'int': 42, + 'datetime': datetime.datetime(1990, 1, 1, 12, 0, 0, + tzinfo=datetime.timezone.utc), + } + + self.test_value_missing_keys = {'int'} + + self.test_value_wrong_type = 42 + + self.present_keys = set(self.test_value) + self.missing_keys = {'missingkey1', 'missingkey2'} + + @istest + def validate_any_key(self): + self.assertTrue( + compound.validate_any_key(self.test_value, self.present_keys)) + + self.assertTrue( + compound.validate_any_key(self.test_value, + self.present_keys | self.missing_keys)) + + @istest + def validate_any_key_missing(self): + with self.assertRaises(ValidationError) as cm: + compound.validate_any_key(self.test_value, self.missing_keys) + + exc = cm.exception + self.assertEqual(exc.code, 'missing-alternative-field') + self.assertEqual(exc.params['missing_fields'], + ', '.join(sorted(self.missing_keys))) + + @istest + def validate_all_keys(self): + self.assertTrue( + compound.validate_all_keys(self.test_value, self.present_keys)) + + @istest + def validate_all_keys_missing(self): + with self.assertRaises(ValidationError) as cm: + compound.validate_all_keys(self.test_value, self.missing_keys) + + exc = cm.exception + self.assertEqual(exc.code, 'missing-mandatory-field') + self.assertEqual(exc.params['missing_fields'], + ', '.join(sorted(self.missing_keys))) + + with self.assertRaises(ValidationError) as cm: + compound.validate_all_keys(self.test_value, + self.present_keys | self.missing_keys) + + exc = cm.exception + self.assertEqual(exc.code, 'missing-mandatory-field') + self.assertEqual(exc.params['missing_fields'], + ', '.join(sorted(self.missing_keys))) + + @istest + def validate_against_schema(self): + self.assertTrue( + compound.validate_against_schema(self.test_model, self.test_schema, + self.test_value)) + + @istest + def validate_against_schema_wrong_type(self): + with self.assertRaises(ValidationError) as cm: + compound.validate_against_schema(self.test_model, self.test_schema, + self.test_value_wrong_type) + + exc = cm.exception + self.assertEqual(exc.code, 'model-unexpected-type') + self.assertEqual(exc.params['model'], self.test_model) + self.assertEqual(exc.params['type'], + self.test_value_wrong_type.__class__.__name__) + + @istest + def validate_against_schema_mandatory_keys(self): + with self.assertRaises(ValidationError) as cm: + compound.validate_against_schema(self.test_model, self.test_schema, + self.test_value_missing) + + # The exception should be of the form: + # ValidationError({ + # 'mandatory_key1': [ValidationError('model-field-mandatory')], + # 'mandatory_key2': [ValidationError('model-field-mandatory')], + # }) + exc = cm.exception + for key in self.test_value_missing_keys: + nested_key = exc.error_dict[key] + self.assertIsInstance(nested_key, list) + self.assertEqual(len(nested_key), 1) + nested = nested_key[0] + self.assertIsInstance(nested, ValidationError) + self.assertEqual(nested.code, 'model-field-mandatory') + self.assertEqual(nested.params['field'], key) + + @istest + def validate_against_schema_whole_schema_shortcut_previous_error(self): + with self.assertRaises(ValidationError) as cm: + compound.validate_against_schema( + self.test_model, + self.test_schema_shortcut, + self.test_value_missing, + ) + + exc = cm.exception + self.assertNotIn(NON_FIELD_ERRORS, exc.error_dict) + + @istest + def validate_against_schema_whole_schema(self): + with self.assertRaises(ValidationError) as cm: + compound.validate_against_schema( + self.test_model, + self.test_schema_shortcut, + self.test_value, + ) + + # The exception should be of the form: + # ValidationError({ + # NON_FIELD_ERRORS: [ValidationError('model-validation-failed')], + # }) + + exc = cm.exception + self.assertEquals(set(exc.error_dict.keys()), {NON_FIELD_ERRORS}) + + non_field_errors = exc.error_dict[NON_FIELD_ERRORS] + self.assertIsInstance(non_field_errors, list) + self.assertEquals(len(non_field_errors), 1) + + nested = non_field_errors[0] + self.assertIsInstance(nested, ValidationError) + self.assertEquals(nested.code, 'model-validation-failed') + self.assertEquals(nested.params['model'], self.test_model) + self.assertEquals(nested.params['validator'], 'validate_never') + + @istest + def validate_against_schema_field_error(self): + with self.assertRaises(ValidationError) as cm: + compound.validate_against_schema(self.test_model, self.test_schema, + self.test_value_str_error) + + # The exception should be of the form: + # ValidationError({ + # 'str': [ValidationError('unexpected-type')], + # }) + + exc = cm.exception + self.assertEquals(set(exc.error_dict.keys()), {'str'}) + + str_errors = exc.error_dict['str'] + self.assertIsInstance(str_errors, list) + self.assertEquals(len(str_errors), 1) + + nested = str_errors[0] + self.assertIsInstance(nested, ValidationError) + self.assertEquals(nested.code, 'unexpected-type') + + @istest + def validate_against_schema_field_failed(self): + with self.assertRaises(ValidationError) as cm: + compound.validate_against_schema(self.test_model, + self.test_schema_field_failed, + self.test_value) + + # The exception should be of the form: + # ValidationError({ + # 'int': [ValidationError('field-validation-failed')], + # }) + + exc = cm.exception + self.assertEquals(set(exc.error_dict.keys()), {'int'}) + + int_errors = exc.error_dict['int'] + self.assertIsInstance(int_errors, list) + self.assertEquals(len(int_errors), 1) + + nested = int_errors[0] + self.assertIsInstance(nested, ValidationError) + self.assertEquals(nested.code, 'field-validation-failed') + self.assertEquals(nested.params['validator'], 'validate_never') + self.assertEquals(nested.params['field'], 'int')