Fix date-time format checking in response schema
Currently datetime attributes in response schema like 'created_at' etc are being validated against type 'string' only not with ISO 8601 date time format. Another issue is with jsonschema validation for built-in 'date-time' format. It needs 'strict_rfc3339' or 'isodate' module to be installed for proper date-time validation as per rfc3339. Otherwise it returns True wihtout doing any validation. This patch define the new format checker for 'iso8601-date-time' format which checks the format as per ISO 8601 with help of oslo_utils.timeutils and validate all the date time attributes against JSON schema 'iso8601-date-time' format. NOTE: date in image API header is returned in different format than ISO 8601 date time format which is not consistent with other date-time format in nova. So validating this as string only. This API is already deprecated so not worth to fix on nova side. Change-Id: Ief7729975daea373dcfa54a23ec76c3ec7754a70 Closes-Bug: #1567640
This commit is contained in:
parent
248b74561b
commit
f9ded35366
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- Added customized JSON schema format checker for 'date-time' format.
|
||||
Compute response schema will be validated against customized format
|
||||
checker.
|
@ -14,17 +14,19 @@
|
||||
|
||||
import copy
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
# create-aggregate api doesn't have 'hosts' and 'metadata' attributes.
|
||||
aggregate_for_create = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'availability_zone': {'type': ['string', 'null']},
|
||||
'created_at': {'type': 'string'},
|
||||
'created_at': parameter_types.date_time,
|
||||
'deleted': {'type': 'boolean'},
|
||||
'deleted_at': {'type': ['string', 'null']},
|
||||
'deleted_at': parameter_types.date_time_or_null,
|
||||
'id': {'type': 'integer'},
|
||||
'name': {'type': 'string'},
|
||||
'updated_at': {'type': ['string', 'null']}
|
||||
'updated_at': parameter_types.date_time_or_null
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['availability_zone', 'created_at', 'deleted',
|
||||
@ -69,9 +71,7 @@ aggregate_set_metadata = get_aggregate
|
||||
# The 'updated_at' attribute of 'update_aggregate' can't be null.
|
||||
update_aggregate = copy.deepcopy(get_aggregate)
|
||||
update_aggregate['response_body']['properties']['aggregate']['properties'][
|
||||
'updated_at'] = {
|
||||
'type': 'string'
|
||||
}
|
||||
'updated_at'] = parameter_types.date_time
|
||||
|
||||
delete_aggregate = {
|
||||
'status_code': [200]
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
import copy
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
|
||||
base = {
|
||||
'status_code': [200],
|
||||
@ -61,7 +63,7 @@ detail = {
|
||||
'properties': {
|
||||
'available': {'type': 'boolean'},
|
||||
'active': {'type': 'boolean'},
|
||||
'updated_at': {'type': ['string', 'null']}
|
||||
'updated_at': parameter_types.date_time_or_null
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['available', 'active', 'updated_at']
|
||||
|
@ -12,6 +12,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
list_extensions = {
|
||||
'status_code': [200],
|
||||
'response_body': {
|
||||
@ -22,10 +24,7 @@ list_extensions = {
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'updated': {
|
||||
'type': 'string',
|
||||
'format': 'data-time'
|
||||
},
|
||||
'updated': parameter_types.date_time,
|
||||
'name': {'type': 'string'},
|
||||
'links': {'type': 'array'},
|
||||
'namespace': {
|
||||
|
@ -26,10 +26,10 @@ common_image_schema = {
|
||||
'properties': {
|
||||
'id': {'type': 'string'},
|
||||
'status': {'enum': image_status_enums},
|
||||
'updated': {'type': 'string'},
|
||||
'updated': parameter_types.date_time,
|
||||
'links': image_links,
|
||||
'name': {'type': ['string', 'null']},
|
||||
'created': {'type': 'string'},
|
||||
'created': parameter_types.date_time,
|
||||
'minDisk': {'type': 'integer'},
|
||||
'minRam': {'type': 'integer'},
|
||||
'progress': {'type': 'integer'},
|
||||
|
@ -12,6 +12,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
get_keypair = {
|
||||
'status_code': [200],
|
||||
'response_body': {
|
||||
@ -25,9 +27,9 @@ get_keypair = {
|
||||
'fingerprint': {'type': 'string'},
|
||||
'user_id': {'type': 'string'},
|
||||
'deleted': {'type': 'boolean'},
|
||||
'created_at': {'type': 'string'},
|
||||
'updated_at': {'type': ['string', 'null']},
|
||||
'deleted_at': {'type': ['string', 'null']},
|
||||
'created_at': parameter_types.date_time,
|
||||
'updated_at': parameter_types.date_time_or_null,
|
||||
'deleted_at': parameter_types.date_time_or_null,
|
||||
'id': {'type': 'integer'}
|
||||
|
||||
},
|
||||
|
@ -12,6 +12,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
list_migrations = {
|
||||
'status_code': [200],
|
||||
'response_body': {
|
||||
@ -32,8 +34,8 @@ list_migrations = {
|
||||
'dest_host': {'type': ['string', 'null']},
|
||||
'old_instance_type_id': {'type': ['integer', 'null']},
|
||||
'new_instance_type_id': {'type': ['integer', 'null']},
|
||||
'created_at': {'type': 'string'},
|
||||
'updated_at': {'type': ['string', 'null']}
|
||||
'created_at': parameter_types.date_time,
|
||||
'updated_at': parameter_types.date_time_or_null
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': [
|
||||
|
@ -81,6 +81,16 @@ addresses = {
|
||||
}
|
||||
}
|
||||
|
||||
date_time = {
|
||||
'type': 'string',
|
||||
'format': 'iso8601-date-time'
|
||||
}
|
||||
|
||||
date_time_or_null = {
|
||||
'type': ['string', 'null'],
|
||||
'format': 'iso8601-date-time'
|
||||
}
|
||||
|
||||
response_header = {
|
||||
'connection': {'type': 'string'},
|
||||
'content-length': {'type': 'string'},
|
||||
@ -89,9 +99,14 @@ response_header = {
|
||||
'x-compute-request-id': {'type': 'string'},
|
||||
'vary': {'type': 'string'},
|
||||
'x-openstack-nova-api-version': {'type': 'string'},
|
||||
# NOTE(gmann): Validating this as string only as this
|
||||
# date in header is returned in different format than
|
||||
# ISO 8601 date time format which is not consistent with
|
||||
# other date-time format in nova.
|
||||
# This API is already deprecated so not worth to fix
|
||||
# on nova side.
|
||||
'date': {
|
||||
'type': 'string',
|
||||
'format': 'data-time'
|
||||
'type': 'string'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,8 +118,8 @@ common_show_server = {
|
||||
},
|
||||
'user_id': {'type': 'string'},
|
||||
'tenant_id': {'type': 'string'},
|
||||
'created': {'type': 'string'},
|
||||
'updated': {'type': 'string'},
|
||||
'created': parameter_types.date_time,
|
||||
'updated': parameter_types.date_time,
|
||||
'progress': {'type': 'integer'},
|
||||
'metadata': {'type': 'object'},
|
||||
'links': parameter_types.links,
|
||||
@ -402,7 +402,7 @@ instance_actions = {
|
||||
'request_id': {'type': 'string'},
|
||||
'user_id': {'type': 'string'},
|
||||
'project_id': {'type': 'string'},
|
||||
'start_time': {'type': 'string'},
|
||||
'start_time': parameter_types.date_time,
|
||||
'message': {'type': ['string', 'null']},
|
||||
'instance_uuid': {'type': 'string'}
|
||||
},
|
||||
@ -417,8 +417,8 @@ instance_action_events = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'event': {'type': 'string'},
|
||||
'start_time': {'type': 'string'},
|
||||
'finish_time': {'type': 'string'},
|
||||
'start_time': parameter_types.date_time,
|
||||
'finish_time': parameter_types.date_time,
|
||||
'result': {'type': 'string'},
|
||||
'traceback': {'type': ['string', 'null']}
|
||||
},
|
||||
|
@ -12,6 +12,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
list_services = {
|
||||
'status_code': [200],
|
||||
'response_body': {
|
||||
@ -29,7 +31,7 @@ list_services = {
|
||||
'state': {'type': 'string'},
|
||||
'binary': {'type': 'string'},
|
||||
'status': {'type': 'string'},
|
||||
'updated_at': {'type': ['string', 'null']},
|
||||
'updated_at': parameter_types.date_time_or_null,
|
||||
'disabled_reason': {'type': ['string', 'null']}
|
||||
},
|
||||
'additionalProperties': False,
|
||||
|
@ -13,6 +13,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
common_snapshot_info = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
@ -20,7 +22,7 @@ common_snapshot_info = {
|
||||
'volumeId': {'type': 'string'},
|
||||
'status': {'type': 'string'},
|
||||
'size': {'type': 'integer'},
|
||||
'createdAt': {'type': 'string'},
|
||||
'createdAt': parameter_types.date_time,
|
||||
'displayName': {'type': ['string', 'null']},
|
||||
'displayDescription': {'type': ['string', 'null']}
|
||||
},
|
||||
|
@ -14,24 +14,21 @@
|
||||
|
||||
import copy
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
_server_usages = {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'ended_at': {
|
||||
'oneOf': [
|
||||
{'type': 'string'},
|
||||
{'type': 'null'}
|
||||
]
|
||||
},
|
||||
'ended_at': parameter_types.date_time_or_null,
|
||||
'flavor': {'type': 'string'},
|
||||
'hours': {'type': 'number'},
|
||||
'instance_id': {'type': 'string'},
|
||||
'local_gb': {'type': 'integer'},
|
||||
'memory_mb': {'type': 'integer'},
|
||||
'name': {'type': 'string'},
|
||||
'started_at': {'type': 'string'},
|
||||
'started_at': parameter_types.date_time,
|
||||
'state': {'type': 'string'},
|
||||
'tenant_id': {'type': 'string'},
|
||||
'uptime': {'type': 'integer'},
|
||||
@ -47,8 +44,8 @@ _tenant_usage_list = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'server_usages': _server_usages,
|
||||
'start': {'type': 'string'},
|
||||
'stop': {'type': 'string'},
|
||||
'start': parameter_types.date_time,
|
||||
'stop': parameter_types.date_time,
|
||||
'tenant_id': {'type': 'string'},
|
||||
'total_hours': {'type': 'number'},
|
||||
'total_local_gb_usage': {'type': 'number'},
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
import copy
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
|
||||
_version = {
|
||||
'type': 'object',
|
||||
@ -33,7 +35,7 @@ _version = {
|
||||
}
|
||||
},
|
||||
'status': {'type': 'string'},
|
||||
'updated': {'type': 'string', 'format': 'date-time'},
|
||||
'updated': parameter_types.date_time,
|
||||
'version': {'type': 'string'},
|
||||
'min_version': {'type': 'string'},
|
||||
'media-types': {
|
||||
|
@ -12,6 +12,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
|
||||
create_get_volume = {
|
||||
'status_code': [200],
|
||||
'response_body': {
|
||||
@ -24,7 +26,7 @@ create_get_volume = {
|
||||
'status': {'type': 'string'},
|
||||
'displayName': {'type': ['string', 'null']},
|
||||
'availabilityZone': {'type': 'string'},
|
||||
'createdAt': {'type': 'string'},
|
||||
'createdAt': parameter_types.date_time,
|
||||
'displayDescription': {'type': ['string', 'null']},
|
||||
'volumeType': {'type': ['string', 'null']},
|
||||
'snapshotId': {'type': ['string', 'null']},
|
||||
@ -75,7 +77,7 @@ list_volumes = {
|
||||
'status': {'type': 'string'},
|
||||
'displayName': {'type': ['string', 'null']},
|
||||
'availabilityZone': {'type': 'string'},
|
||||
'createdAt': {'type': 'string'},
|
||||
'createdAt': parameter_types.date_time,
|
||||
'displayDescription': {'type': ['string', 'null']},
|
||||
'volumeType': {'type': ['string', 'null']},
|
||||
'snapshotId': {'type': ['string', 'null']},
|
||||
|
39
tempest/lib/common/jsonschema_validator.py
Normal file
39
tempest/lib/common/jsonschema_validator.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2016 NEC Corporation.
|
||||
# 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 jsonschema
|
||||
from oslo_utils import timeutils
|
||||
|
||||
# JSON Schema validator and format checker used for JSON Schema validation
|
||||
JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
|
||||
FORMAT_CHECKER = jsonschema.draft4_format_checker
|
||||
|
||||
|
||||
# NOTE(gmann): Add customized format checker for 'date-time' format because:
|
||||
# 1. jsonschema needs strict_rfc3339 or isodate module to be installed
|
||||
# for proper date-time checking as per rfc3339.
|
||||
# 2. Nova or other OpenStack components handle the date time format as
|
||||
# ISO 8601 which is defined in oslo_utils.timeutils
|
||||
# so this checker will validate the date-time as defined in
|
||||
# oslo_utils.timeutils
|
||||
@FORMAT_CHECKER.checks('iso8601-date-time')
|
||||
def _validate_datetime_format(instance):
|
||||
try:
|
||||
if isinstance(instance, jsonschema.compat.str_types):
|
||||
timeutils.parse_isotime(instance)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
@ -25,6 +25,7 @@ from oslo_serialization import jsonutils as json
|
||||
import six
|
||||
|
||||
from tempest.lib.common import http
|
||||
from tempest.lib.common import jsonschema_validator
|
||||
from tempest.lib.common.utils import test_utils
|
||||
from tempest.lib import exceptions
|
||||
|
||||
@ -38,8 +39,8 @@ HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
|
||||
HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
|
||||
|
||||
# JSON Schema validator and format checker used for JSON Schema validation
|
||||
JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
|
||||
FORMAT_CHECKER = jsonschema.draft4_format_checker
|
||||
JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
|
||||
FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
|
||||
|
||||
|
||||
class RestClient(object):
|
||||
|
83
tempest/tests/lib/common/test_jsonschema_validator.py
Normal file
83
tempest/tests/lib/common/test_jsonschema_validator.py
Normal file
@ -0,0 +1,83 @@
|
||||
# Copyright 2016 NEC Corporation. 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.
|
||||
|
||||
from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
|
||||
from tempest.lib.common import rest_client
|
||||
from tempest.lib import exceptions
|
||||
from tempest.tests import base
|
||||
from tempest.tests.lib import fake_http
|
||||
|
||||
|
||||
class TestJSONSchemaDateTimeFormat(base.TestCase):
|
||||
date_time_schema = [
|
||||
{
|
||||
'status_code': [200],
|
||||
'response_body': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'date-time': parameter_types.date_time
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'status_code': [200],
|
||||
'response_body': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'date-time': parameter_types.date_time_or_null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def test_valid_date_time_format(self):
|
||||
valid_instances = ['2016-10-02T10:00:00-05:00',
|
||||
'2016-10-02T10:00:00+09:00',
|
||||
'2016-10-02T15:00:00Z',
|
||||
'2016-10-02T15:00:00.05Z']
|
||||
resp = fake_http.fake_http_response('', status=200)
|
||||
for instance in valid_instances:
|
||||
body = {'date-time': instance}
|
||||
for schema in self.date_time_schema:
|
||||
rest_client.RestClient.validate_response(schema, resp, body)
|
||||
|
||||
def test_invalid_date_time_format(self):
|
||||
invalid_instances = ['2016-10-02 T10:00:00-05:00',
|
||||
'2016-10-02T 15:00:00',
|
||||
'2016-10-02T15:00:00.05 Z',
|
||||
'2016-10-02:15:00:00.05Z',
|
||||
'T15:00:00.05Z',
|
||||
'2016:10:02T15:00:00',
|
||||
'2016-10-02T15-00-00',
|
||||
'2016-10-02T15.05Z',
|
||||
'09MAR2015 11:15',
|
||||
'13 Oct 2015 05:55:36 GMT',
|
||||
'']
|
||||
resp = fake_http.fake_http_response('', status=200)
|
||||
for instance in invalid_instances:
|
||||
body = {'date-time': instance}
|
||||
for schema in self.date_time_schema:
|
||||
self.assertRaises(exceptions.InvalidHTTPResponseBody,
|
||||
rest_client.RestClient.validate_response,
|
||||
schema, resp, body)
|
||||
|
||||
def test_date_time_or_null_format(self):
|
||||
instance = None
|
||||
resp = fake_http.fake_http_response('', status=200)
|
||||
body = {'date-time': instance}
|
||||
rest_client.RestClient.validate_response(self.date_time_schema[1],
|
||||
resp, body)
|
||||
self.assertRaises(exceptions.InvalidHTTPResponseBody,
|
||||
rest_client.RestClient.validate_response,
|
||||
self.date_time_schema[0], resp, body)
|
@ -154,7 +154,7 @@ class TestServersClient(base.BaseServiceTest):
|
||||
"request_id": "16fb98f-46ca-475e-917e-2563e5a8cd19",
|
||||
"user_id": "16fb98f-46ca-475e-917e-2563e5a8cd12",
|
||||
"project_id": "16fb98f-46ca-475e-917e-2563e5a8cd34",
|
||||
"start_time": "09MAR2015 11:15",
|
||||
"start_time": "2016-10-02T10:00:00-05:00",
|
||||
"message": "fake-msg",
|
||||
"instance_uuid": "16fb98f-46ca-475e-917e-2563e5a8cd12"
|
||||
}
|
||||
@ -166,8 +166,8 @@ class TestServersClient(base.BaseServiceTest):
|
||||
|
||||
FAKE_INSTANCE_ACTION_EVENTS = {
|
||||
"event": "fake-event",
|
||||
"start_time": "09MAR2015 11:15",
|
||||
"finish_time": "09MAR2015 11:15",
|
||||
"start_time": "2016-10-02T10:00:00-05:00",
|
||||
"finish_time": "2016-10-02T10:00:00-05:00",
|
||||
"result": "fake-result",
|
||||
"traceback": "fake-trace-back"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user