Fixes failing API Tests

Speed up failure state detection
Fixed test_list_services_multiple_page
Fixed invalid exception thrown
Ignore flavor creation tests when config setting is set to False
Added negative test for origin with duplicate rules
Removed falcon validators
Refactored Service and Flavor Schema Validation functions
Fixed issue with duplicate request_url rules for origin, caching

Change-Id: I83ee6f88e37c1b9033df20ed46dcc7f650d71202
Closes-Bug: 1394720
This commit is contained in:
amitgandhinz 2015-01-21 14:02:08 -05:00
parent 713c8a93d6
commit 8f9f33c5e0
15 changed files with 232 additions and 319 deletions

View File

@ -24,14 +24,13 @@ except ImportError:
use_uwsgi = False
import jsonpatch
import jsonschema
from poppy.common import errors
from poppy.manager import base
from poppy.model import service
from poppy.openstack.common import log
from poppy.transport.validators import helpers as validators
from poppy.transport.validators.schemas import service as service_schema
from poppy.transport.validators.stoplight import exceptions
LOG = log.getLogger(__name__)
@ -81,7 +80,7 @@ class DefaultServicesController(base.ServicesController):
:param project_id
:param service_obj
:raises LoookupError, ValueError
:raises LookupError, ValueError
"""
try:
flavor = self.flavor_controller.get(service_obj.flavor_id)
@ -127,6 +126,7 @@ class DefaultServicesController(base.ServicesController):
:param project_id
:param service_id
:param service_updates
:raises LookupError, ValueError
"""
# get the current service object
try:
@ -140,7 +140,7 @@ class DefaultServicesController(base.ServicesController):
service_old_json = json.loads(json.dumps(service_old.to_dict()))
# remove fileds that cannot be part of PATCH
# remove fields that cannot be part of PATCH
del service_old_json['service_id']
del service_old_json['status']
del service_old_json['provider_details']
@ -149,21 +149,16 @@ class DefaultServicesController(base.ServicesController):
service_old_json, service_updates)
# validate the updates
patch_schema = service_schema.ServiceSchema.get_schema("service",
"POST")
errors_list = list(
jsonschema.Draft3Validator(patch_schema).iter_errors(
service_new_json))
schema = service_schema.ServiceSchema.get_schema("service", "POST")
validators.is_valid_service_configuration(service_new_json, schema)
if len(errors_list) > 0:
details = dict(errors=[{
'message': '-'.join([
"[%s]" % "][".join(repr(p) for p in error.path),
str(getattr(error, "message", error))
])}
for error in errors_list])
raise exceptions.ValidationFailed(json.dumps(details))
try:
self.flavor_controller.get(service_new_json['flavor_id'])
# raise a lookup error if the flavor is not found
except LookupError as e:
raise e
# must be valid, carry on
service_new_json['service_id'] = service_old.service_id
service_new = service.Service.init_from_dict(service_new_json)

View File

@ -73,7 +73,7 @@ class FlavorsController(base.Controller, hooks.HookController):
@pecan.expose('json')
@decorators.validate(
request=rule.Rule(
helpers.json_matches_schema(
helpers.json_matches_flavor_schema(
schema.FlavorSchema.get_schema("flavor", "POST")),
helpers.abort_with_message,
stoplight_helpers.pecan_getter))

View File

@ -155,7 +155,7 @@ class ServicesController(base.Controller, hooks.HookController):
@pecan.expose('json')
@decorators.validate(
request=rule.Rule(
helpers.json_matches_schema(
helpers.json_matches_service_schema(
service.ServiceSchema.get_schema("service", "POST")),
helpers.abort_with_message,
stoplight_helpers.pecan_getter))
@ -198,7 +198,7 @@ class ServicesController(base.Controller, hooks.HookController):
helpers.is_valid_service_id(),
helpers.abort_with_message),
request=rule.Rule(
helpers.json_matches_schema(
helpers.json_matches_service_schema(
service.ServiceSchema.get_schema("service", "PATCH")),
helpers.abort_with_message,
stoplight_helpers.pecan_getter))
@ -216,11 +216,13 @@ class ServicesController(base.Controller, hooks.HookController):
self.project_id, service_id, service_updates)
except exceptions.ValidationFailed as e:
pecan.abort(400, detail=str(e))
except ValueError as e:
except LookupError as e: # error handler for no flavor
pecan.abort(400, detail=str(e))
except ValueError as e: # error handler for existing service name
pecan.abort(400, detail=str(e))
except errors.ServiceNotFound as e:
pecan.abort(404, detail=str(e))
except errors.ServiceStatusNotDeployed as e:
except errors.ServiceStatusNeitherDeployedNorFailed as e:
pecan.abort(400, detail=str(e))
except Exception as e:
pecan.abort(400, detail=str(e))

View File

@ -13,15 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import functools
import json
import uuid
try:
import falcon
except ImportError:
from poppy.transport.validators import fake_falcon as falcon
import jsonschema
import pecan
@ -37,50 +32,6 @@ def req_accepts_json_pecan(request, desired_content_type='application/json'):
raise exceptions.ValidationFailed('Invalid Accept Header')
def require_accepts_json_falcon(req, resp, params=None):
"""Raises an exception if the request does not accept JSON
Meant to be used as a `before` hook.
:param req: request sent
:type req: falcon.request.Request
:param resp: response object to return
:type resp: falcon.response.Response
:param params: additional parameters passed to responders
:type params: dict
:rtype: None
:raises: falcon.HTTPNotAcceptable
"""
if not req.client_accepts('application/json'):
raise falcon.HTTPNotAcceptable(
u"""
Endpoint only serves `application/json`; specify client-side"""
'media type support with the "Accept" header.',
href=u'http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html',
href_text=u'14.1 Accept, Hypertext Transfer Protocol -- HTTP/1.1')
class DummyResponse(object):
pass
def custom_abort_falcon(error_info=None):
"""Error_handler for with_schema
Meant to be used with falcon transport.
param errors: a list of validation exceptions
"""
ret = DummyResponse()
ret.code = 400
if not isinstance(error_info, collections.Iterable):
error_info = [error_info]
details = dict(errors=[{'message': str(getattr(error, "message", error))}
for error in error_info])
ret.message = json.dumps(details)
return ret
def custom_abort_pecan(errors_info):
"""Error_handler for with_schema
@ -98,32 +49,6 @@ def custom_abort_pecan(errors_info):
'Content-Type': "application/json"})
def with_schema_falcon(request, schema=None):
"""Use to decorate a falcon style controller route
:param request: A falcon request
:param schema: a Json schema to validate against
"""
validation_failed = False
v_error = None
if schema is not None:
errors_list = []
try:
data = json.loads(request.body)
errors_list = list(
jsonschema.Draft3Validator(schema).iter_errors(data))
except ValueError:
validation_failed = True
v_error = ["Invalid JSON body in request"]
if len(errors_list) > 0:
validation_failed = True
v_error = errors_list
if validation_failed:
raise exceptions.ValidationFailed(repr(v_error))
def with_schema_pecan(request, schema=None, handler=custom_abort_pecan,
**kwargs):
"""Decorate a Pecan/Flask style controller form validation.
@ -165,17 +90,40 @@ def with_schema_pecan(request, schema=None, handler=custom_abort_pecan,
return decorator
def json_matches_schema_inner(request, schema=None):
errors_list = []
def json_matches_service_schema(input_schema):
return functools.partial(
json_matches_service_schema_inner,
schema=input_schema)
def json_matches_service_schema_inner(request, schema=None):
try:
data = json.loads(request.body.decode('utf-8'))
except ValueError:
raise exceptions.ValidationFailed('Invalid JSON string')
is_valid_service_configuration(data, schema)
def json_matches_flavor_schema(input_schema):
return functools.partial(
json_matches_flavor_schema_inner,
schema=input_schema)
def json_matches_flavor_schema_inner(request, schema=None):
try:
data = json.loads(request.body.decode('utf-8'))
except ValueError:
raise exceptions.ValidationFailed('Invalid JSON string')
is_valid_flavor_configuration(data, schema)
def is_valid_service_configuration(service, schema):
if schema is not None:
errors_list = list(
jsonschema.Draft3Validator(schema).iter_errors(data))
jsonschema.Draft3Validator(schema).iter_errors(service))
if len(errors_list) > 0:
details = dict(errors=[{
@ -185,14 +133,36 @@ def json_matches_schema_inner(request, schema=None):
])}
for error in errors_list])
raise exceptions.ValidationFailed(json.dumps(details))
else:
return
# Schema structure is valid. Check the functional rules.
def json_matches_schema(input_schema):
return functools.partial(
json_matches_schema_inner,
schema=input_schema)
# 1. origin rules must be unique
if 'origins' in service:
origin_rules = []
for origin in service['origins']:
if 'rules' in origin:
for rule in origin['rules']:
request_url = rule['request_url']
if request_url in origin_rules:
raise exceptions.ValidationFailed(
'Origins - the request_url must be unique')
else:
origin_rules.append(request_url)
# 2. caching rules must be unique
if 'caching' in service:
caching_rules = []
for caching in service['caching']:
if 'rules' in caching:
for rule in caching['rules']:
request_url = rule['request_url']
if request_url in caching_rules:
raise exceptions.ValidationFailed(
'Caching Rules - the request_url must be unique')
else:
caching_rules.append(request_url)
return
@decorators.validation_function
@ -203,6 +173,23 @@ def is_valid_service_id(service_id):
raise exceptions.ValidationFailed('Invalid service id')
def is_valid_flavor_configuration(flavor, schema):
if schema is not None:
errors_list = list(
jsonschema.Draft3Validator(schema).iter_errors(flavor))
if len(errors_list) > 0:
details = dict(errors=[{
'message': '-'.join([
"[%s]" % "][".join(repr(p) for p in error.path),
str(getattr(error, "message", error))
])}
for error in errors_list])
raise exceptions.ValidationFailed(json.dumps(details))
return
@decorators.validation_function
def is_valid_flavor_id(flavor_id):
pass
@ -211,3 +198,7 @@ def is_valid_flavor_id(flavor_id):
def abort_with_message(error_info):
pecan.abort(400, detail=getattr(error_info, "message", ""),
headers={'Content-Type': "application/json"})
class DummyResponse(object):
pass

View File

@ -258,24 +258,8 @@ class ServiceSchema(schema_base.SchemaBase):
'referrer': {
'type': 'string',
'minLength': 3,
'maxLength': 1024},
'request_url': {
'type': 'string',
'minLength': 3,
'maxLength': 1024},
'http_host': {
'type': 'string',
'minLength': 3,
'maxLength': 256},
'client_ip': {
'type': 'string'},
'http_method': {
'type': 'string',
'enum': [
'GET',
'PUT',
'POST',
'PATCH']}}},
'maxLength': 1024}
}},
}},
},
},

View File

@ -38,12 +38,22 @@ The API tests require cassandra running in your local machine, in order to
run via tox. It is assumed you already have the Cassandra instance up &
running locally. You can make the API tests part of tox, by overriding the
default positional argument in tox.ini::
example : tox -- --exclude=None
tox -- --exclude=None
Alternatively, you can run tox with docker containers running Cassandra::
This will require docker (or boot2docker for MacOSX) to already be installed on the system.
Dont forget to update your ~/.poppy/tests.conf to point to your docker ip.
This will require docker (or boot2docker for MacOSX) to already be installed on the system.
It will use the fig_local.yaml file to mount your local folder, as described in the Docker folder.
Also, dont forget to update your ~/.poppy/tests.conf to point to your docker ip address.
Example 1: Run all API tests::
tox -e apidocker api
Example 2: Run a particular test function::
tox -e apidocker api/services/test_services.py:TestCreateService -- -m test_create_service_positive
example : tox -e apidocker

View File

@ -33,6 +33,9 @@ class TestCreateFlavors(base.TestBase):
@ddt.file_data('data_create_flavor.json')
def test_create_flavor(self, test_data):
if self.test_config.generate_flavors is False:
self.skipTest(
'Flavor Generation is currently disabled in configuration')
provider_list = test_data['provider_list']
limits = test_data['limits']
@ -61,6 +64,10 @@ class TestCreateFlavors(base.TestBase):
@ddt.file_data('data_create_flavor_negative.json')
def test_create_flavor_negative_tests(self, test_data):
if self.test_config.generate_flavors is False:
self.skipTest(
'Flavor Generation is currently disabled in configuration')
if 'skip_test' in test_data:
self.skipTest('Not Implemented - bp# post-flavors-error-handling')
@ -78,6 +85,9 @@ class TestCreateFlavors(base.TestBase):
self.assertEqual(resp.status_code, 400)
def test_delete_flavor(self):
if self.test_config.generate_flavors is False:
self.skipTest(
'Flavor Generation is currently disabled in configuration')
resp = self.client.delete_flavor(flavor_id=self.flavor_id)
self.assertEqual(resp.status_code, 204)
@ -86,7 +96,8 @@ class TestCreateFlavors(base.TestBase):
self.assertEqual(resp.status_code, 404)
def tearDown(self):
self.client.delete_flavor(flavor_id=self.flavor_id)
if self.test_config.generate_flavors:
self.client.delete_flavor(flavor_id=self.flavor_id)
super(TestCreateFlavors, self).tearDown()

View File

@ -127,18 +127,33 @@
"service_name": "many_origin_list",
"domain_list": [{"domain": "mywebsite.com"},
{"domain": "blog.mywebsite.com"}],
"origin_list": [
{"origin": "origin1"},
{"origin": "origin2", "rules": [{"name" : "index", "request_url" : "/index1.htm"}]},
{"origin": "origin3", "rules": [{"name" : "index", "request_url" : "/index2.htm"}]},
{"origin": "origin4", "rules": [{"name" : "index", "request_url" : "/index3.htm"}]},
{"origin": "origin5", "rules": [{"name" : "index", "request_url" : "/index4.htm"}]},
{"origin": "origin6", "rules": [{"name" : "index", "request_url" : "/index5.htm"}]},
{"origin": "origin7", "rules": [{"name" : "index", "request_url" : "/index6.htm"}]},
{"origin": "origin8", "rules": [{"name" : "index", "request_url" : "/index7.htm"}]},
{"origin": "origin9", "rules": [{"name" : "index", "request_url" : "/index8.htm"}]},
{"origin": "origin10", "rules": [{"name" : "index", "request_url" : "/index9.htm"}]},
{"origin": "origin11", "rules": [{"name" : "index", "request_url" : "/index10.htm"}]}
],
"caching_list": [{"name": "default", "ttl": 3600},
{"name": "home",
"ttl": 1200,
"rules": [{"name" : "index",
"request_url" : "/index.htm"}]}],
"restrictions_list": []
},
"duplicate_origin_rule": {
"service_name": "many_origin_list",
"domain_list": [{"domain": "mywebsite.com"}],
"origin_list": [
{"origin": "origin1"},
{"origin": "origin2", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin3", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin4", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin5", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin6", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin7", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin8", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin9", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin10", "rules": [{"name" : "index", "request_url" : "/index.htm"}]},
{"origin": "origin11", "rules": [{"name" : "index", "request_url" : "/index.htm"}]}
{"origin": "origin3", "rules": [{"name" : "index", "request_url" : "/index.htm"}]}
],
"caching_list": [{"name": "default", "ttl": 3600},
{"name": "home",
@ -241,6 +256,24 @@
"request_url" : "/index.htm"}]}],
"restrictions_list": []
},
"caching_duplicate_rule": {
"service_name": "my_service_name",
"domain_list": [{"domain": "mywebsite.com", "protocol": "http"},
{"domain": "blog.mywebsite.com", "protocol": "http"}],
"origin_list": [{"origin": "mywebsite1.com",
"port": 443,
"ssl": false}],
"caching_list": [{"name": "default", "ttl": 3600},
{"name": "home",
"ttl": 20,
"rules": [{"name" : "index",
"request_url" : "/index.htm"}]},
{"name": "images",
"ttl": 30,
"rules": [{"name" : "images",
"request_url" : "/index.htm"}]}],
"restrictions_list": []
},
"restrictions_empty_item": {
"service_name": "my_service_name",
"domain_list": [{"domain": "mywebsite.com", "protocol": "http"},

View File

@ -55,7 +55,7 @@
{"op": "add",
"path": "/origins/2",
"value": {"origin": "4.2.5.4", "port": 80, "ssl": false,
"rules": [{"name" : "origin", "request_url" : "/origin.htm"}]}
"rules": [{"name" : "origin", "request_url" : "/origin2.htm"}]}
}
],
"add_and_remove_origin": [
@ -71,7 +71,7 @@
{"op": "add",
"path": "/caching/-",
"value": {"name": "cache_name", "ttl": 111,
"rules": [{"name" : "index","request_url" : "/index.htm"}]}}
"rules": [{"name" : "index","request_url" : "/cats.jpg"}]}}
],
"replace_caching": [
{"op": "replace",
@ -86,7 +86,7 @@
{"op": "add",
"path": "/caching/-",
"value": {"name": "cache_name", "ttl": 111,
"rules": [{"name" : "index","request_url" : "/index.htm"}]}
"rules": [{"name" : "index","request_url" : "/dogs.jpg"}]}
},
{"op": "add",
"path": "/caching/-",

View File

@ -1,24 +0,0 @@
{
"update_name": {
"name": "my_new_name"
},
"update_domain_list": {
"domain_list": [{"domain": "mynewwebsite.com"},
{"domain": "newblog.mywebsite.com"}]
},
"update_origin_list": {
"origin_list": [{"origin": "www.neworigin.com",
"port": 443,
"ssl": false}]
},
"update_flavorRef": {
"flavorRef": "premium"
},
"update_caching_list": {
"caching_list": [{"newname": "default", "ttl": 3600},
{"name": "home",
"ttl": 1200,
"rules": [{"name" : "index",
"request_url" : "/index.htm"}]}]
}
}

View File

@ -4,6 +4,18 @@
"path": "/origins/-",
"value": {"origin": "1.2.3.4", "port": 80}}
],
"add_duplicate_origin_rule": [
{"op": "add",
"path": "/origins/1",
"value": {"origin": "1.2.3.4", "port": 80, "ssl": false,
"rules": [{"name" : "origin", "request_url" : "/origin.htm"}]}
},
{"op": "add",
"path": "/origins/2",
"value": {"origin": "4.2.5.4", "port": 80, "ssl": false,
"rules": [{"name" : "origin", "request_url" : "/origin.htm"}]}
}
],
"empty_list": [],
"empty_dict":[{}],
"remove_name": [
@ -57,5 +69,14 @@
{"op": "replace",
"path": "/domains/0",
"value": {}}
],
"remove_flavor": [
{"op": "remove",
"path": "/flavor_id"}
],
"replace_flavor_nonexistant": [
{"op": "replace",
"path": "/flavor_id",
"value": "nonexistant"}
]
}

View File

@ -17,6 +17,7 @@
import cgi
import time
import urlparse
import uuid
import ddt
@ -276,14 +277,26 @@ class TestListServices(base.TestBase):
self.assertEqual(len(body['services']), limit)
self.assertSchema(body, services.list_services)
def test_list_services_multiple_page(self):
self.service_list = [self._create_test_service() for _ in range(15)]
resp = self.client.list_services()
@ddt.data(3)
def test_list_services_multiple_page(self, num):
self.service_list = [self._create_test_service() for _ in range(num)]
url_param = {'limit': num - 1}
resp = self.client.list_services(param=url_param)
self.assertEqual(resp.status_code, 200)
body = resp.json()
# TODO(malini): remove hard coded value with configurable value
self.assertEqual(len(body['services']), 10)
self.assertEqual(len(body['services']), num - 1)
self.assertSchema(body, services.list_services)
# get second page
next_page_uri = urlparse.urlparse(body['links'][0]['href'])
marker = urlparse.parse_qs(next_page_uri.query)['marker'][0]
url_param = {'marker': marker}
resp = self.client.list_services(param=url_param)
self.assertEqual(resp.status_code, 200)
body = resp.json()
self.assertEqual(len(body['services']), 1)
self.assertSchema(body, services.list_services)
@attrib.attr('smoke')
@ -409,7 +422,8 @@ class TestServiceActions(base.TestBase):
# TODO(malini): Add test to verify that a failed service can be
# deleted.
# Placeholder till we figure out how to create provider side failure.
pass
self.skipTest(
'Until we figure out how to create provider side failure.')
def test_get_service(self):
resp = self.client.get_service(location=self.service_url)
@ -437,7 +451,8 @@ class TestServiceActions(base.TestBase):
# TODO(malini): Add test to verify that failed service will return
# status 'failed' on get_service with error message from the provider.
# Placeholder till we figure out how to create provider side failure.
pass
self.skipTest(
'Until we figure out how to create provider side failure.')
def tearDown(self):
self.client.delete_service(location=self.service_url)
@ -471,7 +486,7 @@ class TestServicePatch(base.TestBase):
self.restrictions_list = [
{"name": "website only",
"rules": [{"name": "mywebsite.com",
"http_host": "www.mywebsite.com"}]}]
"referrer": "www.mywebsite.com"}]}]
resp = self.client.create_service(
service_name=self.service_name,
@ -483,7 +498,7 @@ class TestServicePatch(base.TestBase):
self.service_url = resp.headers["location"]
self.expected_service_details = {
self.original_service_details = {
"name": self.service_name,
"domains": self.domain_list,
"origins": self.origin_list,
@ -494,9 +509,15 @@ class TestServicePatch(base.TestBase):
self.client.wait_for_service_status(
location=self.service_url,
status='deployed',
abort_on_status='failed',
retry_interval=self.test_config.status_check_retry_interval,
retry_timeout=self.test_config.status_check_retry_timeout)
resp = self.client.get_service(location=self.service_url)
body = resp.json()
self.assertEqual(resp.status_code, 200)
self.assertEqual(body['status'], 'deployed')
def _assert_service_details(self, actual_response, expected_response):
self.assertEqual(actual_response['name'],
expected_response['name'])
@ -512,7 +533,6 @@ class TestServicePatch(base.TestBase):
expected_response['flavor_id'])
@ddt.file_data('data_patch_service.json')
# @ddt.file_data('data_patch_subset.json')
def test_patch_service(self, test_data):
for item in test_data:
@ -523,23 +543,22 @@ class TestServicePatch(base.TestBase):
item['value']['domain'] = str(uuid.uuid1()) + '.com'
patch = jsonpatch.JsonPatch(test_data)
expected_service_details = patch.apply(self.expected_service_details)
expected_service_details = patch.apply(self.original_service_details)
resp = self.client.patch_service(location=self.service_url,
request_body=test_data)
self.assertEqual(resp.status_code, 202)
resp = self.client.get_service(location=self.service_url)
self.assertEqual(resp.status_code, 200)
self.client.wait_for_service_status(
location=self.service_url,
status='deployed',
abort_on_status='failed',
retry_interval=self.test_config.status_check_retry_interval,
retry_timeout=self.test_config.status_check_retry_timeout)
resp = self.client.get_service(location=self.service_url)
body = resp.json()
self.assertEqual(body['status'], 'deployed')
self._assert_service_details(body, expected_service_details)
@ -550,22 +569,14 @@ class TestServicePatch(base.TestBase):
request_body=test_data)
self.assertEqual(resp.status_code, 400)
# nothing should have changed.
resp = self.client.get_service(location=self.service_url)
self.assertEqual(resp.status_code, 200)
body = resp.json()
self.assertEqual(body['status'], 'deployed')
for item in self.domain_list:
if 'protocol' not in item:
item['protocol'] = 'http'
self.assertEqual(sorted(self.domain_list), sorted(body['domains']))
for item in self.origin_list:
if 'rules' not in item:
item[u'rules'] = []
self.assertEqual(sorted(self.origin_list), sorted(body['origins']))
# TODO(malini): Uncomment below after caching is implemented.
self.assertEqual(sorted(self.caching_list), sorted(body['caching']))
self._assert_service_details(body, self.original_service_details)
def tearDown(self):
self.client.delete_service(location=self.service_url)

View File

@ -210,7 +210,9 @@ class PoppyClient(client.AutoMarshallingHTTPClient):
return self.request('DELETE', url)
def wait_for_service_status(self, location, status, retry_interval=2,
def wait_for_service_status(self, location, status,
abort_on_status=None,
retry_interval=2,
retry_timeout=30):
"""Waits for a service to reach a given status."""
current_status = ''
@ -223,6 +225,11 @@ class PoppyClient(client.AutoMarshallingHTTPClient):
current_status = body['status']
if (current_status == status):
return
if abort_on_status is not None:
if current_status == abort_on_status:
return
current_time = int(time.time())
if current_time > stop_time:
return

View File

@ -104,7 +104,7 @@ list_services_link = {
'href': {'type': 'string',
'pattern':
'(https?)(:/{1,3})([a-z0-9\.\-:]{1,400})'
'(/v1\.0/([a-z0-9]{1,400})/services'
'(/v1\.0/([a-z0-9]{1,400}/)?services'
'\?marker=)(' + uuid4 + ')'
'(&limit=)([1-9]|1[0-9])'}},
'required': ['rel', 'href'],

View File

@ -1,128 +0,0 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# 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 functools
from poppy.common import errors
from poppy.transport.validators import helpers
from poppy.transport.validators.schemas import service
from poppy.transport.validators.stoplight import decorators
from poppy.transport.validators.stoplight import exceptions
from poppy.transport.validators.stoplight import rule
from tests.functional.transport.validator import base
testing_schema = service.ServiceSchema.get_schema("service", "POST")
request_fit_schema = functools.partial(
helpers.with_schema_falcon,
schema=testing_schema)
class DummyFalconEndpoint(object):
# falcon style endpoint
@decorators.validate(
request=rule.Rule(
request_fit_schema,
lambda error_info: base.abort(404)
),
response=rule.Rule(
base.is_response(),
lambda error_info: base.abort(404))
)
def get_falcon_style(self, request, response):
return "Hello, World!"
@decorators.validate(
request=rule.Rule(request_fit_schema,
helpers.custom_abort_falcon),
response=rule.Rule(base.is_response(),
helpers.custom_abort_falcon)
)
def get_falcon_style_custom_abort(self, request, response):
return "Hello, World!"
class TestValidationFunctionsFalcon(base.BaseTestCase):
def setUp(self):
self.ep = DummyFalconEndpoint()
super(TestValidationFunctionsFalcon, self).setUp()
def test_with_schema(self):
self.assertEqual(
helpers.with_schema_falcon(
base.fake_request_good,
schema=testing_schema),
None)
with self.assertRaisesRegexp(exceptions.ValidationFailed, "domain"):
helpers.with_schema_falcon(
base.fake_request_bad_missing_domain,
schema=testing_schema)
with self.assertRaisesRegexp(exceptions.ValidationFailed,
"Invalid JSON body in request"):
helpers.with_schema_falcon(
base.fake_request_bad_invalid_json_body,
schema=testing_schema)
def test_partial_with_schema(self):
self.assertEqual(request_fit_schema(base.fake_request_good), None)
with self.assertRaisesRegexp(exceptions.ValidationFailed, "domain"):
request_fit_schema(base.fake_request_bad_missing_domain)
with self.assertRaisesRegexp(exceptions.ValidationFailed,
"Invalid JSON body in request"):
request_fit_schema(base.fake_request_bad_invalid_json_body)
def test_schema_base(self):
with self.assertRaises(errors.InvalidResourceName):
service.ServiceSchema.get_schema("invalid_resource", "PUT")
with self.assertRaises(errors.InvalidOperation):
service.ServiceSchema.get_schema("service", "INVALID_HTTP_VERB")
def test_accept_header(self):
req = base.DummyRequestWithInvalidHeader()
resp = helpers.DummyResponse()
with self.assertRaises(helpers.falcon.HTTPNotAcceptable):
helpers.require_accepts_json_falcon(req, resp)
def test_falcon_endpoint(self):
class DummyResponse(object):
pass
response = DummyResponse()
global error_count
# Try to call with good inputs
oldcount = base.error_count
ret = self.ep.get_falcon_style(base.fake_request_good, response)
self.assertEqual(oldcount, base.error_count)
self.assertEqual(
ret,
"Hello, World!",
"testing not passed on endpoint: get_falcon_style with valid data")
# Try to call with bad inputs
oldcount = base.error_count
self.ep.get_falcon_style(
base.fake_request_bad_missing_domain,
response)
self.assertEqual(oldcount + 1, base.error_count)
# Try to call with bad inputs
self.ep.get_falcon_style_custom_abort(
base.fake_request_bad_missing_domain,
response)