rally-openstack/tests/unit/doc/test_jsonschemas.py
Andrey Kurilin 8a95c29f55 [jsonschema] Require specifying additionalProperties explicitly
Validation of objects via jsonschema become meaningless if there is no
`additionalProperties: False`. In case of complex schemas, it is easy to
miss this property in some child property.

Let's require always specifing additionalProperties to simplify the life
of reviewers and do not miss anything.

Change-Id: I49693a7968e820010751909a48f06f3f784c5721
2018-02-02 17:02:17 +02:00

180 lines
8.4 KiB
Python

# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import json
from rally.common.plugin import plugin
from rally import plugins
from tests.unit import test
class ConfigSchemasTestCase(test.TestCase):
OBJECT_TYPE_KEYS = {"$schema", "type", "description", "required",
"properties", "patternProperties",
"minProperties", "maxProperties",
"additionalProperties", "oneOf", "anyOf"}
ARRAY_TYPE_KEYS = {"$schema", "type", "description", "items",
"uniqueItems", "minItems", "maxItems",
"additionalItems"}
NUMBER_TYPE_KEYS = {"$schema", "type", "description", "minimum", "maximum",
"exclusiveMinimum"}
STRING_TYPE_KEYS = {"$schema", "type", "description", "pattern"}
def fail(self, p, schema, msg):
super(ConfigSchemasTestCase, self).fail(
"Config schema of plugin '%s' (%s) is invalid. %s "
"Schema: \n%s" % (p.get_name(),
"%s.%s" % (p.__module__, p.__name__),
msg,
json.dumps(schema, indent=3)))
def _check_anyOf_or_oneOf(self, p, schema, definitions):
if "anyOf" in schema or "oneOf" in schema:
key = "anyOf" if "anyOf" in schema else "oneOf"
for case in schema[key]:
if "description" not in case and "$ref" not in case:
self.fail(p, schema, "Each case of '%s' should have "
"description." % key)
full_schema = copy.deepcopy(schema)
full_schema.pop(key)
for k, v in case.items():
full_schema[k] = v
self._check_item(p, full_schema, definitions)
def _check_object_type(self, p, schema, definitions):
unexpected_keys = set(schema.keys()) - self.OBJECT_TYPE_KEYS
if "definitions" in unexpected_keys:
# TODO(andreykurilin): do not use definitions since it is a hard
# task to parse and display them
unexpected_keys -= {"definitions"}
if unexpected_keys:
self.fail(p, schema, ("Found unexpected key(s) for object type: "
"%s." % ", ".join(unexpected_keys)))
if "additionalProperties" not in schema:
self.fail(p, schema,
"'additionalProperties' is required field for objects. "
"Specify `'additionalProperties': True` explicitly to "
"accept not validated properties.")
if "patternProperties" in schema:
if "properties" in schema:
self.fail(p, schema, "Usage both 'patternProperties' and "
"'properties' in one time is restricted.")
if not isinstance(schema["patternProperties"], dict):
self.fail(p, schema, "Field 'patternProperties' should be a "
"dict.")
for pattern, description in schema["patternProperties"].items():
self._check_item(p, description, definitions)
if "properties" in schema:
for property_name, description in schema["properties"].items():
self._check_item(p, description, definitions)
def _check_array_type(self, p, schema, definitions):
unexpected_keys = set(schema.keys()) - self.ARRAY_TYPE_KEYS
if "additionalProperties" in unexpected_keys:
self.fail(p, schema, "Array type doesn't support "
"'additionalProperties' field.")
if unexpected_keys:
self.fail(p, schema, ("Found unexpected key(s) for array type: "
"%s." % ", ".join(unexpected_keys)))
if "items" not in schema:
self.fail(p, schema, "Expected items of array type should be "
"described via 'items' field.")
if isinstance(schema["items"], dict):
self._check_item(p, schema["items"], definitions)
if "additionalItems" in schema:
self.fail(p, schema, "When items is a single schema, the "
"`additionalItems` keyword is "
"meaningless, and it should not be used.")
elif isinstance(schema["items"], list):
for item in schema["items"]:
self._check_item(p, item, definitions)
else:
self.fail(p, schema, ("Field 'items' of array type should be a "
"list or a dict, but not '%s'" %
type(schema["items"])))
def _check_string_type(self, p, schema):
unexpected_keys = set(schema.keys()) - self.STRING_TYPE_KEYS
if unexpected_keys:
self.fail(p, schema, ("Found unexpected key(s) for string type: "
"%s." % ", ".join(unexpected_keys)))
def _check_number_type(self, p, schema):
unexpected_keys = set(schema.keys()) - self.NUMBER_TYPE_KEYS
if unexpected_keys:
self.fail(p, schema, ("Found unexpected key(s) for integer/number "
"type: %s." % ", ".join(unexpected_keys)))
def _check_simpliest_types(self, p, schema, type_name):
unexpected_keys = set(schema.keys()) - {"type", "description"}
if unexpected_keys:
self.fail(p, schema, ("Found unexpected key(s) for %s type: "
"%s." % (type_name,
", ".join(unexpected_keys))))
def _check_item(self, p, schema, definitions):
if "type" in schema or "anyOf" in schema or "oneOf" in schema:
if "anyOf" in schema or "oneOf" in schema:
self._check_anyOf_or_oneOf(p, schema, definitions)
elif "type" in schema:
if schema["type"] == "object":
self._check_object_type(p, schema, definitions)
elif schema["type"] == "array":
self._check_array_type(p, schema, definitions)
elif schema["type"] == "string":
self._check_string_type(p, schema)
elif schema["type"] in ("number", "integer"):
self._check_number_type(p, schema)
elif schema["type"] in ("boolean", "null"):
self._check_simpliest_types(p, schema, schema["type"])
elif isinstance(schema["type"], list):
self._check_simpliest_types(p, schema, "mixed")
else:
self.fail(p, schema,
"Wrong type is used: %s" % schema["type"])
elif "enum" in schema:
pass
elif schema == {}:
# NOTE(andreykurilin): an empty dict means that the user can
# transmit whatever he wants in whatever he wants format. It is
# not the case which we want to support.
self.fail(p, schema, "Empty schema is not allowed.")
elif "$ref" in schema:
definition_name = schema["$ref"].replace("#/definitions/", "")
if definition_name not in definitions:
self.fail(p, schema,
"Definition '%s' is not found." % definition_name)
else:
self.fail(p, schema, "Wrong format.")
@plugins.ensure_plugins_are_loaded
def test_schema_is_valid(self):
for p in plugin.Plugin.get_all():
if not hasattr(p, "CONFIG_SCHEMA") or "tests.unit" in p.__module__:
continue
# allow only top level definitions
definitions = p.CONFIG_SCHEMA.get("definitions", {})
for definition in definitions.values():
self._check_item(p, definition, definitions)
# check schema itself
self._check_item(p, p.CONFIG_SCHEMA, definitions)