summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ironic/api/method.py41
-rw-r--r--ironic/tests/unit/api/test_method.py163
2 files changed, 203 insertions, 1 deletions
diff --git a/ironic/api/method.py b/ironic/api/method.py
index 50f672a29..48bd8caa0 100644
--- a/ironic/api/method.py
+++ b/ironic/api/method.py
@@ -32,7 +32,7 @@ pecan_json_decorate = pecan.expose(
generic=False)
-def expose():
+def expose(status_code=None):
def decorate(f):
@@ -40,6 +40,8 @@ def expose():
def callfunction(self, *args, **kwargs):
try:
result = f(self, *args, **kwargs)
+ if status_code:
+ pecan.response.status = status_code
except Exception:
try:
@@ -58,6 +60,18 @@ def expose():
else:
pecan.response.status = 500
+ def _empty():
+ pecan.request.pecan['content_type'] = None
+ pecan.response.content_type = None
+
+ # never return content for NO_CONTENT
+ if pecan.response.status_code == 204:
+ return _empty()
+
+ # don't encode None for ACCEPTED responses
+ if result is None and pecan.response.status_code == 202:
+ return _empty()
+
return json.dumps(result)
pecan_json_decorate(callfunction)
@@ -66,6 +80,31 @@ def expose():
return decorate
+def body(body_arg):
+ """Decorator which places HTTP request body JSON into a method argument
+
+ :param body_arg: Name of argument to populate with body JSON
+ """
+
+ def inner_function(function):
+
+ @functools.wraps(function)
+ def inner_body(*args, **kwargs):
+
+ data = pecan.request.json
+ if isinstance(data, dict):
+ # remove any keyword arguments which pecan has
+ # extracted from the body
+ for field in data.keys():
+ kwargs.pop(field, None)
+
+ kwargs[body_arg] = data
+
+ return function(*args, **kwargs)
+ return inner_body
+ return inner_function
+
+
def format_exception(excinfo, debug=False):
"""Extract informations that can be sent to the client."""
error = excinfo[1]
diff --git a/ironic/tests/unit/api/test_method.py b/ironic/tests/unit/api/test_method.py
new file mode 100644
index 000000000..2b6535977
--- /dev/null
+++ b/ironic/tests/unit/api/test_method.py
@@ -0,0 +1,163 @@
+# 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 http import client as http_client
+import json
+
+import pecan.rest
+import pecan.testing
+
+from ironic import api
+from ironic.api.controllers import root
+from ironic.api.controllers import v1
+from ironic.api import method
+from ironic.api import types as atypes
+from ironic.common import args
+from ironic.tests.unit.api import base as test_api_base
+
+
+class MyThingController(pecan.rest.RestController):
+
+ _custom_actions = {
+ 'no_content': ['GET'],
+ 'response_content': ['GET'],
+ 'response_custom_status': ['GET'],
+ 'ouch': ['GET'],
+ }
+
+ @method.expose()
+ @args.validate(name=args.string, flag=args.boolean)
+ def get(self, name, flag):
+ return {name: flag}
+
+ @method.expose()
+ def no_content(self):
+ api.response.status_code = 204
+ return 'nothing'
+
+ @method.expose()
+ def response_content(self):
+ resp = atypes.PassthruResponse('nothing', status_code=200)
+ api.response.status_code = resp.status_code
+ return resp.obj
+
+ @method.expose(status_code=202)
+ def response_custom_status(self):
+ return 'accepted'
+
+ @method.expose()
+ def ouch(self):
+ raise Exception('ouch')
+
+ @method.expose(status_code=201)
+ @method.body('body')
+ @args.validate(body=args.schema({
+ 'type': 'object',
+ 'properties': {
+ 'three': {'type': 'string'},
+ 'four': {'type': 'string', 'maxLength': 4},
+ 'five': {'type': 'string'},
+ },
+ 'additionalProperties': False,
+ 'required': ['three']
+ }))
+ def post(self, body):
+ return body
+
+
+class MyV1Controller(v1.Controller):
+
+ things = MyThingController()
+
+
+class MyRootController(root.RootController):
+
+ v1 = MyV1Controller()
+
+
+class TestExpose(test_api_base.BaseApiTest):
+
+ block_execute = False
+
+ root_controller = '%s.%s' % (MyRootController.__module__,
+ MyRootController.__name__)
+
+ def test_expose(self):
+ self.assertEqual(
+ {'foo': True},
+ self.get_json('/things', name='foo', flag=True)
+ )
+
+ def test_expose_validation(self):
+ response = self.get_json('/things', name='foo', flag='truish',
+ expect_errors=True)
+ self.assertEqual(http_client.BAD_REQUEST, response.status_int)
+
+ error = json.loads(response.json['error_message'])
+ self.assertEqual('Client', error['faultcode'])
+ self.assertIsNone(error['debuginfo'])
+ self.assertIn("Unrecognized value 'truish'", error['faultstring'])
+
+ def test_response_204(self):
+ response = self.get_json('/things/no_content', expect_errors=True)
+ self.assertEqual(http_client.NO_CONTENT, response.status_int)
+ self.assertIsNone(response.content_type)
+ self.assertEqual(b'', response.normal_body)
+
+ def test_response_content(self):
+ response = self.get_json('/things/response_content',
+ expect_errors=True)
+ self.assertEqual(http_client.OK, response.status_int)
+ self.assertEqual(b'"nothing"', response.normal_body)
+ self.assertEqual('application/json', response.content_type)
+
+ def test_response_custom_status(self):
+ response = self.get_json('/things/response_custom_status',
+ expect_errors=True)
+ self.assertEqual(http_client.ACCEPTED, response.status_int)
+ self.assertEqual(b'"accepted"', response.normal_body)
+ self.assertEqual('application/json', response.content_type)
+
+ def test_exception(self):
+ response = self.get_json('/things/ouch',
+ expect_errors=True)
+ error_message = json.loads(response.json['error_message'])
+ self.assertEqual(http_client.INTERNAL_SERVER_ERROR,
+ response.status_int)
+ self.assertEqual('application/json', response.content_type)
+ self.assertEqual('Server', error_message['faultcode'])
+ self.assertEqual('ouch', error_message['faultstring'])
+
+ def test_post_body(self):
+ data = {
+ 'three': 'three',
+ 'four': 'four',
+ 'five': 'five'
+ }
+ response = self.post_json('/things/', data, expect_errors=True)
+ self.assertEqual(http_client.CREATED, response.status_int)
+ self.assertEqual(data, response.json)
+
+ def test_post_body_validation(self):
+ data = {
+ 'three': 'three',
+ 'four': 'fourrrr',
+ 'five': 'five'
+ }
+ response = self.post_json('/things/', data, expect_errors=True)
+ self.assertEqual(http_client.BAD_REQUEST, response.status_int)
+ error = json.loads(response.json['error_message'])
+ self.assertEqual('Client', error['faultcode'])
+ self.assertIsNone(error['debuginfo'])
+ self.assertIn("Schema error for body:", error['faultstring'])