Skip to content

Commit

Permalink
Merge pull request #4327 from StackStorm/entry_point_api_endpoint_con…
Browse files Browse the repository at this point in the history
…tent_type_fix

Update action entry point API controller to return correct Content-Type response header
  • Loading branch information
Kami authored Sep 11, 2018
2 parents 6033b2a + 828dfdf commit ffa3b6d
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ Fixed
CentOS. (bug fix) #4297
* Fix a bug with action runner throwing an exception and failing to run an action if there was an
empty pack config inside ``/opt/stackstorm/configs/``. (bug fix) #4325
* Update ``GET /v1/actions/views/entry_point/<action ref>`` to return correct ``Content-Type``
response header based on the entry point type / file extension. Previously it would always
incorrectly return ``application/json``. (improvement) #4327

Deprecated
~~~~~~~~~~
Expand Down
35 changes: 31 additions & 4 deletions st2api/st2api/controllers/v1/action_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from mongoengine import ValidationError
import os
import codecs
import mimetypes

import six
from mongoengine import ValidationError

from st2api.controllers import resource
from st2common.exceptions.db import StackStormDBObjectNotFoundError
Expand All @@ -27,6 +31,7 @@
from st2common.rbac.types import PermissionType
from st2common.rbac import utils as rbac_utils
from st2common.router import abort
from st2common.router import Response

__all__ = [
'OverviewController',
Expand Down Expand Up @@ -186,10 +191,32 @@ def get_one(self, ref_or_id, requester_user):
raise StackStormDBObjectNotFoundError('Action ref_or_id=%s has no entry_point to output'
% ref_or_id)

with open(abs_path) as file:
content = file.read()
with codecs.open(abs_path, 'r') as fp:
content = fp.read()

return content
# Ensure content is utf-8
if isinstance(content, six.binary_type):
content = content.decode('utf-8')

try:
content_type = mimetypes.guess_type(abs_path)[0]
except Exception:
content_type = None

# Special case if /etc/mime.types doesn't contain entry for yaml, py
if not content_type:
_, extension = os.path.splitext(abs_path)
if extension in ['.yaml', '.yml']:
content_type = 'application/x-yaml'
elif extension in ['.py']:
content_type = 'application/x-python'
else:
content_type = 'text/plain'

response = Response()
response.headers['Content-Type'] = content_type
response.text = content
return response


class ActionViewsController(object):
Expand Down
48 changes: 48 additions & 0 deletions st2api/tests/unit/controllers/v1/test_action_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,51 @@ def test_get_one_ref(self):
self.assertEqual(get_resp.status_int, 200)
finally:
self.app.delete('/v1/actions/%s' % action_id)

@mock.patch.object(action_validator, 'validate_action', mock.MagicMock(
return_value=True))
@mock.patch.object(content_utils, 'get_entry_point_abs_path', mock.MagicMock(
return_value='/path/to/file.yaml'))
@mock.patch(mock_open_name, mock.mock_open(read_data='file content'), create=True)
def test_get_one_ref_yaml_content_type(self):
post_resp = self.app.post_json('/v1/actions', ACTION_1)
action_id = post_resp.json['id']
action_ref = '.'.join((post_resp.json['pack'], post_resp.json['name']))
try:
get_resp = self.app.get('/v1/actions/views/entry_point/%s' % action_ref)
self.assertEqual(get_resp.status_int, 200)
self.assertEqual(get_resp.headers['Content-Type'], 'application/x-yaml')
finally:
self.app.delete('/v1/actions/%s' % action_id)

@mock.patch.object(action_validator, 'validate_action', mock.MagicMock(
return_value=True))
@mock.patch.object(content_utils, 'get_entry_point_abs_path', mock.MagicMock(
return_value=__file__.replace('.pyc', '.py')))
@mock.patch(mock_open_name, mock.mock_open(read_data='file content'), create=True)
def test_get_one_ref_python_content_type(self):
post_resp = self.app.post_json('/v1/actions', ACTION_1)
action_id = post_resp.json['id']
action_ref = '.'.join((post_resp.json['pack'], post_resp.json['name']))
try:
get_resp = self.app.get('/v1/actions/views/entry_point/%s' % action_ref)
self.assertEqual(get_resp.status_int, 200)
self.assertEqual(get_resp.headers['Content-Type'], 'application/x-python')
finally:
self.app.delete('/v1/actions/%s' % action_id)

@mock.patch.object(action_validator, 'validate_action', mock.MagicMock(
return_value=True))
@mock.patch.object(content_utils, 'get_entry_point_abs_path', mock.MagicMock(
return_value='/file/does/not/exist'))
@mock.patch(mock_open_name, mock.mock_open(read_data='file content'), create=True)
def test_get_one_ref_text_plain_content_type(self):
post_resp = self.app.post_json('/v1/actions', ACTION_1)
action_id = post_resp.json['id']
action_ref = '.'.join((post_resp.json['pack'], post_resp.json['name']))
try:
get_resp = self.app.get('/v1/actions/views/entry_point/%s' % action_ref)
self.assertEqual(get_resp.status_int, 200)
self.assertEqual(get_resp.headers['Content-Type'], 'text/plain')
finally:
self.app.delete('/v1/actions/%s' % action_id)
7 changes: 6 additions & 1 deletion st2common/st2common/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,12 @@ def __init__(self, **entries):

try:
validator = CustomValidator(response_spec['schema'], resolver=self.spec_resolver)
validator.validate(resp.json)

response_type = response_spec['schema'].get('type', 'json')
if response_type == 'string':
validator.validate(resp.text)
else:
validator.validate(resp.json)
except (jsonschema.ValidationError, ValueError):
LOG.exception('Response validation failed.')
resp.headers.add('Warning', '199 OpenAPI "Response validation failed"')
Expand Down

0 comments on commit ffa3b6d

Please sign in to comment.