diff --git a/shared/validation/user_schema.py b/shared/validation/user_schema.py index 35e270e96..fa60276c8 100644 --- a/shared/validation/user_schema.py +++ b/shared/validation/user_schema.py @@ -78,6 +78,8 @@ custom_status_common_config = { "name_prefix": {"type": "string", "regex": r"^[\w\-\.]+$"}, + # Note that "type" is a reserved word in Cerberus parser so use with caution as a + # key in the schema. See workaround at places that call this schema. "type": {"type": "string", "allowed": ("project", "patch", "changes")}, "target": percent_type_or_auto, "threshold": percent_type, @@ -119,8 +121,17 @@ flags_rule_basic_properties = { "statuses": { - "type": "list", - "schema": {"type": "dict", "schema": flag_status_attributes}, + # Use "anyof" to avoid error in Cerberus when child has a key named "type". More background at https://github.com/codecov/shared/pull/588 + "anyof": [ + { + "type": "list", + "schema": { + "type": "dict", + "schema": flag_status_attributes, + "allow_unknown": False, + }, + }, + ] }, "carryforward_mode": { "type": "string", @@ -134,8 +145,17 @@ component_rule_basic_properties = { "statuses": { - "type": "list", - "schema": {"type": "dict", "schema": component_status_attributes}, + # Use "anyof" to avoid error in Cerberus when child has a key named "type". More background at https://github.com/codecov/shared/pull/588 + "anyof": [ + { + "type": "list", + "schema": { + "type": "dict", + "schema": component_status_attributes, + "allow_unknown": False, + }, + } + ], }, "flag_regexes": {"type": "list", "schema": {"type": "string"}}, "paths": path_list_structure, diff --git a/tests/unit/validation/test_validation.py b/tests/unit/validation/test_validation.py index 75c2fec0d..add107f88 100644 --- a/tests/unit/validation/test_validation.py +++ b/tests/unit/validation/test_validation.py @@ -459,6 +459,61 @@ def test_many_flags_validation(self): } assert validate_yaml(user_input) == expected_result + def test_flags_schema_error_for_key_named_type(self): + user_input = { + "flag_management": { + "default_rules": { + "carryforward": False, + "statuses": [{"name_prefix": "aaa", "type": "patch"}], + }, + "individual_flags": [ + { + "name": "cawcaw", + "paths": ["banana"], + "after_n_builds": 3, + # expected "statuses" to be a list but is an object instead & that + # object contains a key named "type" (reserved word) + "statuses": {"type": "patch"}, + } + ], + }, + } + + with pytest.raises(Exception) as exp: + validate_yaml(user_input) + + err = exp.value + assert err is not None, "validate_yaml didn't raise anything" + assert err.error_location == [ + "flag_management", + "individual_flags", + 0, + "statuses", + ] + assert err.error_message == "no definitions validate" + assert err.error_dict == { + "flag_management": [ + { + "individual_flags": [ + { + 0: [ + { + "statuses": [ + "no definitions validate", + { + "anyof definition 0": [ + "must be of list type" + ] + }, + ] + } + ], + } + ] + } + ] + } + def test_validate_bot_none(self): user_input = {"codecov": {"bot": None}} expected_result = {"codecov": {"bot": None}} @@ -797,14 +852,30 @@ def test_yaml_with_flag_management_statuses_with_flags(self): } with pytest.raises(InvalidYamlException) as exc: validate_yaml(user_input) - assert exc.value.error_location == [ - "flag_management", - "default_rules", - "statuses", - 0, - "flags", + assert exc.value.error_location == [ + "flag_management", + "default_rules", + "statuses", + ] + assert exc.value.error_message == "no definitions validate" + assert exc.value.error_dict == { + "flag_management": [ + { + "default_rules": [ + { + "statuses": [ + "no definitions validate", + { + "anyof definition 0": [ + {0: [{"flags": ["unknown field"]}]} + ] + }, + ] + } + ] + } ] - assert exc.value.error_message == "extra keys not allowed" + } def test_github_checks(self): user_input = {"github_checks": True} @@ -1331,6 +1402,57 @@ def test_components_schema_error(): } +def test_components_schema_error_for_key_named_type(): + user_input = { + "component_management": { + "default_rules": { + "flag_regexes": ["global_flag"], + }, + "individual_components": [ + { + "name": "fruits", + "component_id": "app_0", + "flag_regexes": ["fruit_.*", "^specific_flag$"], + "paths": ["src/.*"], + # expected "statuses" to be a list but is an object instead & that + # object contains a key named "type" (reserved word) + "statuses": {"type": "patch", "name_prefix": "co", "target": 90}, + } + ], + } + } + + with pytest.raises(Exception) as exp: + validate_yaml(user_input) + err = exp.value + assert err is not None, "validate_yaml didn't raise anything" + assert err.error_location == [ + "component_management", + "individual_components", + 0, + "statuses", + ] + assert err.error_message == "no definitions validate" + assert err.error_dict == { + "component_management": [ + { + "individual_components": [ + { + 0: [ + { + "statuses": [ + "no definitions validate", + {"anyof definition 0": ["must be of list type"]}, + ] + } + ], + } + ] + } + ] + } + + def test_removed_code_behavior_config_valid(): user_input = { "coverage": {