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:
ghanshyam 2016-04-12 17:03:01 +09:00 committed by ghanshyam
parent 248b74561b
commit f9ded35366
18 changed files with 197 additions and 44 deletions

View File

@ -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.

View File

@ -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]

View File

@ -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']

View File

@ -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': {

View File

@ -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'},

View File

@ -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'}
},

View File

@ -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': [

View File

@ -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'
}
}

View File

@ -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']}
},

View File

@ -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,

View File

@ -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']}
},

View File

@ -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'},

View File

@ -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': {

View File

@ -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']},

View 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

View File

@ -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):

View 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)

View File

@ -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"
}