diff --git a/swh/web/ui/renderers.py b/swh/web/ui/renderers.py index c9d414d7..f5495c8d 100644 --- a/swh/web/ui/renderers.py +++ b/swh/web/ui/renderers.py @@ -1,109 +1,110 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import yaml from flask import make_response, request from flask.ext.api import renderers, parsers from flask_api.mediatypes import MediaType from swh.web.ui import utils class SWHFilterRenderer(): """Base renderer for swh's common behavior. """ def filter_by_fields(self, data): fields = request.args.get('fields') if fields: + fields = set(fields.split(',')) data = utils.filter_field_keys(data, fields) return data class PlainRenderer(renderers.BaseRenderer): """Renderer for plain/text, do nothing but send the data as is. """ media_type = 'text/plain' def render(self, data, media_type, **options): return data class YAMLRenderer(renderers.BaseRenderer, SWHFilterRenderer): """Renderer for application/yaml. Orchestrate from python data structure to yaml. """ media_type = 'application/yaml' def render(self, data, media_type, **options): data = self.filter_by_fields(data) return yaml.dump(data, encoding=self.charset) class JSONPRenderer(renderers.JSONRenderer, SWHFilterRenderer): """Renderer for application/json. Serializes in json the data and returns it. Also deals with jsonp. If callback is found in request parameter, wrap the result as a function with name the value of the parameter query 'callback'. """ def render(self, data, media_type, **options): data = self.filter_by_fields(data) res = super().render(data, media_type, **options) jsonp = request.args.get('callback') if jsonp: return '%s(%s)' % (jsonp, res) return res RENDERERS = [ 'swh.web.ui.renderers.JSONPRenderer', 'flask.ext.api.renderers.BrowsableAPIRenderer', 'flask.ext.api.parsers.URLEncodedParser', 'swh.web.ui.renderers.YAMLRenderer', 'swh.web.ui.renderers.PlainRenderer', ] RENDERERS_INSTANCE = [ JSONPRenderer(), renderers.BrowsableAPIRenderer(), parsers.URLEncodedParser(), YAMLRenderer(), ] RENDERERS_BY_TYPE = { r.media_type: r for r in RENDERERS_INSTANCE } def error_response(default_error_msg, error_code, error): """Private function to create a custom error response. """ # if nothing is requested by client, use json default_application_type = 'application/json' accept_type = request.headers.get('Accept', default_application_type) renderer = RENDERERS_BY_TYPE.get( accept_type, RENDERERS_BY_TYPE[default_application_type]) # for edge cases, use the elected renderer's media type accept_type = renderer.media_type response = make_response(default_error_msg, error_code) response.headers['Content-Type'] = accept_type response.data = renderer.render({"error": str(error)}, media_type=MediaType(accept_type), status=error_code, headers={'Content-Type': accept_type}) return response diff --git a/swh/web/ui/tests/test_renderers.py b/swh/web/ui/tests/test_renderers.py index 2d1c7cd0..931cc166 100644 --- a/swh/web/ui/tests/test_renderers.py +++ b/swh/web/ui/tests/test_renderers.py @@ -1,163 +1,164 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import json import unittest import yaml from flask_api.mediatypes import MediaType from nose.tools import istest from unittest.mock import patch from swh.web.ui import renderers class RendererTestCase(unittest.TestCase): @patch('swh.web.ui.renderers.request') @istest def SWHFilterRenderer_do_nothing(self, mock_request): # given mock_request.args = {} swhFilterRenderer = renderers.SWHFilterRenderer() input_data = {'a': 'some-data'} # when actual_data = swhFilterRenderer.filter_by_fields(input_data) # then self.assertEquals(actual_data, input_data) @patch('swh.web.ui.renderers.utils') @patch('swh.web.ui.renderers.request') @istest def SWHFilterRenderer_do_filter(self, mock_request, mock_utils): # given mock_request.args = {'fields': 'a,c'} mock_utils.filter_field_keys.return_value = {'a': 'some-data'} swhFilterRenderer = renderers.SWHFilterRenderer() input_data = {'a': 'some-data', 'b': 'some-other-data'} # when actual_data = swhFilterRenderer.filter_by_fields(input_data) # then self.assertEquals(actual_data, {'a': 'some-data'}) - mock_utils.filter_field_keys.assert_called_once_with(input_data, 'a,c') + mock_utils.filter_field_keys.assert_called_once_with(input_data, + {'a', 'c'}) @istest def plainRenderer(self): # given plainRenderer = renderers.PlainRenderer() input_data = 'some data' # when actual_data = plainRenderer.render(input_data, 'some-media-type') # then self.assertEqual(actual_data, input_data) # do nothing on data @patch('swh.web.ui.renderers.request') @istest def yamlRenderer_without_filter(self, mock_request): # given mock_request.args = {} yamlRenderer = renderers.YAMLRenderer() input_data = {'target': 'sha1-dir', 'type': 'dir', 'dir-id': 'dir-id-sha1-git'} expected_data = input_data # when actual_data = yamlRenderer.render(input_data, 'application/yaml') # then self.assertEqual(yaml.load(actual_data), expected_data) @patch('swh.web.ui.renderers.request') @istest def yamlRenderer(self, mock_request): # given mock_request.args = {'fields': 'type,target'} yamlRenderer = renderers.YAMLRenderer() input_data = {'target': 'sha1-dir', 'type': 'dir', 'dir-id': 'dir-id-sha1-git'} expected_data = {'target': 'sha1-dir', 'type': 'dir'} # when actual_data = yamlRenderer.render(input_data, 'application/yaml') # then self.assertEqual(yaml.load(actual_data), expected_data) @patch('swh.web.ui.renderers.request') @istest def jsonRenderer_basic(self, mock_request): # given mock_request.args = {} jsonRenderer = renderers.JSONPRenderer() input_data = {'target': 'sha1-dir', 'type': 'dir', 'dir-id': 'dir-id-sha1-git'} expected_data = input_data # when actual_data = jsonRenderer.render(input_data, MediaType( 'application/json')) # then self.assertEqual(json.loads(actual_data), expected_data) @patch('swh.web.ui.renderers.request') @istest def jsonRenderer_basic_with_filter(self, mock_request): # given mock_request.args = {'fields': 'target'} jsonRenderer = renderers.JSONPRenderer() input_data = {'target': 'sha1-dir', 'type': 'dir', 'dir-id': 'dir-id-sha1-git'} expected_data = {'target': 'sha1-dir'} # when actual_data = jsonRenderer.render(input_data, MediaType( 'application/json')) # then self.assertEqual(json.loads(actual_data), expected_data) @patch('swh.web.ui.renderers.request') @istest def jsonRenderer_basic_with_filter_and_jsonp(self, mock_request): # given mock_request.args = {'fields': 'target', 'callback': 'jsonpfn'} jsonRenderer = renderers.JSONPRenderer() input_data = {'target': 'sha1-dir', 'type': 'dir', 'dir-id': 'dir-id-sha1-git'} # when actual_data = jsonRenderer.render(input_data, MediaType( 'application/json')) # then self.assertEqual(actual_data, 'jsonpfn({"target": "sha1-dir"})') diff --git a/swh/web/ui/tests/test_utils.py b/swh/web/ui/tests/test_utils.py index 49ead65c..75b1ecc8 100644 --- a/swh/web/ui/tests/test_utils.py +++ b/swh/web/ui/tests/test_utils.py @@ -1,166 +1,174 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import unittest from unittest.mock import patch from nose.tools import istest from swh.web.ui import utils class Rule(object): rule = "" endpoint = None methods = [] def __init__(self, rule, methods, endpoint): self.rule = rule self.endpoint = endpoint self.methods = methods class Map(object): _rules = [] def __init__(self, rules): self._rules = rules class UtilsTestCase(unittest.TestCase): def setUp(self): self.url_map = Map([Rule('/other/', methods=set(['GET', 'POST', 'HEAD']), endpoint='foo'), Rule('/some/old/url/', methods=set(['GET', 'POST']), endpoint='blablafn'), Rule('/other/old/url/', methods=set(['GET', 'HEAD']), endpoint='bar'), Rule('/other', methods=set([]), endpoint=None), Rule('/other2', methods=set([]), endpoint=None)]) @istest def filter_endpoints_1(self): # when actual_data = utils.filter_endpoints(self.url_map, '/some') # then self.assertEquals(actual_data, { '/some/old/url/': { 'methods': ['GET', 'POST'], 'endpoint': 'blablafn' } }) @istest def filter_endpoints_2(self): # when actual_data = utils.filter_endpoints(self.url_map, '/other', blacklist=['/other2']) # then # rules /other is skipped because its' exactly the prefix url # rules /other2 is skipped because it's blacklisted self.assertEquals(actual_data, { '/other/': { 'methods': ['GET', 'HEAD', 'POST'], 'endpoint': 'foo' }, '/other/old/url/': { 'methods': ['GET', 'HEAD'], 'endpoint': 'bar' } }) @patch('swh.web.ui.utils.flask') @istest def prepare_directory_listing(self, mock_flask): # given def mock_url_for(url_key, **kwds): if url_key == 'browse_directory': sha1_git = kwds['sha1_git'] return '/path/to/url/dir' + '/' + sha1_git else: sha1_git = kwds['q'] return '/path/to/url/file' + '/' + sha1_git mock_flask.url_for.side_effect = mock_url_for inputs = [{'type': 'dir', 'target': '123', 'name': 'some-dir-name'}, {'type': 'file', 'sha1': '654', 'name': 'some-filename'}, {'type': 'dir', 'target': '987', 'name': 'some-other-dirname'}] expected_output = [{'link': '/path/to/url/dir/123', 'name': 'some-dir-name', 'type': 'dir'}, {'link': '/path/to/url/file/654', 'name': 'some-filename', 'type': 'file'}, {'link': '/path/to/url/dir/987', 'name': 'some-other-dirname', 'type': 'dir'}] # when actual_outputs = utils.prepare_directory_listing(inputs) # then self.assertEquals(actual_outputs, expected_output) @istest def filter_field_keys_dict_unknown_keys(self): # when - actual_res = utils.filter_field_keys({'a': 1, 'c': 2, 'b': 3}, 'd') + actual_res = utils.filter_field_keys( + {'directory': 1, 'file': 2, 'link': 3}, + {'directory1', 'file2'}) # then self.assertEqual(actual_res, {}) @istest def filter_field_keys_dict(self): # when - actual_res = utils.filter_field_keys({'a': 1, 'c': 2, 'b': 3}, 'a,b') + actual_res = utils.filter_field_keys( + {'directory': 1, 'file': 2, 'link': 3}, + {'directory', 'link'}) # then - self.assertEqual(actual_res, {'a': 1, 'b': 3}) + self.assertEqual(actual_res, {'directory': 1, 'link': 3}) @istest def filter_field_keys_list_unknown_keys(self): # when - actual_res = utils.filter_field_keys([{'a': 1, 'c': 2, 'b': 3}, - {'1': 1, '2': 2, 'b': 3}], 'd') + actual_res = utils.filter_field_keys( + [{'directory': 1, 'file': 2, 'link': 3}, + {'1': 1, '2': 2, 'link': 3}], + {'d'}) # then self.assertEqual(actual_res, [{}, {}]) @istest def filter_field_keys_list(self): # when - actual_res = utils.filter_field_keys([{'a': 1, 'c': 2, 'b': 3}, - {'1': 1, '2': 2, 'b': 3}], 'a,1') + actual_res = utils.filter_field_keys( + [{'directory': 1, 'file': 2, 'link': 3}, + {'dir': 1, 'fil': 2, 'lin': 3}], + {'directory', 'dir'}) # then - self.assertEqual(actual_res, [{'a': 1}, {'1': 1}]) + self.assertEqual(actual_res, [{'directory': 1}, {'dir': 1}]) @istest def filter_field_keys_other(self): # given - inputSet = set([1, 2]) + inputSet = {1, 2} # when - actual_res = utils.filter_field_keys(inputSet, 'a,1') + actual_res = utils.filter_field_keys(inputSet, {'a', '1'}) # then self.assertEqual(actual_res, inputSet)