diff --git a/swh/web/ui/controller.py b/swh/web/ui/controller.py index a50df7740..c893ec2d4 100644 --- a/swh/web/ui/controller.py +++ b/swh/web/ui/controller.py @@ -1,265 +1,265 @@ # 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, request, flash 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.main import app from swh.web.ui import service, renderers from swh.web.ui.exc import BadInputExc, NotFoundExc hash_filter_keys = ALGORITHMS @app.route('/') @set_renderers(HTMLRenderer) def main(): """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': '', 'found': None} try: if q: env.update(service.lookup_hash(q)) except BadInputExc: env['message'] = 'Error: invalid query string' 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(hash, sha): """Show content information. Args: hash: hash according to HASH_ALGO, where HASH_ALGO is one of: 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 sha1_git """ env = {'hash': hash, 'sha': sha} if hash not in hash_filter_keys: message = 'The checksum must be one of sha1, sha1_git, sha256' else: q = "%s:%s" % (hash, sha) found = service.lookup_hash(q)['found'] if not found: message = "Hash %s was not found." % sha else: origin = service.lookup_hash_origin(q) message = _origin_seen(hash, origin) env['message'] = message return render_template('content.html', **env) @app.route('/api/1/stat/counters') def api_stats(): """Return statistics as a JSON object""" return service.stat_counters() @app.errorhandler(ValueError) def value_error_as_bad_request(error): """Compute a bad request and add body as payload. """ return renderers.error_response('Bad request', 400, error) @app.errorhandler(NotFoundExc) def value_not_found(error): """Compute a not found and add body as payload. """ return renderers.error_response('Not found', 404, error) @app.route('/api/1/search//') def api_search(q): """Return search results as a JSON object""" return {'found': service.lookup_hash(q)} def _api_lookup(criteria, lookup_fn, error_msg_if_not_found): """Factorize function regarding the api to lookup for data.""" res = lookup_fn(criteria) if not res: raise NotFoundExc(error_msg_if_not_found) return res @app.route('/api/1/origin/') def api_origin(origin_id): """Return information about origin.""" return _api_lookup( origin_id, lookup_fn=service.lookup_origin, error_msg_if_not_found='Origin with id %s not found.' % origin_id) @app.route('/api/1/person/') def api_person(person_id): """Return information about person.""" return _api_lookup( person_id, lookup_fn=service.lookup_person, error_msg_if_not_found='Person with id %s not found.' % person_id) @app.route('/api/1/release/') def api_release(sha1_git): """Return information about release with id sha1_git.""" error_msg = 'Release with sha1_git %s not found.' % sha1_git return _api_lookup( sha1_git, lookup_fn=service.lookup_release, error_msg_if_not_found=error_msg) @app.route('/api/1/revision/') def api_revision(sha1_git): """Return information about revision with id sha1_git. """ error_msg = 'Revision with sha1_git %s not found.' % sha1_git return _api_lookup( sha1_git, lookup_fn=service.lookup_revision, error_msg_if_not_found=error_msg) @app.route('/api/1/directory/') def api_directory(sha1_git): """Return information about release with id sha1_git.""" recursive_flag = request.args.get('recursive', False) directory_entries = service.lookup_directory(sha1_git, recursive_flag) if not directory_entries: raise NotFoundExc('Directory with sha1_git %s not found.' % sha1_git) - return {'directory_entries': list(directory_entries)} + return list(directory_entries) @app.route('/api/1/content//') def api_content_with_details(q): """Return content information up to its origin if found. Args: q is of the form algo_hash:hash Raises: BadInputExc in case of unknown algo_hash or bad hash NotFoundExc if the content is not found. """ content = service.lookup_content(q) if not content: raise NotFoundExc('Content with %s not found.' % q) origin_detail = service.lookup_hash_origin(q) output = {'origin': origin_detail if origin_detail else None} for key, value in content.items(): output[key] = value return output @app.route('/api/1/uploadnsearch/', methods=['POST']) def api_uploadnsearch(): """Upload the file's content in the post body request. Compute the hash and determine if it exists in the storage. """ file = request.files['filename'] filename, sha1, found = service.upload_and_search(file) return {'sha1': sha1, 'filename': filename, 'found': found} diff --git a/swh/web/ui/tests/test_controller.py b/swh/web/ui/tests/test_controller.py index ad3925966..01d1ef605 100644 --- a/swh/web/ui/tests/test_controller.py +++ b/swh/web/ui/tests/test_controller.py @@ -1,528 +1,528 @@ # 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 import json import yaml from nose.tools import istest from unittest.mock import patch, MagicMock from swh.web.ui.tests import test_app class ApiTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.app, _, _ = test_app.init_app() @istest def info(self): # when rv = self.app.get('/about') self.assertEquals(rv.status_code, 200) self.assertIn(b'About', rv.data) # @istest def search_1(self): # when rv = self.app.get('/search') self.assertEquals(rv.status_code, 200) # check this api self.assertRegexpMatches(rv.data, b'name=q value=>') # @istest def search_2(self): # when rv = self.app.get('/search?q=one-hash-to-look-for:another-one') self.assertEquals(rv.status_code, 200) # check this api self.assertRegexpMatches( rv.data, b'name=q value=one-hash-to-look-for:another-one') @patch('swh.web.ui.controller.service') @istest def api_content_with_details(self, mock_service): # given mock_service.lookup_hash_origin.return_value = { 'origin_type': 'git', 'origin_url': 'https://url/user/repo', 'branch': 'master', 'revision': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', 'path': '/some/path/to/somewhere', } mock_service.lookup_content.return_value = { 'data': 'some content data', 'sha1': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', 'sha1_git': 'b4e8f472ffcb01a03875b26e462eb568739f6882', 'sha256': '83c0e67cc80f60caf1fcbec2d84b0ccd7968b3be4735637006560' 'cde9b067a4f', 'length': 17, 'status': 'visible' } # when rv = self.app.get( '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/') self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'origin': { 'origin_type': 'git', 'origin_url': 'https://url/user/repo', 'branch': 'master', 'revision': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', 'path': '/some/path/to/somewhere', }, 'data': 'some content data', 'sha1': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', 'sha1_git': 'b4e8f472ffcb01a03875b26e462eb568739f6882', 'sha256': '83c0e67cc80f60caf1fcbec2d84b0ccd7968b3be4735637006560c' 'de9b067a4f', 'length': 17, 'status': 'visible' }) mock_service.lookup_hash_origin.assert_called_once_with( 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') mock_service.lookup_content.assert_called_once_with( 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') @patch('swh.web.ui.controller.service') @istest def api_content_origin_not_found(self, mock_service): # given mock_service.lookup_hash_origin.return_value = None mock_service.lookup_content.return_value = { 'data': 'some content data..', 'sha1': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', 'sha1_git': 'b4e8f472ffcb01a03875b26e462eb568739f6882', 'sha256': '83c0e67cc80f60caf1fcbec2d84b0ccd7968b3be4735637006560' 'cde9b067a4f', 'length': 19, 'status': 'absent' } # when rv = self.app.get('/api/1/content/sha1_git:b4e8f472ffcb01a03875b26e4' '62eb568739f6882/') self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'origin': None, 'data': 'some content data..', 'sha1': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', 'sha1_git': 'b4e8f472ffcb01a03875b26e462eb568739f6882', 'sha256': '83c0e67cc80f60caf1fcbec2d84b0ccd7968b3be4735637006560c' 'de9b067a4f', 'length': 19, 'status': 'absent' }) mock_service.lookup_hash_origin.assert_called_once_with( 'sha1_git:b4e8f472ffcb01a03875b26e462eb568739f6882') mock_service.lookup_content.assert_called_once_with( 'sha1_git:b4e8f472ffcb01a03875b26e462eb568739f6882') @patch('swh.web.ui.controller.service') @istest def api_content_not_found_as_json(self, mock_service): # given mock_service.lookup_content.return_value = None mock_service.lookup_hash_origin = MagicMock() # when rv = self.app.get( '/api/1/content/sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' 'be4735637006560c/') self.assertEquals(rv.status_code, 404) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'error': 'Content with sha256:83c0e67cc80f60caf1fcbec2d84b0ccd79' '68b3be4735637006560c not found.' }) mock_service.lookup_content.assert_called_once_with( 'sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' 'be4735637006560c') mock_service.lookup_hash_origin.called = False @patch('swh.web.ui.controller.service') @istest def api_content_not_found_as_yaml(self, mock_service): # given mock_service.lookup_content.return_value = None mock_service.lookup_hash_origin = MagicMock() # when rv = self.app.get( '/api/1/content/sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' 'be4735637006560c/', headers={'accept': 'application/yaml'}) self.assertEquals(rv.status_code, 404) self.assertEquals(rv.mimetype, 'application/yaml') response_data = yaml.load(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'error': 'Content with sha256:83c0e67cc80f60caf1fcbec2d84b0ccd79' '68b3be4735637006560c not found.' }) mock_service.lookup_content.assert_called_once_with( 'sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' 'be4735637006560c') mock_service.lookup_hash_origin.called = False @patch('swh.web.ui.controller.service') @istest def api_search(self, mock_service): # given mock_service.lookup_hash.return_value = True # when rv = self.app.get('/api/1/search/sha1:blah/') self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, {'found': True}) mock_service.lookup_hash.assert_called_once_with('sha1:blah') @patch('swh.web.ui.controller.service') @istest def api_search_as_yaml(self, mock_service): # given mock_service.lookup_hash.return_value = True # when rv = self.app.get('/api/1/search/sha1:halb/', headers={'Accept': 'application/yaml'}) self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/yaml') response_data = yaml.load(rv.data.decode('utf-8')) self.assertEquals(response_data, {'found': True}) mock_service.lookup_hash.assert_called_once_with('sha1:halb') @patch('swh.web.ui.controller.service') @istest def api_search_not_found(self, mock_service): # given mock_service.lookup_hash.return_value = False # when rv = self.app.get('/api/1/search/sha1:halb/') self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, {'found': False}) mock_service.lookup_hash.assert_called_once_with('sha1:halb') @patch('swh.web.ui.controller.service') @istest def api_1_stat_counters_raise_error(self, mock_service): # given mock_service.stat_counters.side_effect = ValueError( 'voluntary error to check the bad request middleware.') # when rv = self.app.get('/api/1/stat/counters') # then self.assertEquals(rv.status_code, 400) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'error': 'voluntary error to check the bad request middleware.'}) @patch('swh.web.ui.controller.service') @istest def api_1_stat_counters(self, mock_service): # given stub_stats = { "content": 1770830, "directory": 211683, "directory_entry_dir": 209167, "directory_entry_file": 1807094, "directory_entry_rev": 0, "entity": 0, "entity_history": 0, "occurrence": 0, "occurrence_history": 19600, "origin": 1096, "person": 0, "release": 8584, "revision": 7792, "revision_history": 0, "skipped_content": 0 } mock_service.stat_counters.return_value = stub_stats # when rv = self.app.get('/api/1/stat/counters') self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, stub_stats) mock_service.stat_counters.assert_called_once_with() @patch('swh.web.ui.controller.service') @patch('swh.web.ui.controller.request') @istest def api_uploadnsearch(self, mock_request, mock_service): # given mock_request.files = {'filename': 'simple-filename'} mock_service.upload_and_search.return_value = ( 'simple-filename', 'some-hex-sha1', False) # when rv = self.app.post('/api/1/uploadnsearch/') self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, {'filename': 'simple-filename', 'sha1': 'some-hex-sha1', 'found': False}) mock_service.upload_and_search.assert_called_once_with( 'simple-filename') @patch('swh.web.ui.controller.service') @istest def api_origin(self, mock_service): # given stub_origin = { 'id': 1234, 'lister': 'uuid-lister-0', 'project': 'uuid-project-0', 'url': 'ftp://some/url/to/origin/0', 'type': 'ftp' } mock_service.lookup_origin.return_value = stub_origin # when rv = self.app.get('/api/1/origin/1234') # then self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, stub_origin) mock_service.lookup_origin.assert_called_with(1234) @patch('swh.web.ui.controller.service') @istest def api_origin_not_found(self, mock_service): # given mock_service.lookup_origin.return_value = None # when rv = self.app.get('/api/1/origin/4321') # then self.assertEquals(rv.status_code, 404) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'error': 'Origin with id 4321 not found.' }) mock_service.lookup_origin.assert_called_with(4321) @patch('swh.web.ui.controller.service') @istest def api_release(self, mock_service): # given stub_release = { 'id': 'release-0', 'revision': 'revision-sha1', 'author_name': 'author release name', 'author_email': 'author@email', } mock_service.lookup_release.return_value = stub_release # when rv = self.app.get('/api/1/release/release-0') # then self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, stub_release) @patch('swh.web.ui.controller.service') @istest def api_release_not_found(self, mock_service): # given mock_service.lookup_release.return_value = None # when rv = self.app.get('/api/1/release/release-0') # then self.assertEquals(rv.status_code, 404) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'error': 'Release with sha1_git release-0 not found.' }) @patch('swh.web.ui.controller.service') @istest def api_revision(self, mock_service): # given stub_revision = { 'id': '18d8be353ed3480476f032475e7c233eff7371d5', 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 'author_name': 'Software Heritage', 'author_email': 'robot@softwareheritage.org', 'committer_name': 'Software Heritage', 'committer_email': 'robot@softwareheritage.org', 'message': 'synthetic revision message', 'date_offset': 0, 'committer_date_offset': 0, 'parents': [], 'type': 'tar', 'synthetic': True, 'metadata': { 'original_artifact': [{ 'archive_type': 'tar', 'name': 'webbase-5.7.0.tar.gz', 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd', 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1', 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f' '309d36484e7edf7bb912' }] }, } mock_service.lookup_revision.return_value = stub_revision # when rv = self.app.get('/api/1/revision/revision-0') # then self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, stub_revision) @patch('swh.web.ui.controller.service') @istest def api_revision_not_found(self, mock_service): # given mock_service.lookup_revision.return_value = None # when rv = self.app.get('/api/1/revision/revision-0') # then self.assertEquals(rv.status_code, 404) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'error': 'Revision with sha1_git revision-0 not found.'}) @patch('swh.web.ui.controller.service') @istest def api_person(self, mock_service): # given stub_person = { 'id': '198003', 'name': 'Software Heritage', 'email': 'robot@softwareheritage.org', } mock_service.lookup_person.return_value = stub_person # when rv = self.app.get('/api/1/person/198003') # then self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, stub_person) @patch('swh.web.ui.controller.service') @istest def api_person_not_found(self, mock_service): # given mock_service.lookup_person.return_value = None # when rv = self.app.get('/api/1/person/666') # then self.assertEquals(rv.status_code, 404) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'error': 'Person with id 666 not found.'}) @patch('swh.web.ui.controller.service') @istest def api_directory(self, mock_service): # given stub_directory = [{ 'sha1_git': '18d8be353ed3480476f032475e7c233eff7371d5', }] mock_service.lookup_directory.return_value = stub_directory # when rv = self.app.get('/api/1/directory/' '18d8be353ed3480476f032475e7c233eff7371d5') # then self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) - self.assertEquals(response_data, {'directory_entries': stub_directory}) + self.assertEquals(response_data, stub_directory) @patch('swh.web.ui.controller.service') @istest def api_directory_not_found(self, mock_service): # given mock_service.lookup_directory.return_value = [] # when rv = self.app.get('/api/1/directory/' '66618d8be353ed3480476f032475e7c233eff737') # then self.assertEquals(rv.status_code, 404) self.assertEquals(rv.mimetype, 'application/json') response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, { 'error': 'Directory with sha1_git ' '66618d8be353ed3480476f032475e7c233eff737 not found.'})