diff --git a/swh/web/ui/tests/test_utils.py b/swh/web/ui/tests/test_utils.py index cfd08c60..a59e8768 100644 --- a/swh/web/ui/tests/test_utils.py +++ b/swh/web/ui/tests/test_utils.py @@ -1,80 +1,118 @@ # 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'}, + {'link': '/path/to/url/file/654', + 'name': 'some-filename'}, + {'link': '/path/to/url/dir/987', + 'name': 'some-other-dirname'}] + + # when + actual_outputs = utils.prepare_directory_listing(inputs) + + # then + self.assertEquals(actual_outputs, expected_output) diff --git a/swh/web/ui/tests/test_views.py b/swh/web/ui/tests/test_views.py index a1b897b1..6d7275bb 100644 --- a/swh/web/ui/tests/test_views.py +++ b/swh/web/ui/tests/test_views.py @@ -1,152 +1,232 @@ # 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 from nose.tools import istest from swh.web.ui.tests import test_app from unittest.mock import patch, MagicMock from swh.web.ui.exc import BadInputExc class ViewTestCase(test_app.SWHViewTestCase): render_template = False @istest def info(self): # when rv = self.client.get('/about') self.assertEquals(rv.status_code, 200) self.assert_template_used('about.html') self.assertIn(b'About', rv.data) @istest def search_default(self): # when rv = self.client.get('/search') self.assertEquals(rv.status_code, 200) self.assertEqual(self.get_context_variable('q'), '') self.assertEqual(self.get_context_variable('message'), '') self.assert_template_used('search.html') @patch('swh.web.ui.views.service') @istest def search_content_found(self, mock_service): # given mock_service.lookup_hash.return_value = { 'found': True, 'algo': 'sha1' } # when rv = self.client.get('/search?q=sha1:123') self.assertEquals(rv.status_code, 200) self.assert_template_used('search.html') self.assertEqual(self.get_context_variable('q'), 'sha1:123') self.assertEqual(self.get_context_variable('message'), 'Content with hash sha1:123 found!') mock_service.lookup_hash.assert_called_once_with('sha1:123') @patch('swh.web.ui.views.service') @istest def search_content_not_found(self, mock_service): # given mock_service.lookup_hash.return_value = { 'found': False, 'algo': 'sha1' } # when rv = self.client.get('/search?q=sha1:456') self.assertEquals(rv.status_code, 200) self.assert_template_used('search.html') self.assertEqual(self.get_context_variable('q'), 'sha1:456') self.assertEqual(self.get_context_variable('message'), 'Content with hash sha1:456 not found!') mock_service.lookup_hash.assert_called_once_with('sha1:456') @patch('swh.web.ui.views.service') @istest def search_content_invalid_query(self, mock_service): # given mock_service.lookup_hash = MagicMock( side_effect=BadInputExc('Invalid query!') ) # when rv = self.client.get('/search?q=sha1:invalid-hash') self.assertEquals(rv.status_code, 200) self.assert_template_used('search.html') self.assertEqual(self.get_context_variable('q'), 'sha1:invalid-hash') self.assertEqual(self.get_context_variable('message'), 'Invalid query!') mock_service.lookup_hash.assert_called_once_with('sha1:invalid-hash') @patch('swh.web.ui.views.service') @istest def show_content(self, mock_service): # given stub_content_raw = { 'sha1': 'sha1-hash', 'data': 'some-data' } mock_service.lookup_content_raw.return_value = stub_content_raw # when rv = self.client.get('/browse/content/sha1:sha1-hash/raw') self.assertEquals(rv.status_code, 200) self.assert_template_used('display_content.html') self.assertEqual(self.get_context_variable('message'), 'Content sha1-hash') self.assertEqual(self.get_context_variable('content'), stub_content_raw) mock_service.lookup_content_raw.assert_called_once_with( 'sha1:sha1-hash') @patch('swh.web.ui.views.service') @istest def show_content_not_found(self, mock_service): # given mock_service.lookup_content_raw.return_value = None # when rv = self.client.get('/browse/content/sha1:sha1-unknown/raw') - print(self.templates) self.assertEquals(rv.status_code, 200) self.assert_template_used('display_content.html') self.assertEqual(self.get_context_variable('message'), 'Content with sha1:sha1-unknown not found.') self.assertEqual(self.get_context_variable('content'), None) mock_service.lookup_content_raw.assert_called_once_with( 'sha1:sha1-unknown') @patch('swh.web.ui.views.service') @istest def show_content_invalid_hash(self, mock_service): # given mock_service.lookup_content_raw.side_effect = BadInputExc( 'Invalid hash') # when rv = self.client.get('/browse/content/sha2:sha1-invalid/raw') - print(self.templates) self.assertEquals(rv.status_code, 200) self.assert_template_used('display_content.html') self.assertEqual(self.get_context_variable('message'), 'Invalid hash') self.assertEqual(self.get_context_variable('content'), None) mock_service.lookup_content_raw.assert_called_once_with( 'sha2:sha1-invalid') + + @patch('swh.web.ui.views.service') + @patch('swh.web.ui.utils') + @istest + def browse_directory_bad_input(self, mock_utils, mock_service): + # given + mock_service.lookup_directory.side_effect = BadInputExc('Invalid hash') + + # when + rv = self.client.get('/browse/directory/sha2-invalid') + + # then + self.assertEquals(rv.status_code, 200) + self.assert_template_used('directory.html') + self.assertEqual(self.get_context_variable('message'), + 'Invalid hash') + self.assertEqual(self.get_context_variable('files'), []) + mock_service.lookup_directory.assert_called_once_with( + 'sha2-invalid') + + @patch('swh.web.ui.views.service') + @patch('swh.web.ui.utils') + @istest + def browse_directory_empty_result(self, mock_utils, mock_service): + # given + mock_service.lookup_directory.return_value = None + + # when + rv = self.client.get('/browse/directory/some-sha1') + + # then + self.assertEquals(rv.status_code, 200) + self.assert_template_used('directory.html') + self.assertEqual(self.get_context_variable('message'), + 'Directory some-sha1 not found.') + self.assertEqual(self.get_context_variable('files'), []) + mock_service.lookup_directory.assert_called_once_with( + 'some-sha1') + + @patch('swh.web.ui.views.service') + @patch('swh.web.ui.views.utils') + @istest + def browse_directory(self, mock_utils, mock_service): + # given + stub_directory_ls = [ + {'type': 'dir', + 'target': '123', + 'name': 'some-dir-name'}, + {'type': 'file', + 'sha1': '654', + 'name': 'some-filename'}, + {'type': 'dir', + 'target': '987', + 'name': 'some-other-dirname'} + ] + mock_service.lookup_directory.return_value = stub_directory_ls + stub_directory_map = [ + {'link': '/path/to/url/dir/123', + 'name': 'some-dir-name'}, + {'link': '/path/to/url/file/654', + 'name': 'some-filename'}, + {'link': '/path/to/url/dir/987', + 'name': 'some-other-dirname'} + ] + mock_utils.prepare_directory_listing.return_value = stub_directory_map + + # when + rv = self.client.get('/browse/directory/some-sha1') + + # then + print(self.templates) + self.assertEquals(rv.status_code, 200) + self.assert_template_used('directory.html') + self.assertEqual(self.get_context_variable('message'), + 'Listing for directory some-sha1:') + self.assertEqual(self.get_context_variable('files'), + stub_directory_map) + + mock_service.lookup_directory.assert_called_once_with( + 'some-sha1') + mock_utils.prepare_directory_listing.assert_called_once_with( + stub_directory_ls) diff --git a/swh/web/ui/utils.py b/swh/web/ui/utils.py index 161239fe..0c39b53a 100644 --- a/swh/web/ui/utils.py +++ b/swh/web/ui/utils.py @@ -1,31 +1,52 @@ # 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 flask + def filter_endpoints(url_map, prefix_url_rule, blacklist=[]): """Filter endpoints by prefix url rule. Args: - url_map: Url Werkzeug.Map of rules - prefix_url_rule: prefix url string - blacklist: blacklist of some url Returns: Dictionary of url_rule with values methods and endpoint. The key is the url, the associated value is a dictionary of 'methods' (possible http methods) and 'endpoint' (python function) """ out = {} for r in url_map._rules: rule = r.rule if rule == prefix_url_rule or rule in blacklist: continue if rule.startswith(prefix_url_rule): out[r.rule] = {'methods': sorted(map(str, r.methods)), 'endpoint': r.endpoint} return out + + +def prepare_directory_listing(files): + """Given a list of dictionary files, return a view ready dictionary. + + """ + ls = [] + for entry in files: + new_entry = {} + if entry['type'] == 'dir': + new_entry['link'] = flask.url_for('browse_directory', + sha1_git=entry['target']) + else: + new_entry['link'] = flask.url_for('show_content', + q=entry['sha1']) + new_entry['name'] = entry['name'] + ls.append(new_entry) + + return ls diff --git a/swh/web/ui/views.py b/swh/web/ui/views.py index 79f12c75..d203e922 100644 --- a/swh/web/ui/views.py +++ b/swh/web/ui/views.py @@ -1,235 +1,216 @@ # 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 -from flask import render_template, flash, request, url_for +from flask import render_template, flash, request from flask.ext.api.decorators import set_renderers from flask.ext.api.renderers import HTMLRenderer from swh.core.hashutil import ALGORITHMS -from swh.web.ui import service +from swh.web.ui import service, utils from swh.web.ui.exc import BadInputExc from swh.web.ui.main import app hash_filter_keys = ALGORITHMS @app.route('/') @set_renderers(HTMLRenderer) def homepage(): """Home page """ flash('This Web app is still work in progress, use at your own risk', 'warning') # return redirect(url_for('about')) return render_template('home.html') @app.route('/about') @set_renderers(HTMLRenderer) def about(): return render_template('about.html') @app.route('/search') @set_renderers(HTMLRenderer) def search(): """Search for hashes in swh-storage. """ q = request.args.get('q', '') env = {'q': q, 'message': ''} try: if q: r = service.lookup_hash(q) env['message'] = 'Content with hash %s%sfound!' % ( q, ' ' if r['found'] == True else ' not ' ) except BadInputExc as e: env['message'] = str(e) return render_template('search.html', **env) @app.route('/uploadnsearch', methods=['GET', 'POST']) @set_renderers(HTMLRenderer) def uploadnsearch(): """Upload and search for hashes in swh-storage. """ env = {'filename': None, 'message': '', 'found': None} if request.method == 'POST': file = request.files['filename'] try: filename, sha1, found = service.upload_and_search(file) message = 'The file %s with hash %s has%sbeen found.' % ( filename, sha1, ' ' if found else ' not ') env.update({ 'filename': filename, 'sha1': sha1, 'found': found, 'message': message }) except BadInputExc: env['message'] = 'Error: invalid query string' return render_template('upload_and_search.html', **env) def _origin_seen(hash, data): """Given an origin, compute a message string with the right information. Args: origin: a dictionary with keys: - origin: a dictionary with type and url keys - occurrence: a dictionary with a validity range Returns: message as a string """ if data is None: return 'Content with hash %s is unknown as of now.' % hash origin_type = data['origin_type'] origin_url = data['origin_url'] revision = data['revision'] branch = data['branch'] path = data['path'] return """The content with hash %s has been seen on origin with type '%s' at url '%s'. The revision was identified at '%s' on branch '%s'. The file's path referenced was '%s'.""" % (hash, origin_type, origin_url, revision, branch, path) @app.route('/browse/content/') @set_renderers(HTMLRenderer) def content_with_origin(q): """Show content information. Args: - q: query string of the form with `algo_hash` in sha1, sha1_git, sha256. This means that several different URLs (at least one per HASH_ALGO) will point to the same content sha: the sha with 'hash' format Returns: The content's information at for a given checksum. """ env = {'q': q} try: content = service.lookup_hash(q) found = content['found'] if not found: message = "Hash %s was not found." % content['algo'] else: origin = service.lookup_hash_origin(q) message = _origin_seen(hash, origin) except BadInputExc as e: # do not like it but do not duplicate code message = str(e) env['message'] = message return render_template('content.html', **env) @app.route('/browse/content//raw') @set_renderers(HTMLRenderer) def show_content(q): """Given a hash and a checksum, display the content's raw data. Args: q is of the form algo_hash:hash with algo_hash in (sha1, sha1_git, sha256) Returns: Information on one possible origin for such content. Raises: BadInputExc in case of unknown algo_hash or bad hash NotFoundExc if the content is not found. """ env = {} try: content = service.lookup_content_raw(q) if content: message = 'Content %s' % content['sha1'] else: message = 'Content with %s not found.' % q except BadInputExc as e: message = str(e) content = None env['message'] = message env['content'] = content return render_template('display_content.html', **env) -def prepare_directory_listing(files): - """Given a list of dictionary files, return a view ready dictionary. - - """ - ls = [] - for entry in files: - new_entry = {} - if entry['type'] == 'dir': - new_entry['link'] = url_for('browse_directory', - sha1_git=entry['target']) - else: - new_entry['link'] = url_for('show_content', - q=entry['sha1']) - new_entry['name'] = entry['name'] - ls.append(new_entry) - - return ls - - @app.route('/browse/directory/') @set_renderers(HTMLRenderer) def browse_directory(sha1_git): """Show directory information. Args: - sha1_git: the directory's sha1 git identifier. Returns: The content's information at sha1_git """ env = {'sha1_git': sha1_git} try: - files = service.lookup_directory(sha1_git) - if files: + directory_files = service.lookup_directory(sha1_git) + if directory_files: message = "Listing for directory %s:" % sha1_git - files = prepare_directory_listing(files) + files = utils.prepare_directory_listing(directory_files) else: - message = "Directory %s was not found." % sha1_git + message = "Directory %s not found." % sha1_git files = [] except BadInputExc as e: # do not like it but do not duplicate code message = str(e) files = [] env['message'] = message env['files'] = files return render_template('directory.html', **env)