diff --git a/swh/web/api/tests/test_views.py b/swh/web/api/tests/test_views.py deleted file mode 100644 index 6b08204a6..000000000 --- a/swh/web/api/tests/test_views.py +++ /dev/null @@ -1,2394 +0,0 @@ -# Copyright (C) 2015-2017 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 nose.tools import istest -from unittest.mock import patch, MagicMock - -from swh.storage.exc import StorageDBError, StorageAPIError - -from .swh_api_testcase import SWHApiTestCase -from swh.web.api.exc import NotFoundExc, BadInputExc -from swh.web.api import views - - -class ApiTestCase(SWHApiTestCase): - - def setUp(self): - self.origin_visit1 = { - 'date': 1104616800.0, - 'origin': 10, - 'visit': 100, - 'metadata': None, - 'status': 'full', - } - - self.origin1 = { - 'id': 1234, - 'lister': 'uuid-lister-0', - 'project': 'uuid-project-0', - 'url': 'ftp://some/url/to/origin/0', - 'type': 'ftp' - } - - @istest - def generic_api_lookup_nothing_is_found(self): - # given - def test_generic_lookup_fn(sha1, another_unused_arg): - assert another_unused_arg == 'unused_arg' - assert sha1 == 'sha1' - return None - - # when - with self.assertRaises(NotFoundExc) as cm: - views._api_lookup( - test_generic_lookup_fn, 'sha1', 'unused_arg', - notfound_msg='This will be raised because None is returned.') - - self.assertIn('This will be raised because None is returned.', - cm.exception.args[0]) - - @istest - def generic_api_map_are_enriched_and_transformed_to_list(self): - # given - def test_generic_lookup_fn_1(criteria0, param0, param1): - assert criteria0 == 'something' - return map(lambda x: x + 1, [1, 2, 3]) - - # when - actual_result = views._api_lookup( - test_generic_lookup_fn_1, 'something', 'some param 0', - 'some param 1', - notfound_msg=('This is not the error message you are looking for. ' - 'Move along.'), - enrich_fn=lambda x: x * 2) - - self.assertEqual(actual_result, [4, 6, 8]) - - @istest - def generic_api_list_are_enriched_too(self): - # given - def test_generic_lookup_fn_2(crit): - assert crit == 'something' - return ['a', 'b', 'c'] - - # when - actual_result = views._api_lookup( - test_generic_lookup_fn_2, 'something', - notfound_msg=('Not the error message you are looking for, it is. ' - 'Along, you move!'), - enrich_fn=lambda x: ''. join(['=', x, '='])) - - self.assertEqual(actual_result, ['=a=', '=b=', '=c=']) - - @istest - def generic_api_generator_are_enriched_and_returned_as_list(self): - # given - def test_generic_lookup_fn_3(crit): - assert crit == 'crit' - return (i for i in [4, 5, 6]) - - # when - actual_result = views._api_lookup( - test_generic_lookup_fn_3, 'crit', - notfound_msg='Move!', - enrich_fn=lambda x: x - 1) - - self.assertEqual(actual_result, [3, 4, 5]) - - @istest - def generic_api_simple_data_are_enriched_and_returned_too(self): - # given - def test_generic_lookup_fn_4(crit): - assert crit == '123' - return {'a': 10} - - def test_enrich_data(x): - x['a'] = x['a'] * 10 - return x - - # when - actual_result = views._api_lookup( - test_generic_lookup_fn_4, '123', - notfound_msg='Nothing to do', - enrich_fn=test_enrich_data) - - self.assertEqual(actual_result, {'a': 100}) - - @patch('swh.web.api.views.service') - @istest - def api_content_filetype(self, mock_service): - stub_filetype = { - 'accepted_media_type': 'application/xml', - 'encoding': 'ascii', - 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - } - mock_service.lookup_content_filetype.return_value = stub_filetype - - # when - rv = self.client.get( - '/api/1/content/' - 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f/filetype/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'accepted_media_type': 'application/xml', - 'encoding': 'ascii', - 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'content_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', - }) - - mock_service.lookup_content_filetype.assert_called_once_with( - 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f') - - @patch('swh.web.api.views.service') - @istest - def api_content_filetype_sha_not_found(self, mock_service): - # given - mock_service.lookup_content_filetype.return_value = None - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/' - 'filetype/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'No filetype information found for content ' - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03.' - }) - - mock_service.lookup_content_filetype.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_language(self, mock_service): - stub_language = { - 'lang': 'lisp', - 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - } - mock_service.lookup_content_language.return_value = stub_language - - # when - rv = self.client.get( - '/api/1/content/' - 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f/language/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'lang': 'lisp', - 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'content_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', - }) - - mock_service.lookup_content_language.assert_called_once_with( - 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f') - - @patch('swh.web.api.views.service') - @istest - def api_content_language_sha_not_found(self, mock_service): - # given - mock_service.lookup_content_language.return_value = None - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' - '/language/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'No language information found for content ' - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03.' - }) - - mock_service.lookup_content_language.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_symbol(self, mock_service): - stub_ctag = [{ - 'sha1': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'name': 'foobar', - 'kind': 'Haskell', - 'line': 10, - }] - mock_service.lookup_expression.return_value = stub_ctag - - # when - rv = self.client.get('/api/1/content/symbol/foo/?last_sha1=sha1') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, [{ - 'sha1': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'name': 'foobar', - 'kind': 'Haskell', - 'line': 10, - 'content_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', - 'data_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/raw/', - 'license_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/license/', - 'language_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/language/', - 'filetype_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/filetype/', - }]) - self.assertFalse('Link' in rv) - - mock_service.lookup_expression.assert_called_once_with( - 'foo', 'sha1', 10) - - @patch('swh.web.api.views.service') - @istest - def api_content_symbol_2(self, mock_service): - stub_ctag = [{ - 'sha1': '12371b8614fcd89ccd17ca2b1d9e66c5b00a6456', - 'name': 'foobar', - 'kind': 'Haskell', - 'line': 10, - }, { - 'sha1': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6678', - 'name': 'foo', - 'kind': 'Lisp', - 'line': 10, - }] - mock_service.lookup_expression.return_value = stub_ctag - - # when - rv = self.client.get( - '/api/1/content/symbol/foo/?last_sha1=prev-sha1&per_page=2') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, stub_ctag) - self.assertTrue( - rv['Link'] == '; rel="next"' or # noqa - rv['Link'] == '; rel="next"' # noqa - ) - mock_service.lookup_expression.assert_called_once_with( - 'foo', 'prev-sha1', 2) - - @patch('swh.web.api.views.service') - # @istest - def api_content_symbol_3(self, mock_service): - stub_ctag = [{ - 'sha1': '67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'name': 'foo', - 'kind': 'variable', - 'line': 100, - }] - mock_service.lookup_expression.return_value = stub_ctag - - # when - rv = self.client.get('/api/1/content/symbol/foo/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, [{ - 'sha1': '67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'name': 'foo', - 'kind': 'variable', - 'line': 100, - 'content_url': '/api/1/content/' - 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', - 'data_url': '/api/1/content/' - 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/raw/', - 'license_url': '/api/1/content/' - 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/license/', - 'language_url': '/api/1/content/' - 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/language/', - 'filetype_url': '/api/1/content/' - 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/filetype/', - }]) - self.assertFalse(rv.has_header('Link')) - - mock_service.lookup_expression.assert_called_once_with('foo', None, 10) - - @patch('swh.web.api.views.service') - @istest - def api_content_symbol_not_found(self, mock_service): - # given - mock_service.lookup_expression.return_value = [] - - # when - rv = self.client.get('/api/1/content/symbol/bar/?last_sha1=hash') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'No indexed raw content match expression \'bar\'.' - }) - self.assertFalse('Link' in rv) - - mock_service.lookup_expression.assert_called_once_with( - 'bar', 'hash', 10) - - @patch('swh.web.api.views.service') - @istest - def api_content_ctags(self, mock_service): - stub_ctags = { - 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'ctags': [] - } - mock_service.lookup_content_ctags.return_value = stub_ctags - - # when - rv = self.client.get( - '/api/1/content/' - 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f/ctags/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'ctags': [], - 'content_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', - }) - - mock_service.lookup_content_ctags.assert_called_once_with( - 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f') - - @patch('swh.web.api.views.service') - @istest - def api_content_license(self, mock_service): - stub_license = { - 'licenses': ['No_license_found', 'Apache-2.0'], - 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'tool_name': 'nomos', - } - mock_service.lookup_content_license.return_value = stub_license - - # when - rv = self.client.get( - '/api/1/content/' - 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f/license/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'licenses': ['No_license_found', 'Apache-2.0'], - 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'tool_name': 'nomos', - 'content_url': '/api/1/content/' - 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', - }) - - mock_service.lookup_content_license.assert_called_once_with( - 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f') - - @patch('swh.web.api.views.service') - @istest - def api_content_license_sha_not_found(self, mock_service): - # given - mock_service.lookup_content_license.return_value = None - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/' - 'license/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'No license information found for content ' - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03.' - }) - - mock_service.lookup_content_license.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_provenance(self, mock_service): - stub_provenances = [{ - 'origin': 1, - 'visit': 2, - 'revision': 'b04caf10e9535160d90e874b45aa426de762f19f', - 'content': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'path': 'octavio-3.4.0/octave.html/doc_002dS_005fISREG.html' - }] - mock_service.lookup_content_provenance.return_value = stub_provenances - - # when - rv = self.client.get( - '/api/1/content/' - 'sha1_git:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/provenance/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, [{ - 'origin': 1, - 'visit': 2, - 'origin_url': '/api/1/origin/1/', - 'origin_visits_url': '/api/1/origin/1/visits/', - 'origin_visit_url': '/api/1/origin/1/visit/2/', - 'revision': 'b04caf10e9535160d90e874b45aa426de762f19f', - 'revision_url': '/api/1/revision/' - 'b04caf10e9535160d90e874b45aa426de762f19f/', - 'content': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'content_url': '/api/1/content/' - 'sha1_git:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', - 'path': 'octavio-3.4.0/octave.html/doc_002dS_005fISREG.html' - }]) - - mock_service.lookup_content_provenance.assert_called_once_with( - 'sha1_git:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_provenance_sha_not_found(self, mock_service): - # given - mock_service.lookup_content_provenance.return_value = None - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/' - 'provenance/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Content with sha1:40e71b8614fcd89ccd17ca2b1d9e6' - '6c5b00a6d03 not found.' - }) - - mock_service.lookup_content_provenance.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_metadata(self, mock_service): - # given - mock_service.lookup_content.return_value = { - 'sha1': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'sha1_git': 'b4e8f472ffcb01a03875b26e462eb568739f6882', - 'sha256': '83c0e67cc80f60caf1fcbec2d84b0ccd7968b3be4735637006560' - 'cde9b067a4f', - 'length': 17, - 'status': 'visible' - } - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'data_url': '/api/1/content/' - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/raw/', - 'filetype_url': '/api/1/content/' - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/filetype/', - 'language_url': '/api/1/content/' - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/language/', - 'license_url': '/api/1/content/' - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/license/', - 'sha1': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', - 'sha1_git': 'b4e8f472ffcb01a03875b26e462eb568739f6882', - 'sha256': '83c0e67cc80f60caf1fcbec2d84b0ccd7968b3be4735637006560c' - 'de9b067a4f', - 'length': 17, - 'status': 'visible' - }) - - mock_service.lookup_content.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_not_found_as_json(self, mock_service): - # given - mock_service.lookup_content.return_value = None - mock_service.lookup_content_provenance = MagicMock() - - # when - rv = self.client.get( - '/api/1/content/sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' - 'be4735637006560c/') - - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Content with sha256:83c0e67cc80f60caf1fcbec2d84b0ccd79' - '68b3be4735637006560c not found.' - }) - - mock_service.lookup_content.assert_called_once_with( - 'sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' - 'be4735637006560c') - mock_service.lookup_content_provenance.called = False - - @patch('swh.web.api.views.service') - @istest - def api_content_not_found_as_yaml(self, mock_service): - # given - mock_service.lookup_content.return_value = None - mock_service.lookup_content_provenance = MagicMock() - - # when - rv = self.client.get( - '/api/1/content/sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' - 'be4735637006560c/', - HTTP_ACCEPT='application/yaml') - - self.assertEquals(rv.status_code, 404) - self.assertTrue('application/yaml' in rv['Content-Type']) - - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Content with sha256:83c0e67cc80f60caf1fcbec2d84b0ccd79' - '68b3be4735637006560c not found.' - }) - - mock_service.lookup_content.assert_called_once_with( - 'sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' - 'be4735637006560c') - mock_service.lookup_content_provenance.called = False - - @patch('swh.web.api.views.service') - @istest - def api_content_raw_ko_not_found(self, mock_service): - # given - mock_service.lookup_content_raw.return_value = None - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' - '/raw/') - - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Content sha1:40e71b8614fcd89ccd17ca2b1d9e6' - '6c5b00a6d03 is not found.' - }) - - mock_service.lookup_content_raw.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_raw_text(self, mock_service): - # given - stub_content = {'data': b'some content data'} - mock_service.lookup_content_raw.return_value = stub_content - mock_service.lookup_content_filetype.return_value = { - 'mimetype': 'text/html' - } - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' - '/raw/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/octet-stream') - self.assertEquals( - rv['Content-disposition'], - 'attachment; filename=content_sha1_' - '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03_raw') - self.assertEquals( - rv['Content-Type'], 'application/octet-stream') - self.assertEquals(rv.content, stub_content['data']) - - mock_service.lookup_content_raw.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - mock_service.lookup_content_filetype.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_raw_text_with_filename(self, mock_service): - # given - stub_content = {'data': b'some content data'} - mock_service.lookup_content_raw.return_value = stub_content - mock_service.lookup_content_filetype.return_value = { - 'mimetype': 'text/html' - } - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' - '/raw/?filename=filename.txt') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/octet-stream') - self.assertEquals( - rv['Content-disposition'], - 'attachment; filename=filename.txt') - self.assertEquals( - rv['Content-Type'], 'application/octet-stream') - self.assertEquals(rv.content, stub_content['data']) - - mock_service.lookup_content_raw.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - mock_service.lookup_content_filetype.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_raw_no_accepted_media_type_text_is_not_available_for_download( # noqa - self, mock_service): - # given - stub_content = {'data': b'some content data'} - mock_service.lookup_content_raw.return_value = stub_content - mock_service.lookup_content_filetype.return_value = { - 'mimetype': 'application/octet-stream' - } - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' - '/raw/') - - self.assertEquals(rv.status_code, 403) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'ForbiddenExc', - 'reason': 'Only textual content is available for download. ' - 'Actual content mimetype is application/octet-stream.' - }) - - mock_service.lookup_content_raw.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - mock_service.lookup_content_filetype.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_content_raw_no_accepted_media_type_found_so_not_available_for_download( # noqa - self, mock_service): - # given - stub_content = {'data': b'some content data'} - mock_service.lookup_content_raw.return_value = stub_content - mock_service.lookup_content_filetype.return_value = None - - # when - rv = self.client.get( - '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' - '/raw/') - - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Content sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03 ' - 'is not available for download.' - }) - - mock_service.lookup_content_raw.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - mock_service.lookup_content_filetype.assert_called_once_with( - 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') - - @patch('swh.web.api.views.service') - @istest - def api_check_content_known(self, mock_service): - # given - mock_service.lookup_multiple_hashes.return_value = [ - {'found': True, - 'filename': None, - 'sha1': 'sha1:blah'} - ] - - expected_result = { - 'search_stats': {'nbfiles': 1, 'pct': 100}, - 'search_res': [{'sha1': 'sha1:blah', - 'found': True}] - } - - # when - rv = self.client.get('/api/1/content/known/sha1:blah/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_result) - mock_service.lookup_multiple_hashes.assert_called_once_with( - [{'filename': None, 'sha1': 'sha1:blah'}]) - - @patch('swh.web.api.views.service') - @istest - def api_check_content_known_as_yaml(self, mock_service): - # given - mock_service.lookup_multiple_hashes.return_value = [ - {'found': True, - 'filename': None, - 'sha1': 'sha1:halb'}, - {'found': False, - 'filename': None, - 'sha1': 'sha1_git:hello'} - ] - - expected_result = { - 'search_stats': {'nbfiles': 2, 'pct': 50}, - 'search_res': [{'sha1': 'sha1:halb', - 'found': True}, - {'sha1': 'sha1_git:hello', - 'found': False}] - } - - # when - rv = self.client.get('/api/1/content/known/sha1:halb,sha1_git:hello/', - HTTP_ACCEPT='application/yaml') - - self.assertEquals(rv.status_code, 200) - self.assertTrue('application/yaml' in rv['Content-Type']) - self.assertEquals(rv.data, expected_result) - - mock_service.lookup_multiple_hashes.assert_called_once_with( - [{'filename': None, 'sha1': 'sha1:halb'}, - {'filename': None, 'sha1': 'sha1_git:hello'}]) - - @patch('swh.web.api.views.service') - @istest - def api_check_content_known_post_as_yaml(self, mock_service): - # given - stub_result = [{'sha1': '7e62b1fe10c88a3eddbba930b156bee2956b2435', - 'found': True}, - {'filename': 'filepath', - 'sha1': '8e62b1fe10c88a3eddbba930b156bee2956b2435', - 'found': True}, - {'filename': 'filename', - 'sha1': '64025b5d1520c615061842a6ce6a456cad962a3f', - 'found': False}] - mock_service.lookup_multiple_hashes.return_value = stub_result - - expected_result = { - 'search_stats': {'nbfiles': 3, 'pct': 2/3 * 100}, - 'search_res': stub_result - } - - # when - rv = self.client.post( - '/api/1/content/known/search/', - data=dict( - q='7e62b1fe10c88a3eddbba930b156bee2956b2435', - filepath='8e62b1fe10c88a3eddbba930b156bee2956b2435', - filename='64025b5d1520c615061842a6ce6a456cad962a3f'), - HTTP_ACCEPT='application/yaml' - ) - - self.assertEquals(rv.status_code, 200) - self.assertTrue('application/yaml' in rv['Content-Type']) - self.assertEquals(rv.data, expected_result) - - @patch('swh.web.api.views.service') - @istest - def api_check_content_known_not_found(self, mock_service): - # given - stub_result = [{'sha1': 'sha1:halb', - 'found': False}] - mock_service.lookup_multiple_hashes.return_value = stub_result - - expected_result = { - 'search_stats': {'nbfiles': 1, 'pct': 0.0}, - 'search_res': stub_result - } - - # when - rv = self.client.get('/api/1/content/known/sha1:halb/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_result) - - mock_service.lookup_multiple_hashes.assert_called_once_with( - [{'filename': None, 'sha1': 'sha1:halb'}]) - - @patch('swh.web.api.views.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.client.get('/api/1/stat/counters/') - # then - self.assertEquals(rv.status_code, 400) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'ValueError', - 'reason': 'voluntary error to check the bad request middleware.'}) - - @patch('swh.web.api.views.service') - @istest - def api_1_stat_counters_raise_swh_storage_error_db(self, mock_service): - # given - mock_service.stat_counters.side_effect = StorageDBError( - 'SWH Storage exploded! Will be back online shortly!') - # when - rv = self.client.get('/api/1/stat/counters/') - # then - self.assertEquals(rv.status_code, 503) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'StorageDBError', - 'reason': - 'An unexpected error occurred in the backend: ' - 'SWH Storage exploded! Will be back online shortly!'}) - - @patch('swh.web.api.views.service') - @istest - def api_1_stat_counters_raise_swh_storage_error_api(self, mock_service): - # given - mock_service.stat_counters.side_effect = StorageAPIError( - 'SWH Storage API dropped dead! Will resurrect from its ashes asap!' - ) - # when - rv = self.client.get('/api/1/stat/counters/') - # then - self.assertEquals(rv.status_code, 503) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'StorageAPIError', - 'reason': - 'An unexpected error occurred in the api backend: ' - 'SWH Storage API dropped dead! Will resurrect from its ashes asap!' - }) - - @patch('swh.web.api.views.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.client.get('/api/1/stat/counters/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, stub_stats) - - mock_service.stat_counters.assert_called_once_with() - - @patch('swh.web.api.views.service') - @istest - def api_1_lookup_origin_visits_raise_error(self, mock_service): - # given - mock_service.lookup_origin_visits.side_effect = ValueError( - 'voluntary error to check the bad request middleware.') - # when - rv = self.client.get('/api/1/origin/2/visits/') - # then - self.assertEquals(rv.status_code, 400) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'ValueError', - 'reason': 'voluntary error to check the bad request middleware.'}) - - @patch('swh.web.api.views.service') - @istest - def api_1_lookup_origin_visits_raise_swh_storage_error_db( - self, mock_service): - # given - mock_service.lookup_origin_visits.side_effect = StorageDBError( - 'SWH Storage exploded! Will be back online shortly!') - # when - rv = self.client.get('/api/1/origin/2/visits/') - # then - self.assertEquals(rv.status_code, 503) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'StorageDBError', - 'reason': - 'An unexpected error occurred in the backend: ' - 'SWH Storage exploded! Will be back online shortly!'}) - - @patch('swh.web.api.views.service') - @istest - def api_1_lookup_origin_visits_raise_swh_storage_error_api( - self, mock_service): - # given - mock_service.lookup_origin_visits.side_effect = StorageAPIError( - 'SWH Storage API dropped dead! Will resurrect from its ashes asap!' - ) - # when - rv = self.client.get('/api/1/origin/2/visits/') - # then - self.assertEquals(rv.status_code, 503) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'StorageAPIError', - 'reason': - 'An unexpected error occurred in the api backend: ' - 'SWH Storage API dropped dead! Will resurrect from its ashes asap!' - }) - - @patch('swh.web.api.views.service') - @istest - def api_1_lookup_origin_visits(self, mock_service): - # given - stub_visits = [ - { - 'date': 1293919200.0, - 'origin': 2, - 'visit': 2 - }, - { - 'date': 1420149600.0, - 'origin': 2, - 'visit': 3 - } - ] - - mock_service.lookup_origin_visits.return_value = stub_visits - - # when - rv = self.client.get('/api/1/origin/2/visits/?per_page=2&last_visit=1') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, [ - { - 'date': 1293919200.0, - 'origin': 2, - 'visit': 2, - 'origin_visit_url': '/api/1/origin/2/visit/2/', - }, - { - 'date': 1420149600.0, - 'origin': 2, - 'visit': 3, - 'origin_visit_url': '/api/1/origin/2/visit/3/', - } - ]) - - mock_service.lookup_origin_visits.assert_called_once_with( - '2', last_visit=1, per_page=2) - - @patch('swh.web.api.views.service') - @istest - def api_1_lookup_origin_visit(self, mock_service): - # given - origin_visit = self.origin_visit1.copy() - origin_visit.update({ - 'occurrences': { - 'master': { - 'target_type': 'revision', - 'target': '98564', - } - } - }) - - mock_service.lookup_origin_visit.return_value = origin_visit - - expected_origin_visit = self.origin_visit1.copy() - expected_origin_visit.update({ - 'origin_url': '/api/1/origin/10/', - 'occurrences': { - 'master': { - 'target_type': 'revision', - 'target': '98564', - 'target_url': '/api/1/revision/98564/' - } - } - }) - - # when - rv = self.client.get('/api/1/origin/10/visit/100/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_origin_visit) - - mock_service.lookup_origin_visit.assert_called_once_with('10', '100') - - @patch('swh.web.api.views.service') - @istest - def api_1_lookup_origin_visit_not_found(self, mock_service): - # given - mock_service.lookup_origin_visit.return_value = None - - # when - rv = self.client.get('/api/1/origin/1/visit/1000/') - - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'No visit 1000 for origin 1 found' - }) - - mock_service.lookup_origin_visit.assert_called_once_with('1', '1000') - - @patch('swh.web.api.views.service') - @istest - def api_origin_by_id(self, mock_service): - # given - mock_service.lookup_origin.return_value = self.origin1 - - expected_origin = self.origin1.copy() - expected_origin.update({ - 'origin_visits_url': '/api/1/origin/1234/visits/' - }) - - # when - rv = self.client.get('/api/1/origin/1234/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_origin) - - mock_service.lookup_origin.assert_called_with({'id': '1234'}) - - @patch('swh.web.api.views.service') - @istest - def api_origin_by_type_url(self, mock_service): - # given - stub_origin = self.origin1.copy() - stub_origin.update({ - 'id': 987 - }) - mock_service.lookup_origin.return_value = stub_origin - - expected_origin = stub_origin.copy() - expected_origin.update({ - 'origin_visits_url': '/api/1/origin/987/visits/' - }) - - # when - rv = self.client.get('/api/1/origin/ftp/url' - '/ftp://some/url/to/origin/0/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_origin) - - mock_service.lookup_origin.assert_called_with( - {'url': 'ftp://some/url/to/origin/0/', - 'type': 'ftp'}) - - @patch('swh.web.api.views.service') - @istest - def api_origin_not_found(self, mock_service): - # given - mock_service.lookup_origin.return_value = None - - # when - rv = self.client.get('/api/1/origin/4321/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Origin with id 4321 not found.' - }) - - mock_service.lookup_origin.assert_called_with({'id': '4321'}) - - @patch('swh.web.api.views.service') - @istest - def api_release(self, mock_service): - release_id = '7045404f3d1c54e6473' - target_id = '6072557b6c10cd9a211' - # given - stub_release = { - 'id': release_id, - 'target_type': 'revision', - 'target': target_id, - "date": "Mon, 10 Mar 1997 08:00:00 GMT", - "synthetic": True, - 'author': { - 'id': 10, - 'name': 'author release name', - 'email': 'author@email', - }, - } - - expected_release = { - 'id': release_id, - 'target_type': 'revision', - 'target': target_id, - 'target_url': '/api/1/revision/%s/' % target_id, - "date": "Mon, 10 Mar 1997 08:00:00 GMT", - "synthetic": True, - 'author_url': '/api/1/person/10/', - 'author': { - 'id': 10, - 'name': 'author release name', - 'email': 'author@email', - }, - } - - mock_service.lookup_release.return_value = stub_release - - # when - rv = self.client.get('/api/1/release/%s/' % release_id) - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_release) - - mock_service.lookup_release.assert_called_once_with(release_id) - - @patch('swh.web.api.views.service') - @istest - def api_release_target_type_not_a_revision(self, mock_service): - release = '8d56a78' - target = '9a5c3f' - # given - stub_release = { - 'id': release, - 'target_type': 'other-stuff', - 'target': target, - "date": "Mon, 10 Mar 1997 08:00:00 GMT", - "synthetic": True, - 'author': { - 'id': 9, - 'name': 'author release name', - 'email': 'author@email', - }, - } - - expected_release = { - 'id': release, - 'target_type': 'other-stuff', - 'target': target, - "date": "Mon, 10 Mar 1997 08:00:00 GMT", - "synthetic": True, - 'author_url': '/api/1/person/9/', - 'author': { - 'id': 9, - 'name': 'author release name', - 'email': 'author@email', - }, - } - - mock_service.lookup_release.return_value = stub_release - - # when - rv = self.client.get('/api/1/release/%s/' % release) - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_release) - - mock_service.lookup_release.assert_called_once_with(release) - - @patch('swh.web.api.views.service') - @istest - def api_release_not_found(self, mock_service): - # given - mock_service.lookup_release.return_value = None - - # when - rv = self.client.get('/api/1/release/c54e6473c71bbb716529/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Release with sha1_git c54e6473c71bbb716529 not found.' - }) - - @patch('swh.web.api.views.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': ['8734ef7e7c357ce2af928115c6c6a42b7e2a44e7'], - '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 - - expected_revision = { - 'id': '18d8be353ed3480476f032475e7c233eff7371d5', - 'url': '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5/', - 'history_url': '/api/1/revision/18d8be353ed3480476f032475e7c233e' - 'ff7371d5/log/', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'directory_url': '/api/1/directory/7834ef7e7c357ce2af928115c6c6' - 'a42b7e2a44e6/', - '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': [{ - 'id': '8734ef7e7c357ce2af928115c6c6a42b7e2a44e7', - 'url': '/api/1/revision/8734ef7e7c357ce2af928115c6c6a42b7e2a44e7/' # noqa - }], - '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' - }] - }, - } - - # when - rv = self.client.get('/api/1/revision/' - '18d8be353ed3480476f032475e7c233eff7371d5/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(expected_revision, rv.data) - - mock_service.lookup_revision.assert_called_once_with( - '18d8be353ed3480476f032475e7c233eff7371d5') - - @patch('swh.web.api.views.service') - @istest - def api_revision_not_found(self, mock_service): - # given - mock_service.lookup_revision.return_value = None - - # when - rv = self.client.get('/api/1/revision/12345/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Revision with sha1_git 12345 not found.'}) - - @patch('swh.web.api.views.service') - @istest - def api_revision_raw_ok(self, mock_service): - # given - stub_revision = {'message': 'synthetic revision message'} - - mock_service.lookup_revision_message.return_value = stub_revision - - # when - rv = self.client.get('/api/1/revision/18d8be353ed3480476f032475e7c2' - '33eff7371d5/raw/') - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/octet-stream') - self.assertEquals(rv.content, b'synthetic revision message') - - mock_service.lookup_revision_message.assert_called_once_with( - '18d8be353ed3480476f032475e7c233eff7371d5') - - @patch('swh.web.api.views.service') - @istest - def api_revision_raw_ok_no_msg(self, mock_service): - # given - mock_service.lookup_revision_message.side_effect = NotFoundExc( - 'No message for revision') - - # when - rv = self.client.get('/api/1/revision/' - '18d8be353ed3480476f032475e7c233eff7371d5/raw/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'No message for revision'}) - - self.assertEquals - mock_service.lookup_revision_message.assert_called_once_with( - '18d8be353ed3480476f032475e7c233eff7371d5') - - @patch('swh.web.api.views.service') - @istest - def api_revision_raw_ko_no_rev(self, mock_service): - # given - mock_service.lookup_revision_message.side_effect = NotFoundExc( - 'No revision found') - - # when - rv = self.client.get('/api/1/revision/' - '18d8be353ed3480476f032475e7c233eff7371d5/raw/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'No revision found'}) - - mock_service.lookup_revision_message.assert_called_once_with( - '18d8be353ed3480476f032475e7c233eff7371d5') - - @patch('swh.web.api.views.service') - @istest - def api_revision_with_origin_not_found(self, mock_service): - mock_service.lookup_revision_by.return_value = None - - rv = self.client.get('/api/1/revision/origin/123/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertIn('Revision with (origin_id: 123', rv.data['reason']) - self.assertIn('not found', rv.data['reason']) - self.assertEqual('NotFoundExc', rv.data['exception']) - - mock_service.lookup_revision_by.assert_called_once_with( - '123', - 'refs/heads/master', - None) - - @patch('swh.web.api.views.service') - @istest - def api_revision_with_origin(self, mock_service): - mock_revision = { - 'id': '32', - 'directory': '21', - 'message': 'message 1', - 'type': 'deb', - } - expected_revision = { - 'id': '32', - 'url': '/api/1/revision/32/', - 'history_url': '/api/1/revision/32/log/', - 'directory': '21', - 'directory_url': '/api/1/directory/21/', - 'message': 'message 1', - 'type': 'deb', - } - mock_service.lookup_revision_by.return_value = mock_revision - - rv = self.client.get('/api/1/revision/origin/1/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEqual(rv.data, expected_revision) - - mock_service.lookup_revision_by.assert_called_once_with( - '1', - 'refs/heads/master', - None) - - @patch('swh.web.api.views.service') - @istest - def api_revision_with_origin_and_branch_name(self, mock_service): - mock_revision = { - 'id': '12', - 'directory': '23', - 'message': 'message 2', - 'type': 'tar', - } - mock_service.lookup_revision_by.return_value = mock_revision - - expected_revision = { - 'id': '12', - 'url': '/api/1/revision/12/', - 'history_url': '/api/1/revision/12/log/', - 'directory': '23', - 'directory_url': '/api/1/directory/23/', - 'message': 'message 2', - 'type': 'tar', - } - - rv = self.client.get('/api/1/revision/origin/1' - '/branch/refs/origin/dev/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEqual(rv.data, expected_revision) - - mock_service.lookup_revision_by.assert_called_once_with( - '1', - 'refs/origin/dev', - None) - - @patch('swh.web.api.views.service') - @patch('swh.web.api.views.utils') - @istest - def api_revision_with_origin_and_branch_name_and_timestamp(self, - mock_utils, - mock_service): - mock_revision = { - 'id': '123', - 'directory': '456', - 'message': 'message 3', - 'type': 'tar', - } - mock_service.lookup_revision_by.return_value = mock_revision - - expected_revision = { - 'id': '123', - 'url': '/api/1/revision/123/', - 'history_url': '/api/1/revision/123/log/', - 'directory': '456', - 'directory_url': '/api/1/directory/456/', - 'message': 'message 3', - 'type': 'tar', - } - - mock_utils.parse_timestamp.return_value = 'parsed-date' - mock_utils.enrich_revision.return_value = expected_revision - - rv = self.client.get('/api/1/revision' - '/origin/1' - '/branch/refs/origin/dev' - '/ts/1452591542/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEqual(rv.data, expected_revision) - - mock_service.lookup_revision_by.assert_called_once_with( - '1', - 'refs/origin/dev', - 'parsed-date') - mock_utils.parse_timestamp.assert_called_once_with('1452591542') - mock_utils.enrich_revision.assert_called_once_with( - mock_revision) - - @patch('swh.web.api.views.service') - @patch('swh.web.api.views.utils') - @istest - def api_revision_with_origin_and_branch_name_and_timestamp_with_escapes( - self, - mock_utils, - mock_service): - mock_revision = { - 'id': '999', - } - mock_service.lookup_revision_by.return_value = mock_revision - - expected_revision = { - 'id': '999', - 'url': '/api/1/revision/999/', - 'history_url': '/api/1/revision/999/log/', - } - - mock_utils.parse_timestamp.return_value = 'parsed-date' - mock_utils.enrich_revision.return_value = expected_revision - - rv = self.client.get('/api/1/revision' - '/origin/1' - '/branch/refs%2Forigin%2Fdev' - '/ts/Today%20is%20' - 'January%201,%202047%20at%208:21:00AM/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEqual(rv.data, expected_revision) - - mock_service.lookup_revision_by.assert_called_once_with( - '1', - 'refs/origin/dev', - 'parsed-date') - mock_utils.parse_timestamp.assert_called_once_with( - 'Today is January 1, 2047 at 8:21:00AM') - mock_utils.enrich_revision.assert_called_once_with( - mock_revision) - - @patch('swh.web.api.views.service') - @istest - def revision_directory_by_ko_raise(self, mock_service): - # given - mock_service.lookup_directory_through_revision.side_effect = NotFoundExc('not') # noqa - - # when - with self.assertRaises(NotFoundExc): - views._revision_directory_by( - {'sha1_git': 'id'}, - None, - '/api/1/revision/sha1/directory/') - - # then - mock_service.lookup_directory_through_revision.assert_called_once_with( - {'sha1_git': 'id'}, - None, limit=100, with_data=False) - - @patch('swh.web.api.views.service') - @istest - def revision_directory_by_type_dir(self, mock_service): - # given - mock_service.lookup_directory_through_revision.return_value = ( - 'rev-id', - { - 'type': 'dir', - 'revision': 'rev-id', - 'path': 'some/path', - 'content': [] - }) - # when - actual_dir_content = views._revision_directory_by( - {'sha1_git': 'blah-id'}, - 'some/path', '/api/1/revision/sha1/directory/') - - # then - self.assertEquals(actual_dir_content, { - 'type': 'dir', - 'revision': 'rev-id', - 'path': 'some/path', - 'content': [] - }) - - mock_service.lookup_directory_through_revision.assert_called_once_with( - {'sha1_git': 'blah-id'}, - 'some/path', limit=100, with_data=False) - - @patch('swh.web.api.views.service') - @istest - def revision_directory_by_type_file(self, mock_service): - # given - mock_service.lookup_directory_through_revision.return_value = ( - 'rev-id', - { - 'type': 'file', - 'revision': 'rev-id', - 'path': 'some/path', - 'content': {'blah': 'blah'} - }) - # when - actual_dir_content = views._revision_directory_by( - {'sha1_git': 'sha1'}, - 'some/path', - '/api/1/revision/origin/2/directory/', - limit=1000, with_data=True) - - # then - self.assertEquals(actual_dir_content, { - 'type': 'file', - 'revision': 'rev-id', - 'path': 'some/path', - 'content': {'blah': 'blah'} - }) - - mock_service.lookup_directory_through_revision.assert_called_once_with( - {'sha1_git': 'sha1'}, - 'some/path', limit=1000, with_data=True) - - @patch('swh.web.api.views._revision_directory_by') - @patch('swh.web.api.views.utils') - @istest - def api_directory_through_revision_origin_ko_not_found(self, - mock_utils, - mock_rev_dir): - mock_rev_dir.side_effect = NotFoundExc('not found') - mock_utils.parse_timestamp.return_value = '2012-10-20 00:00:00' - - rv = self.client.get('/api/1/revision' - '/origin/10' - '/branch/refs/remote/origin/dev' - '/ts/2012-10-20' - '/directory/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEqual(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'not found'}) - - mock_rev_dir.assert_called_once_with( - {'origin_id': '10', - 'branch_name': 'refs/remote/origin/dev', - 'ts': '2012-10-20 00:00:00'}, None, - '/api/1/revision' - '/origin/10' - '/branch/refs/remote/origin/dev' - '/ts/2012-10-20' - '/directory/', - with_data=False) - - @patch('swh.web.api.views._revision_directory_by') - @istest - def api_directory_through_revision_origin(self, - mock_revision_dir): - expected_res = [{ - 'id': '123' - }] - mock_revision_dir.return_value = expected_res - - rv = self.client.get('/api/1/revision/origin/3/directory/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEqual(rv.data, expected_res) - - mock_revision_dir.assert_called_once_with({ - 'origin_id': '3', - 'branch_name': 'refs/heads/master', - 'ts': None}, None, '/api/1/revision/origin/3/directory/', - with_data=False) - - @patch('swh.web.api.views.service') - @istest - def api_revision_log(self, mock_service): - # given - stub_revisions = [{ - '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': ['7834ef7e7c357ce2af928115c6c6a42b7e2a4345'], - 'type': 'tar', - 'synthetic': True, - }] - mock_service.lookup_revision_log.return_value = stub_revisions - - expected_revisions = [{ - 'id': '18d8be353ed3480476f032475e7c233eff7371d5', - 'url': '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5/', - 'history_url': '/api/1/revision/18d8be353ed3480476f032475e7c233ef' - 'f7371d5/log/', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'directory_url': '/api/1/directory/7834ef7e7c357ce2af928115c6c6a' - '42b7e2a44e6/', - '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': [{ - 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a4345', - 'url': '/api/1/revision/7834ef7e7c357ce2af928115c6c6a42b7e2a4345/', # noqa - }], - 'type': 'tar', - 'synthetic': True, - }] - - # when - rv = self.client.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6a42' - 'b7e2a44e6/log/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - - self.assertEquals(rv.data, expected_revisions) - self.assertFalse(rv.has_header('Link')) - - mock_service.lookup_revision_log.assert_called_once_with( - '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 11) - - @patch('swh.web.api.views.service') - @istest - def api_revision_log_with_next(self, mock_service): - # given - stub_revisions = [] - for i in range(27): - stub_revisions.append({'id': i}) - - mock_service.lookup_revision_log.return_value = stub_revisions[:26] - - expected_revisions = [x for x in stub_revisions if x['id'] < 25] - for e in expected_revisions: - e['url'] = '/api/1/revision/%s/' % e['id'] - e['history_url'] = '/api/1/revision/%s/log/' % e['id'] - - # when - rv = self.client.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6a42' - 'b7e2a44e6/log/?per_page=25') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_revisions) - self.assertEquals(rv['Link'], - '; rel="next"') - - mock_service.lookup_revision_log.assert_called_once_with( - '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 26) - - @patch('swh.web.api.views.service') - @istest - def api_revision_log_not_found(self, mock_service): - # given - mock_service.lookup_revision_log.return_value = None - - # when - rv = self.client.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6' - 'a42b7e2a44e6/log/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Revision with sha1_git' - ' 8834ef7e7c357ce2af928115c6c6a42b7e2a44e6 not found.'}) - self.assertFalse(rv.has_header('Link')) - - mock_service.lookup_revision_log.assert_called_once_with( - '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 11) - - @patch('swh.web.api.views.service') - @istest - def api_revision_log_context(self, mock_service): - # given - stub_revisions = [{ - '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': ['7834ef7e7c357ce2af928115c6c6a42b7e2a4345'], - 'type': 'tar', - 'synthetic': True, - }] - - mock_service.lookup_revision_log.return_value = stub_revisions - mock_service.lookup_revision_multiple.return_value = [{ - 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'directory': '18d8be353ed3480476f032475e7c233eff7371d5', - 'author_name': 'Name Surname', - 'author_email': 'name@surname.com', - 'committer_name': 'Name Surname', - 'committer_email': 'name@surname.com', - 'message': 'amazing revision message', - 'date_offset': 0, - 'committer_date_offset': 0, - 'parents': ['adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'], - 'type': 'tar', - 'synthetic': True, - }] - - expected_revisions = [ - { - 'url': '/api/1/revision/' - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6/', - 'history_url': '/api/1/revision/' - '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6/log/', - 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'directory': '18d8be353ed3480476f032475e7c233eff7371d5', - 'directory_url': '/api/1/directory/' - '18d8be353ed3480476f032475e7c233eff7371d5/', - 'author_name': 'Name Surname', - 'author_email': 'name@surname.com', - 'committer_name': 'Name Surname', - 'committer_email': 'name@surname.com', - 'message': 'amazing revision message', - 'date_offset': 0, - 'committer_date_offset': 0, - 'parents': [{ - 'id': 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', - 'url': '/api/1/revision/adc83b19e793491b1c6ea0fd8b46cd9f32e592fc/', # noqa - }], - 'type': 'tar', - 'synthetic': True, - }, - { - 'url': '/api/1/revision/' - '18d8be353ed3480476f032475e7c233eff7371d5/', - 'history_url': '/api/1/revision/' - '18d8be353ed3480476f032475e7c233eff7371d5/log/', - 'id': '18d8be353ed3480476f032475e7c233eff7371d5', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'directory_url': '/api/1/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': [{ - 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a4345', - 'url': '/api/1/revision/7834ef7e7c357ce2af928115c6c6a42b7e2a4345/', # noqa - }], - 'type': 'tar', - 'synthetic': True, - }] - - # when - rv = self.client.get('/api/1/revision/18d8be353ed3480476f0' - '32475e7c233eff7371d5/prev/21145781e2' - '6ad1f978e/log/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(expected_revisions, rv.data) - self.assertFalse(rv.has_header('Link')) - - mock_service.lookup_revision_log.assert_called_once_with( - '18d8be353ed3480476f032475e7c233eff7371d5', 11) - mock_service.lookup_revision_multiple.assert_called_once_with( - ['21145781e26ad1f978e']) - - @patch('swh.web.api.views.service') - @istest - def api_revision_log_by(self, mock_service): - # given - stub_revisions = [{ - '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': ['7834ef7e7c357ce2af928115c6c6a42b7e2a4345'], - 'type': 'tar', - 'synthetic': True, - }] - mock_service.lookup_revision_log_by.return_value = stub_revisions - - expected_revisions = [{ - 'id': '18d8be353ed3480476f032475e7c233eff7371d5', - 'url': '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5/', - 'history_url': '/api/1/revision/18d8be353ed3480476f032475e7c233ef' - 'f7371d5/log/', - 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', - 'directory_url': '/api/1/directory/7834ef7e7c357ce2af928115c6c6a' - '42b7e2a44e6/', - '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': [{ - 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a4345', - 'url': '/api/1/revision/7834ef7e7c357ce2af928115c6c6a42b7e2a4345/' # noqa - }], - 'type': 'tar', - 'synthetic': True, - }] - - # when - rv = self.client.get('/api/1/revision/origin/1/log/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_revisions) - self.assertFalse(rv.has_header('Link')) - - mock_service.lookup_revision_log_by.assert_called_once_with( - '1', 'refs/heads/master', None, 11) - - @patch('swh.web.api.views.service') - @istest - def api_revision_log_by_with_next(self, mock_service): - # given - stub_revisions = [] - for i in range(27): - stub_revisions.append({'id': i}) - - mock_service.lookup_revision_log_by.return_value = stub_revisions[:26] - - expected_revisions = [x for x in stub_revisions if x['id'] < 25] - for e in expected_revisions: - e['url'] = '/api/1/revision/%s/' % e['id'] - e['history_url'] = '/api/1/revision/%s/log/' % e['id'] - - # when - rv = self.client.get('/api/1/revision/origin/1/log/?per_page=25') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertIsNotNone(rv['Link']) - self.assertEquals(rv.data, expected_revisions) - - mock_service.lookup_revision_log_by.assert_called_once_with( - '1', 'refs/heads/master', None, 26) - - @patch('swh.web.api.views.service') - @istest - def api_revision_log_by_norev(self, mock_service): - # given - mock_service.lookup_revision_log_by.side_effect = NotFoundExc( - 'No revision') - - # when - rv = self.client.get('/api/1/revision/origin/1/log/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertFalse(rv.has_header('Link')) - self.assertEquals(rv.data, {'exception': 'NotFoundExc', - 'reason': 'No revision'}) - - mock_service.lookup_revision_log_by.assert_called_once_with( - '1', 'refs/heads/master', None, 11) - - @patch('swh.web.api.views.service') - @istest - def api_revision_history(self, mock_service): - # for readability purposes, we use: - # - sha1 as 3 letters (url are way too long otherwise to respect pep8) - # - only keys with modification steps (all other keys are kept as is) - - # given - stub_revision = { - 'id': '883', - 'children': ['777', '999'], - 'parents': [], - 'directory': '272' - } - - mock_service.lookup_revision.return_value = stub_revision - - # then - rv = self.client.get('/api/1/revision/883/prev/999/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'id': '883', - 'url': '/api/1/revision/883/', - 'history_url': '/api/1/revision/883/log/', - 'history_context_url': '/api/1/revision/883/prev/999/log/', - 'children': ['777', '999'], - 'children_urls': ['/api/1/revision/777/', - '/api/1/revision/999/'], - 'parents': [], - 'directory': '272', - 'directory_url': '/api/1/directory/272/' - }) - - mock_service.lookup_revision.assert_called_once_with('883') - - @patch('swh.web.api.views._revision_directory_by') - @istest - def api_revision_directory_ko_not_found(self, mock_rev_dir): - # given - mock_rev_dir.side_effect = NotFoundExc('Not found') - - # then - rv = self.client.get('/api/1/revision/999/directory/some/path/to/dir/') - - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Not found'}) - - mock_rev_dir.assert_called_once_with( - {'sha1_git': '999'}, - 'some/path/to/dir', - '/api/1/revision/999/directory/some/path/to/dir/', - with_data=False) - - @patch('swh.web.api.views._revision_directory_by') - @istest - def api_revision_directory_ok_returns_dir_entries(self, mock_rev_dir): - stub_dir = { - 'type': 'dir', - 'revision': '999', - 'content': [ - { - 'sha1_git': '789', - 'type': 'file', - 'target': '101', - 'target_url': '/api/1/content/sha1_git:101/', - 'name': 'somefile', - 'file_url': '/api/1/revision/999/directory/some/path/' - 'somefile/' - }, - { - 'sha1_git': '123', - 'type': 'dir', - 'target': '456', - 'target_url': '/api/1/directory/456/', - 'name': 'to-subdir', - 'dir_url': '/api/1/revision/999/directory/some/path/' - 'to-subdir/', - }] - } - - # given - mock_rev_dir.return_value = stub_dir - - # then - rv = self.client.get('/api/1/revision/999/directory/some/path/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, stub_dir) - - mock_rev_dir.assert_called_once_with( - {'sha1_git': '999'}, - 'some/path', - '/api/1/revision/999/directory/some/path/', - with_data=False) - - @patch('swh.web.api.views._revision_directory_by') - @istest - def api_revision_directory_ok_returns_content(self, mock_rev_dir): - stub_content = { - 'type': 'file', - 'revision': '999', - 'content': { - 'sha1_git': '789', - 'sha1': '101', - 'data_url': '/api/1/content/101/raw/', - } - } - - # given - mock_rev_dir.return_value = stub_content - - # then - url = '/api/1/revision/666/directory/some/other/path/' - rv = self.client.get(url) - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, stub_content) - - mock_rev_dir.assert_called_once_with( - {'sha1_git': '666'}, 'some/other/path', url, with_data=False) - - @patch('swh.web.api.views.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.client.get('/api/1/person/198003/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, stub_person) - - @patch('swh.web.api.views.service') - @istest - def api_person_not_found(self, mock_service): - # given - mock_service.lookup_person.return_value = None - - # when - rv = self.client.get('/api/1/person/666/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Person with id 666 not found.'}) - - @patch('swh.web.api.views.service') - @istest - def api_directory(self, mock_service): - # given - stub_directories = [ - { - 'sha1_git': '18d8be353ed3480476f032475e7c233eff7371d5', - 'type': 'file', - 'target': '4568be353ed3480476f032475e7c233eff737123', - }, - { - 'sha1_git': '1d518d8be353ed3480476f032475e7c233eff737', - 'type': 'dir', - 'target': '8be353ed3480476f032475e7c233eff737123456', - }] - - expected_directories = [ - { - 'sha1_git': '18d8be353ed3480476f032475e7c233eff7371d5', - 'type': 'file', - 'target': '4568be353ed3480476f032475e7c233eff737123', - 'target_url': '/api/1/content/' - 'sha1_git:4568be353ed3480476f032475e7c233eff737123/', - }, - { - 'sha1_git': '1d518d8be353ed3480476f032475e7c233eff737', - 'type': 'dir', - 'target': '8be353ed3480476f032475e7c233eff737123456', - 'target_url': - '/api/1/directory/8be353ed3480476f032475e7c233eff737123456/', - }] - - mock_service.lookup_directory.return_value = stub_directories - - # when - rv = self.client.get('/api/1/directory/' - '18d8be353ed3480476f032475e7c233eff7371d5/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_directories) - - mock_service.lookup_directory.assert_called_once_with( - '18d8be353ed3480476f032475e7c233eff7371d5') - - @patch('swh.web.api.views.service') - @istest - def api_directory_not_found(self, mock_service): - # given - mock_service.lookup_directory.return_value = [] - - # when - rv = self.client.get('/api/1/directory/' - '66618d8be353ed3480476f032475e7c233eff737/') - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': 'Directory with sha1_git ' - '66618d8be353ed3480476f032475e7c233eff737 not found.'}) - - @patch('swh.web.api.views.service') - @istest - def api_directory_with_path_found(self, mock_service): - # given - expected_dir = { - 'sha1_git': '18d8be353ed3480476f032475e7c233eff7371d5', - 'type': 'file', - 'name': 'bla', - 'target': '4568be353ed3480476f032475e7c233eff737123', - 'target_url': '/api/1/content/' - 'sha1_git:4568be353ed3480476f032475e7c233eff737123/', - } - - mock_service.lookup_directory_with_path.return_value = expected_dir - - # when - rv = self.client.get('/api/1/directory/' - '18d8be353ed3480476f032475e7c233eff7371d5/bla/') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_dir) - - mock_service.lookup_directory_with_path.assert_called_once_with( - '18d8be353ed3480476f032475e7c233eff7371d5', 'bla') - - @patch('swh.web.api.views.service') - @istest - def api_directory_with_path_not_found(self, mock_service): - # given - mock_service.lookup_directory_with_path.return_value = None - path = 'some/path/to/dir/' - - # when - rv = self.client.get(('/api/1/directory/' - '66618d8be353ed3480476f032475e7c233eff737/%s') - % path) - path = path.strip('/') # Path stripped of lead/trail separators - - # then - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': (('Entry with path %s relative to ' - 'directory with sha1_git ' - '66618d8be353ed3480476f032475e7c233eff737 not found.') - % path)}) - - @patch('swh.web.api.views.service') - @istest - def api_lookup_entity_by_uuid_not_found(self, mock_service): - # when - mock_service.lookup_entity_by_uuid.return_value = [] - - # when - rv = self.client.get('/api/1/entity/' - '5f4d4c51-498a-4e28-88b3-b3e4e8396cba/') - - self.assertEquals(rv.status_code, 404) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'NotFoundExc', - 'reason': - "Entity with uuid '5f4d4c51-498a-4e28-88b3-b3e4e8396cba' not " + - "found."}) - - mock_service.lookup_entity_by_uuid.assert_called_once_with( - '5f4d4c51-498a-4e28-88b3-b3e4e8396cba') - - @patch('swh.web.api.views.service') - @istest - def api_lookup_entity_by_uuid_bad_request(self, mock_service): - # when - mock_service.lookup_entity_by_uuid.side_effect = BadInputExc( - 'bad input: uuid malformed!') - - # when - rv = self.client.get('/api/1/entity/uuid malformed/') - - self.assertEquals(rv.status_code, 400) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, { - 'exception': 'BadInputExc', - 'reason': 'bad input: uuid malformed!'}) - mock_service.lookup_entity_by_uuid.assert_called_once_with( - 'uuid malformed') - - @patch('swh.web.api.views.service') - @istest - def api_lookup_entity_by_uuid(self, mock_service): - # when - stub_entities = [ - { - 'uuid': '34bd6b1b-463f-43e5-a697-785107f598e4', - 'parent': 'aee991a0-f8d7-4295-a201-d1ce2efc9fb2' - }, - { - 'uuid': 'aee991a0-f8d7-4295-a201-d1ce2efc9fb2' - } - ] - mock_service.lookup_entity_by_uuid.return_value = stub_entities - - expected_entities = [ - { - 'uuid': '34bd6b1b-463f-43e5-a697-785107f598e4', - 'uuid_url': '/api/1/entity/34bd6b1b-463f-43e5-a697-' - '785107f598e4/', - 'parent': 'aee991a0-f8d7-4295-a201-d1ce2efc9fb2', - 'parent_url': '/api/1/entity/aee991a0-f8d7-4295-a201-' - 'd1ce2efc9fb2/' - }, - { - 'uuid': 'aee991a0-f8d7-4295-a201-d1ce2efc9fb2', - 'uuid_url': '/api/1/entity/aee991a0-f8d7-4295-a201-' - 'd1ce2efc9fb2/' - } - ] - - # when - rv = self.client.get('/api/1/entity' - '/34bd6b1b-463f-43e5-a697-785107f598e4/') - - self.assertEquals(rv.status_code, 200) - self.assertEquals(rv['Content-Type'], 'application/json') - self.assertEquals(rv.data, expected_entities) - mock_service.lookup_entity_by_uuid.assert_called_once_with( - '34bd6b1b-463f-43e5-a697-785107f598e4') - - -class ApiUtils(unittest.TestCase): - - @istest - def api_lookup_not_found(self): - # when - with self.assertRaises(NotFoundExc) as e: - views._api_lookup( - lambda x: None, 'something', - notfound_msg='this is the error message raised as it is None') - - self.assertEqual(e.exception.args[0], - 'this is the error message raised as it is None') - - @istest - def api_lookup_with_result(self): - # when - actual_result = views._api_lookup( - lambda x: x + '!', 'something', - notfound_msg='this is the error which won\'t be used here') - - self.assertEqual(actual_result, 'something!') - - @istest - def api_lookup_with_result_as_map(self): - # when - actual_result = views._api_lookup( - lambda x: map(lambda y: y+1, x), [1, 2, 3], - notfound_msg='this is the error which won\'t be used here') - - self.assertEqual(actual_result, [2, 3, 4]) diff --git a/swh/web/api/tests/views/__init__.py b/swh/web/api/tests/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/swh/web/api/tests/views/test_api_lookup.py b/swh/web/api/tests/views/test_api_lookup.py new file mode 100644 index 000000000..09e6c26f5 --- /dev/null +++ b/swh/web/api/tests/views/test_api_lookup.py @@ -0,0 +1,126 @@ +# Copyright (C) 2015-2017 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_api_testcase import SWHApiTestCase +from swh.web.api.exc import NotFoundExc +from swh.web.api import views + + +class ApiLookupTestCase(SWHApiTestCase): + + @istest + def generic_api_lookup_nothing_is_found(self): + # given + def test_generic_lookup_fn(sha1, another_unused_arg): + assert another_unused_arg == 'unused_arg' + assert sha1 == 'sha1' + return None + + # when + with self.assertRaises(NotFoundExc) as cm: + views._api_lookup( + test_generic_lookup_fn, 'sha1', 'unused_arg', + notfound_msg='This will be raised because None is returned.') + + self.assertIn('This will be raised because None is returned.', + cm.exception.args[0]) + + @istest + def generic_api_map_are_enriched_and_transformed_to_list(self): + # given + def test_generic_lookup_fn_1(criteria0, param0, param1): + assert criteria0 == 'something' + return map(lambda x: x + 1, [1, 2, 3]) + + # when + actual_result = views._api_lookup( + test_generic_lookup_fn_1, 'something', 'some param 0', + 'some param 1', + notfound_msg=('This is not the error message you are looking for. ' + 'Move along.'), + enrich_fn=lambda x: x * 2) + + self.assertEqual(actual_result, [4, 6, 8]) + + @istest + def generic_api_list_are_enriched_too(self): + # given + def test_generic_lookup_fn_2(crit): + assert crit == 'something' + return ['a', 'b', 'c'] + + # when + actual_result = views._api_lookup( + test_generic_lookup_fn_2, 'something', + notfound_msg=('Not the error message you are looking for, it is. ' + 'Along, you move!'), + enrich_fn=lambda x: ''. join(['=', x, '='])) + + self.assertEqual(actual_result, ['=a=', '=b=', '=c=']) + + @istest + def generic_api_generator_are_enriched_and_returned_as_list(self): + # given + def test_generic_lookup_fn_3(crit): + assert crit == 'crit' + return (i for i in [4, 5, 6]) + + # when + actual_result = views._api_lookup( + test_generic_lookup_fn_3, 'crit', + notfound_msg='Move!', + enrich_fn=lambda x: x - 1) + + self.assertEqual(actual_result, [3, 4, 5]) + + @istest + def generic_api_simple_data_are_enriched_and_returned_too(self): + # given + def test_generic_lookup_fn_4(crit): + assert crit == '123' + return {'a': 10} + + def test_enrich_data(x): + x['a'] = x['a'] * 10 + return x + + # when + actual_result = views._api_lookup( + test_generic_lookup_fn_4, '123', + notfound_msg='Nothing to do', + enrich_fn=test_enrich_data) + + self.assertEqual(actual_result, {'a': 100}) + + @istest + def api_lookup_not_found(self): + # when + with self.assertRaises(NotFoundExc) as e: + views._api_lookup( + lambda x: None, 'something', + notfound_msg='this is the error message raised as it is None') + + self.assertEqual(e.exception.args[0], + 'this is the error message raised as it is None') + + @istest + def api_lookup_with_result(self): + # when + actual_result = views._api_lookup( + lambda x: x + '!', 'something', + notfound_msg='this is the error which won\'t be used here') + + self.assertEqual(actual_result, 'something!') + + @istest + def api_lookup_with_result_as_map(self): + # when + actual_result = views._api_lookup( + lambda x: map(lambda y: y+1, x), [1, 2, 3], + notfound_msg='this is the error which won\'t be used here') + + self.assertEqual(actual_result, [2, 3, 4]) diff --git a/swh/web/api/tests/views/test_content.py b/swh/web/api/tests/views/test_content.py new file mode 100644 index 000000000..518ee8136 --- /dev/null +++ b/swh/web/api/tests/views/test_content.py @@ -0,0 +1,722 @@ +# Copyright (C) 2015-2017 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 unittest.mock import patch, MagicMock + +from ..swh_api_testcase import SWHApiTestCase + + +class ContentApiTestCase(SWHApiTestCase): + @patch('swh.web.api.views.content.service') + @istest + def api_content_filetype(self, mock_service): + stub_filetype = { + 'accepted_media_type': 'application/xml', + 'encoding': 'ascii', + 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + } + mock_service.lookup_content_filetype.return_value = stub_filetype + + # when + rv = self.client.get( + '/api/1/content/' + 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f/filetype/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'accepted_media_type': 'application/xml', + 'encoding': 'ascii', + 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'content_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', + }) + + mock_service.lookup_content_filetype.assert_called_once_with( + 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_filetype_sha_not_found(self, mock_service): + # given + mock_service.lookup_content_filetype.return_value = None + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/' + 'filetype/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'No filetype information found for content ' + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03.' + }) + + mock_service.lookup_content_filetype.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_language(self, mock_service): + stub_language = { + 'lang': 'lisp', + 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + } + mock_service.lookup_content_language.return_value = stub_language + + # when + rv = self.client.get( + '/api/1/content/' + 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f/language/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'lang': 'lisp', + 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'content_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', + }) + + mock_service.lookup_content_language.assert_called_once_with( + 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_language_sha_not_found(self, mock_service): + # given + mock_service.lookup_content_language.return_value = None + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' + '/language/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'No language information found for content ' + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03.' + }) + + mock_service.lookup_content_language.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_symbol(self, mock_service): + stub_ctag = [{ + 'sha1': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'name': 'foobar', + 'kind': 'Haskell', + 'line': 10, + }] + mock_service.lookup_expression.return_value = stub_ctag + + # when + rv = self.client.get('/api/1/content/symbol/foo/?last_sha1=sha1') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, [{ + 'sha1': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'name': 'foobar', + 'kind': 'Haskell', + 'line': 10, + 'content_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', + 'data_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/raw/', + 'license_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/license/', + 'language_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/language/', + 'filetype_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/filetype/', + }]) + self.assertFalse('Link' in rv) + + mock_service.lookup_expression.assert_called_once_with( + 'foo', 'sha1', 10) + + @patch('swh.web.api.views.content.service') + @istest + def api_content_symbol_2(self, mock_service): + stub_ctag = [{ + 'sha1': '12371b8614fcd89ccd17ca2b1d9e66c5b00a6456', + 'name': 'foobar', + 'kind': 'Haskell', + 'line': 10, + }, { + 'sha1': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6678', + 'name': 'foo', + 'kind': 'Lisp', + 'line': 10, + }] + mock_service.lookup_expression.return_value = stub_ctag + + # when + rv = self.client.get( + '/api/1/content/symbol/foo/?last_sha1=prev-sha1&per_page=2') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, stub_ctag) + self.assertTrue( + rv['Link'] == '; rel="next"' or # noqa + rv['Link'] == '; rel="next"' # noqa + ) + mock_service.lookup_expression.assert_called_once_with( + 'foo', 'prev-sha1', 2) + + @patch('swh.web.api.views.content.service') + # @istest + def api_content_symbol_3(self, mock_service): + stub_ctag = [{ + 'sha1': '67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'name': 'foo', + 'kind': 'variable', + 'line': 100, + }] + mock_service.lookup_expression.return_value = stub_ctag + + # when + rv = self.client.get('/api/1/content/symbol/foo/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, [{ + 'sha1': '67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'name': 'foo', + 'kind': 'variable', + 'line': 100, + 'content_url': '/api/1/content/' + 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', + 'data_url': '/api/1/content/' + 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/raw/', + 'license_url': '/api/1/content/' + 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/license/', + 'language_url': '/api/1/content/' + 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/language/', + 'filetype_url': '/api/1/content/' + 'sha1:67891b8614fcd89ccd17ca2b1d9e66c5b00a6d03/filetype/', + }]) + self.assertFalse(rv.has_header('Link')) + + mock_service.lookup_expression.assert_called_once_with('foo', None, 10) + + @patch('swh.web.api.views.content.service') + @istest + def api_content_symbol_not_found(self, mock_service): + # given + mock_service.lookup_expression.return_value = [] + + # when + rv = self.client.get('/api/1/content/symbol/bar/?last_sha1=hash') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'No indexed raw content match expression \'bar\'.' + }) + self.assertFalse('Link' in rv) + + mock_service.lookup_expression.assert_called_once_with( + 'bar', 'hash', 10) + + @patch('swh.web.api.views.content.service') + @istest + def api_content_ctags(self, mock_service): + stub_ctags = { + 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'ctags': [] + } + mock_service.lookup_content_ctags.return_value = stub_ctags + + # when + rv = self.client.get( + '/api/1/content/' + 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f/ctags/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'ctags': [], + 'content_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', + }) + + mock_service.lookup_content_ctags.assert_called_once_with( + 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_license(self, mock_service): + stub_license = { + 'licenses': ['No_license_found', 'Apache-2.0'], + 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'tool_name': 'nomos', + } + mock_service.lookup_content_license.return_value = stub_license + + # when + rv = self.client.get( + '/api/1/content/' + 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f/license/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'licenses': ['No_license_found', 'Apache-2.0'], + 'id': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'tool_name': 'nomos', + 'content_url': '/api/1/content/' + 'sha1:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', + }) + + mock_service.lookup_content_license.assert_called_once_with( + 'sha1_git:b04caf10e9535160d90e874b45aa426de762f19f') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_license_sha_not_found(self, mock_service): + # given + mock_service.lookup_content_license.return_value = None + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/' + 'license/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'No license information found for content ' + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03.' + }) + + mock_service.lookup_content_license.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_provenance(self, mock_service): + stub_provenances = [{ + 'origin': 1, + 'visit': 2, + 'revision': 'b04caf10e9535160d90e874b45aa426de762f19f', + 'content': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'path': 'octavio-3.4.0/octave.html/doc_002dS_005fISREG.html' + }] + mock_service.lookup_content_provenance.return_value = stub_provenances + + # when + rv = self.client.get( + '/api/1/content/' + 'sha1_git:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/provenance/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, [{ + 'origin': 1, + 'visit': 2, + 'origin_url': '/api/1/origin/1/', + 'origin_visits_url': '/api/1/origin/1/visits/', + 'origin_visit_url': '/api/1/origin/1/visit/2/', + 'revision': 'b04caf10e9535160d90e874b45aa426de762f19f', + 'revision_url': '/api/1/revision/' + 'b04caf10e9535160d90e874b45aa426de762f19f/', + 'content': '34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'content_url': '/api/1/content/' + 'sha1_git:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03/', + 'path': 'octavio-3.4.0/octave.html/doc_002dS_005fISREG.html' + }]) + + mock_service.lookup_content_provenance.assert_called_once_with( + 'sha1_git:34571b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_provenance_sha_not_found(self, mock_service): + # given + mock_service.lookup_content_provenance.return_value = None + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/' + 'provenance/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Content with sha1:40e71b8614fcd89ccd17ca2b1d9e6' + '6c5b00a6d03 not found.' + }) + + mock_service.lookup_content_provenance.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_metadata(self, mock_service): + # given + mock_service.lookup_content.return_value = { + 'sha1': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'sha1_git': 'b4e8f472ffcb01a03875b26e462eb568739f6882', + 'sha256': '83c0e67cc80f60caf1fcbec2d84b0ccd7968b3be4735637006560' + 'cde9b067a4f', + 'length': 17, + 'status': 'visible' + } + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'data_url': '/api/1/content/' + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/raw/', + 'filetype_url': '/api/1/content/' + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/filetype/', + 'language_url': '/api/1/content/' + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/language/', + 'license_url': '/api/1/content/' + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03/license/', + 'sha1': '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03', + 'sha1_git': 'b4e8f472ffcb01a03875b26e462eb568739f6882', + 'sha256': '83c0e67cc80f60caf1fcbec2d84b0ccd7968b3be4735637006560c' + 'de9b067a4f', + 'length': 17, + 'status': 'visible' + }) + + mock_service.lookup_content.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_not_found_as_json(self, mock_service): + # given + mock_service.lookup_content.return_value = None + mock_service.lookup_content_provenance = MagicMock() + + # when + rv = self.client.get( + '/api/1/content/sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' + 'be4735637006560c/') + + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Content with sha256:83c0e67cc80f60caf1fcbec2d84b0ccd79' + '68b3be4735637006560c not found.' + }) + + mock_service.lookup_content.assert_called_once_with( + 'sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' + 'be4735637006560c') + mock_service.lookup_content_provenance.called = False + + @patch('swh.web.api.views.content.service') + @istest + def api_content_not_found_as_yaml(self, mock_service): + # given + mock_service.lookup_content.return_value = None + mock_service.lookup_content_provenance = MagicMock() + + # when + rv = self.client.get( + '/api/1/content/sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' + 'be4735637006560c/', + HTTP_ACCEPT='application/yaml') + + self.assertEquals(rv.status_code, 404) + self.assertTrue('application/yaml' in rv['Content-Type']) + + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Content with sha256:83c0e67cc80f60caf1fcbec2d84b0ccd79' + '68b3be4735637006560c not found.' + }) + + mock_service.lookup_content.assert_called_once_with( + 'sha256:83c0e67cc80f60caf1fcbec2d84b0ccd7968b3' + 'be4735637006560c') + mock_service.lookup_content_provenance.called = False + + @patch('swh.web.api.views.content.service') + @istest + def api_content_raw_ko_not_found(self, mock_service): + # given + mock_service.lookup_content_raw.return_value = None + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' + '/raw/') + + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Content sha1:40e71b8614fcd89ccd17ca2b1d9e6' + '6c5b00a6d03 is not found.' + }) + + mock_service.lookup_content_raw.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_raw_text(self, mock_service): + # given + stub_content = {'data': b'some content data'} + mock_service.lookup_content_raw.return_value = stub_content + mock_service.lookup_content_filetype.return_value = { + 'mimetype': 'text/html' + } + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' + '/raw/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/octet-stream') + self.assertEquals( + rv['Content-disposition'], + 'attachment; filename=content_sha1_' + '40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03_raw') + self.assertEquals( + rv['Content-Type'], 'application/octet-stream') + self.assertEquals(rv.content, stub_content['data']) + + mock_service.lookup_content_raw.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + mock_service.lookup_content_filetype.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_raw_text_with_filename(self, mock_service): + # given + stub_content = {'data': b'some content data'} + mock_service.lookup_content_raw.return_value = stub_content + mock_service.lookup_content_filetype.return_value = { + 'mimetype': 'text/html' + } + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' + '/raw/?filename=filename.txt') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/octet-stream') + self.assertEquals( + rv['Content-disposition'], + 'attachment; filename=filename.txt') + self.assertEquals( + rv['Content-Type'], 'application/octet-stream') + self.assertEquals(rv.content, stub_content['data']) + + mock_service.lookup_content_raw.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + mock_service.lookup_content_filetype.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_raw_no_accepted_media_type_text_is_not_available_for_download( # noqa + self, mock_service): + # given + stub_content = {'data': b'some content data'} + mock_service.lookup_content_raw.return_value = stub_content + mock_service.lookup_content_filetype.return_value = { + 'mimetype': 'application/octet-stream' + } + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' + '/raw/') + + self.assertEquals(rv.status_code, 403) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'ForbiddenExc', + 'reason': 'Only textual content is available for download. ' + 'Actual content mimetype is application/octet-stream.' + }) + + mock_service.lookup_content_raw.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + mock_service.lookup_content_filetype.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_content_raw_no_accepted_media_type_found_so_not_available_for_download( # noqa + self, mock_service): + # given + stub_content = {'data': b'some content data'} + mock_service.lookup_content_raw.return_value = stub_content + mock_service.lookup_content_filetype.return_value = None + + # when + rv = self.client.get( + '/api/1/content/sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03' + '/raw/') + + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Content sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03 ' + 'is not available for download.' + }) + + mock_service.lookup_content_raw.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + mock_service.lookup_content_filetype.assert_called_once_with( + 'sha1:40e71b8614fcd89ccd17ca2b1d9e66c5b00a6d03') + + @patch('swh.web.api.views.content.service') + @istest + def api_check_content_known(self, mock_service): + # given + mock_service.lookup_multiple_hashes.return_value = [ + {'found': True, + 'filename': None, + 'sha1': 'sha1:blah'} + ] + + expected_result = { + 'search_stats': {'nbfiles': 1, 'pct': 100}, + 'search_res': [{'sha1': 'sha1:blah', + 'found': True}] + } + + # when + rv = self.client.get('/api/1/content/known/sha1:blah/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_result) + mock_service.lookup_multiple_hashes.assert_called_once_with( + [{'filename': None, 'sha1': 'sha1:blah'}]) + + @patch('swh.web.api.views.content.service') + @istest + def api_check_content_known_as_yaml(self, mock_service): + # given + mock_service.lookup_multiple_hashes.return_value = [ + {'found': True, + 'filename': None, + 'sha1': 'sha1:halb'}, + {'found': False, + 'filename': None, + 'sha1': 'sha1_git:hello'} + ] + + expected_result = { + 'search_stats': {'nbfiles': 2, 'pct': 50}, + 'search_res': [{'sha1': 'sha1:halb', + 'found': True}, + {'sha1': 'sha1_git:hello', + 'found': False}] + } + + # when + rv = self.client.get('/api/1/content/known/sha1:halb,sha1_git:hello/', + HTTP_ACCEPT='application/yaml') + + self.assertEquals(rv.status_code, 200) + self.assertTrue('application/yaml' in rv['Content-Type']) + self.assertEquals(rv.data, expected_result) + + mock_service.lookup_multiple_hashes.assert_called_once_with( + [{'filename': None, 'sha1': 'sha1:halb'}, + {'filename': None, 'sha1': 'sha1_git:hello'}]) + + @patch('swh.web.api.views.content.service') + @istest + def api_check_content_known_post_as_yaml(self, mock_service): + # given + stub_result = [{'sha1': '7e62b1fe10c88a3eddbba930b156bee2956b2435', + 'found': True}, + {'filename': 'filepath', + 'sha1': '8e62b1fe10c88a3eddbba930b156bee2956b2435', + 'found': True}, + {'filename': 'filename', + 'sha1': '64025b5d1520c615061842a6ce6a456cad962a3f', + 'found': False}] + mock_service.lookup_multiple_hashes.return_value = stub_result + + expected_result = { + 'search_stats': {'nbfiles': 3, 'pct': 2/3 * 100}, + 'search_res': stub_result + } + + # when + rv = self.client.post( + '/api/1/content/known/search/', + data=dict( + q='7e62b1fe10c88a3eddbba930b156bee2956b2435', + filepath='8e62b1fe10c88a3eddbba930b156bee2956b2435', + filename='64025b5d1520c615061842a6ce6a456cad962a3f'), + HTTP_ACCEPT='application/yaml' + ) + + self.assertEquals(rv.status_code, 200) + self.assertTrue('application/yaml' in rv['Content-Type']) + self.assertEquals(rv.data, expected_result) + + @patch('swh.web.api.views.content.service') + @istest + def api_check_content_known_not_found(self, mock_service): + # given + stub_result = [{'sha1': 'sha1:halb', + 'found': False}] + mock_service.lookup_multiple_hashes.return_value = stub_result + + expected_result = { + 'search_stats': {'nbfiles': 1, 'pct': 0.0}, + 'search_res': stub_result + } + + # when + rv = self.client.get('/api/1/content/known/sha1:halb/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_result) + + mock_service.lookup_multiple_hashes.assert_called_once_with( + [{'filename': None, 'sha1': 'sha1:halb'}]) diff --git a/swh/web/api/tests/views/test_directory.py b/swh/web/api/tests/views/test_directory.py new file mode 100644 index 000000000..51ab49fd2 --- /dev/null +++ b/swh/web/api/tests/views/test_directory.py @@ -0,0 +1,126 @@ +# Copyright (C) 2015-2017 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 unittest.mock import patch + +from ..swh_api_testcase import SWHApiTestCase + + +class DirectoryApiTestCase(SWHApiTestCase): + + @patch('swh.web.api.views.directory.service') + @istest + def api_directory(self, mock_service): + # given + stub_directories = [ + { + 'sha1_git': '18d8be353ed3480476f032475e7c233eff7371d5', + 'type': 'file', + 'target': '4568be353ed3480476f032475e7c233eff737123', + }, + { + 'sha1_git': '1d518d8be353ed3480476f032475e7c233eff737', + 'type': 'dir', + 'target': '8be353ed3480476f032475e7c233eff737123456', + }] + + expected_directories = [ + { + 'sha1_git': '18d8be353ed3480476f032475e7c233eff7371d5', + 'type': 'file', + 'target': '4568be353ed3480476f032475e7c233eff737123', + 'target_url': '/api/1/content/' + 'sha1_git:4568be353ed3480476f032475e7c233eff737123/', + }, + { + 'sha1_git': '1d518d8be353ed3480476f032475e7c233eff737', + 'type': 'dir', + 'target': '8be353ed3480476f032475e7c233eff737123456', + 'target_url': + '/api/1/directory/8be353ed3480476f032475e7c233eff737123456/', + }] + + mock_service.lookup_directory.return_value = stub_directories + + # when + rv = self.client.get('/api/1/directory/' + '18d8be353ed3480476f032475e7c233eff7371d5/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_directories) + + mock_service.lookup_directory.assert_called_once_with( + '18d8be353ed3480476f032475e7c233eff7371d5') + + @patch('swh.web.api.views.directory.service') + @istest + def api_directory_not_found(self, mock_service): + # given + mock_service.lookup_directory.return_value = [] + + # when + rv = self.client.get('/api/1/directory/' + '66618d8be353ed3480476f032475e7c233eff737/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Directory with sha1_git ' + '66618d8be353ed3480476f032475e7c233eff737 not found.'}) + + @patch('swh.web.api.views.directory.service') + @istest + def api_directory_with_path_found(self, mock_service): + # given + expected_dir = { + 'sha1_git': '18d8be353ed3480476f032475e7c233eff7371d5', + 'type': 'file', + 'name': 'bla', + 'target': '4568be353ed3480476f032475e7c233eff737123', + 'target_url': '/api/1/content/' + 'sha1_git:4568be353ed3480476f032475e7c233eff737123/', + } + + mock_service.lookup_directory_with_path.return_value = expected_dir + + # when + rv = self.client.get('/api/1/directory/' + '18d8be353ed3480476f032475e7c233eff7371d5/bla/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_dir) + + mock_service.lookup_directory_with_path.assert_called_once_with( + '18d8be353ed3480476f032475e7c233eff7371d5', 'bla') + + @patch('swh.web.api.views.directory.service') + @istest + def api_directory_with_path_not_found(self, mock_service): + # given + mock_service.lookup_directory_with_path.return_value = None + path = 'some/path/to/dir/' + + # when + rv = self.client.get(('/api/1/directory/' + '66618d8be353ed3480476f032475e7c233eff737/%s') + % path) + path = path.strip('/') # Path stripped of lead/trail separators + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': (('Entry with path %s relative to ' + 'directory with sha1_git ' + '66618d8be353ed3480476f032475e7c233eff737 not found.') + % path)}) diff --git a/swh/web/api/tests/views/test_entity.py b/swh/web/api/tests/views/test_entity.py new file mode 100644 index 000000000..ea8e25629 --- /dev/null +++ b/swh/web/api/tests/views/test_entity.py @@ -0,0 +1,94 @@ +# Copyright (C) 2015-2017 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 unittest.mock import patch + +from swh.web.api.exc import BadInputExc + +from ..swh_api_testcase import SWHApiTestCase + + +class EntityApiTestCase(SWHApiTestCase): + + @patch('swh.web.api.views.entity.service') + @istest + def api_lookup_entity_by_uuid_not_found(self, mock_service): + # when + mock_service.lookup_entity_by_uuid.return_value = [] + + # when + rv = self.client.get('/api/1/entity/' + '5f4d4c51-498a-4e28-88b3-b3e4e8396cba/') + + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': + "Entity with uuid '5f4d4c51-498a-4e28-88b3-b3e4e8396cba' not " + + "found."}) + + mock_service.lookup_entity_by_uuid.assert_called_once_with( + '5f4d4c51-498a-4e28-88b3-b3e4e8396cba') + + @patch('swh.web.api.views.entity.service') + @istest + def api_lookup_entity_by_uuid_bad_request(self, mock_service): + # when + mock_service.lookup_entity_by_uuid.side_effect = BadInputExc( + 'bad input: uuid malformed!') + + # when + rv = self.client.get('/api/1/entity/uuid malformed/') + + self.assertEquals(rv.status_code, 400) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'BadInputExc', + 'reason': 'bad input: uuid malformed!'}) + mock_service.lookup_entity_by_uuid.assert_called_once_with( + 'uuid malformed') + + @patch('swh.web.api.views.entity.service') + @istest + def api_lookup_entity_by_uuid(self, mock_service): + # when + stub_entities = [ + { + 'uuid': '34bd6b1b-463f-43e5-a697-785107f598e4', + 'parent': 'aee991a0-f8d7-4295-a201-d1ce2efc9fb2' + }, + { + 'uuid': 'aee991a0-f8d7-4295-a201-d1ce2efc9fb2' + } + ] + mock_service.lookup_entity_by_uuid.return_value = stub_entities + + expected_entities = [ + { + 'uuid': '34bd6b1b-463f-43e5-a697-785107f598e4', + 'uuid_url': '/api/1/entity/34bd6b1b-463f-43e5-a697-' + '785107f598e4/', + 'parent': 'aee991a0-f8d7-4295-a201-d1ce2efc9fb2', + 'parent_url': '/api/1/entity/aee991a0-f8d7-4295-a201-' + 'd1ce2efc9fb2/' + }, + { + 'uuid': 'aee991a0-f8d7-4295-a201-d1ce2efc9fb2', + 'uuid_url': '/api/1/entity/aee991a0-f8d7-4295-a201-' + 'd1ce2efc9fb2/' + } + ] + + # when + rv = self.client.get('/api/1/entity' + '/34bd6b1b-463f-43e5-a697-785107f598e4/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_entities) + mock_service.lookup_entity_by_uuid.assert_called_once_with( + '34bd6b1b-463f-43e5-a697-785107f598e4') diff --git a/swh/web/api/tests/views/test_origin.py b/swh/web/api/tests/views/test_origin.py new file mode 100644 index 000000000..f6878e2d7 --- /dev/null +++ b/swh/web/api/tests/views/test_origin.py @@ -0,0 +1,249 @@ +# Copyright (C) 2015-2017 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 unittest.mock import patch + +from swh.storage.exc import StorageDBError, StorageAPIError + +from ..swh_api_testcase import SWHApiTestCase + + +class OriginApiTestCase(SWHApiTestCase): + + def setUp(self): + self.origin_visit1 = { + 'date': 1104616800.0, + 'origin': 10, + 'visit': 100, + 'metadata': None, + 'status': 'full', + } + + self.origin1 = { + 'id': 1234, + 'lister': 'uuid-lister-0', + 'project': 'uuid-project-0', + 'url': 'ftp://some/url/to/origin/0', + 'type': 'ftp' + } + + @patch('swh.web.api.views.origin.service') + @istest + def api_1_lookup_origin_visits_raise_error(self, mock_service): + # given + mock_service.lookup_origin_visits.side_effect = ValueError( + 'voluntary error to check the bad request middleware.') + # when + rv = self.client.get('/api/1/origin/2/visits/') + # then + self.assertEquals(rv.status_code, 400) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'ValueError', + 'reason': 'voluntary error to check the bad request middleware.'}) + + @patch('swh.web.api.views.origin.service') + @istest + def api_1_lookup_origin_visits_raise_swh_storage_error_db( + self, mock_service): + # given + mock_service.lookup_origin_visits.side_effect = StorageDBError( + 'SWH Storage exploded! Will be back online shortly!') + # when + rv = self.client.get('/api/1/origin/2/visits/') + # then + self.assertEquals(rv.status_code, 503) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'StorageDBError', + 'reason': + 'An unexpected error occurred in the backend: ' + 'SWH Storage exploded! Will be back online shortly!'}) + + @patch('swh.web.api.views.origin.service') + @istest + def api_1_lookup_origin_visits_raise_swh_storage_error_api( + self, mock_service): + # given + mock_service.lookup_origin_visits.side_effect = StorageAPIError( + 'SWH Storage API dropped dead! Will resurrect from its ashes asap!' + ) + # when + rv = self.client.get('/api/1/origin/2/visits/') + # then + self.assertEquals(rv.status_code, 503) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'StorageAPIError', + 'reason': + 'An unexpected error occurred in the api backend: ' + 'SWH Storage API dropped dead! Will resurrect from its ashes asap!' + }) + + @patch('swh.web.api.views.origin.service') + @istest + def api_1_lookup_origin_visits(self, mock_service): + # given + stub_visits = [ + { + 'date': 1293919200.0, + 'origin': 2, + 'visit': 2 + }, + { + 'date': 1420149600.0, + 'origin': 2, + 'visit': 3 + } + ] + + mock_service.lookup_origin_visits.return_value = stub_visits + + # when + rv = self.client.get('/api/1/origin/2/visits/?per_page=2&last_visit=1') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, [ + { + 'date': 1293919200.0, + 'origin': 2, + 'visit': 2, + 'origin_visit_url': '/api/1/origin/2/visit/2/', + }, + { + 'date': 1420149600.0, + 'origin': 2, + 'visit': 3, + 'origin_visit_url': '/api/1/origin/2/visit/3/', + } + ]) + + mock_service.lookup_origin_visits.assert_called_once_with( + '2', last_visit=1, per_page=2) + + @patch('swh.web.api.views.origin.service') + @istest + def api_1_lookup_origin_visit(self, mock_service): + # given + origin_visit = self.origin_visit1.copy() + origin_visit.update({ + 'occurrences': { + 'master': { + 'target_type': 'revision', + 'target': '98564', + } + } + }) + + mock_service.lookup_origin_visit.return_value = origin_visit + + expected_origin_visit = self.origin_visit1.copy() + expected_origin_visit.update({ + 'origin_url': '/api/1/origin/10/', + 'occurrences': { + 'master': { + 'target_type': 'revision', + 'target': '98564', + 'target_url': '/api/1/revision/98564/' + } + } + }) + + # when + rv = self.client.get('/api/1/origin/10/visit/100/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_origin_visit) + + mock_service.lookup_origin_visit.assert_called_once_with('10', '100') + + @patch('swh.web.api.views.origin.service') + @istest + def api_1_lookup_origin_visit_not_found(self, mock_service): + # given + mock_service.lookup_origin_visit.return_value = None + + # when + rv = self.client.get('/api/1/origin/1/visit/1000/') + + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'No visit 1000 for origin 1 found' + }) + + mock_service.lookup_origin_visit.assert_called_once_with('1', '1000') + + @patch('swh.web.api.views.origin.service') + @istest + def api_origin_by_id(self, mock_service): + # given + mock_service.lookup_origin.return_value = self.origin1 + + expected_origin = self.origin1.copy() + expected_origin.update({ + 'origin_visits_url': '/api/1/origin/1234/visits/' + }) + + # when + rv = self.client.get('/api/1/origin/1234/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_origin) + + mock_service.lookup_origin.assert_called_with({'id': '1234'}) + + @patch('swh.web.api.views.origin.service') + @istest + def api_origin_by_type_url(self, mock_service): + # given + stub_origin = self.origin1.copy() + stub_origin.update({ + 'id': 987 + }) + mock_service.lookup_origin.return_value = stub_origin + + expected_origin = stub_origin.copy() + expected_origin.update({ + 'origin_visits_url': '/api/1/origin/987/visits/' + }) + + # when + rv = self.client.get('/api/1/origin/ftp/url' + '/ftp://some/url/to/origin/0/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_origin) + + mock_service.lookup_origin.assert_called_with( + {'url': 'ftp://some/url/to/origin/0/', + 'type': 'ftp'}) + + @patch('swh.web.api.views.origin.service') + @istest + def api_origin_not_found(self, mock_service): + # given + mock_service.lookup_origin.return_value = None + + # when + rv = self.client.get('/api/1/origin/4321/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Origin with id 4321 not found.' + }) + + mock_service.lookup_origin.assert_called_with({'id': '4321'}) diff --git a/swh/web/api/tests/views/test_person.py b/swh/web/api/tests/views/test_person.py new file mode 100644 index 000000000..38969820b --- /dev/null +++ b/swh/web/api/tests/views/test_person.py @@ -0,0 +1,47 @@ +# Copyright (C) 2015-2017 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 unittest.mock import patch + +from ..swh_api_testcase import SWHApiTestCase + + +class PersonApiTestCase(SWHApiTestCase): + + @patch('swh.web.api.views.person.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.client.get('/api/1/person/198003/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, stub_person) + + @patch('swh.web.api.views.person.service') + @istest + def api_person_not_found(self, mock_service): + # given + mock_service.lookup_person.return_value = None + + # when + rv = self.client.get('/api/1/person/666/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Person with id 666 not found.'}) diff --git a/swh/web/api/tests/views/test_release.py b/swh/web/api/tests/views/test_release.py new file mode 100644 index 000000000..8c74385c5 --- /dev/null +++ b/swh/web/api/tests/views/test_release.py @@ -0,0 +1,120 @@ +# Copyright (C) 2015-2017 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 unittest.mock import patch + +from ..swh_api_testcase import SWHApiTestCase + + +class ReleaseApiTestCase(SWHApiTestCase): + + @patch('swh.web.api.views.release.service') + @istest + def api_release(self, mock_service): + release_id = '7045404f3d1c54e6473' + target_id = '6072557b6c10cd9a211' + # given + stub_release = { + 'id': release_id, + 'target_type': 'revision', + 'target': target_id, + "date": "Mon, 10 Mar 1997 08:00:00 GMT", + "synthetic": True, + 'author': { + 'id': 10, + 'name': 'author release name', + 'email': 'author@email', + }, + } + + expected_release = { + 'id': release_id, + 'target_type': 'revision', + 'target': target_id, + 'target_url': '/api/1/revision/%s/' % target_id, + "date": "Mon, 10 Mar 1997 08:00:00 GMT", + "synthetic": True, + 'author_url': '/api/1/person/10/', + 'author': { + 'id': 10, + 'name': 'author release name', + 'email': 'author@email', + }, + } + + mock_service.lookup_release.return_value = stub_release + + # when + rv = self.client.get('/api/1/release/%s/' % release_id) + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_release) + + mock_service.lookup_release.assert_called_once_with(release_id) + + @patch('swh.web.api.views.release.service') + @istest + def api_release_target_type_not_a_revision(self, mock_service): + release = '8d56a78' + target = '9a5c3f' + # given + stub_release = { + 'id': release, + 'target_type': 'other-stuff', + 'target': target, + "date": "Mon, 10 Mar 1997 08:00:00 GMT", + "synthetic": True, + 'author': { + 'id': 9, + 'name': 'author release name', + 'email': 'author@email', + }, + } + + expected_release = { + 'id': release, + 'target_type': 'other-stuff', + 'target': target, + "date": "Mon, 10 Mar 1997 08:00:00 GMT", + "synthetic": True, + 'author_url': '/api/1/person/9/', + 'author': { + 'id': 9, + 'name': 'author release name', + 'email': 'author@email', + }, + } + + mock_service.lookup_release.return_value = stub_release + + # when + rv = self.client.get('/api/1/release/%s/' % release) + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_release) + + mock_service.lookup_release.assert_called_once_with(release) + + @patch('swh.web.api.views.release.service') + @istest + def api_release_not_found(self, mock_service): + # given + mock_service.lookup_release.return_value = None + + # when + rv = self.client.get('/api/1/release/c54e6473c71bbb716529/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Release with sha1_git c54e6473c71bbb716529 not found.' + }) diff --git a/swh/web/api/tests/views/test_revision.py b/swh/web/api/tests/views/test_revision.py new file mode 100644 index 000000000..802f59b96 --- /dev/null +++ b/swh/web/api/tests/views/test_revision.py @@ -0,0 +1,911 @@ +# Copyright (C) 2015-2017 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 unittest.mock import patch + +from swh.web.api.exc import NotFoundExc +from swh.web.api.views.revision import ( + _revision_directory_by +) + +from ..swh_api_testcase import SWHApiTestCase + + +class ReleaseApiTestCase(SWHApiTestCase): + + @patch('swh.web.api.views.revision.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': ['8734ef7e7c357ce2af928115c6c6a42b7e2a44e7'], + '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 + + expected_revision = { + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'url': '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5/', + 'history_url': '/api/1/revision/18d8be353ed3480476f032475e7c233e' + 'ff7371d5/log/', + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory_url': '/api/1/directory/7834ef7e7c357ce2af928115c6c6' + 'a42b7e2a44e6/', + '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': [{ + 'id': '8734ef7e7c357ce2af928115c6c6a42b7e2a44e7', + 'url': '/api/1/revision/8734ef7e7c357ce2af928115c6c6a42b7e2a44e7/' # noqa + }], + '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' + }] + }, + } + + # when + rv = self.client.get('/api/1/revision/' + '18d8be353ed3480476f032475e7c233eff7371d5/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(expected_revision, rv.data) + + mock_service.lookup_revision.assert_called_once_with( + '18d8be353ed3480476f032475e7c233eff7371d5') + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_not_found(self, mock_service): + # given + mock_service.lookup_revision.return_value = None + + # when + rv = self.client.get('/api/1/revision/12345/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Revision with sha1_git 12345 not found.'}) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_raw_ok(self, mock_service): + # given + stub_revision = {'message': 'synthetic revision message'} + + mock_service.lookup_revision_message.return_value = stub_revision + + # when + rv = self.client.get('/api/1/revision/18d8be353ed3480476f032475e7c2' + '33eff7371d5/raw/') + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/octet-stream') + self.assertEquals(rv.content, b'synthetic revision message') + + mock_service.lookup_revision_message.assert_called_once_with( + '18d8be353ed3480476f032475e7c233eff7371d5') + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_raw_ok_no_msg(self, mock_service): + # given + mock_service.lookup_revision_message.side_effect = NotFoundExc( + 'No message for revision') + + # when + rv = self.client.get('/api/1/revision/' + '18d8be353ed3480476f032475e7c233eff7371d5/raw/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'No message for revision'}) + + self.assertEquals + mock_service.lookup_revision_message.assert_called_once_with( + '18d8be353ed3480476f032475e7c233eff7371d5') + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_raw_ko_no_rev(self, mock_service): + # given + mock_service.lookup_revision_message.side_effect = NotFoundExc( + 'No revision found') + + # when + rv = self.client.get('/api/1/revision/' + '18d8be353ed3480476f032475e7c233eff7371d5/raw/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'No revision found'}) + + mock_service.lookup_revision_message.assert_called_once_with( + '18d8be353ed3480476f032475e7c233eff7371d5') + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_with_origin_not_found(self, mock_service): + mock_service.lookup_revision_by.return_value = None + + rv = self.client.get('/api/1/revision/origin/123/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertIn('Revision with (origin_id: 123', rv.data['reason']) + self.assertIn('not found', rv.data['reason']) + self.assertEqual('NotFoundExc', rv.data['exception']) + + mock_service.lookup_revision_by.assert_called_once_with( + '123', + 'refs/heads/master', + None) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_with_origin(self, mock_service): + mock_revision = { + 'id': '32', + 'directory': '21', + 'message': 'message 1', + 'type': 'deb', + } + expected_revision = { + 'id': '32', + 'url': '/api/1/revision/32/', + 'history_url': '/api/1/revision/32/log/', + 'directory': '21', + 'directory_url': '/api/1/directory/21/', + 'message': 'message 1', + 'type': 'deb', + } + mock_service.lookup_revision_by.return_value = mock_revision + + rv = self.client.get('/api/1/revision/origin/1/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEqual(rv.data, expected_revision) + + mock_service.lookup_revision_by.assert_called_once_with( + '1', + 'refs/heads/master', + None) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_with_origin_and_branch_name(self, mock_service): + mock_revision = { + 'id': '12', + 'directory': '23', + 'message': 'message 2', + 'type': 'tar', + } + mock_service.lookup_revision_by.return_value = mock_revision + + expected_revision = { + 'id': '12', + 'url': '/api/1/revision/12/', + 'history_url': '/api/1/revision/12/log/', + 'directory': '23', + 'directory_url': '/api/1/directory/23/', + 'message': 'message 2', + 'type': 'tar', + } + + rv = self.client.get('/api/1/revision/origin/1' + '/branch/refs/origin/dev/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEqual(rv.data, expected_revision) + + mock_service.lookup_revision_by.assert_called_once_with( + '1', + 'refs/origin/dev', + None) + + @patch('swh.web.api.views.revision.service') + @patch('swh.web.api.views.revision.utils') + @istest + def api_revision_with_origin_and_branch_name_and_timestamp(self, + mock_utils, + mock_service): + mock_revision = { + 'id': '123', + 'directory': '456', + 'message': 'message 3', + 'type': 'tar', + } + mock_service.lookup_revision_by.return_value = mock_revision + + expected_revision = { + 'id': '123', + 'url': '/api/1/revision/123/', + 'history_url': '/api/1/revision/123/log/', + 'directory': '456', + 'directory_url': '/api/1/directory/456/', + 'message': 'message 3', + 'type': 'tar', + } + + mock_utils.parse_timestamp.return_value = 'parsed-date' + mock_utils.enrich_revision.return_value = expected_revision + + rv = self.client.get('/api/1/revision' + '/origin/1' + '/branch/refs/origin/dev' + '/ts/1452591542/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEqual(rv.data, expected_revision) + + mock_service.lookup_revision_by.assert_called_once_with( + '1', + 'refs/origin/dev', + 'parsed-date') + mock_utils.parse_timestamp.assert_called_once_with('1452591542') + mock_utils.enrich_revision.assert_called_once_with( + mock_revision) + + @patch('swh.web.api.views.revision.service') + @patch('swh.web.api.views.revision.utils') + @istest + def api_revision_with_origin_and_branch_name_and_timestamp_with_escapes( + self, + mock_utils, + mock_service): + mock_revision = { + 'id': '999', + } + mock_service.lookup_revision_by.return_value = mock_revision + + expected_revision = { + 'id': '999', + 'url': '/api/1/revision/999/', + 'history_url': '/api/1/revision/999/log/', + } + + mock_utils.parse_timestamp.return_value = 'parsed-date' + mock_utils.enrich_revision.return_value = expected_revision + + rv = self.client.get('/api/1/revision' + '/origin/1' + '/branch/refs%2Forigin%2Fdev' + '/ts/Today%20is%20' + 'January%201,%202047%20at%208:21:00AM/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEqual(rv.data, expected_revision) + + mock_service.lookup_revision_by.assert_called_once_with( + '1', + 'refs/origin/dev', + 'parsed-date') + mock_utils.parse_timestamp.assert_called_once_with( + 'Today is January 1, 2047 at 8:21:00AM') + mock_utils.enrich_revision.assert_called_once_with( + mock_revision) + + @patch('swh.web.api.views.revision.service') + @istest + def revision_directory_by_ko_raise(self, mock_service): + # given + mock_service.lookup_directory_through_revision.side_effect = NotFoundExc('not') # noqa + + # when + with self.assertRaises(NotFoundExc): + _revision_directory_by( + {'sha1_git': 'id'}, + None, + '/api/1/revision/sha1/directory/') + + # then + mock_service.lookup_directory_through_revision.assert_called_once_with( + {'sha1_git': 'id'}, + None, limit=100, with_data=False) + + @patch('swh.web.api.views.revision.service') + @istest + def revision_directory_by_type_dir(self, mock_service): + # given + mock_service.lookup_directory_through_revision.return_value = ( + 'rev-id', + { + 'type': 'dir', + 'revision': 'rev-id', + 'path': 'some/path', + 'content': [] + }) + # when + actual_dir_content = _revision_directory_by( + {'sha1_git': 'blah-id'}, + 'some/path', '/api/1/revision/sha1/directory/') + + # then + self.assertEquals(actual_dir_content, { + 'type': 'dir', + 'revision': 'rev-id', + 'path': 'some/path', + 'content': [] + }) + + mock_service.lookup_directory_through_revision.assert_called_once_with( + {'sha1_git': 'blah-id'}, + 'some/path', limit=100, with_data=False) + + @patch('swh.web.api.views.revision.service') + @istest + def revision_directory_by_type_file(self, mock_service): + # given + mock_service.lookup_directory_through_revision.return_value = ( + 'rev-id', + { + 'type': 'file', + 'revision': 'rev-id', + 'path': 'some/path', + 'content': {'blah': 'blah'} + }) + # when + actual_dir_content = _revision_directory_by( + {'sha1_git': 'sha1'}, + 'some/path', + '/api/1/revision/origin/2/directory/', + limit=1000, with_data=True) + + # then + self.assertEquals(actual_dir_content, { + 'type': 'file', + 'revision': 'rev-id', + 'path': 'some/path', + 'content': {'blah': 'blah'} + }) + + mock_service.lookup_directory_through_revision.assert_called_once_with( + {'sha1_git': 'sha1'}, + 'some/path', limit=1000, with_data=True) + + @patch('swh.web.api.views.revision._revision_directory_by') + @patch('swh.web.api.views.revision.utils') + @istest + def api_directory_through_revision_origin_ko_not_found(self, + mock_utils, + mock_rev_dir): + mock_rev_dir.side_effect = NotFoundExc('not found') + mock_utils.parse_timestamp.return_value = '2012-10-20 00:00:00' + + rv = self.client.get('/api/1/revision' + '/origin/10' + '/branch/refs/remote/origin/dev' + '/ts/2012-10-20' + '/directory/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEqual(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'not found'}) + + mock_rev_dir.assert_called_once_with( + {'origin_id': '10', + 'branch_name': 'refs/remote/origin/dev', + 'ts': '2012-10-20 00:00:00'}, None, + '/api/1/revision' + '/origin/10' + '/branch/refs/remote/origin/dev' + '/ts/2012-10-20' + '/directory/', + with_data=False) + + @patch('swh.web.api.views.revision._revision_directory_by') + @istest + def api_directory_through_revision_origin(self, + mock_revision_dir): + expected_res = [{ + 'id': '123' + }] + mock_revision_dir.return_value = expected_res + + rv = self.client.get('/api/1/revision/origin/3/directory/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEqual(rv.data, expected_res) + + mock_revision_dir.assert_called_once_with({ + 'origin_id': '3', + 'branch_name': 'refs/heads/master', + 'ts': None}, None, '/api/1/revision/origin/3/directory/', + with_data=False) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_log(self, mock_service): + # given + stub_revisions = [{ + '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': ['7834ef7e7c357ce2af928115c6c6a42b7e2a4345'], + 'type': 'tar', + 'synthetic': True, + }] + mock_service.lookup_revision_log.return_value = stub_revisions + + expected_revisions = [{ + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'url': '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5/', + 'history_url': '/api/1/revision/18d8be353ed3480476f032475e7c233ef' + 'f7371d5/log/', + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory_url': '/api/1/directory/7834ef7e7c357ce2af928115c6c6a' + '42b7e2a44e6/', + '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': [{ + 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a4345', + 'url': '/api/1/revision/7834ef7e7c357ce2af928115c6c6a42b7e2a4345/', # noqa + }], + 'type': 'tar', + 'synthetic': True, + }] + + # when + rv = self.client.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6a42' + 'b7e2a44e6/log/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + + self.assertEquals(rv.data, expected_revisions) + self.assertFalse(rv.has_header('Link')) + + mock_service.lookup_revision_log.assert_called_once_with( + '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 11) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_log_with_next(self, mock_service): + # given + stub_revisions = [] + for i in range(27): + stub_revisions.append({'id': i}) + + mock_service.lookup_revision_log.return_value = stub_revisions[:26] + + expected_revisions = [x for x in stub_revisions if x['id'] < 25] + for e in expected_revisions: + e['url'] = '/api/1/revision/%s/' % e['id'] + e['history_url'] = '/api/1/revision/%s/log/' % e['id'] + + # when + rv = self.client.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6a42' + 'b7e2a44e6/log/?per_page=25') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_revisions) + self.assertEquals(rv['Link'], + '; rel="next"') + + mock_service.lookup_revision_log.assert_called_once_with( + '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 26) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_log_not_found(self, mock_service): + # given + mock_service.lookup_revision_log.return_value = None + + # when + rv = self.client.get('/api/1/revision/8834ef7e7c357ce2af928115c6c6' + 'a42b7e2a44e6/log/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Revision with sha1_git' + ' 8834ef7e7c357ce2af928115c6c6a42b7e2a44e6 not found.'}) + self.assertFalse(rv.has_header('Link')) + + mock_service.lookup_revision_log.assert_called_once_with( + '8834ef7e7c357ce2af928115c6c6a42b7e2a44e6', 11) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_log_context(self, mock_service): + # given + stub_revisions = [{ + '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': ['7834ef7e7c357ce2af928115c6c6a42b7e2a4345'], + 'type': 'tar', + 'synthetic': True, + }] + + mock_service.lookup_revision_log.return_value = stub_revisions + mock_service.lookup_revision_multiple.return_value = [{ + 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory': '18d8be353ed3480476f032475e7c233eff7371d5', + 'author_name': 'Name Surname', + 'author_email': 'name@surname.com', + 'committer_name': 'Name Surname', + 'committer_email': 'name@surname.com', + 'message': 'amazing revision message', + 'date_offset': 0, + 'committer_date_offset': 0, + 'parents': ['adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'], + 'type': 'tar', + 'synthetic': True, + }] + + expected_revisions = [ + { + 'url': '/api/1/revision/' + '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6/', + 'history_url': '/api/1/revision/' + '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6/log/', + 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory': '18d8be353ed3480476f032475e7c233eff7371d5', + 'directory_url': '/api/1/directory/' + '18d8be353ed3480476f032475e7c233eff7371d5/', + 'author_name': 'Name Surname', + 'author_email': 'name@surname.com', + 'committer_name': 'Name Surname', + 'committer_email': 'name@surname.com', + 'message': 'amazing revision message', + 'date_offset': 0, + 'committer_date_offset': 0, + 'parents': [{ + 'id': 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', + 'url': '/api/1/revision/adc83b19e793491b1c6ea0fd8b46cd9f32e592fc/', # noqa + }], + 'type': 'tar', + 'synthetic': True, + }, + { + 'url': '/api/1/revision/' + '18d8be353ed3480476f032475e7c233eff7371d5/', + 'history_url': '/api/1/revision/' + '18d8be353ed3480476f032475e7c233eff7371d5/log/', + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory_url': '/api/1/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': [{ + 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a4345', + 'url': '/api/1/revision/7834ef7e7c357ce2af928115c6c6a42b7e2a4345/', # noqa + }], + 'type': 'tar', + 'synthetic': True, + }] + + # when + rv = self.client.get('/api/1/revision/18d8be353ed3480476f0' + '32475e7c233eff7371d5/prev/21145781e2' + '6ad1f978e/log/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(expected_revisions, rv.data) + self.assertFalse(rv.has_header('Link')) + + mock_service.lookup_revision_log.assert_called_once_with( + '18d8be353ed3480476f032475e7c233eff7371d5', 11) + mock_service.lookup_revision_multiple.assert_called_once_with( + ['21145781e26ad1f978e']) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_log_by(self, mock_service): + # given + stub_revisions = [{ + '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': ['7834ef7e7c357ce2af928115c6c6a42b7e2a4345'], + 'type': 'tar', + 'synthetic': True, + }] + mock_service.lookup_revision_log_by.return_value = stub_revisions + + expected_revisions = [{ + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'url': '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5/', + 'history_url': '/api/1/revision/18d8be353ed3480476f032475e7c233ef' + 'f7371d5/log/', + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory_url': '/api/1/directory/7834ef7e7c357ce2af928115c6c6a' + '42b7e2a44e6/', + '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': [{ + 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a4345', + 'url': '/api/1/revision/7834ef7e7c357ce2af928115c6c6a42b7e2a4345/' # noqa + }], + 'type': 'tar', + 'synthetic': True, + }] + + # when + rv = self.client.get('/api/1/revision/origin/1/log/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, expected_revisions) + self.assertFalse(rv.has_header('Link')) + + mock_service.lookup_revision_log_by.assert_called_once_with( + '1', 'refs/heads/master', None, 11) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_log_by_with_next(self, mock_service): + # given + stub_revisions = [] + for i in range(27): + stub_revisions.append({'id': i}) + + mock_service.lookup_revision_log_by.return_value = stub_revisions[:26] + + expected_revisions = [x for x in stub_revisions if x['id'] < 25] + for e in expected_revisions: + e['url'] = '/api/1/revision/%s/' % e['id'] + e['history_url'] = '/api/1/revision/%s/log/' % e['id'] + + # when + rv = self.client.get('/api/1/revision/origin/1/log/?per_page=25') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertIsNotNone(rv['Link']) + self.assertEquals(rv.data, expected_revisions) + + mock_service.lookup_revision_log_by.assert_called_once_with( + '1', 'refs/heads/master', None, 26) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_log_by_norev(self, mock_service): + # given + mock_service.lookup_revision_log_by.side_effect = NotFoundExc( + 'No revision') + + # when + rv = self.client.get('/api/1/revision/origin/1/log/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertFalse(rv.has_header('Link')) + self.assertEquals(rv.data, {'exception': 'NotFoundExc', + 'reason': 'No revision'}) + + mock_service.lookup_revision_log_by.assert_called_once_with( + '1', 'refs/heads/master', None, 11) + + @patch('swh.web.api.views.revision.service') + @istest + def api_revision_history(self, mock_service): + # for readability purposes, we use: + # - sha1 as 3 letters (url are way too long otherwise to respect pep8) + # - only keys with modification steps (all other keys are kept as is) + + # given + stub_revision = { + 'id': '883', + 'children': ['777', '999'], + 'parents': [], + 'directory': '272' + } + + mock_service.lookup_revision.return_value = stub_revision + + # then + rv = self.client.get('/api/1/revision/883/prev/999/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'id': '883', + 'url': '/api/1/revision/883/', + 'history_url': '/api/1/revision/883/log/', + 'history_context_url': '/api/1/revision/883/prev/999/log/', + 'children': ['777', '999'], + 'children_urls': ['/api/1/revision/777/', + '/api/1/revision/999/'], + 'parents': [], + 'directory': '272', + 'directory_url': '/api/1/directory/272/' + }) + + mock_service.lookup_revision.assert_called_once_with('883') + + @patch('swh.web.api.views.revision._revision_directory_by') + @istest + def api_revision_directory_ko_not_found(self, mock_rev_dir): + # given + mock_rev_dir.side_effect = NotFoundExc('Not found') + + # then + rv = self.client.get('/api/1/revision/999/directory/some/path/to/dir/') + + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'NotFoundExc', + 'reason': 'Not found'}) + + mock_rev_dir.assert_called_once_with( + {'sha1_git': '999'}, + 'some/path/to/dir', + '/api/1/revision/999/directory/some/path/to/dir/', + with_data=False) + + @patch('swh.web.api.views.revision._revision_directory_by') + @istest + def api_revision_directory_ok_returns_dir_entries(self, mock_rev_dir): + stub_dir = { + 'type': 'dir', + 'revision': '999', + 'content': [ + { + 'sha1_git': '789', + 'type': 'file', + 'target': '101', + 'target_url': '/api/1/content/sha1_git:101/', + 'name': 'somefile', + 'file_url': '/api/1/revision/999/directory/some/path/' + 'somefile/' + }, + { + 'sha1_git': '123', + 'type': 'dir', + 'target': '456', + 'target_url': '/api/1/directory/456/', + 'name': 'to-subdir', + 'dir_url': '/api/1/revision/999/directory/some/path/' + 'to-subdir/', + }] + } + + # given + mock_rev_dir.return_value = stub_dir + + # then + rv = self.client.get('/api/1/revision/999/directory/some/path/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, stub_dir) + + mock_rev_dir.assert_called_once_with( + {'sha1_git': '999'}, + 'some/path', + '/api/1/revision/999/directory/some/path/', + with_data=False) + + @patch('swh.web.api.views.revision._revision_directory_by') + @istest + def api_revision_directory_ok_returns_content(self, mock_rev_dir): + stub_content = { + 'type': 'file', + 'revision': '999', + 'content': { + 'sha1_git': '789', + 'sha1': '101', + 'data_url': '/api/1/content/101/raw/', + } + } + + # given + mock_rev_dir.return_value = stub_content + + # then + url = '/api/1/revision/666/directory/some/other/path/' + rv = self.client.get(url) + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, stub_content) + + mock_rev_dir.assert_called_once_with( + {'sha1_git': '666'}, 'some/other/path', url, with_data=False) diff --git a/swh/web/api/tests/views/test_stat.py b/swh/web/api/tests/views/test_stat.py new file mode 100644 index 000000000..8afdba42b --- /dev/null +++ b/swh/web/api/tests/views/test_stat.py @@ -0,0 +1,96 @@ +# Copyright (C) 2015-2017 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 unittest.mock import patch + +from swh.storage.exc import StorageDBError, StorageAPIError + +from ..swh_api_testcase import SWHApiTestCase + + +class StatApiTestCase(SWHApiTestCase): + @patch('swh.web.api.views.stat.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.client.get('/api/1/stat/counters/') + # then + self.assertEquals(rv.status_code, 400) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'ValueError', + 'reason': 'voluntary error to check the bad request middleware.'}) + + @patch('swh.web.api.views.stat.service') + @istest + def api_1_stat_counters_raise_swh_storage_error_db(self, mock_service): + # given + mock_service.stat_counters.side_effect = StorageDBError( + 'SWH Storage exploded! Will be back online shortly!') + # when + rv = self.client.get('/api/1/stat/counters/') + # then + self.assertEquals(rv.status_code, 503) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'StorageDBError', + 'reason': + 'An unexpected error occurred in the backend: ' + 'SWH Storage exploded! Will be back online shortly!'}) + + @patch('swh.web.api.views.stat.service') + @istest + def api_1_stat_counters_raise_swh_storage_error_api(self, mock_service): + # given + mock_service.stat_counters.side_effect = StorageAPIError( + 'SWH Storage API dropped dead! Will resurrect from its ashes asap!' + ) + # when + rv = self.client.get('/api/1/stat/counters/') + # then + self.assertEquals(rv.status_code, 503) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, { + 'exception': 'StorageAPIError', + 'reason': + 'An unexpected error occurred in the api backend: ' + 'SWH Storage API dropped dead! Will resurrect from its ashes asap!' + }) + + @patch('swh.web.api.views.stat.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.client.get('/api/1/stat/counters/') + + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv['Content-Type'], 'application/json') + self.assertEquals(rv.data, stub_stats) + + mock_service.stat_counters.assert_called_once_with() diff --git a/swh/web/api/urls.py b/swh/web/api/urls.py index 7d5f15041..c42bd3750 100644 --- a/swh/web/api/urls.py +++ b/swh/web/api/urls.py @@ -1,8 +1,17 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -from swh.web.api import views +import swh.web.api.views.origin # noqa +import swh.web.api.views.content # noqa +import swh.web.api.views.person # noqa +import swh.web.api.views.release # noqa +import swh.web.api.views.revision # noqa +import swh.web.api.views.directory # noqa +import swh.web.api.views.entity # noqa +import swh.web.api.views.stat # noqa -urlpatterns = views.get_url_patterns() +from swh.web.api.apiurls import APIUrls + +urlpatterns = APIUrls.get_url_patterns() diff --git a/swh/web/api/views.py b/swh/web/api/views.py deleted file mode 100644 index 966200ed0..000000000 --- a/swh/web/api/views.py +++ /dev/null @@ -1,1112 +0,0 @@ -# Copyright (C) 2017 The Software Heritage developers -# See the AUTHORS file at the top-level directory of this distribution -# License: GNU General Public License version 3, or any later version -# See top-level LICENSE file for more information - -import functools - -from django.http import QueryDict -from django.conf.urls import url -from django.urls import reverse -from django.http import HttpResponse - -from rest_framework.response import Response -from rest_framework.decorators import api_view - -from types import GeneratorType - -from swh.web.api import service, utils -from swh.web.api import apidoc as api_doc -from swh.web.api.exc import NotFoundExc, ForbiddenExc -from swh.web.api.apiurls import APIUrls, api_route - -# canned doc string snippets that are used in several doc strings -_doc_arg_content_id = """A "[hash_type:]hash" content identifier, where - hash_type is one of "sha1" (the default), "sha1_git", "sha256", and hash is - a checksum obtained with the hash_type hashing algorithm.""" -_doc_arg_last_elt = 'element to start listing from, for pagination purposes' -_doc_arg_per_page = 'number of elements to list, for pagination purposes' - -_doc_exc_bad_id = 'syntax error in the given identifier(s)' -_doc_exc_id_not_found = 'no object matching the given criteria could be found' - -_doc_ret_revision_meta = 'metadata of the revision identified by sha1_git' -_doc_ret_revision_log = """list of dictionaries representing the metadata of - each revision found in the commit log heading to revision sha1_git. - For each commit at least the following information are returned: - author/committer, authoring/commit timestamps, revision id, commit message, - parent (i.e., immediately preceding) commits, "root" directory id.""" - -_doc_header_link = """indicates that a subsequent result page is available, - pointing to it""" - - -def get_url_patterns(): - return APIUrls.get_url_patterns() - - -def _api_lookup(lookup_fn, *args, - notfound_msg='Object not found', - enrich_fn=lambda x: x): - """Capture a redundant behavior of: - - looking up the backend with a criteria (be it an identifier or checksum) - passed to the function lookup_fn - - if nothing is found, raise an NotFoundExc exception with error - message notfound_msg. - - Otherwise if something is returned: - - either as list, map or generator, map the enrich_fn function to it - and return the resulting data structure as list. - - either as dict and pass to enrich_fn and return the dict enriched. - - Args: - - criteria: discriminating criteria to lookup - - lookup_fn: function expects one criteria and optional supplementary - *args. - - notfound_msg: if nothing matching the criteria is found, - raise NotFoundExc with this error message. - - enrich_fn: Function to use to enrich the result returned by - lookup_fn. Default to the identity function if not provided. - - *args: supplementary arguments to pass to lookup_fn. - - Raises: - NotFoundExp or whatever `lookup_fn` raises. - - """ - res = lookup_fn(*args) - if not res: - raise NotFoundExc(notfound_msg) - if isinstance(res, (map, list, GeneratorType)): - return [enrich_fn(x) for x in res] - return enrich_fn(res) - - -@api_view() -def api_home(request): - return Response({}, template_name='api.html') - - -APIUrls.urlpatterns.append(url(r'^$', api_home, name='homepage')) - - -@api_route(r'/', 'endpoints') -def api_endpoints(request): - """Display the list of opened api endpoints. - - """ - routes = APIUrls.get_app_endpoints().copy() - for route, doc in routes.items(): - doc['doc_intro'] = doc['docstring'].split('\n\n')[0] - # Return a list of routes with consistent ordering - env = { - 'doc_routes': sorted(routes.items()) - } - return Response(env, template_name="api-endpoints.html") - - -@api_route(r'/origin/(?P[0-9]+)/', 'origin') -@api_route(r'/origin/(?P[a-z]+)/url/(?P.+)', - 'origin') -@api_doc.route('/origin/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc='origin identifier (when looking up by ID)') -@api_doc.arg('origin_type', - default='git', - argtype=api_doc.argtypes.str, - argdoc='origin type (when looking up by type+URL)') -@api_doc.arg('origin_url', - default='https://github.com/hylang/hy', - argtype=api_doc.argtypes.path, - argdoc='origin URL (when looking up by type+URL)') -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""The metadata of the origin corresponding to the given - criteria""") -def api_origin(request, origin_id=None, origin_type=None, origin_url=None): - """Get information about a software origin. - - Software origins might be looked up by origin type and canonical URL (e.g., - "git" + a "git clone" URL), or by their unique (but otherwise meaningless) - identifier. - - """ - ori_dict = { - 'id': origin_id, - 'type': origin_type, - 'url': origin_url - } - ori_dict = {k: v for k, v in ori_dict.items() if ori_dict[k]} - if 'id' in ori_dict: - error_msg = 'Origin with id %s not found.' % ori_dict['id'] - else: - error_msg = 'Origin with type %s and URL %s not found' % ( - ori_dict['type'], ori_dict['url']) - - def _enrich_origin(origin): - if 'id' in origin: - o = origin.copy() - o['origin_visits_url'] = \ - reverse('origin-visits', kwargs={'origin_id': origin['id']}) - return o - - return origin - - return _api_lookup( - service.lookup_origin, ori_dict, - notfound_msg=error_msg, - enrich_fn=_enrich_origin) - - -@api_route(r'/stat/counters/', 'stat-counters') -@api_doc.route('/stat/counters/', noargs=True) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""dictionary mapping object types to the amount of - corresponding objects currently available in the archive""") -def api_stats(request): - """Get statistics about the content of the archive. - - """ - return service.stat_counters() - - -@api_route(r'/origin/(?P[0-9]+)/visits/', 'origin-visits') -@api_doc.route('/origin/visits/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc='software origin identifier') -@api_doc.header('Link', doc=_doc_header_link) -@api_doc.param('last_visit', default=None, - argtype=api_doc.argtypes.int, - doc=_doc_arg_last_elt) -@api_doc.param('per_page', default=10, - argtype=api_doc.argtypes.int, - doc=_doc_arg_per_page) -@api_doc.returns(rettype=api_doc.rettypes.list, - retdoc="""a list of dictionaries describing individual visits. - For each visit, its identifier, timestamp (as UNIX time), - outcome, and visit-specific URL for more information are - given.""") -def api_origin_visits(request, origin_id): - """Get information about all visits of a given software origin. - - """ - result = {} - per_page = int(request.query_params.get('per_page', '10')) - last_visit = request.query_params.get('last_visit') - if last_visit: - last_visit = int(last_visit) - - def _lookup_origin_visits( - origin_id, last_visit=last_visit, per_page=per_page): - return service.lookup_origin_visits( - origin_id, last_visit=last_visit, per_page=per_page) - - def _enrich_origin_visit(origin_visit): - ov = origin_visit.copy() - ov['origin_visit_url'] = reverse('origin-visit', - kwargs={'origin_id': origin_id, - 'visit_id': ov['visit']}) - return ov - - r = _api_lookup( - _lookup_origin_visits, origin_id, - notfound_msg='No origin {} found'.format(origin_id), - enrich_fn=_enrich_origin_visit) - - if r: - l = len(r) - if l == per_page: - new_last_visit = r[-1]['visit'] - query_params = QueryDict('', mutable=True) - query_params['last_visit'] = new_last_visit - - if request.query_params.get('per_page'): - query_params['per_page'] = per_page - - result['headers'] = { - 'link-next': reverse('origin-visits', - kwargs={'origin_id': origin_id}) + - '?' + query_params.urlencode() - } - - result.update({ - 'results': r - }) - - return result - - -@api_route(r'/origin/(?P[0-9]+)/visit/(?P[0-9]+)/', - 'origin-visit') -@api_doc.route('/origin/visit/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc='software origin identifier') -@api_doc.arg('visit_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc="""visit identifier, relative to the origin identified by - origin_id""") -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""dictionary containing both metadata for the entire - visit (e.g., timestamp as UNIX time, visit outcome, etc.) and - what was at the software origin during the visit (i.e., a - mapping from branches to other archive objects)""") -def api_origin_visit(request, origin_id, visit_id): - """Get information about a specific visit of a software origin. - - """ - def _enrich_origin_visit(origin_visit): - ov = origin_visit.copy() - ov['origin_url'] = reverse('origin', - kwargs={'origin_id': ov['origin']}) - if 'occurrences' in ov: - ov['occurrences'] = { - k: utils.enrich_object(v) - for k, v in ov['occurrences'].items() - } - return ov - - return _api_lookup( - service.lookup_origin_visit, origin_id, visit_id, - notfound_msg=('No visit {} for origin {} found' - .format(visit_id, origin_id)), - enrich_fn=_enrich_origin_visit) - - -@api_route(r'/content/symbol/search/', 'content-symbol', methods=['POST']) -@api_route(r'/content/symbol/(?P.+)/', 'content-symbol') -@api_doc.route('/content/symbol/', tags=['upcoming']) -@api_doc.arg('q', - default='hello', - argtype=api_doc.argtypes.str, - argdoc="""An expression string to lookup in swh's raw content""") -@api_doc.header('Link', doc=_doc_header_link) -@api_doc.param('last_sha1', default=None, - argtype=api_doc.argtypes.str, - doc=_doc_arg_last_elt) -@api_doc.param('per_page', default=10, - argtype=api_doc.argtypes.int, - doc=_doc_arg_per_page) -@api_doc.returns(rettype=api_doc.rettypes.list, - retdoc="""A list of dict whose content matches the expression. - Each dict has the following keys: - - id (bytes): identifier of the content - - name (text): symbol whose content match the expression - - kind (text): kind of the symbol that matched - - lang (text): Language for that entry - - line (int): Number line for the symbol - """) -def api_content_symbol(request, q=None): - """Search content objects by `Ctags `_-style - symbol (e.g., function name, data type, method, ...). - - """ - result = {} - last_sha1 = request.query_params.get('last_sha1', None) - per_page = int(request.query_params.get('per_page', '10')) - - def lookup_exp(exp, last_sha1=last_sha1, per_page=per_page): - return service.lookup_expression(exp, last_sha1, per_page) - - symbols = _api_lookup( - lookup_exp, q, - notfound_msg="No indexed raw content match expression '{}'.".format(q), - enrich_fn=functools.partial(utils.enrich_content, top_url=True)) - - if symbols: - l = len(symbols) - - if l == per_page: - query_params = QueryDict('', mutable=True) - new_last_sha1 = symbols[-1]['sha1'] - query_params['last_sha1'] = new_last_sha1 - if request.query_params.get('per_page'): - query_params['per_page'] = per_page - - result['headers'] = { - 'link-next': reverse('content-symbol', kwargs={'q': q}) + '?' + - query_params.urlencode() - } - - result.update({ - 'results': symbols - }) - - return result - - -@api_route(r'/content/known/search/', 'content-known', methods=['POST']) -@api_route(r'/content/known/(?P(?!search).*)/', 'content-known') -@api_doc.route('/content/known/', tags=['hidden']) -@api_doc.arg('q', - default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', - argtype=api_doc.argtypes.sha1, - argdoc='content identifier as a sha1 checksum') -@api_doc.param('q', default=None, - argtype=api_doc.argtypes.str, - doc="""(POST request) An algo_hash:hash string, where algo_hash - is one of sha1, sha1_git or sha256 and hash is the hash to - search for in SWH""") -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""a dictionary with results (found/not found for each given - identifier) and statistics about how many identifiers - were found""") -def api_check_content_known(request, q=None): - """Check whether some content (AKA "blob") is present in the archive. - - Lookup can be performed by various means: - - - a GET request with one or several hashes, separated by ',' - - a POST request with one or several hashes, passed as (multiple) values - for parameter 'q' - - """ - response = {'search_res': None, - 'search_stats': None} - search_stats = {'nbfiles': 0, 'pct': 0} - search_res = None - - queries = [] - # GET: Many hash separated values request - if q: - hashes = q.split(',') - for v in hashes: - queries.append({'filename': None, 'sha1': v}) - - # POST: Many hash requests in post form submission - elif request.method == 'POST': - data = request.data - # Remove potential inputs with no associated value - for k, v in data.items(): - if v is not None: - if k == 'q' and len(v) > 0: - queries.append({'filename': None, 'sha1': v}) - elif v != '': - queries.append({'filename': k, 'sha1': v}) - - if queries: - lookup = service.lookup_multiple_hashes(queries) - result = [] - l = len(queries) - for el in lookup: - res_d = {'sha1': el['sha1'], - 'found': el['found']} - if 'filename' in el and el['filename']: - res_d['filename'] = el['filename'] - result.append(res_d) - search_res = result - nbfound = len([x for x in lookup if x['found']]) - search_stats['nbfiles'] = l - search_stats['pct'] = (nbfound / l) * 100 - - response['search_res'] = search_res - response['search_stats'] = search_stats - return response - - -@api_route(r'/person/(?P[0-9]+)/', 'person') -@api_doc.route('/person/') -@api_doc.arg('person_id', - default=42, - argtype=api_doc.argtypes.int, - argdoc='person identifier') -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='The metadata of the person identified by person_id') -def api_person(request, person_id): - """Get information about a person. - - """ - return _api_lookup( - service.lookup_person, person_id, - notfound_msg='Person with id {} not found.'.format(person_id)) - - -@api_route(r'/release/(?P[0-9a-f]+)/', 'release') -@api_doc.route('/release/') -@api_doc.arg('sha1_git', - default='7045404f3d1c54e6473c71bbb716529fbad4be24', - argtype=api_doc.argtypes.sha1_git, - argdoc='release identifier') -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='The metadata of the release identified by sha1_git') -def api_release(request, sha1_git): - """Get information about a release. - - Releases are identified by SHA1 checksums, compatible with Git tag - identifiers. See ``release_identifier`` in our `data model module - `_ - for details about how they are computed. - - """ - error_msg = 'Release with sha1_git %s not found.' % sha1_git - return _api_lookup( - service.lookup_release, sha1_git, - notfound_msg=error_msg, - enrich_fn=utils.enrich_release) - - -def _revision_directory_by(revision, path, request_path, - limit=100, with_data=False): - """Compute the revision matching criterion's directory or content data. - - Args: - revision: dictionary of criterions representing a revision to lookup - path: directory's path to lookup - request_path: request path which holds the original context to - limit: optional query parameter to limit the revisions log - (default to 100). For now, note that this limit could impede the - transitivity conclusion about sha1_git not being an ancestor of - with_data: indicate to retrieve the content's raw data if path resolves - to a content. - - """ - def enrich_directory_local(dir, context_url=request_path): - return utils.enrich_directory(dir, context_url) - - rev_id, result = service.lookup_directory_through_revision( - revision, path, limit=limit, with_data=with_data) - - content = result['content'] - if result['type'] == 'dir': # dir_entries - result['content'] = list(map(enrich_directory_local, content)) - else: # content - result['content'] = utils.enrich_content(content) - - return result - - -@api_route(r'/revision/origin/(?P[0-9]+)/log/', - 'revision-origin-log') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/ts/(?P.+)/log/', - 'revision-origin-log') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/branch/(?P.+)' - r'/ts/(?P.+)/log/', - 'revision-origin-log') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/branch/(?P.+)/log/', - 'revision-origin-log') -@api_doc.route('/revision/origin/log/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc="The revision's SWH origin identifier") -@api_doc.arg('branch_name', - default='refs/heads/master', - argtype=api_doc.argtypes.path, - argdoc="""(Optional) The revision's branch name within the origin specified. -Defaults to 'refs/heads/master'.""") -@api_doc.arg('ts', - default='2000-01-17T11:23:54+00:00', - argtype=api_doc.argtypes.ts, - argdoc="""(Optional) A time or timestamp string to parse""") -@api_doc.header('Link', doc=_doc_header_link) -@api_doc.param('per_page', default=10, - argtype=api_doc.argtypes.int, - doc=_doc_arg_per_page) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=_doc_ret_revision_log) -def api_revision_log_by(request, origin_id, - branch_name='refs/heads/master', - ts=None): - """Show the commit log for a revision, searching for it based on software - origin, branch name, and/or visit timestamp. - - This endpoint behaves like ``/log``, but operates on the revision that - has been found at a given software origin, close to a given point in time, - pointed by a given branch. - """ - result = {} - per_page = int(request.query_params.get('per_page', '10')) - - if ts: - ts = utils.parse_timestamp(ts) - - def lookup_revision_log_by_with_limit(o_id, br, ts, limit=per_page+1): - return service.lookup_revision_log_by(o_id, br, ts, limit) - - error_msg = 'No revision matching origin %s ' % origin_id - error_msg += ', branch name %s' % branch_name - error_msg += (' and time stamp %s.' % ts) if ts else '.' - - rev_get = _api_lookup( - lookup_revision_log_by_with_limit, origin_id, branch_name, ts, - notfound_msg=error_msg, - enrich_fn=utils.enrich_revision) - l = len(rev_get) - if l == per_page+1: - revisions = rev_get[:-1] - last_sha1_git = rev_get[-1]['id'] - - params = {k: v for k, v in {'origin_id': origin_id, - 'branch_name': branch_name, - 'ts': ts, - }.items() if v is not None} - - query_params = QueryDict('', mutable=True) - query_params['sha1_git'] = last_sha1_git - - if request.query_params.get('per_page'): - query_params['per_page'] = per_page - - result['headers'] = { - 'link-next': reverse('revision-origin-log', kwargs=params) + - (('?' + query_params.urlencode()) if len(query_params) > 0 else '') - } - - else: - revisions = rev_get - - result.update({'results': revisions}) - - return result - - -@api_route(r'/revision/origin/(?P[0-9]+)/directory/', - 'revision-directory') -@api_route(r'/revision/origin/(?P[0-9]+)/directory/(?P.+)/', - 'revision-directory') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/branch/(?P.+)/directory/', - 'revision-directory') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/branch/(?P.+)/ts/(?P.+)/directory/', - 'revision-directory') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/branch/(?P.+)/directory/(?P.+)/', - 'revision-directory') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/branch/(?P.+)/ts/(?P.+)' - r'/directory/(?P.+)/', - 'revision-directory') -@api_doc.route('/revision/origin/directory/', tags=['hidden']) -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc="The revision's origin's SWH identifier") -@api_doc.arg('branch_name', - default='refs/heads/master', - argtype=api_doc.argtypes.path, - argdoc="""The optional branch for the given origin (default - to master""") -@api_doc.arg('ts', - default='2000-01-17T11:23:54+00:00', - argtype=api_doc.argtypes.ts, - argdoc="""Optional timestamp (default to the nearest time - crawl of timestamp)""") -@api_doc.arg('path', - default='Dockerfile', - argtype=api_doc.argtypes.path, - argdoc='The path to the directory or file to display') -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""The metadata of the revision corresponding to the - given criteria""") -def api_directory_through_revision_origin(request, origin_id, - branch_name="refs/heads/master", - ts=None, - path=None, - with_data=False): - """Display directory or content information through a revision identified - by origin/branch/timestamp. - """ - if ts: - ts = utils.parse_timestamp(ts) - - return _revision_directory_by({'origin_id': origin_id, - 'branch_name': branch_name, - 'ts': ts - }, - path, request.path, - with_data=with_data) - - -@api_route(r'/revision/origin/(?P[0-9]+)/', - 'revision-origin') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/branch/(?P.+)/', - 'revision-origin') -@api_route(r'/revision/origin/(?P[0-9]+)' - r'/branch/(?P.+)/ts/(?P.+)/', - 'revision-origin') -@api_route(r'/revision/origin/(?P[0-9]+)/ts/(?P.+)/', - 'revision-origin') -@api_doc.route('/revision/origin/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc='software origin identifier') -@api_doc.arg('branch_name', - default='refs/heads/master', - argtype=api_doc.argtypes.path, - argdoc="""(optional) fully-qualified branch name, e.g., - "refs/heads/master". Defaults to the master branch.""") -@api_doc.arg('ts', - default=None, - argtype=api_doc.argtypes.ts, - argdoc="""(optional) timestamp close to which the revision pointed by - the given branch should be looked up. Defaults to now.""") -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=_doc_ret_revision_meta) -def api_revision_with_origin(request, origin_id, - branch_name="refs/heads/master", - ts=None): - """Get information about a revision, searching for it based on software - origin, branch name, and/or visit timestamp. - - This endpoint behaves like ``/revision``, but operates on the revision that - has been found at a given software origin, close to a given point in time, - pointed by a given branch. - - """ - ts = utils.parse_timestamp(ts) - return _api_lookup( - service.lookup_revision_by, origin_id, branch_name, ts, - notfound_msg=('Revision with (origin_id: {}, branch_name: {}' - ', ts: {}) not found.'.format(origin_id, - branch_name, ts)), - enrich_fn=utils.enrich_revision) - - -@api_route(r'/revision/(?P[0-9a-f]+)/prev/(?P[0-9a-f/]+)/', - 'revision-context') -@api_doc.route('/revision/prev/', tags=['hidden']) -@api_doc.arg('sha1_git', - default='ec72c666fb345ea5f21359b7bc063710ce558e39', - argtype=api_doc.argtypes.sha1_git, - argdoc="The revision's sha1_git identifier") -@api_doc.arg('context', - default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a', - argtype=api_doc.argtypes.path, - argdoc='The navigation breadcrumbs -- use at your own risk') -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='The metadata of the revision identified by sha1_git') -def api_revision_with_context(request, sha1_git, context): - """Return information about revision with id sha1_git. - """ - def _enrich_revision(revision, context=context): - return utils.enrich_revision(revision, context) - - return _api_lookup( - service.lookup_revision, sha1_git, - notfound_msg='Revision with sha1_git %s not found.' % sha1_git, - enrich_fn=_enrich_revision) - - -@api_route(r'/revision/(?P[0-9a-f]+)/', 'revision') -@api_doc.route('/revision/') -@api_doc.arg('sha1_git', - default='aafb16d69fd30ff58afdd69036a26047f3aebdc6', - argtype=api_doc.argtypes.sha1_git, - argdoc="revision identifier") -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=_doc_ret_revision_meta) -def api_revision(request, sha1_git): - """Get information about a revision. - - Revisions are identified by SHA1 checksums, compatible with Git commit - identifiers. See ``revision_identifier`` in our `data model module - `_ - for details about how they are computed. - - """ - return _api_lookup( - service.lookup_revision, sha1_git, - notfound_msg='Revision with sha1_git {} not found.'.format(sha1_git), - enrich_fn=utils.enrich_revision) - - -@api_route(r'/revision/(?P[0-9a-f]+)/raw/', 'revision-raw-message') -@api_doc.route('/revision/raw/', tags=['hidden'], handle_response=True) -@api_doc.arg('sha1_git', - default='ec72c666fb345ea5f21359b7bc063710ce558e39', - argtype=api_doc.argtypes.sha1_git, - argdoc="The queried revision's sha1_git identifier") -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.octet_stream, - retdoc="""The message of the revision identified by sha1_git - as a downloadable octet stream""") -def api_revision_raw_message(request, sha1_git): - """Return the raw data of the message of revision identified by sha1_git - """ - raw = service.lookup_revision_message(sha1_git) - response = HttpResponse(raw['message'], - content_type='application/octet-stream') - response['Content-disposition'] = \ - 'attachment;filename=rev_%s_raw' % sha1_git - return response - - -@api_route(r'/revision/(?P[0-9a-f]+)/directory/', - 'revision-directory') -@api_route(r'/revision/(?P[0-9a-f]+)/directory/(?P.+)/', - 'revision-directory') -@api_doc.route('/revision/directory/') -@api_doc.arg('sha1_git', - default='ec72c666fb345ea5f21359b7bc063710ce558e39', - argtype=api_doc.argtypes.sha1_git, - argdoc='revision identifier') -@api_doc.arg('dir_path', - default='Documentation/BUG-HUNTING', - argtype=api_doc.argtypes.path, - argdoc="""path relative to the root directory of revision identifier by - sha1_git""") -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""either a list of directory entries with their metadata, - or the metadata of a single directory entry""") -def api_revision_directory(request, sha1_git, - dir_path=None, - with_data=False): - """Get information about directory (entry) objects associated to revisions. - - Each revision is associated to a single "root" directory. This endpoint - behaves like ``/directory/``, but operates on the root directory associated - to a given revision. - - """ - return _revision_directory_by({'sha1_git': sha1_git}, - dir_path, request.path, - with_data=with_data) - - -@api_route(r'/revision/(?P[0-9a-f]+)/log/', 'revision-log') -@api_route(r'/revision/(?P[0-9a-f]+)' - r'/prev/(?P[0-9a-f/]+)/log/', - 'revision-log') -@api_doc.route('/revision/log/') -@api_doc.arg('sha1_git', - default='37fc9e08d0c4b71807a4f1ecb06112e78d91c283', - argtype=api_doc.argtypes.sha1_git, - argdoc='revision identifier') -@api_doc.arg('prev_sha1s', - default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a', - argtype=api_doc.argtypes.path, - argdoc="""(Optional) Navigation breadcrumbs (descendant revisions -previously visited). If multiple values, use / as delimiter. """) -@api_doc.header('Link', doc=_doc_header_link) -@api_doc.param('per_page', default=10, - argtype=api_doc.argtypes.int, - doc=_doc_arg_per_page) -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=_doc_ret_revision_log) -def api_revision_log(request, sha1_git, prev_sha1s=None): - """Get a list of all revisions heading to a given one, i.e., show the - commit log. - - """ - result = {} - per_page = int(request.query_params.get('per_page', '10')) - - def lookup_revision_log_with_limit(s, limit=per_page+1): - return service.lookup_revision_log(s, limit) - - error_msg = 'Revision with sha1_git %s not found.' % sha1_git - rev_get = _api_lookup(lookup_revision_log_with_limit, sha1_git, - notfound_msg=error_msg, - enrich_fn=utils.enrich_revision) - - l = len(rev_get) - if l == per_page+1: - rev_backward = rev_get[:-1] - new_last_sha1 = rev_get[-1]['id'] - query_params = QueryDict('', mutable=True) - - if request.query_params.get('per_page'): - query_params['per_page'] = per_page - - result['headers'] = { - 'link-next': reverse('revision-log', - kwargs={'sha1_git': new_last_sha1}) + - (('?' + query_params.urlencode()) if len(query_params) > 0 else '') - } - - else: - rev_backward = rev_get - - if not prev_sha1s: # no nav breadcrumbs, so we're done - revisions = rev_backward - - else: - rev_forward_ids = prev_sha1s.split('/') - rev_forward = _api_lookup( - service.lookup_revision_multiple, rev_forward_ids, - notfound_msg=error_msg, - enrich_fn=utils.enrich_revision) - revisions = rev_forward + rev_backward - - result.update({ - 'results': revisions - }) - return result - - -@api_route(r'/directory/(?P[0-9a-f]+)/', 'directory') -@api_route(r'/directory/(?P[0-9a-f]+)/(?P.+)/', 'directory') -@api_doc.route('/directory/') -@api_doc.arg('sha1_git', - default='1bd0e65f7d2ff14ae994de17a1e7fe65111dcad8', - argtype=api_doc.argtypes.sha1_git, - argdoc='directory identifier') -@api_doc.arg('path', - default='codec/demux', - argtype=api_doc.argtypes.path, - argdoc='path relative to directory identified by sha1_git') -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""either a list of directory entries with their metadata, - or the metadata of a single directory entry""") -def api_directory(request, sha1_git, path=None): - """Get information about directory or directory entry objects. - - Directories are identified by SHA1 checksums, compatible with Git directory - identifiers. See ``directory_identifier`` in our `data model module - `_ - for details about how they are computed. - - When given only a directory identifier, this endpoint returns information - about the directory itself, returning its content (usually a list of - directory entries). When given a directory identifier and a path, this - endpoint returns information about the directory entry pointed by the - relative path, starting path resolution from the given directory. - - """ - if path: - error_msg_path = ('Entry with path %s relative to directory ' - 'with sha1_git %s not found.') % (path, sha1_git) - return _api_lookup( - service.lookup_directory_with_path, sha1_git, path, - notfound_msg=error_msg_path, - enrich_fn=utils.enrich_directory) - else: - error_msg_nopath = 'Directory with sha1_git %s not found.' % sha1_git - return _api_lookup( - service.lookup_directory, sha1_git, - notfound_msg=error_msg_nopath, - enrich_fn=utils.enrich_directory) - - -@api_route(r'/content/(?P.+)/provenance/', 'content-provenance') -@api_doc.route('/content/provenance/', tags=['hidden']) -@api_doc.arg('q', - default='sha1_git:88b9b366facda0b5ff8d8640ee9279bed346f242', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=_doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""List of provenance information (dict) for the matched - content.""") -def api_content_provenance(request, q): - """Return content's provenance information if any. - - """ - def _enrich_revision(provenance): - p = provenance.copy() - p['revision_url'] = \ - reverse('revision', kwargs={'sha1_git': provenance['revision']}) - p['content_url'] = \ - reverse('content', - kwargs={'q': 'sha1_git:%s' % provenance['content']}) - p['origin_url'] = \ - reverse('origin', kwargs={'origin_id': provenance['origin']}) - p['origin_visits_url'] = \ - reverse('origin-visits', - kwargs={'origin_id': provenance['origin']}) - p['origin_visit_url'] = \ - reverse('origin-visit', kwargs={'origin_id': provenance['origin'], - 'visit_id': provenance['visit']}) - return p - - return _api_lookup( - service.lookup_content_provenance, q, - notfound_msg='Content with {} not found.'.format(q), - enrich_fn=_enrich_revision) - - -@api_route(r'/content/(?P.+)/filetype/', 'content-filetype') -@api_doc.route('/content/filetype/', tags=['upcoming']) -@api_doc.arg('q', - default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=_doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""Filetype information (dict) for the matched - content.""") -def api_content_filetype(request, q): - """Get information about the detected MIME type of a content object. - - """ - return _api_lookup( - service.lookup_content_filetype, q, - notfound_msg='No filetype information found for content {}.'.format(q), - enrich_fn=utils.enrich_metadata_endpoint) - - -@api_route(r'/content/(?P.+)/language/', 'content-language') -@api_doc.route('/content/language/', tags=['upcoming']) -@api_doc.arg('q', - default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=_doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""Language information (dict) for the matched - content.""") -def api_content_language(request, q): - """Get information about the detected (programming) language of a content - object. - - """ - return _api_lookup( - service.lookup_content_language, q, - notfound_msg='No language information found for content {}.'.format(q), - enrich_fn=utils.enrich_metadata_endpoint) - - -@api_route(r'/content/(?P.+)/license/', 'content-license') -@api_doc.route('/content/license/', tags=['upcoming']) -@api_doc.arg('q', - default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=_doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""License information (dict) for the matched - content.""") -def api_content_license(request, q): - """Get information about the detected license of a content object. - - """ - return _api_lookup( - service.lookup_content_license, q, - notfound_msg='No license information found for content {}.'.format(q), - enrich_fn=utils.enrich_metadata_endpoint) - - -@api_route(r'/content/(?P.+)/ctags/', 'content-ctags') -@api_doc.route('/content/ctags/', tags=['upcoming']) -@api_doc.arg('q', - default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=_doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""Ctags symbol (dict) for the matched - content.""") -def api_content_ctags(request, q): - """Get information about all `Ctags `_-style - symbols defined in a content object. - - """ - return _api_lookup( - service.lookup_content_ctags, q, - notfound_msg='No ctags symbol found for content {}.'.format(q), - enrich_fn=utils.enrich_metadata_endpoint) - - -@api_route(r'/content/(?P.+)/raw/', 'content-raw') -@api_doc.route('/content/raw/', handle_response=True) -@api_doc.arg('q', - default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=_doc_arg_content_id) -@api_doc.param('filename', default=None, - argtype=api_doc.argtypes.str, - doc='User\'s desired filename. If provided, the downloaded' - ' content will get that filename.') -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.octet_stream, - retdoc='The raw content data as an octet stream') -def api_content_raw(request, q): - """Get the raw content of a content object (AKA "blob"), as a byte - sequence. - - """ - def generate(content): - yield content['data'] - - content_raw = service.lookup_content_raw(q) - if not content_raw: - raise NotFoundExc('Content %s is not found.' % q) - - content_filetype = service.lookup_content_filetype(q) - if not content_filetype: - raise NotFoundExc('Content %s is not available for download.' % q) - - mimetype = content_filetype['mimetype'] - if 'text/' not in mimetype: - raise ForbiddenExc('Only textual content is available for download. ' - 'Actual content mimetype is %s.' % mimetype) - - filename = request.query_params.get('filename') - if not filename: - filename = 'content_%s_raw' % q.replace(':', '_') - - response = HttpResponse(generate(content_raw), - content_type='application/octet-stream') - response['Content-disposition'] = 'attachment; filename=%s' % filename - return response - - -@api_route(r'/content/(?P.+)/', 'content') -@api_doc.route('/content/') -@api_doc.arg('q', - default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=_doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""known metadata for content identified by q""") -def api_content_metadata(request, q): - """Get information about a content (AKA "blob") object. - - """ - return _api_lookup( - service.lookup_content, q, - notfound_msg='Content with {} not found.'.format(q), - enrich_fn=utils.enrich_content) - - -@api_route(r'/entity/(?P.+)/', 'entity') -@api_doc.route('/entity/', tags=['hidden']) -@api_doc.arg('uuid', - default='5f4d4c51-498a-4e28-88b3-b3e4e8396cba', - argtype=api_doc.argtypes.uuid, - argdoc="The entity's uuid identifier") -@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='The metadata of the entity identified by uuid') -def api_entity_by_uuid(request, uuid): - """Return content information if content is found. - - """ - return _api_lookup( - service.lookup_entity_by_uuid, uuid, - notfound_msg="Entity with uuid '%s' not found." % uuid, - enrich_fn=utils.enrich_entity) diff --git a/swh/web/api/views/__init__.py b/swh/web/api/views/__init__.py new file mode 100644 index 000000000..8f1783fd7 --- /dev/null +++ b/swh/web/api/views/__init__.py @@ -0,0 +1,92 @@ +# Copyright (C) 2015-2017 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 django.conf.urls import url + +from rest_framework.response import Response +from rest_framework.decorators import api_view + +from types import GeneratorType + +from swh.web.api.exc import NotFoundExc +from swh.web.api.apiurls import APIUrls, api_route + +# canned doc string snippets that are used in several doc strings +_doc_arg_content_id = """A "[hash_type:]hash" content identifier, where + hash_type is one of "sha1" (the default), "sha1_git", "sha256", and hash is + a checksum obtained with the hash_type hashing algorithm.""" +_doc_arg_last_elt = 'element to start listing from, for pagination purposes' +_doc_arg_per_page = 'number of elements to list, for pagination purposes' + +_doc_exc_bad_id = 'syntax error in the given identifier(s)' +_doc_exc_id_not_found = 'no object matching the given criteria could be found' + +_doc_ret_revision_meta = 'metadata of the revision identified by sha1_git' +_doc_ret_revision_log = """list of dictionaries representing the metadata of + each revision found in the commit log heading to revision sha1_git. + For each commit at least the following information are returned: + author/committer, authoring/commit timestamps, revision id, commit message, + parent (i.e., immediately preceding) commits, "root" directory id.""" + +_doc_header_link = """indicates that a subsequent result page is available, + pointing to it""" + + +def _api_lookup(lookup_fn, *args, + notfound_msg='Object not found', + enrich_fn=lambda x: x): + """Capture a redundant behavior of: + - looking up the backend with a criteria (be it an identifier or checksum) + passed to the function lookup_fn + - if nothing is found, raise an NotFoundExc exception with error + message notfound_msg. + - Otherwise if something is returned: + - either as list, map or generator, map the enrich_fn function to it + and return the resulting data structure as list. + - either as dict and pass to enrich_fn and return the dict enriched. + + Args: + - criteria: discriminating criteria to lookup + - lookup_fn: function expects one criteria and optional supplementary + *args. + - notfound_msg: if nothing matching the criteria is found, + raise NotFoundExc with this error message. + - enrich_fn: Function to use to enrich the result returned by + lookup_fn. Default to the identity function if not provided. + - *args: supplementary arguments to pass to lookup_fn. + + Raises: + NotFoundExp or whatever `lookup_fn` raises. + + """ + res = lookup_fn(*args) + if not res: + raise NotFoundExc(notfound_msg) + if isinstance(res, (map, list, GeneratorType)): + return [enrich_fn(x) for x in res] + return enrich_fn(res) + + +@api_view() +def api_home(request): + return Response({}, template_name='api.html') + + +APIUrls.urlpatterns.append(url(r'^$', api_home, name='homepage')) + + +@api_route(r'/', 'endpoints') +def api_endpoints(request): + """Display the list of opened api endpoints. + + """ + routes = APIUrls.get_app_endpoints().copy() + for route, doc in routes.items(): + doc['doc_intro'] = doc['docstring'].split('\n\n')[0] + # Return a list of routes with consistent ordering + env = { + 'doc_routes': sorted(routes.items()) + } + return Response(env, template_name="api-endpoints.html") diff --git a/swh/web/api/views/content.py b/swh/web/api/views/content.py new file mode 100644 index 000000000..67593acb7 --- /dev/null +++ b/swh/web/api/views/content.py @@ -0,0 +1,341 @@ +# Copyright (C) 2015-2017 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 functools + +from django.http import QueryDict +from django.urls import reverse +from django.http import HttpResponse + +from swh.web.api import service, utils +from swh.web.api import apidoc as api_doc +from swh.web.api.exc import NotFoundExc, ForbiddenExc +from swh.web.api.apiurls import api_route +from swh.web.api.views import ( + _api_lookup, _doc_exc_id_not_found, _doc_header_link, + _doc_arg_last_elt, _doc_arg_per_page, _doc_exc_bad_id, + _doc_arg_content_id +) + + +@api_route(r'/content/(?P.+)/provenance/', 'content-provenance') +@api_doc.route('/content/provenance/', tags=['hidden']) +@api_doc.arg('q', + default='sha1_git:88b9b366facda0b5ff8d8640ee9279bed346f242', + argtype=api_doc.argtypes.algo_and_hash, + argdoc=_doc_arg_content_id) +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""List of provenance information (dict) for the matched + content.""") +def api_content_provenance(request, q): + """Return content's provenance information if any. + + """ + def _enrich_revision(provenance): + p = provenance.copy() + p['revision_url'] = \ + reverse('revision', kwargs={'sha1_git': provenance['revision']}) + p['content_url'] = \ + reverse('content', + kwargs={'q': 'sha1_git:%s' % provenance['content']}) + p['origin_url'] = \ + reverse('origin', kwargs={'origin_id': provenance['origin']}) + p['origin_visits_url'] = \ + reverse('origin-visits', + kwargs={'origin_id': provenance['origin']}) + p['origin_visit_url'] = \ + reverse('origin-visit', kwargs={'origin_id': provenance['origin'], + 'visit_id': provenance['visit']}) + return p + + return _api_lookup( + service.lookup_content_provenance, q, + notfound_msg='Content with {} not found.'.format(q), + enrich_fn=_enrich_revision) + + +@api_route(r'/content/(?P.+)/filetype/', 'content-filetype') +@api_doc.route('/content/filetype/', tags=['upcoming']) +@api_doc.arg('q', + default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', + argtype=api_doc.argtypes.algo_and_hash, + argdoc=_doc_arg_content_id) +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""Filetype information (dict) for the matched + content.""") +def api_content_filetype(request, q): + """Get information about the detected MIME type of a content object. + + """ + return _api_lookup( + service.lookup_content_filetype, q, + notfound_msg='No filetype information found for content {}.'.format(q), + enrich_fn=utils.enrich_metadata_endpoint) + + +@api_route(r'/content/(?P.+)/language/', 'content-language') +@api_doc.route('/content/language/', tags=['upcoming']) +@api_doc.arg('q', + default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', + argtype=api_doc.argtypes.algo_and_hash, + argdoc=_doc_arg_content_id) +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""Language information (dict) for the matched + content.""") +def api_content_language(request, q): + """Get information about the detected (programming) language of a content + object. + + """ + return _api_lookup( + service.lookup_content_language, q, + notfound_msg='No language information found for content {}.'.format(q), + enrich_fn=utils.enrich_metadata_endpoint) + + +@api_route(r'/content/(?P.+)/license/', 'content-license') +@api_doc.route('/content/license/', tags=['upcoming']) +@api_doc.arg('q', + default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', + argtype=api_doc.argtypes.algo_and_hash, + argdoc=_doc_arg_content_id) +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""License information (dict) for the matched + content.""") +def api_content_license(request, q): + """Get information about the detected license of a content object. + + """ + return _api_lookup( + service.lookup_content_license, q, + notfound_msg='No license information found for content {}.'.format(q), + enrich_fn=utils.enrich_metadata_endpoint) + + +@api_route(r'/content/(?P.+)/ctags/', 'content-ctags') +@api_doc.route('/content/ctags/', tags=['upcoming']) +@api_doc.arg('q', + default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', + argtype=api_doc.argtypes.algo_and_hash, + argdoc=_doc_arg_content_id) +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""Ctags symbol (dict) for the matched + content.""") +def api_content_ctags(request, q): + """Get information about all `Ctags `_-style + symbols defined in a content object. + + """ + return _api_lookup( + service.lookup_content_ctags, q, + notfound_msg='No ctags symbol found for content {}.'.format(q), + enrich_fn=utils.enrich_metadata_endpoint) + + +@api_route(r'/content/(?P.+)/raw/', 'content-raw') +@api_doc.route('/content/raw/', handle_response=True) +@api_doc.arg('q', + default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', + argtype=api_doc.argtypes.algo_and_hash, + argdoc=_doc_arg_content_id) +@api_doc.param('filename', default=None, + argtype=api_doc.argtypes.str, + doc='User\'s desired filename. If provided, the downloaded' + ' content will get that filename.') +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.octet_stream, + retdoc='The raw content data as an octet stream') +def api_content_raw(request, q): + """Get the raw content of a content object (AKA "blob"), as a byte + sequence. + + """ + def generate(content): + yield content['data'] + + content_raw = service.lookup_content_raw(q) + if not content_raw: + raise NotFoundExc('Content %s is not found.' % q) + + content_filetype = service.lookup_content_filetype(q) + if not content_filetype: + raise NotFoundExc('Content %s is not available for download.' % q) + + mimetype = content_filetype['mimetype'] + if 'text/' not in mimetype: + raise ForbiddenExc('Only textual content is available for download. ' + 'Actual content mimetype is %s.' % mimetype) + + filename = request.query_params.get('filename') + if not filename: + filename = 'content_%s_raw' % q.replace(':', '_') + + response = HttpResponse(generate(content_raw), + content_type='application/octet-stream') + response['Content-disposition'] = 'attachment; filename=%s' % filename + return response + + +@api_route(r'/content/symbol/search/', 'content-symbol', methods=['POST']) +@api_route(r'/content/symbol/(?P.+)/', 'content-symbol') +@api_doc.route('/content/symbol/', tags=['upcoming']) +@api_doc.arg('q', + default='hello', + argtype=api_doc.argtypes.str, + argdoc="""An expression string to lookup in swh's raw content""") +@api_doc.header('Link', doc=_doc_header_link) +@api_doc.param('last_sha1', default=None, + argtype=api_doc.argtypes.str, + doc=_doc_arg_last_elt) +@api_doc.param('per_page', default=10, + argtype=api_doc.argtypes.int, + doc=_doc_arg_per_page) +@api_doc.returns(rettype=api_doc.rettypes.list, + retdoc="""A list of dict whose content matches the expression. + Each dict has the following keys: + - id (bytes): identifier of the content + - name (text): symbol whose content match the expression + - kind (text): kind of the symbol that matched + - lang (text): Language for that entry + - line (int): Number line for the symbol + """) +def api_content_symbol(request, q=None): + """Search content objects by `Ctags `_-style + symbol (e.g., function name, data type, method, ...). + + """ + result = {} + last_sha1 = request.query_params.get('last_sha1', None) + per_page = int(request.query_params.get('per_page', '10')) + + def lookup_exp(exp, last_sha1=last_sha1, per_page=per_page): + return service.lookup_expression(exp, last_sha1, per_page) + + symbols = _api_lookup( + lookup_exp, q, + notfound_msg="No indexed raw content match expression '{}'.".format(q), + enrich_fn=functools.partial(utils.enrich_content, top_url=True)) + + if symbols: + l = len(symbols) + + if l == per_page: + query_params = QueryDict('', mutable=True) + new_last_sha1 = symbols[-1]['sha1'] + query_params['last_sha1'] = new_last_sha1 + if request.query_params.get('per_page'): + query_params['per_page'] = per_page + + result['headers'] = { + 'link-next': reverse('content-symbol', kwargs={'q': q}) + '?' + + query_params.urlencode() + } + + result.update({ + 'results': symbols + }) + + return result + + +@api_route(r'/content/known/search/', 'content-known', methods=['POST']) +@api_route(r'/content/known/(?P(?!search).*)/', 'content-known') +@api_doc.route('/content/known/', tags=['hidden']) +@api_doc.arg('q', + default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', + argtype=api_doc.argtypes.sha1, + argdoc='content identifier as a sha1 checksum') +@api_doc.param('q', default=None, + argtype=api_doc.argtypes.str, + doc="""(POST request) An algo_hash:hash string, where algo_hash + is one of sha1, sha1_git or sha256 and hash is the hash to + search for in SWH""") +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""a dictionary with results (found/not found for each given + identifier) and statistics about how many identifiers + were found""") +def api_check_content_known(request, q=None): + """Check whether some content (AKA "blob") is present in the archive. + + Lookup can be performed by various means: + + - a GET request with one or several hashes, separated by ',' + - a POST request with one or several hashes, passed as (multiple) values + for parameter 'q' + + """ + response = {'search_res': None, + 'search_stats': None} + search_stats = {'nbfiles': 0, 'pct': 0} + search_res = None + + queries = [] + # GET: Many hash separated values request + if q: + hashes = q.split(',') + for v in hashes: + queries.append({'filename': None, 'sha1': v}) + + # POST: Many hash requests in post form submission + elif request.method == 'POST': + data = request.data + # Remove potential inputs with no associated value + for k, v in data.items(): + if v is not None: + if k == 'q' and len(v) > 0: + queries.append({'filename': None, 'sha1': v}) + elif v != '': + queries.append({'filename': k, 'sha1': v}) + + if queries: + lookup = service.lookup_multiple_hashes(queries) + result = [] + l = len(queries) + for el in lookup: + res_d = {'sha1': el['sha1'], + 'found': el['found']} + if 'filename' in el and el['filename']: + res_d['filename'] = el['filename'] + result.append(res_d) + search_res = result + nbfound = len([x for x in lookup if x['found']]) + search_stats['nbfiles'] = l + search_stats['pct'] = (nbfound / l) * 100 + + response['search_res'] = search_res + response['search_stats'] = search_stats + return response + + +@api_route(r'/content/(?P.+)/', 'content') +@api_doc.route('/content/') +@api_doc.arg('q', + default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', + argtype=api_doc.argtypes.algo_and_hash, + argdoc=_doc_arg_content_id) +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""known metadata for content identified by q""") +def api_content_metadata(request, q): + """Get information about a content (AKA "blob") object. + + """ + return _api_lookup( + service.lookup_content, q, + notfound_msg='Content with {} not found.'.format(q), + enrich_fn=utils.enrich_content) diff --git a/swh/web/api/views/directory.py b/swh/web/api/views/directory.py new file mode 100644 index 000000000..bce90159e --- /dev/null +++ b/swh/web/api/views/directory.py @@ -0,0 +1,58 @@ +# Copyright (C) 2015-2017 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 swh.web.api import service, utils +from swh.web.api import apidoc as api_doc +from swh.web.api.apiurls import api_route +from swh.web.api.views import ( + _api_lookup, _doc_exc_id_not_found, + _doc_exc_bad_id, +) + + +@api_route(r'/directory/(?P[0-9a-f]+)/', 'directory') +@api_route(r'/directory/(?P[0-9a-f]+)/(?P.+)/', 'directory') +@api_doc.route('/directory/') +@api_doc.arg('sha1_git', + default='1bd0e65f7d2ff14ae994de17a1e7fe65111dcad8', + argtype=api_doc.argtypes.sha1_git, + argdoc='directory identifier') +@api_doc.arg('path', + default='codec/demux', + argtype=api_doc.argtypes.path, + argdoc='path relative to directory identified by sha1_git') +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""either a list of directory entries with their metadata, + or the metadata of a single directory entry""") +def api_directory(request, sha1_git, path=None): + """Get information about directory or directory entry objects. + + Directories are identified by SHA1 checksums, compatible with Git directory + identifiers. See ``directory_identifier`` in our `data model module + `_ + for details about how they are computed. + + When given only a directory identifier, this endpoint returns information + about the directory itself, returning its content (usually a list of + directory entries). When given a directory identifier and a path, this + endpoint returns information about the directory entry pointed by the + relative path, starting path resolution from the given directory. + + """ + if path: + error_msg_path = ('Entry with path %s relative to directory ' + 'with sha1_git %s not found.') % (path, sha1_git) + return _api_lookup( + service.lookup_directory_with_path, sha1_git, path, + notfound_msg=error_msg_path, + enrich_fn=utils.enrich_directory) + else: + error_msg_nopath = 'Directory with sha1_git %s not found.' % sha1_git + return _api_lookup( + service.lookup_directory, sha1_git, + notfound_msg=error_msg_nopath, + enrich_fn=utils.enrich_directory) diff --git a/swh/web/api/views/entity.py b/swh/web/api/views/entity.py new file mode 100644 index 000000000..35f9e4d08 --- /dev/null +++ b/swh/web/api/views/entity.py @@ -0,0 +1,32 @@ +# Copyright (C) 2015-2017 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 swh.web.api import service, utils +from swh.web.api import apidoc as api_doc +from swh.web.api.apiurls import api_route +from swh.web.api.views import ( + _api_lookup, _doc_exc_id_not_found, + _doc_exc_bad_id +) + + +@api_route(r'/entity/(?P.+)/', 'entity') +@api_doc.route('/entity/', tags=['hidden']) +@api_doc.arg('uuid', + default='5f4d4c51-498a-4e28-88b3-b3e4e8396cba', + argtype=api_doc.argtypes.uuid, + argdoc="The entity's uuid identifier") +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc='The metadata of the entity identified by uuid') +def api_entity_by_uuid(request, uuid): + """Return content information if content is found. + + """ + return _api_lookup( + service.lookup_entity_by_uuid, uuid, + notfound_msg="Entity with uuid '%s' not found." % uuid, + enrich_fn=utils.enrich_entity) diff --git a/swh/web/api/views/origin.py b/swh/web/api/views/origin.py new file mode 100644 index 000000000..114d26653 --- /dev/null +++ b/swh/web/api/views/origin.py @@ -0,0 +1,178 @@ +# Copyright (C) 2015-2017 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 django.http import QueryDict +from django.urls import reverse + +from swh.web.api import service, utils +from swh.web.api import apidoc as api_doc +from swh.web.api.apiurls import api_route +from swh.web.api.views import ( + _api_lookup, _doc_exc_id_not_found, _doc_header_link, + _doc_arg_last_elt, _doc_arg_per_page +) + + +@api_route(r'/origin/(?P[0-9]+)/', 'origin') +@api_route(r'/origin/(?P[a-z]+)/url/(?P.+)', + 'origin') +@api_doc.route('/origin/') +@api_doc.arg('origin_id', + default=1, + argtype=api_doc.argtypes.int, + argdoc='origin identifier (when looking up by ID)') +@api_doc.arg('origin_type', + default='git', + argtype=api_doc.argtypes.str, + argdoc='origin type (when looking up by type+URL)') +@api_doc.arg('origin_url', + default='https://github.com/hylang/hy', + argtype=api_doc.argtypes.path, + argdoc='origin URL (when looking up by type+URL)') +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""The metadata of the origin corresponding to the given + criteria""") +def api_origin(request, origin_id=None, origin_type=None, origin_url=None): + """Get information about a software origin. + + Software origins might be looked up by origin type and canonical URL (e.g., + "git" + a "git clone" URL), or by their unique (but otherwise meaningless) + identifier. + + """ + ori_dict = { + 'id': origin_id, + 'type': origin_type, + 'url': origin_url + } + ori_dict = {k: v for k, v in ori_dict.items() if ori_dict[k]} + if 'id' in ori_dict: + error_msg = 'Origin with id %s not found.' % ori_dict['id'] + else: + error_msg = 'Origin with type %s and URL %s not found' % ( + ori_dict['type'], ori_dict['url']) + + def _enrich_origin(origin): + if 'id' in origin: + o = origin.copy() + o['origin_visits_url'] = \ + reverse('origin-visits', kwargs={'origin_id': origin['id']}) + return o + + return origin + + return _api_lookup( + service.lookup_origin, ori_dict, + notfound_msg=error_msg, + enrich_fn=_enrich_origin) + + +@api_route(r'/origin/(?P[0-9]+)/visits/', 'origin-visits') +@api_doc.route('/origin/visits/') +@api_doc.arg('origin_id', + default=1, + argtype=api_doc.argtypes.int, + argdoc='software origin identifier') +@api_doc.header('Link', doc=_doc_header_link) +@api_doc.param('last_visit', default=None, + argtype=api_doc.argtypes.int, + doc=_doc_arg_last_elt) +@api_doc.param('per_page', default=10, + argtype=api_doc.argtypes.int, + doc=_doc_arg_per_page) +@api_doc.returns(rettype=api_doc.rettypes.list, + retdoc="""a list of dictionaries describing individual visits. + For each visit, its identifier, timestamp (as UNIX time), + outcome, and visit-specific URL for more information are + given.""") +def api_origin_visits(request, origin_id): + """Get information about all visits of a given software origin. + + """ + result = {} + per_page = int(request.query_params.get('per_page', '10')) + last_visit = request.query_params.get('last_visit') + if last_visit: + last_visit = int(last_visit) + + def _lookup_origin_visits( + origin_id, last_visit=last_visit, per_page=per_page): + return service.lookup_origin_visits( + origin_id, last_visit=last_visit, per_page=per_page) + + def _enrich_origin_visit(origin_visit): + ov = origin_visit.copy() + ov['origin_visit_url'] = reverse('origin-visit', + kwargs={'origin_id': origin_id, + 'visit_id': ov['visit']}) + return ov + + r = _api_lookup( + _lookup_origin_visits, origin_id, + notfound_msg='No origin {} found'.format(origin_id), + enrich_fn=_enrich_origin_visit) + + if r: + l = len(r) + if l == per_page: + new_last_visit = r[-1]['visit'] + query_params = QueryDict('', mutable=True) + query_params['last_visit'] = new_last_visit + + if request.query_params.get('per_page'): + query_params['per_page'] = per_page + + result['headers'] = { + 'link-next': reverse('origin-visits', + kwargs={'origin_id': origin_id}) + + '?' + query_params.urlencode() + } + + result.update({ + 'results': r + }) + + return result + + +@api_route(r'/origin/(?P[0-9]+)/visit/(?P[0-9]+)/', + 'origin-visit') +@api_doc.route('/origin/visit/') +@api_doc.arg('origin_id', + default=1, + argtype=api_doc.argtypes.int, + argdoc='software origin identifier') +@api_doc.arg('visit_id', + default=1, + argtype=api_doc.argtypes.int, + argdoc="""visit identifier, relative to the origin identified by + origin_id""") +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""dictionary containing both metadata for the entire + visit (e.g., timestamp as UNIX time, visit outcome, etc.) and + what was at the software origin during the visit (i.e., a + mapping from branches to other archive objects)""") +def api_origin_visit(request, origin_id, visit_id): + """Get information about a specific visit of a software origin. + + """ + def _enrich_origin_visit(origin_visit): + ov = origin_visit.copy() + ov['origin_url'] = reverse('origin', + kwargs={'origin_id': ov['origin']}) + if 'occurrences' in ov: + ov['occurrences'] = { + k: utils.enrich_object(v) + for k, v in ov['occurrences'].items() + } + return ov + + return _api_lookup( + service.lookup_origin_visit, origin_id, visit_id, + notfound_msg=('No visit {} for origin {} found' + .format(visit_id, origin_id)), + enrich_fn=_enrich_origin_visit) diff --git a/swh/web/api/views/person.py b/swh/web/api/views/person.py new file mode 100644 index 000000000..be4b228ff --- /dev/null +++ b/swh/web/api/views/person.py @@ -0,0 +1,29 @@ +# Copyright (C) 2015-2017 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 swh.web.api import service +from swh.web.api import apidoc as api_doc +from swh.web.api.apiurls import api_route +from swh.web.api.views import ( + _api_lookup, _doc_exc_id_not_found, +) + + +@api_route(r'/person/(?P[0-9]+)/', 'person') +@api_doc.route('/person/') +@api_doc.arg('person_id', + default=42, + argtype=api_doc.argtypes.int, + argdoc='person identifier') +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc='The metadata of the person identified by person_id') +def api_person(request, person_id): + """Get information about a person. + + """ + return _api_lookup( + service.lookup_person, person_id, + notfound_msg='Person with id {} not found.'.format(person_id)) diff --git a/swh/web/api/views/release.py b/swh/web/api/views/release.py new file mode 100644 index 000000000..53147436e --- /dev/null +++ b/swh/web/api/views/release.py @@ -0,0 +1,37 @@ +# Copyright (C) 2015-2017 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 swh.web.api import service, utils +from swh.web.api import apidoc as api_doc +from swh.web.api.apiurls import api_route +from swh.web.api.views import ( + _api_lookup, _doc_exc_id_not_found, _doc_exc_bad_id +) + + +@api_route(r'/release/(?P[0-9a-f]+)/', 'release') +@api_doc.route('/release/') +@api_doc.arg('sha1_git', + default='7045404f3d1c54e6473c71bbb716529fbad4be24', + argtype=api_doc.argtypes.sha1_git, + argdoc='release identifier') +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc='The metadata of the release identified by sha1_git') +def api_release(request, sha1_git): + """Get information about a release. + + Releases are identified by SHA1 checksums, compatible with Git tag + identifiers. See ``release_identifier`` in our `data model module + `_ + for details about how they are computed. + + """ + error_msg = 'Release with sha1_git %s not found.' % sha1_git + return _api_lookup( + service.lookup_release, sha1_git, + notfound_msg=error_msg, + enrich_fn=utils.enrich_release) diff --git a/swh/web/api/views/revision.py b/swh/web/api/views/revision.py new file mode 100644 index 000000000..4be5cf01c --- /dev/null +++ b/swh/web/api/views/revision.py @@ -0,0 +1,420 @@ +# Copyright (C) 2015-2017 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 django.http import QueryDict +from django.urls import reverse +from django.http import HttpResponse + +from swh.web.api import service, utils +from swh.web.api import apidoc as api_doc +from swh.web.api.apiurls import api_route +from swh.web.api.views import ( + _api_lookup, _doc_exc_id_not_found, _doc_header_link, + _doc_arg_per_page, _doc_exc_bad_id, + _doc_ret_revision_log, _doc_ret_revision_meta +) + + +def _revision_directory_by(revision, path, request_path, + limit=100, with_data=False): + """Compute the revision matching criterion's directory or content data. + + Args: + revision: dictionary of criterions representing a revision to lookup + path: directory's path to lookup + request_path: request path which holds the original context to + limit: optional query parameter to limit the revisions log + (default to 100). For now, note that this limit could impede the + transitivity conclusion about sha1_git not being an ancestor of + with_data: indicate to retrieve the content's raw data if path resolves + to a content. + + """ + def enrich_directory_local(dir, context_url=request_path): + return utils.enrich_directory(dir, context_url) + + rev_id, result = service.lookup_directory_through_revision( + revision, path, limit=limit, with_data=with_data) + + content = result['content'] + if result['type'] == 'dir': # dir_entries + result['content'] = list(map(enrich_directory_local, content)) + else: # content + result['content'] = utils.enrich_content(content) + + return result + + +@api_route(r'/revision/origin/(?P[0-9]+)/log/', + 'revision-origin-log') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/ts/(?P.+)/log/', + 'revision-origin-log') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/branch/(?P.+)' + r'/ts/(?P.+)/log/', + 'revision-origin-log') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/branch/(?P.+)/log/', + 'revision-origin-log') +@api_doc.route('/revision/origin/log/') +@api_doc.arg('origin_id', + default=1, + argtype=api_doc.argtypes.int, + argdoc="The revision's SWH origin identifier") +@api_doc.arg('branch_name', + default='refs/heads/master', + argtype=api_doc.argtypes.path, + argdoc="""(Optional) The revision's branch name within the origin specified. +Defaults to 'refs/heads/master'.""") +@api_doc.arg('ts', + default='2000-01-17T11:23:54+00:00', + argtype=api_doc.argtypes.ts, + argdoc="""(Optional) A time or timestamp string to parse""") +@api_doc.header('Link', doc=_doc_header_link) +@api_doc.param('per_page', default=10, + argtype=api_doc.argtypes.int, + doc=_doc_arg_per_page) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=_doc_ret_revision_log) +def api_revision_log_by(request, origin_id, + branch_name='refs/heads/master', + ts=None): + """Show the commit log for a revision, searching for it based on software + origin, branch name, and/or visit timestamp. + + This endpoint behaves like ``/log``, but operates on the revision that + has been found at a given software origin, close to a given point in time, + pointed by a given branch. + """ + result = {} + per_page = int(request.query_params.get('per_page', '10')) + + if ts: + ts = utils.parse_timestamp(ts) + + def lookup_revision_log_by_with_limit(o_id, br, ts, limit=per_page+1): + return service.lookup_revision_log_by(o_id, br, ts, limit) + + error_msg = 'No revision matching origin %s ' % origin_id + error_msg += ', branch name %s' % branch_name + error_msg += (' and time stamp %s.' % ts) if ts else '.' + + rev_get = _api_lookup( + lookup_revision_log_by_with_limit, origin_id, branch_name, ts, + notfound_msg=error_msg, + enrich_fn=utils.enrich_revision) + l = len(rev_get) + if l == per_page+1: + revisions = rev_get[:-1] + last_sha1_git = rev_get[-1]['id'] + + params = {k: v for k, v in {'origin_id': origin_id, + 'branch_name': branch_name, + 'ts': ts, + }.items() if v is not None} + + query_params = QueryDict('', mutable=True) + query_params['sha1_git'] = last_sha1_git + + if request.query_params.get('per_page'): + query_params['per_page'] = per_page + + result['headers'] = { + 'link-next': reverse('revision-origin-log', kwargs=params) + + (('?' + query_params.urlencode()) if len(query_params) > 0 else '') + } + + else: + revisions = rev_get + + result.update({'results': revisions}) + + return result + + +@api_route(r'/revision/origin/(?P[0-9]+)/directory/', + 'revision-directory') +@api_route(r'/revision/origin/(?P[0-9]+)/directory/(?P.+)/', + 'revision-directory') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/branch/(?P.+)/directory/', + 'revision-directory') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/branch/(?P.+)/ts/(?P.+)/directory/', + 'revision-directory') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/branch/(?P.+)/directory/(?P.+)/', + 'revision-directory') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/branch/(?P.+)/ts/(?P.+)' + r'/directory/(?P.+)/', + 'revision-directory') +@api_doc.route('/revision/origin/directory/', tags=['hidden']) +@api_doc.arg('origin_id', + default=1, + argtype=api_doc.argtypes.int, + argdoc="The revision's origin's SWH identifier") +@api_doc.arg('branch_name', + default='refs/heads/master', + argtype=api_doc.argtypes.path, + argdoc="""The optional branch for the given origin (default + to master""") +@api_doc.arg('ts', + default='2000-01-17T11:23:54+00:00', + argtype=api_doc.argtypes.ts, + argdoc="""Optional timestamp (default to the nearest time + crawl of timestamp)""") +@api_doc.arg('path', + default='Dockerfile', + argtype=api_doc.argtypes.path, + argdoc='The path to the directory or file to display') +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""The metadata of the revision corresponding to the + given criteria""") +def api_directory_through_revision_origin(request, origin_id, + branch_name="refs/heads/master", + ts=None, + path=None, + with_data=False): + """Display directory or content information through a revision identified + by origin/branch/timestamp. + """ + if ts: + ts = utils.parse_timestamp(ts) + + return _revision_directory_by({'origin_id': origin_id, + 'branch_name': branch_name, + 'ts': ts + }, + path, request.path, + with_data=with_data) + + +@api_route(r'/revision/origin/(?P[0-9]+)/', + 'revision-origin') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/branch/(?P.+)/', + 'revision-origin') +@api_route(r'/revision/origin/(?P[0-9]+)' + r'/branch/(?P.+)/ts/(?P.+)/', + 'revision-origin') +@api_route(r'/revision/origin/(?P[0-9]+)/ts/(?P.+)/', + 'revision-origin') +@api_doc.route('/revision/origin/') +@api_doc.arg('origin_id', + default=1, + argtype=api_doc.argtypes.int, + argdoc='software origin identifier') +@api_doc.arg('branch_name', + default='refs/heads/master', + argtype=api_doc.argtypes.path, + argdoc="""(optional) fully-qualified branch name, e.g., + "refs/heads/master". Defaults to the master branch.""") +@api_doc.arg('ts', + default=None, + argtype=api_doc.argtypes.ts, + argdoc="""(optional) timestamp close to which the revision pointed by + the given branch should be looked up. Defaults to now.""") +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=_doc_ret_revision_meta) +def api_revision_with_origin(request, origin_id, + branch_name="refs/heads/master", + ts=None): + """Get information about a revision, searching for it based on software + origin, branch name, and/or visit timestamp. + + This endpoint behaves like ``/revision``, but operates on the revision that + has been found at a given software origin, close to a given point in time, + pointed by a given branch. + + """ + ts = utils.parse_timestamp(ts) + return _api_lookup( + service.lookup_revision_by, origin_id, branch_name, ts, + notfound_msg=('Revision with (origin_id: {}, branch_name: {}' + ', ts: {}) not found.'.format(origin_id, + branch_name, ts)), + enrich_fn=utils.enrich_revision) + + +@api_route(r'/revision/(?P[0-9a-f]+)/prev/(?P[0-9a-f/]+)/', + 'revision-context') +@api_doc.route('/revision/prev/', tags=['hidden']) +@api_doc.arg('sha1_git', + default='ec72c666fb345ea5f21359b7bc063710ce558e39', + argtype=api_doc.argtypes.sha1_git, + argdoc="The revision's sha1_git identifier") +@api_doc.arg('context', + default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a', + argtype=api_doc.argtypes.path, + argdoc='The navigation breadcrumbs -- use at your own risk') +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc='The metadata of the revision identified by sha1_git') +def api_revision_with_context(request, sha1_git, context): + """Return information about revision with id sha1_git. + """ + def _enrich_revision(revision, context=context): + return utils.enrich_revision(revision, context) + + return _api_lookup( + service.lookup_revision, sha1_git, + notfound_msg='Revision with sha1_git %s not found.' % sha1_git, + enrich_fn=_enrich_revision) + + +@api_route(r'/revision/(?P[0-9a-f]+)/', 'revision') +@api_doc.route('/revision/') +@api_doc.arg('sha1_git', + default='aafb16d69fd30ff58afdd69036a26047f3aebdc6', + argtype=api_doc.argtypes.sha1_git, + argdoc="revision identifier") +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=_doc_ret_revision_meta) +def api_revision(request, sha1_git): + """Get information about a revision. + + Revisions are identified by SHA1 checksums, compatible with Git commit + identifiers. See ``revision_identifier`` in our `data model module + `_ + for details about how they are computed. + + """ + return _api_lookup( + service.lookup_revision, sha1_git, + notfound_msg='Revision with sha1_git {} not found.'.format(sha1_git), + enrich_fn=utils.enrich_revision) + + +@api_route(r'/revision/(?P[0-9a-f]+)/raw/', 'revision-raw-message') +@api_doc.route('/revision/raw/', tags=['hidden'], handle_response=True) +@api_doc.arg('sha1_git', + default='ec72c666fb345ea5f21359b7bc063710ce558e39', + argtype=api_doc.argtypes.sha1_git, + argdoc="The queried revision's sha1_git identifier") +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.octet_stream, + retdoc="""The message of the revision identified by sha1_git + as a downloadable octet stream""") +def api_revision_raw_message(request, sha1_git): + """Return the raw data of the message of revision identified by sha1_git + """ + raw = service.lookup_revision_message(sha1_git) + response = HttpResponse(raw['message'], + content_type='application/octet-stream') + response['Content-disposition'] = \ + 'attachment;filename=rev_%s_raw' % sha1_git + return response + + +@api_route(r'/revision/(?P[0-9a-f]+)/directory/', + 'revision-directory') +@api_route(r'/revision/(?P[0-9a-f]+)/directory/(?P.+)/', + 'revision-directory') +@api_doc.route('/revision/directory/') +@api_doc.arg('sha1_git', + default='ec72c666fb345ea5f21359b7bc063710ce558e39', + argtype=api_doc.argtypes.sha1_git, + argdoc='revision identifier') +@api_doc.arg('dir_path', + default='Documentation/BUG-HUNTING', + argtype=api_doc.argtypes.path, + argdoc="""path relative to the root directory of revision identifier by + sha1_git""") +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""either a list of directory entries with their metadata, + or the metadata of a single directory entry""") +def api_revision_directory(request, sha1_git, + dir_path=None, + with_data=False): + """Get information about directory (entry) objects associated to revisions. + + Each revision is associated to a single "root" directory. This endpoint + behaves like ``/directory/``, but operates on the root directory associated + to a given revision. + + """ + return _revision_directory_by({'sha1_git': sha1_git}, + dir_path, request.path, + with_data=with_data) + + +@api_route(r'/revision/(?P[0-9a-f]+)/log/', 'revision-log') +@api_route(r'/revision/(?P[0-9a-f]+)' + r'/prev/(?P[0-9a-f/]+)/log/', + 'revision-log') +@api_doc.route('/revision/log/') +@api_doc.arg('sha1_git', + default='37fc9e08d0c4b71807a4f1ecb06112e78d91c283', + argtype=api_doc.argtypes.sha1_git, + argdoc='revision identifier') +@api_doc.arg('prev_sha1s', + default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a', + argtype=api_doc.argtypes.path, + argdoc="""(Optional) Navigation breadcrumbs (descendant revisions +previously visited). If multiple values, use / as delimiter. """) +@api_doc.header('Link', doc=_doc_header_link) +@api_doc.param('per_page', default=10, + argtype=api_doc.argtypes.int, + doc=_doc_arg_per_page) +@api_doc.raises(exc=api_doc.excs.badinput, doc=_doc_exc_bad_id) +@api_doc.raises(exc=api_doc.excs.notfound, doc=_doc_exc_id_not_found) +@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=_doc_ret_revision_log) +def api_revision_log(request, sha1_git, prev_sha1s=None): + """Get a list of all revisions heading to a given one, i.e., show the + commit log. + + """ + result = {} + per_page = int(request.query_params.get('per_page', '10')) + + def lookup_revision_log_with_limit(s, limit=per_page+1): + return service.lookup_revision_log(s, limit) + + error_msg = 'Revision with sha1_git %s not found.' % sha1_git + rev_get = _api_lookup(lookup_revision_log_with_limit, sha1_git, + notfound_msg=error_msg, + enrich_fn=utils.enrich_revision) + + l = len(rev_get) + if l == per_page+1: + rev_backward = rev_get[:-1] + new_last_sha1 = rev_get[-1]['id'] + query_params = QueryDict('', mutable=True) + + if request.query_params.get('per_page'): + query_params['per_page'] = per_page + + result['headers'] = { + 'link-next': reverse('revision-log', + kwargs={'sha1_git': new_last_sha1}) + + (('?' + query_params.urlencode()) if len(query_params) > 0 else '') + } + + else: + rev_backward = rev_get + + if not prev_sha1s: # no nav breadcrumbs, so we're done + revisions = rev_backward + + else: + rev_forward_ids = prev_sha1s.split('/') + rev_forward = _api_lookup( + service.lookup_revision_multiple, rev_forward_ids, + notfound_msg=error_msg, + enrich_fn=utils.enrich_revision) + revisions = rev_forward + rev_backward + + result.update({ + 'results': revisions + }) + return result diff --git a/swh/web/api/views/stat.py b/swh/web/api/views/stat.py new file mode 100644 index 000000000..8fb585496 --- /dev/null +++ b/swh/web/api/views/stat.py @@ -0,0 +1,20 @@ +# Copyright (C) 2015-2017 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 swh.web.api import service +from swh.web.api import apidoc as api_doc +from swh.web.api.apiurls import api_route + + +@api_route(r'/stat/counters/', 'stat-counters') +@api_doc.route('/stat/counters/', noargs=True) +@api_doc.returns(rettype=api_doc.rettypes.dict, + retdoc="""dictionary mapping object types to the amount of + corresponding objects currently available in the archive""") +def api_stats(request): + """Get statistics about the content of the archive. + + """ + return service.stat_counters()