diff --git a/swh/web/api/apiresponse.py b/swh/web/api/apiresponse.py --- a/swh/web/api/apiresponse.py +++ b/swh/web/api/apiresponse.py @@ -18,7 +18,7 @@ from swh.web.config import get_config -def compute_link_header(request, rv, options): +def compute_link_header(rv, options): """Add Link header in returned value results. Args: @@ -43,11 +43,9 @@ rv_headers = rv['headers'] if 'link-next' in rv_headers: - link_headers.append('<%s>; rel="next"' % ( - request.build_absolute_uri(rv_headers['link-next']))) + link_headers.append('<%s>; rel="next"' % rv_headers['link-next']) if 'link-prev' in rv_headers: - link_headers.append('<%s>; rel="previous"' % ( - request.build_absolute_uri(rv_headers['link-prev']))) + link_headers.append('<%s>; rel="previous"' % rv_headers['link-prev']) if link_headers: link_header_str = ','.join(link_headers) @@ -109,7 +107,7 @@ """ if data: - options['headers'] = compute_link_header(request, data, options) + options['headers'] = compute_link_header(data, options) data = transform(data) data = filter_by_fields(request, data) doc_env = doc_data @@ -135,11 +133,6 @@ indent=4, separators=(',', ': ')) doc_env['response_data'] = data - doc_env['request'] = { - 'path': request.path, - 'method': request.method, - 'absolute_uri': request.build_absolute_uri(), - } doc_env['heading'] = shorten_path(str(request.path)) if 'route' in doc_env: diff --git a/swh/web/api/utils.py b/swh/web/api/utils.py --- a/swh/web/api/utils.py +++ b/swh/web/api/utils.py @@ -3,8 +3,11 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +from typing import Dict, Optional, Any -from swh.web.common.utils import reverse +from django.http import HttpRequest + +from swh.web.common.utils import reverse, resolve_branch_alias from swh.web.common.query import parse_hash @@ -41,78 +44,100 @@ return ''.join([person['name'], ' <', person['email'], '>']) -def enrich_object(object): +def enrich_object(object: Dict[str, str], + request: Optional[HttpRequest] = None) -> Dict[str, str]: """Enrich an object (revision, release) with link to the 'target' of type 'target_type'. Args: object: An object with target and target_type keys (e.g. release, revision) + request: Absolute URIs will be generated if provided Returns: - Object enriched with target_url pointing to the right - swh.web.ui.api urls for the pointing object (revision, - release, content, directory) + Object enriched with target object url (revision, release, content, + directory) """ - obj = object.copy() - if 'target' in obj and 'target_type' in obj: - if obj['target_type'] in ('revision', 'release', 'directory'): - obj['target_url'] = \ - reverse('api-1-%s' % obj['target_type'], - url_args={'sha1_git': obj['target']}) - elif obj['target_type'] == 'content': - obj['target_url'] = \ - reverse('api-1-content', - url_args={'q': 'sha1_git:' + obj['target']}) - elif obj['target_type'] == 'snapshot': - obj['target_url'] = \ - reverse('api-1-snapshot', - url_args={'snapshot_id': obj['target']}) - - return obj + if 'target' in object and 'target_type' in object: + if object['target_type'] in ('revision', 'release', 'directory'): + object['target_url'] = reverse( + 'api-1-%s' % object['target_type'], + url_args={'sha1_git': object['target']}, + request=request) + elif object['target_type'] == 'content': + object['target_url'] = reverse( + 'api-1-content', + url_args={'q': 'sha1_git:' + object['target']}, + request=request) + elif object['target_type'] == 'snapshot': + object['target_url'] = reverse( + 'api-1-snapshot', + url_args={'snapshot_id': object['target']}, + request=request) + + return object enrich_release = enrich_object -def enrich_directory(directory, context_url=None): +def enrich_directory(directory: Dict[str, str], + request: Optional[HttpRequest] = None) -> Dict[str, str]: """Enrich directory with url to content or directory. + Args: + directory: dict of data associated to a swh directory object + request: Absolute URIs will be generated if provided + + Returns: + An enriched directory dict filled with additional urls """ if 'type' in directory: target_type = directory['type'] target = directory['target'] if target_type == 'file': directory['target_url'] = reverse( - 'api-1-content', url_args={'q': 'sha1_git:%s' % target}) - if context_url: - directory['file_url'] = context_url + directory['name'] + '/' + 'api-1-content', + url_args={'q': 'sha1_git:%s' % target}, + request=request) elif target_type == 'dir': directory['target_url'] = reverse( - 'api-1-directory', url_args={'sha1_git': target}) - if context_url: - directory['dir_url'] = context_url + directory['name'] + '/' + 'api-1-directory', + url_args={'sha1_git': target}, + request=request) else: directory['target_url'] = reverse( - 'api-1-revision', url_args={'sha1_git': target}) - if context_url: - directory['rev_url'] = context_url + directory['name'] + '/' + 'api-1-revision', + url_args={'sha1_git': target}, + request=request) return directory -def enrich_metadata_endpoint(content): - """Enrich metadata endpoint with link to the upper metadata endpoint. +def enrich_metadata_endpoint(content_metadata: Dict[str, str], + request: Optional[HttpRequest] = None + ) -> Dict[str, str]: + """Enrich content metadata dict with link to the upper metadata endpoint. + Args: + content_metadata: dict of data associated to a swh content metadata + request: Absolute URIs will be generated if provided + + Returns: + An enriched content metadata dict filled with an additional url """ - c = content.copy() + c = content_metadata c['content_url'] = reverse('api-1-content', - url_args={'q': 'sha1:%s' % c['id']}) + url_args={'q': 'sha1:%s' % c['id']}, + request=request) return c -def enrich_content(content, top_url=False, query_string=None): +def enrich_content(content: Dict[str, Any], + top_url: Optional[bool] = False, + query_string: Optional[str] = None, + request: Optional[HttpRequest] = None) -> Dict[str, str]: """Enrich content with links to: - data_url: its raw data - filetype_url: its filetype information @@ -127,6 +152,7 @@ used when requesting the content, it acts as a hint for picking the same hash method when computing the url listed above + request: Absolute URIs will be generated if provided Returns: An enriched content dict filled with additional urls @@ -143,40 +169,59 @@ if top_url: content['content_url'] = reverse( 'api-1-content', url_args={'q': q}) - content['data_url'] = reverse('api-1-content-raw', url_args={'q': q}) + content['data_url'] = reverse('api-1-content-raw', + url_args={'q': q}, + request=request) content['filetype_url'] = reverse( - 'api-1-content-filetype', url_args={'q': q}) + 'api-1-content-filetype', + url_args={'q': q}, + request=request) content['language_url'] = reverse( - 'api-1-content-language', url_args={'q': q}) + 'api-1-content-language', + url_args={'q': q}, + request=request) content['license_url'] = reverse( - 'api-1-content-license', url_args={'q': q}) + 'api-1-content-license', + url_args={'q': q}, + request=request) return content -def enrich_revision(revision): +def enrich_revision(revision: Dict[str, Any], + request: Optional[HttpRequest] = None) -> Dict[str, Any]: """Enrich revision with links where it makes sense (directory, parents). Keep track of the navigation breadcrumbs if they are specified. Args: revision: the revision as a dict + request: Absolute URIs will be generated if provided + + Returns: + An enriched revision dict filled with additional urls """ revision['url'] = reverse('api-1-revision', - url_args={'sha1_git': revision['id']}) + url_args={'sha1_git': revision['id']}, + request=request) revision['history_url'] = reverse('api-1-revision-log', - url_args={'sha1_git': revision['id']}) + url_args={'sha1_git': revision['id']}, + request=request) if 'directory' in revision: revision['directory_url'] = reverse( - 'api-1-directory', url_args={'sha1_git': revision['directory']}) + 'api-1-directory', + url_args={'sha1_git': revision['directory']}, + request=request) if 'parents' in revision: parents = [] for parent in revision['parents']: parents.append({ 'id': parent, - 'url': reverse('api-1-revision', url_args={'sha1_git': parent}) + 'url': reverse('api-1-revision', + url_args={'sha1_git': parent}, + request=request) }) revision['parents'] = parents @@ -185,12 +230,95 @@ children = [] for child in revision['children']: children.append(reverse( - 'api-1-revision', url_args={'sha1_git': child})) + 'api-1-revision', + url_args={'sha1_git': child}, + request=request)) revision['children_urls'] = children if 'message_decoding_failed' in revision: - revision['message_url'] = \ - reverse('api-1-revision-raw-message', - url_args={'sha1_git': revision['id']}) + revision['message_url'] = reverse( + 'api-1-revision-raw-message', + url_args={'sha1_git': revision['id']}, + request=request) return revision + + +def enrich_snapshot(snapshot: Dict[str, Any], + request: Optional[HttpRequest] = None) -> Dict[str, Any]: + """Enrich snapshot with links to the branch targets + + Args: + snapshot: the snapshot as a dict + request: Absolute URIs will be generated if provided + + Returns: + An enriched snapshot dict filled with additional urls + """ + if 'branches' in snapshot: + snapshot['branches'] = { + k: enrich_object(v, request) if v else None + for k, v in snapshot['branches'].items() + } + for k, v in snapshot['branches'].items(): + if v and v['target_type'] == 'alias': + branch = resolve_branch_alias(snapshot, v) + if branch: + branch = enrich_object(branch, request) + v['target_url'] = branch['target_url'] + return snapshot + + +def enrich_origin(origin: Dict[str, Any], + request: Optional[HttpRequest] = None) -> Dict[str, Any]: + """Enrich origin dict with link to its visits + + Args: + origin: the origin as a dict + request: Absolute URIs will be generated if provided + + Returns: + An enriched origin dict filled with an additional url + """ + if 'url' in origin: + origin['origin_visits_url'] = reverse( + 'api-1-origin-visits', + url_args={'origin_url': origin['url']}, + request=request) + + return origin + + +def enrich_origin_visit(origin_visit: Dict[str, Any], *, + with_origin_link: bool, with_origin_visit_link: bool, + request: Optional[HttpRequest] = None + ) -> Dict[str, Any]: + """Enrich origin visit dict with additional links + + Args: + origin_visit: the origin visit as a dict + with_origin_link: whether to add link to origin + with_origin_visit_link: whether to add link to origin visit + request: Absolute URIs will be generated if provided + + Returns: + An enriched origin visit dict filled with additional urls + """ + ov = origin_visit + if with_origin_link: + ov['origin_url'] = reverse('api-1-origin', + url_args={'origin_url': ov['origin']}, + request=request) + if with_origin_visit_link: + ov['origin_visit_url'] = reverse('api-1-origin-visit', + url_args={'origin_url': ov['origin'], + 'visit_id': ov['visit']}, + request=request) + snapshot = ov['snapshot'] + if snapshot: + ov['snapshot_url'] = reverse('api-1-snapshot', + url_args={'snapshot_id': snapshot}, + request=request) + else: + ov['snapshot_url'] = None + return ov diff --git a/swh/web/api/views/content.py b/swh/web/api/views/content.py --- a/swh/web/api/views/content.py +++ b/swh/web/api/views/content.py @@ -57,7 +57,8 @@ return api_lookup( service.lookup_content_filetype, q, notfound_msg='No filetype information found for content {}.'.format(q), - enrich_fn=utils.enrich_metadata_endpoint) + enrich_fn=utils.enrich_metadata_endpoint, + request=request) @api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/language/', @@ -102,7 +103,8 @@ return api_lookup( service.lookup_content_language, q, notfound_msg='No language information found for content {}.'.format(q), - enrich_fn=utils.enrich_metadata_endpoint) + enrich_fn=utils.enrich_metadata_endpoint, + request=request) @api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/license/', @@ -145,7 +147,8 @@ return api_lookup( service.lookup_content_license, q, notfound_msg='No license information found for content {}.'.format(q), - enrich_fn=utils.enrich_metadata_endpoint) + enrich_fn=utils.enrich_metadata_endpoint, + request=request) @api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/ctags/', @@ -159,7 +162,8 @@ return api_lookup( service.lookup_content_ctags, q, notfound_msg='No ctags symbol found for content {}.'.format(q), - enrich_fn=utils.enrich_metadata_endpoint) + enrich_fn=utils.enrich_metadata_endpoint, + request=request) @api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/raw/', 'api-1-content-raw', @@ -228,7 +232,8 @@ 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)) + enrich_fn=functools.partial(utils.enrich_content, top_url=True), + request=request) if symbols: nb_symbols = len(symbols) @@ -242,7 +247,8 @@ result['headers'] = { 'link-next': reverse('api-1-content-symbol', url_args={'q': q}, - query_params=query_params) + query_params=query_params, + request=request) } result.update({ @@ -374,4 +380,5 @@ return api_lookup( service.lookup_content, q, notfound_msg='Content with {} not found.'.format(q), - enrich_fn=functools.partial(utils.enrich_content, query_string=q)) + enrich_fn=functools.partial(utils.enrich_content, query_string=q), + request=request) diff --git a/swh/web/api/views/directory.py b/swh/web/api/views/directory.py --- a/swh/web/api/views/directory.py +++ b/swh/web/api/views/directory.py @@ -67,10 +67,12 @@ return api_lookup( service.lookup_directory_with_path, sha1_git, path, notfound_msg=error_msg_path, - enrich_fn=utils.enrich_directory) + enrich_fn=utils.enrich_directory, + request=request) 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) + enrich_fn=utils.enrich_directory, + request=request) diff --git a/swh/web/api/views/identifiers.py b/swh/web/api/views/identifiers.py --- a/swh/web/api/views/identifiers.py +++ b/swh/web/api/views/identifiers.py @@ -58,5 +58,6 @@ service.lookup_object(object_type, object_id) # id is well-formed and the pointed object exists swh_id_data = swh_id_parsed._asdict() - swh_id_data['browse_url'] = swh_id_resolved['browse_url'] + swh_id_data['browse_url'] = request.build_absolute_uri( + swh_id_resolved['browse_url']) return swh_id_data diff --git a/swh/web/api/views/origin.py b/swh/web/api/views/origin.py --- a/swh/web/api/views/origin.py +++ b/swh/web/api/views/origin.py @@ -12,6 +12,7 @@ from swh.web.common.utils import reverse from swh.web.api.apidoc import api_doc, format_docstring from swh.web.api.apiurls import api_route +from swh.web.api.utils import enrich_origin, enrich_origin_visit from swh.web.api.views.utils import api_lookup @@ -54,35 +55,6 @@ ''' -def _enrich_origin(origin): - if 'url' in origin: - o = origin.copy() - o['origin_visits_url'] = reverse( - 'api-1-origin-visits', url_args={'origin_url': origin['url']}) - return o - - return origin - - -def _enrich_origin_visit(origin_visit, *, - with_origin_link, with_origin_visit_link): - ov = origin_visit.copy() - if with_origin_link: - ov['origin_url'] = reverse('api-1-origin', - url_args={'origin_url': ov['origin']}) - if with_origin_visit_link: - ov['origin_visit_url'] = reverse('api-1-origin-visit', - url_args={'origin_url': ov['origin'], - 'visit_id': ov['visit']}) - snapshot = ov['snapshot'] - if snapshot: - ov['snapshot_url'] = reverse('api-1-snapshot', - url_args={'snapshot_id': snapshot}) - else: - ov['snapshot_url'] = None - return ov - - @api_route(r'/origins/', 'api-1-origins') @api_doc('/origins/', noargs=True) @format_docstring(return_origin_array=DOC_RETURN_ORIGIN_ARRAY) @@ -123,14 +95,16 @@ origin_count = min(origin_count, 10000) results = api_lookup( service.lookup_origins, origin_from, origin_count+1, - enrich_fn=_enrich_origin) + enrich_fn=enrich_origin, + request=request) response = {'results': results, 'headers': {}} if len(results) > origin_count: origin_from = results.pop()['id'] response['headers']['link-next'] = reverse( 'api-1-origins', query_params={'origin_from': origin_from, - 'origin_count': origin_count}) + 'origin_count': origin_count}, + request=request) return response @@ -171,7 +145,8 @@ return api_lookup( service.lookup_origin, ori_dict, notfound_msg=error_msg, - enrich_fn=_enrich_origin) + enrich_fn=enrich_origin, + request=request) @api_route(r'/origin/search/(?P.+)/', @@ -224,7 +199,7 @@ (results, page_token) = api_lookup( service.search_origin, url_pattern, limit, bool(strtobool(with_visit)), page_token, - enrich_fn=_enrich_origin) + enrich_fn=enrich_origin, request=request) if page_token is not None: query_params = {} @@ -234,7 +209,8 @@ result['headers'] = { 'link-next': reverse('api-1-origin-search', url_args={'url_pattern': url_pattern}, - query_params=query_params) + query_params=query_params, + request=request) } result.update({ @@ -284,7 +260,8 @@ content = '"fulltext" must be provided and non-empty.' raise BadInputExc(content) - results = api_lookup(service.search_origin_metadata, fulltext, limit) + results = api_lookup(service.search_origin_metadata, fulltext, limit, + request=request) return { 'results': results, @@ -353,9 +330,10 @@ results = api_lookup(_lookup_origin_visits, origin_query, notfound_msg=notfound_msg, - enrich_fn=partial(_enrich_origin_visit, + enrich_fn=partial(enrich_origin_visit, with_origin_link=False, - with_origin_visit_link=True)) + with_origin_visit_link=True), + request=request) if results: nb_results = len(results) @@ -370,7 +348,8 @@ result['headers'] = { 'link-next': reverse('api-1-origin-visits', url_args=url_args_next, - query_params=query_params) + query_params=query_params, + request=request) } result.update({ @@ -418,9 +397,10 @@ bool(strtobool(require_snapshot)), notfound_msg=('No visit for origin {} found' .format(origin_url)), - enrich_fn=partial(_enrich_origin_visit, + enrich_fn=partial(enrich_origin_visit, with_origin_link=True, - with_origin_visit_link=False)) + with_origin_visit_link=False), + request=request) @api_route(r'/origin/(?P.*)/visit/(?P[0-9]+)/', @@ -457,9 +437,10 @@ service.lookup_origin_visit, origin_url, int(visit_id), notfound_msg=('No visit {} for origin {} found' .format(visit_id, origin_url)), - enrich_fn=partial(_enrich_origin_visit, + enrich_fn=partial(enrich_origin_visit, with_origin_link=True, - with_origin_visit_link=False)) + with_origin_visit_link=False), + request=request) @api_route(r'/origin/(?P.+)' @@ -499,4 +480,5 @@ return api_lookup( service.lookup_origin_intrinsic_metadata, ori_dict, notfound_msg=error_msg, - enrich_fn=_enrich_origin) + enrich_fn=enrich_origin, + request=request) diff --git a/swh/web/api/views/release.py b/swh/web/api/views/release.py --- a/swh/web/api/views/release.py +++ b/swh/web/api/views/release.py @@ -56,4 +56,5 @@ return api_lookup( service.lookup_release, sha1_git, notfound_msg=error_msg, - enrich_fn=utils.enrich_release) + enrich_fn=utils.enrich_release, + request=request) diff --git a/swh/web/api/views/revision.py b/swh/web/api/views/revision.py --- a/swh/web/api/views/revision.py +++ b/swh/web/api/views/revision.py @@ -110,7 +110,8 @@ return api_lookup( service.lookup_revision, sha1_git, notfound_msg='Revision with sha1_git {} not found.'.format(sha1_git), - enrich_fn=utils.enrich_revision) + enrich_fn=utils.enrich_revision, + request=request) @api_route(r'/revision/(?P[0-9a-f]+)/raw/', @@ -219,7 +220,8 @@ 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) + enrich_fn=utils.enrich_revision, + request=request) nb_rev = len(rev_get) if nb_rev == per_page+1: @@ -233,7 +235,8 @@ result['headers'] = { 'link-next': reverse('api-1-revision-log', url_args={'sha1_git': new_last_sha1}, - query_params=query_params) + query_params=query_params, + request=request) } else: @@ -247,7 +250,8 @@ rev_forward = api_lookup( service.lookup_revision_multiple, rev_forward_ids, notfound_msg=error_msg, - enrich_fn=utils.enrich_revision) + enrich_fn=utils.enrich_revision, + request=request) revisions = rev_forward + rev_backward result.update({ diff --git a/swh/web/api/views/snapshot.py b/swh/web/api/views/snapshot.py --- a/swh/web/api/views/snapshot.py +++ b/swh/web/api/views/snapshot.py @@ -7,8 +7,8 @@ from swh.web.common.utils import reverse from swh.web.config import get_config from swh.web.api.apidoc import api_doc, format_docstring -from swh.web.api import utils from swh.web.api.apiurls import api_route +from swh.web.api.utils import enrich_snapshot from swh.web.api.views.utils import api_lookup @@ -68,35 +68,6 @@ :swh_web_api:`snapshot/6a3a2cf0b2b90ce7ae1cf0a221ed68035b686f5a/` """ - def _resolve_alias(snapshot, branch): - while branch and branch['target_type'] == 'alias': - if branch['target'] in snapshot['branches']: - branch = snapshot['branches'][branch['target']] - else: - snp = service.lookup_snapshot( - snapshot['id'], branches_from=branch['target'], - branches_count=1) - if snp and branch['target'] in snp['branches']: - branch = snp['branches'][branch['target']] - else: - branch = None - return branch - - def _enrich_snapshot(snapshot): - s = snapshot.copy() - if 'branches' in s: - s['branches'] = { - k: utils.enrich_object(v) if v else None - for k, v in s['branches'].items() - } - for k, v in s['branches'].items(): - if v and v['target_type'] == 'alias': - branch = _resolve_alias(snapshot, v) - if branch: - branch = utils.enrich_object(branch) - v['target_url'] = branch['target_url'] - return s - snapshot_content_max_size = get_config()['snapshot_content_max_size'] branches_from = request.GET.get('branches_from', '') @@ -109,16 +80,18 @@ service.lookup_snapshot, snapshot_id, branches_from, branches_count, target_types, notfound_msg='Snapshot with id {} not found.'.format(snapshot_id), - enrich_fn=_enrich_snapshot) + enrich_fn=enrich_snapshot, + request=request) response = {'results': results, 'headers': {}} if results['next_branch'] is not None: - response['headers']['link-next'] = \ - reverse('api-1-snapshot', - url_args={'snapshot_id': snapshot_id}, - query_params={'branches_from': results['next_branch'], - 'branches_count': branches_count, - 'target_types': target_types}) + response['headers']['link-next'] = reverse( + 'api-1-snapshot', + url_args={'snapshot_id': snapshot_id}, + query_params={'branches_from': results['next_branch'], + 'branches_count': branches_count, + 'target_types': target_types}, + request=request) return response diff --git a/swh/web/api/views/utils.py b/swh/web/api/views/utils.py --- a/swh/web/api/views/utils.py +++ b/swh/web/api/views/utils.py @@ -3,18 +3,28 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +from types import GeneratorType +from typing import Callable, Any, Optional, Mapping, Dict +from typing_extensions import Protocol + +from django.http import HttpRequest + from rest_framework.response import Response from rest_framework.decorators import api_view -from types import GeneratorType - from swh.web.common.exc import NotFoundExc from swh.web.api.apiurls import APIUrls, api_route -def api_lookup(lookup_fn, *args, - notfound_msg='Object not found', - enrich_fn=None): +class EnrichFunction(Protocol): + def __call__(self, input: Mapping[str, str], + request: Optional[HttpRequest]) -> Dict[str, str]: ... + + +def api_lookup(lookup_fn: Callable[..., Any], *args: Any, + notfound_msg: Optional[str] = 'Object not found', + enrich_fn: Optional[EnrichFunction] = None, + request: Optional[HttpRequest] = None): r""" Capture a redundant behavior of: - looking up the backend with a criteria (be it an identifier or @@ -30,24 +40,27 @@ Args: - lookup_fn: function expects one criteria and optional supplementary \*args. + - \*args: supplementary arguments to pass to lookup_fn. - 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. + - request: Input HTTP request that will be provided as parameter + to enrich_fn. + Raises: NotFoundExp or whatever `lookup_fn` raises. """ if enrich_fn is None: - enrich_fn = (lambda x: x) + enrich_fn = (lambda x, request: x) res = lookup_fn(*args) if res is None: raise NotFoundExc(notfound_msg) - if isinstance(res, (map, list, GeneratorType)): - return [enrich_fn(x) for x in res] - return enrich_fn(res) + if isinstance(res, (list, GeneratorType)) or type(res) == map: + return [enrich_fn(x, request=request) for x in res] + return enrich_fn(res, request=request) @api_view(['GET', 'HEAD']) diff --git a/swh/web/api/views/vault.py b/swh/web/api/views/vault.py --- a/swh/web/api/views/vault.py +++ b/swh/web/api/views/vault.py @@ -23,13 +23,15 @@ return api_lookup( service.vault_progress, obj_type, obj_id, notfound_msg=("{} '{}' was never requested." - .format(object_name, hex_id))) + .format(object_name, hex_id)), + request=request) elif request.method == 'POST': email = request.POST.get('email', request.GET.get('email', None)) return api_lookup( service.vault_cook, obj_type, obj_id, email, notfound_msg=("{} '{}' not found." - .format(object_name, hex_id))) + .format(object_name, hex_id)), + request=request) @api_route(r'/vault/directory/(?P[0-9a-f]+)/', @@ -125,7 +127,8 @@ dir_id, ['sha1'], 'Only sha1_git is supported.') res = api_lookup( service.vault_fetch, 'directory', obj_id, - notfound_msg="Directory with ID '{}' not found.".format(dir_id)) + notfound_msg="Directory with ID '{}' not found.".format(dir_id), + request=request) fname = '{}.tar.gz'.format(dir_id) response = HttpResponse(res, content_type='application/gzip') response['Content-disposition'] = 'attachment; filename={}'.format(fname) @@ -226,7 +229,8 @@ rev_id, ['sha1'], 'Only sha1_git is supported.') res = api_lookup( service.vault_fetch, 'revision_gitfast', obj_id, - notfound_msg="Revision with ID '{}' not found.".format(rev_id)) + notfound_msg="Revision with ID '{}' not found.".format(rev_id), + request=request) fname = '{}.gitfast.gz'.format(rev_id) response = HttpResponse(res, content_type='application/gzip') response['Content-disposition'] = 'attachment; filename={}'.format(fname) diff --git a/swh/web/common/swh_templatetags.py b/swh/web/common/swh_templatetags.py --- a/swh/web/common/swh_templatetags.py +++ b/swh/web/common/swh_templatetags.py @@ -67,12 +67,9 @@ """ try: if 'href="' not in text: - text = re.sub(r'(/api/[^"<]*|/browse/[^"<]*|http.*$)', - r'\1', - text) + text = re.sub(r'(http.*)', r'\1', text) return re.sub(r'([^ <>"]+@[^ <>"]+)', - r'\1', - text) + r'\1', text) except Exception as exc: sentry_sdk.capture_exception(exc) @@ -94,8 +91,7 @@ links = text.split(',') ret = '' for i, link in enumerate(links): - ret += re.sub(r'<(/api/.*|/browse/.*)>', r'<\1>', - link) + ret += re.sub(r'<(http.*)>', r'<\1>', link) # add one link per line and align them if i != len(links) - 1: ret += '\n ' diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py --- a/swh/web/common/utils.py +++ b/swh/web/common/utils.py @@ -11,8 +11,10 @@ from dateutil import parser as date_parser from dateutil import tz +from typing import Optional, Dict, Any + from django.urls import reverse as django_reverse -from django.http import QueryDict +from django.http import QueryDict, HttpRequest from prometheus_client.registry import CollectorRegistry @@ -27,6 +29,7 @@ from swh.web.common.exc import BadInputExc from swh.web.config import get_config + SWH_WEB_METRICS_REGISTRY = CollectorRegistry(auto_describe=True) swh_object_icons = { @@ -44,17 +47,22 @@ } -def reverse(viewname, url_args=None, query_params=None, - current_app=None, urlconf=None): +def reverse(viewname: str, + url_args: Optional[Dict[str, Any]] = None, + query_params: Optional[Dict[str, Any]] = None, + current_app: Optional[str] = None, + urlconf: Optional[str] = None, + request: Optional[HttpRequest] = None) -> str: """An override of django reverse function supporting query parameters. Args: - viewname (str): the name of the django view from which to compute a url - url_args (dict): dictionary of url arguments indexed by their names - query_params (dict): dictionary of query parameters to append to the + viewname: the name of the django view from which to compute a url + url_args: dictionary of url arguments indexed by their names + query_params: dictionary of query parameters to append to the reversed url - current_app (str): the name of the django app tighten to the view - urlconf (str): url configuration module + current_app: the name of the django app tighten to the view + urlconf: url configuration module + request: build an absolute URI if provided Returns: str: the url of the requested view with processed arguments and @@ -76,6 +84,9 @@ query_dict[k] = query_params[k] url += ('?' + query_dict.urlencode(safe='/;:')) + if request is not None: + url = request.build_absolute_uri(url) + return url @@ -344,3 +355,30 @@ user = getattr(request._request, 'user', None) self.enforce_csrf(request) return (user, None) + + +def resolve_branch_alias(snapshot: Dict[str, Any], + branch: Optional[Dict[str, Any]] + ) -> Optional[Dict[str, Any]]: + """ + Resolve branch alias in snapshot content. + + Args: + snapshot: a full snapshot content + branch: a branch alias contained in the snapshot + Returns: + The real snapshot branch that got aliased. + """ + while branch and branch['target_type'] == 'alias': + if branch['target'] in snapshot['branches']: + branch = snapshot['branches'][branch['target']] + else: + from swh.web.common import service + snp = service.lookup_snapshot( + snapshot['id'], branches_from=branch['target'], + branches_count=1) + if snp and branch['target'] in snp['branches']: + branch = snp['branches'][branch['target']] + else: + branch = None + return branch diff --git a/swh/web/templates/api/apidoc.html b/swh/web/templates/api/apidoc.html --- a/swh/web/templates/api/apidoc.html +++ b/swh/web/templates/api/apidoc.html @@ -41,7 +41,7 @@ {% if response_data is not None %}

Request

-
{{ request.method }} {{ request.path }}
+
{{ request.method }} {{ request.build_absolute_uri }}

Response

{% if status_code != 200 %} diff --git a/swh/web/templates/browse/person.html b/swh/web/templates/browse/person.html --- a/swh/web/templates/browse/person.html +++ b/swh/web/templates/browse/person.html @@ -27,10 +27,10 @@ {{ key }} -
{{ val | safe | urlize_links_and_mails | safe }}
+
{{ val | safe | urlize_links_and_mails:request | safe }}
{% endfor %} {% endblock %} \ No newline at end of file diff --git a/swh/web/tests/api/test_api_lookup.py b/swh/web/tests/api/test_api_lookup.py --- a/swh/web/tests/api/test_api_lookup.py +++ b/swh/web/tests/api/test_api_lookup.py @@ -37,7 +37,7 @@ 'some param 1', notfound_msg=('This is not the error message you are looking for. ' 'Move along.'), - enrich_fn=lambda x: x * 2) + enrich_fn=lambda x, request: x * 2) assert actual_result == [4, 6, 8] @@ -52,7 +52,7 @@ 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, '='])) + enrich_fn=lambda x, request: ''. join(['=', x, '='])) assert actual_result == ['=a=', '=b=', '=c='] @@ -66,7 +66,7 @@ actual_result = utils.api_lookup( test_generic_lookup_fn_3, 'crit', notfound_msg='Move!', - enrich_fn=lambda x: x - 1) + enrich_fn=lambda x, request: x - 1) assert actual_result == [3, 4, 5] @@ -77,7 +77,7 @@ assert crit == '123' return {'a': 10} - def test_enrich_data(x): + def test_enrich_data(x, request): x['a'] = x['a'] * 10 return x diff --git a/swh/web/tests/api/test_apiresponse.py b/swh/web/tests/api/test_apiresponse.py --- a/swh/web/tests/api/test_apiresponse.py +++ b/swh/web/tests/api/test_apiresponse.py @@ -11,7 +11,7 @@ ) -def test_compute_link_header(api_request_factory): +def test_compute_link_header(): next_link = '/api/endpoint/next' prev_link = '/api/endpoint/prev' rv = { @@ -20,61 +20,51 @@ } options = {} - request = api_request_factory.get('/api/endpoint/') - - headers = compute_link_header(request, rv, options) + headers = compute_link_header(rv, options) assert headers == { - 'Link': (f'<{request.build_absolute_uri(next_link)}>; rel="next",' - f'<{request.build_absolute_uri(prev_link)}>; rel="previous"') + 'Link': (f'<{next_link}>; rel="next",' + f'<{prev_link}>; rel="previous"') } -def test_compute_link_header_nothing_changed(api_request_factory): +def test_compute_link_header_nothing_changed(): rv = {} options = {} - request = api_request_factory.get('/api/test/path/') - - headers = compute_link_header(request, rv, options) + headers = compute_link_header(rv, options) assert headers == {} -def test_compute_link_header_nothing_changed_2(api_request_factory): +def test_compute_link_header_nothing_changed_2(): rv = {'headers': {}} options = {} - request = api_request_factory.get('/api/test/path/') - - headers = compute_link_header(request, rv, options) + headers = compute_link_header(rv, options) assert headers == {} def test_transform_only_return_results_1(): rv = {'results': {'some-key': 'some-value'}} - assert transform(rv) == {'some-key': 'some-value'} def test_transform_only_return_results_2(): rv = {'headers': {'something': 'do changes'}, 'results': {'some-key': 'some-value'}} - assert transform(rv) == {'some-key': 'some-value'} def test_transform_do_remove_headers(): rv = {'headers': {'something': 'do changes'}, 'some-key': 'some-value'} - assert transform(rv) == {'some-key': 'some-value'} def test_transform_do_nothing(): rv = {'some-key': 'some-value'} - assert transform(rv) == {'some-key': 'some-value'} @@ -106,11 +96,6 @@ expected_data = { 'response_data': json.dumps(data), - 'request': { - 'path': request.path, - 'method': request.method, - 'absolute_uri': request.build_absolute_uri() - }, 'headers_data': {}, 'heading': 'my_short_path', 'status_code': 200 diff --git a/swh/web/tests/api/test_utils.py b/swh/web/tests/api/test_utils.py --- a/swh/web/tests/api/test_utils.py +++ b/swh/web/tests/api/test_utils.py @@ -3,7 +3,18 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +import random + +from hypothesis import given + +from swh.model.hashutil import DEFAULT_ALGORITHMS + from swh.web.api import utils +from swh.web.common.origin_visits import get_origin_visits +from swh.web.common.utils import reverse, resolve_branch_alias +from swh.web.tests.strategies import ( + release, directory, content, revision, snapshot, origin +) url_map = [ @@ -34,15 +45,6 @@ } ] -sample_content_hashes = { - 'blake2s256': ('791e07fcea240ade6dccd0a9309141673' - 'c31242cae9c237cf3855e151abc78e9'), - 'sha1': 'dc2830a9e72f23c1dfebef4413003221baa5fb62', - 'sha1_git': 'fe95a46679d128ff167b7c55df5d02356c5a1ae1', - 'sha256': ('b5c7fe0536f44ef60c8780b6065d30bca74a5cd06' - 'd78a4a71ba1ad064770f0c9') -} - def test_filter_field_keys_dict_unknown_keys(): actual_res = utils.filter_field_keys( @@ -100,446 +102,500 @@ 'email': 'foo@bar'}) == 'raboof ' -def test_enrich_release_0(): +def test_enrich_release_empty(): actual_release = utils.enrich_release({}) assert actual_release == {} -def test_enrich_release_1(mocker): - - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - - def reverse_test_context(view_name, url_args): - if view_name == 'api-1-content': - id = url_args['q'] - return '/api/1/content/%s/' % id - else: - raise ValueError('This should not happened so fail if it does.') - - mock_django_reverse.side_effect = reverse_test_context - - actual_release = utils.enrich_release({ - 'target': '123', - 'target_type': 'content', - 'author': { - 'id': 100, - 'name': 'author release name', - 'email': 'author@email', - }, - }) - - assert actual_release == { - 'target': '123', - 'target_type': 'content', - 'target_url': '/api/1/content/sha1_git:123/', - 'author': { - 'id': 100, - 'name': 'author release name', - 'email': 'author@email', - }, - } +@given(release()) +def test_enrich_release_content_target(api_request_factory, + archive_data, release): - mock_django_reverse.assert_has_calls([ - mocker.call('api-1-content', url_args={'q': 'sha1_git:123'}), - ]) + release_data = archive_data.release_get(release) + release_data['target_type'] = 'content' + url = reverse('api-1-release', url_args={'sha1_git': release}) + request = api_request_factory.get(url) -def test_enrich_release_2(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.return_value = '/api/1/dir/23/' + actual_release = utils.enrich_release(release_data, request) - actual_release = utils.enrich_release({'target': '23', - 'target_type': 'directory'}) + release_data['target_url'] = reverse( + 'api-1-content', + url_args={'q': f'sha1_git:{release_data["target"]}'}, + request=request) - assert actual_release == { - 'target': '23', - 'target_type': 'directory', - 'target_url': '/api/1/dir/23/' - } + assert actual_release == release_data - mock_django_reverse.assert_called_once_with('api-1-directory', - url_args={'sha1_git': '23'}) +@given(release()) +def test_enrich_release_directory_target(api_request_factory, + archive_data, release): -def test_enrich_release_3(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.return_value = '/api/1/rev/3/' + release_data = archive_data.release_get(release) + release_data['target_type'] = 'directory' - actual_release = utils.enrich_release({'target': '3', - 'target_type': 'revision'}) + url = reverse('api-1-release', url_args={'sha1_git': release}) + request = api_request_factory.get(url) - assert actual_release == { - 'target': '3', - 'target_type': 'revision', - 'target_url': '/api/1/rev/3/' - } + actual_release = utils.enrich_release(release_data, request) - mock_django_reverse.assert_called_once_with('api-1-revision', - url_args={'sha1_git': '3'}) + release_data['target_url'] = reverse( + 'api-1-directory', + url_args={'sha1_git': release_data['target']}, + request=request) + assert actual_release == release_data -def test_enrich_release_4(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.return_value = '/api/1/rev/4/' - actual_release = utils.enrich_release({'target': '4', - 'target_type': 'release'}) +@given(release()) +def test_enrich_release_revision_target(api_request_factory, + archive_data, release): - assert actual_release == { - 'target': '4', - 'target_type': 'release', - 'target_url': '/api/1/rev/4/' - } + release_data = archive_data.release_get(release) + release_data['target_type'] = 'revision' + + url = reverse('api-1-release', url_args={'sha1_git': release}) + request = api_request_factory.get(url) + + actual_release = utils.enrich_release(release_data, request) + + release_data['target_url'] = reverse( + 'api-1-revision', + url_args={'sha1_git': release_data['target']}, + request=request) - mock_django_reverse.assert_called_once_with('api-1-release', - url_args={'sha1_git': '4'}) + assert actual_release == release_data -def test_enrich_directory_no_type(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') +@given(release()) +def test_enrich_release_release_target(api_request_factory, + archive_data, release): + + release_data = archive_data.release_get(release) + release_data['target_type'] = 'release' + + url = reverse('api-1-release', url_args={'sha1_git': release}) + request = api_request_factory.get(url) + + actual_release = utils.enrich_release(release_data, request) + + release_data['target_url'] = reverse( + 'api-1-release', + url_args={'sha1_git': release_data['target']}, + request=request) + + assert actual_release == release_data + + +def test_enrich_directory_no_type(): assert utils.enrich_directory({'id': 'dir-id'}) == {'id': 'dir-id'} - mock_django_reverse.return_value = '/api/content/sha1_git:123/' - actual_directory = utils.enrich_directory({ - 'id': 'dir-id', - 'type': 'file', - 'target': '123', - }) +@given(directory()) +def test_enrich_directory_with_type(api_request_factory, + archive_data, directory): - assert actual_directory == { - 'id': 'dir-id', - 'type': 'file', - 'target': '123', - 'target_url': '/api/content/sha1_git:123/', - } + dir_content = archive_data.directory_ls(directory) - mock_django_reverse.assert_called_once_with( - 'api-1-content', url_args={'q': 'sha1_git:123'}) - - -def test_enrich_directory_with_context_and_type_file(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.return_value = '/api/content/sha1_git:123/' - - actual_directory = utils.enrich_directory({ - 'id': 'dir-id', - 'type': 'file', - 'name': 'hy', - 'target': '789', - }, context_url='/api/revision/revsha1/directory/prefix/path/') - - assert actual_directory == { - 'id': 'dir-id', - 'type': 'file', - 'name': 'hy', - 'target': '789', - 'target_url': '/api/content/sha1_git:123/', - 'file_url': '/api/revision/revsha1/directory' - '/prefix/path/hy/' - } + dir_entry = random.choice(dir_content) - mock_django_reverse.assert_called_once_with( - 'api-1-content', url_args={'q': 'sha1_git:789'}) - - -def test_enrich_directory_with_context_and_type_dir(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.return_value = '/api/directory/456/' - - actual_directory = utils.enrich_directory({ - 'id': 'dir-id', - 'type': 'dir', - 'name': 'emacs-42', - 'target_type': 'file', - 'target': '456', - }, context_url='/api/revision/origin/2/directory/some/prefix/path/') - - assert actual_directory == { - 'id': 'dir-id', - 'type': 'dir', - 'target_type': 'file', - 'name': 'emacs-42', - 'target': '456', - 'target_url': '/api/directory/456/', - 'dir_url': '/api/revision/origin/2/directory' - '/some/prefix/path/emacs-42/' - } + url = reverse('api-1-directory', url_args={'sha1_git': directory}) + request = api_request_factory.get(url) - mock_django_reverse.assert_called_once_with('api-1-directory', - url_args={'sha1_git': '456'}) + actual_directory = utils.enrich_directory(dir_entry, request) + + if dir_entry['type'] == 'file': + dir_entry['target_url'] = reverse( + 'api-1-content', + url_args={'q': f'sha1_git:{dir_entry["target"]}'}, + request=request) + + elif dir_entry['type'] == 'dir': + dir_entry['target_url'] = reverse( + 'api-1-directory', + url_args={'sha1_git': dir_entry['target']}, + request=request) + + elif dir_entry['type'] == 'rev': + dir_entry['target_url'] = reverse( + 'api-1-revision', + url_args={'sha1_git': dir_entry['target']}, + request=request) + + assert actual_directory == dir_entry def test_enrich_content_without_hashes(): assert utils.enrich_content({'id': '123'}) == {'id': '123'} -def test_enrich_content_with_hashes(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - for algo, hash in sample_content_hashes.items(): - - query_string = '%s:%s' % (algo, hash) - - mock_django_reverse.side_effect = [ - '/api/content/%s/raw/' % query_string, - '/api/filetype/%s/' % query_string, - '/api/language/%s/' % query_string, - '/api/license/%s/' % query_string - ] - - enriched_content = utils.enrich_content({algo: hash}, - query_string=query_string) - - assert enriched_content == { - algo: hash, - 'data_url': '/api/content/%s/raw/' % query_string, - 'filetype_url': '/api/filetype/%s/' % query_string, - 'language_url': '/api/language/%s/' % query_string, - 'license_url': '/api/license/%s/' % query_string, - } - - mock_django_reverse.assert_has_calls([ - mocker.call('api-1-content-raw', url_args={'q': query_string}), - mocker.call('api-1-content-filetype', - url_args={'q': query_string}), - mocker.call('api-1-content-language', - url_args={'q': query_string}), - mocker.call('api-1-content-license', - url_args={'q': query_string}), - ]) - - mock_django_reverse.reset() - - -def test_enrich_content_with_hashes_and_top_level_url(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - for algo, hash in sample_content_hashes.items(): - - query_string = '%s:%s' % (algo, hash) - - mock_django_reverse.side_effect = [ - '/api/content/%s/' % query_string, - '/api/content/%s/raw/' % query_string, - '/api/filetype/%s/' % query_string, - '/api/language/%s/' % query_string, - '/api/license/%s/' % query_string, - ] - - enriched_content = utils.enrich_content({algo: hash}, top_url=True, - query_string=query_string) - - assert enriched_content == { - algo: hash, - 'content_url': '/api/content/%s/' % query_string, - 'data_url': '/api/content/%s/raw/' % query_string, - 'filetype_url': '/api/filetype/%s/' % query_string, - 'language_url': '/api/language/%s/' % query_string, - 'license_url': '/api/license/%s/' % query_string, - } - - mock_django_reverse.assert_has_calls([ - mocker.call('api-1-content', url_args={'q': query_string}), - mocker.call('api-1-content-raw', url_args={'q': query_string}), - mocker.call('api-1-content-filetype', - url_args={'q': query_string}), - mocker.call('api-1-content-language', - url_args={'q': query_string}), - mocker.call('api-1-content-license', url_args={'q': query_string}), - ]) - - mock_django_reverse.reset() - - -def _reverse_context_test(view_name, url_args): - if view_name == 'api-1-revision': - return '/api/revision/%s/' % url_args['sha1_git'] - elif view_name == 'api-1-revision-context': - return ('/api/revision/%s/prev/%s/' % - (url_args['sha1_git'], url_args['context'])) - elif view_name == 'api-1-revision-log': - if 'prev_sha1s' in url_args: - return ('/api/revision/%s/prev/%s/log/' % - (url_args['sha1_git'], url_args['prev_sha1s'])) - else: - return '/api/revision/%s/log/' % url_args['sha1_git'] - - -def test_enrich_revision_without_children_or_parent(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - - def reverse_test(view_name, url_args): - if view_name == 'api-1-revision': - return '/api/revision/' + url_args['sha1_git'] + '/' - elif view_name == 'api-1-revision-log': - return '/api/revision/' + url_args['sha1_git'] + '/log/' - elif view_name == 'api-1-directory': - return '/api/directory/' + url_args['sha1_git'] + '/' - - mock_django_reverse.side_effect = reverse_test - - actual_revision = utils.enrich_revision({ - 'id': 'rev-id', - 'directory': '123', - 'author': {'id': '1'}, - 'committer': {'id': '2'}, - }) - - expected_revision = { - 'id': 'rev-id', - 'directory': '123', - 'url': '/api/revision/rev-id/', - 'history_url': '/api/revision/rev-id/log/', - 'directory_url': '/api/directory/123/', - 'author': {'id': '1'}, - 'committer': {'id': '2'}, - } +@given(content()) +def test_enrich_content_with_hashes(api_request_factory, content): - assert actual_revision == expected_revision + for algo in DEFAULT_ALGORITHMS: - mock_django_reverse.assert_has_calls([ - mocker.call('api-1-revision', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision-log', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-directory', url_args={'sha1_git': '123'}) - ]) + content_data = dict(content) + query_string = '%s:%s' % (algo, content_data[algo]) -def test_enrich_revision_with_children_and_parent_no_dir(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.side_effect = _reverse_context_test + url = reverse('api-1-content', url_args={'q': query_string}) + request = api_request_factory.get(url) - actual_revision = utils.enrich_revision({ - 'id': 'rev-id', - 'parents': ['123'], - 'children': ['456'], - }) + enriched_content = utils.enrich_content(content_data, + query_string=query_string, + request=request) - expected_revision = { - 'id': 'rev-id', - 'url': '/api/revision/rev-id/', - 'history_url': '/api/revision/rev-id/log/', - 'parents': [{'id': '123', 'url': '/api/revision/123/'}], - 'children': ['456'], - 'children_urls': ['/api/revision/456/'], - } + content_data['data_url'] = reverse('api-1-content-raw', + url_args={'q': query_string}, + request=request) - assert actual_revision == expected_revision - - mock_django_reverse.assert_has_calls([ - mocker.call('api-1-revision', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision-log', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision', url_args={'sha1_git': '123'}), - mocker.call('api-1-revision', url_args={'sha1_git': '456'}) - ]) - - -def test_enrich_revision_no_context(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.side_effect = _reverse_context_test - - actual_revision = utils.enrich_revision({ - 'id': 'rev-id', - 'parents': ['123'], - 'children': ['456'], - }) - - expected_revision = { - 'id': 'rev-id', - 'url': '/api/revision/rev-id/', - 'history_url': '/api/revision/rev-id/log/', - 'parents': [{'id': '123', 'url': '/api/revision/123/'}], - 'children': ['456'], - 'children_urls': ['/api/revision/456/'] - } + content_data['filetype_url'] = reverse('api-1-content-filetype', + url_args={'q': query_string}, + request=request) - assert actual_revision == expected_revision - - mock_django_reverse.assert_has_calls([ - mocker.call('api-1-revision', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision-log', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision', url_args={'sha1_git': '123'}), - mocker.call('api-1-revision', url_args={'sha1_git': '456'}) - ]) - - -def _reverse_rev_message_test(view_name, url_args): - if view_name == 'api-1-revision': - return '/api/revision/%s/' % url_args['sha1_git'] - elif view_name == 'api-1-revision-log': - if 'prev_sha1s' in url_args and url_args['prev_sha1s'] is not None: - return ('/api/revision/%s/prev/%s/log/' % - (url_args['sha1_git'], url_args['prev_sha1s'])) - else: - return '/api/revision/%s/log/' % url_args['sha1_git'] - elif view_name == 'api-1-revision-raw-message': - return '/api/revision/' + url_args['sha1_git'] + '/raw/' - else: - return ('/api/revision/%s/prev/%s/' % - (url_args['sha1_git'], url_args['context'])) - - -def test_enrich_revision_with_no_message(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.side_effect = _reverse_rev_message_test - - expected_revision = { - 'id': 'rev-id', - 'url': '/api/revision/rev-id/', - 'history_url': '/api/revision/rev-id/log/', - 'message': None, - 'parents': [{'id': '123', 'url': '/api/revision/123/'}], - 'children': ['456'], - 'children_urls': ['/api/revision/456/'], - } + content_data['language_url'] = reverse('api-1-content-language', + url_args={'q': query_string}, + request=request) + + content_data['license_url'] = reverse('api-1-content-license', + url_args={'q': query_string}, + request=request) + + assert enriched_content == content_data + + +@given(content()) +def test_enrich_content_with_hashes_and_top_level_url(api_request_factory, + content): + + for algo in DEFAULT_ALGORITHMS: + + content_data = dict(content) + + query_string = '%s:%s' % (algo, content_data[algo]) + + url = reverse('api-1-content', url_args={'q': query_string}) + request = api_request_factory.get(url) + + enriched_content = utils.enrich_content(content_data, + query_string=query_string, + top_url=True, + request=request) + + content_data['content_url'] = reverse('api-1-content', + url_args={'q': query_string}, + request=request) + + content_data['data_url'] = reverse('api-1-content-raw', + url_args={'q': query_string}, + request=request) + + content_data['filetype_url'] = reverse('api-1-content-filetype', + url_args={'q': query_string}, + request=request) - actual_revision = utils.enrich_revision({ - 'id': 'rev-id', - 'message': None, - 'parents': ['123'], - 'children': ['456'], - }) - - assert actual_revision == expected_revision - - mock_django_reverse.assert_has_calls([ - mocker.call('api-1-revision', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision-log', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision', url_args={'sha1_git': '123'}), - mocker.call('api-1-revision', url_args={'sha1_git': '456'}) - ]) - - -def test_enrich_revision_with_invalid_message(mocker): - mock_django_reverse = mocker.patch('swh.web.api.utils.reverse') - mock_django_reverse.side_effect = _reverse_rev_message_test - - actual_revision = utils.enrich_revision({ - 'id': 'rev-id', - 'message': None, - 'message_decoding_failed': True, - 'parents': ['123'], - 'children': ['456'], - }) - - expected_revision = { - 'id': 'rev-id', - 'url': '/api/revision/rev-id/', - 'history_url': '/api/revision/rev-id/log/', - 'message': None, - 'message_decoding_failed': True, - 'message_url': '/api/revision/rev-id/raw/', - 'parents': [{'id': '123', 'url': '/api/revision/123/'}], - 'children': ['456'], - 'children_urls': ['/api/revision/456/'], + content_data['language_url'] = reverse('api-1-content-language', + url_args={'q': query_string}, + request=request) + + content_data['license_url'] = reverse('api-1-content-license', + url_args={'q': query_string}, + request=request) + + assert enriched_content == content_data + + +@given(revision()) +def test_enrich_revision_without_children_or_parent(api_request_factory, + archive_data, revision): + + revision_data = archive_data.revision_get(revision) + del revision_data['parents'] + + url = reverse('api-1-revision', url_args={'sha1_git': revision}) + request = api_request_factory.get(url) + + actual_revision = utils.enrich_revision(revision_data, request) + + revision_data['url'] = reverse( + 'api-1-revision', + url_args={'sha1_git': revision}, + request=request) + + revision_data['history_url'] = reverse( + 'api-1-revision-log', + url_args={'sha1_git': revision}, + request=request) + + revision_data['directory_url'] = reverse( + 'api-1-directory', + url_args={'sha1_git': revision_data['directory']}, + request=request) + + assert actual_revision == revision_data + + +@given(revision(), revision(), revision()) +def test_enrich_revision_with_children_and_parent_no_dir(api_request_factory, + archive_data, + revision, + parent_revision, + child_revision): + + revision_data = archive_data.revision_get(revision) + del revision_data['directory'] + revision_data['parents'].append(parent_revision) + revision_data['children'] = child_revision + + url = reverse('api-1-revision', url_args={'sha1_git': revision}) + request = api_request_factory.get(url) + + actual_revision = utils.enrich_revision(revision_data, request) + + revision_data['url'] = reverse( + 'api-1-revision', + url_args={'sha1_git': revision}, + request=request) + + revision_data['history_url'] = reverse( + 'api-1-revision-log', + url_args={'sha1_git': revision}, + request=request) + + revision_data['parents'] = [ + {'id': p['id'], 'url': reverse('api-1-revision', + url_args={'sha1_git': p['id']}, + request=request)} + for p in revision_data['parents'] + ] + + revision_data['children_urls'] = [ + reverse('api-1-revision', + url_args={'sha1_git': child_revision}, + request=request) + ] + + assert actual_revision == revision_data + + +@given(revision(), revision(), revision()) +def test_enrich_revision_no_context(api_request_factory, + revision, + parent_revision, + child_revision): + + revision_data = { + 'id': revision, + 'parents': [parent_revision], + 'children': [child_revision] } - assert actual_revision == expected_revision + url = reverse('api-1-revision', url_args={'sha1_git': revision}) + request = api_request_factory.get(url) + + actual_revision = utils.enrich_revision(revision_data, request) + + revision_data['url'] = reverse( + 'api-1-revision', + url_args={'sha1_git': revision}, + request=request) + + revision_data['history_url'] = reverse( + 'api-1-revision-log', + url_args={'sha1_git': revision}, + request=request) + + revision_data['parents'] = [{ + 'id': parent_revision, + 'url': reverse('api-1-revision', + url_args={'sha1_git': parent_revision}, + request=request) + }] + + revision_data['children_urls'] = [ + reverse('api-1-revision', + url_args={'sha1_git': child_revision}, + request=request) + ] + + assert actual_revision == revision_data + + +@given(revision(), revision(), revision()) +def test_enrich_revision_with_no_message(api_request_factory, + archive_data, + revision, + parent_revision, + child_revision): + + revision_data = archive_data.revision_get(revision) + revision_data['message'] = None + revision_data['parents'].append(parent_revision) + revision_data['children'] = child_revision + + url = reverse('api-1-revision', url_args={'sha1_git': revision}) + request = api_request_factory.get(url) + + actual_revision = utils.enrich_revision(revision_data, request) + + revision_data['url'] = reverse( + 'api-1-revision', + url_args={'sha1_git': revision}, + request=request) + + revision_data['directory_url'] = reverse( + 'api-1-directory', + url_args={'sha1_git': revision_data['directory']}, + request=request) + + revision_data['history_url'] = reverse( + 'api-1-revision-log', + url_args={'sha1_git': revision}, + request=request) + + revision_data['parents'] = [ + {'id': p['id'], 'url': reverse('api-1-revision', + url_args={'sha1_git': p['id']}, + request=request)} + for p in revision_data['parents'] + ] + + revision_data['children_urls'] = [ + reverse('api-1-revision', + url_args={'sha1_git': child_revision}, + request=request) + ] + + assert actual_revision == revision_data + + +@given(revision(), revision(), revision()) +def test_enrich_revision_with_invalid_message(api_request_factory, + archive_data, + revision, + parent_revision, + child_revision): + + revision_data = archive_data.revision_get(revision) + revision_data['message'] = None + revision_data['message_decoding_failed'] = True, + revision_data['parents'].append(parent_revision) + revision_data['children'] = child_revision + + url = reverse('api-1-revision', url_args={'sha1_git': revision}) + request = api_request_factory.get(url) + + actual_revision = utils.enrich_revision(revision_data, request) + + revision_data['url'] = reverse( + 'api-1-revision', + url_args={'sha1_git': revision}, + request=request) + + revision_data['message_url'] = reverse( + 'api-1-revision-raw-message', + url_args={'sha1_git': revision}, + request=request) + + revision_data['directory_url'] = reverse( + 'api-1-directory', + url_args={'sha1_git': revision_data['directory']}, + request=request) + + revision_data['history_url'] = reverse( + 'api-1-revision-log', + url_args={'sha1_git': revision}, + request=request) + + revision_data['parents'] = [ + {'id': p['id'], 'url': reverse('api-1-revision', + url_args={'sha1_git': p['id']}, + request=request)} + for p in revision_data['parents'] + ] + + revision_data['children_urls'] = [ + reverse('api-1-revision', + url_args={'sha1_git': child_revision}, + request=request) + ] + + assert actual_revision == revision_data + + +@given(snapshot()) +def test_enrich_snapshot(api_request_factory, archive_data, snapshot): + snapshot_data = archive_data.snapshot_get(snapshot) + + url = reverse('api-1-snapshot', url_args={'snapshot_id': snapshot}) + request = api_request_factory.get(url) + + actual_snapshot = utils.enrich_snapshot(snapshot_data, request) + + for _, b in snapshot_data['branches'].items(): + if b['target_type'] in ('directory', 'revision', 'release'): + b['target_url'] = reverse(f'api-1-{b["target_type"]}', + url_args={'sha1_git': b['target']}, + request=request) + elif b['target_type'] == 'content': + b['target_url'] = reverse( + 'api-1-content', + url_args={'q': f'sha1_git:{b["target"]}'}, + request=request) + + for _, b in snapshot_data['branches'].items(): + if b['target_type'] == 'alias': + target = resolve_branch_alias(snapshot_data, b) + b['target_url'] = target['target_url'] + + assert actual_snapshot == snapshot_data + + +@given(origin()) +def test_enrich_origin(api_request_factory, archive_data, origin): + url = reverse('api-1-origin', url_args={'origin_url': origin['url']}) + request = api_request_factory.get(url) + + origin_data = {'url': origin['url']} + actual_origin = utils.enrich_origin(origin_data, request) + + origin_data['origin_visits_url'] = reverse( + 'api-1-origin-visits', + url_args={'origin_url': origin['url']}, + request=request) + + assert actual_origin == origin_data + + +@given(origin()) +def test_enrich_origin_visit(api_request_factory, archive_data, origin): + + origin_visit = random.choice(get_origin_visits(origin)) + + url = reverse('api-1-origin-visit', + url_args={'origin_url': origin['url'], + 'visit_id': origin_visit['visit']}) + request = api_request_factory.get(url) + + actual_origin_visit = utils.enrich_origin_visit( + origin_visit, with_origin_link=True, + with_origin_visit_link=True, request=request) + + origin_visit['origin_url'] = reverse( + 'api-1-origin', + url_args={'origin_url': origin['url']}, + request=request) + + origin_visit['origin_visit_url'] = reverse( + 'api-1-origin-visit', + url_args={'origin_url': origin['url'], + 'visit_id': origin_visit['visit']}, + request=request) + + origin_visit['snapshot_url'] = reverse( + 'api-1-snapshot', + url_args={'snapshot_id': origin_visit['snapshot']}, + request=request) - mock_django_reverse.assert_has_calls([ - mocker.call('api-1-revision', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision-log', url_args={'sha1_git': 'rev-id'}), - mocker.call('api-1-revision', url_args={'sha1_git': '123'}), - mocker.call('api-1-revision', url_args={'sha1_git': '456'}), - mocker.call('api-1-revision-raw-message', - url_args={'sha1_git': 'rev-id'}) - ]) + assert actual_origin_visit == origin_visit diff --git a/swh/web/tests/api/views/test_content.py b/swh/web/tests/api/views/test_content.py --- a/swh/web/tests/api/views/test_content.py +++ b/swh/web/tests/api/views/test_content.py @@ -23,7 +23,8 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' content_url = reverse('api-1-content', - url_args={'q': 'sha1:%s' % content['sha1']}) + url_args={'q': 'sha1:%s' % content['sha1']}, + request=rv.wsgi_request) expected_data = indexer_data.content_get_mimetype(content['sha1']) expected_data['content_url'] = content_url assert rv.data == expected_data @@ -56,7 +57,8 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' content_url = reverse('api-1-content', - url_args={'q': 'sha1:%s' % content['sha1']}) + url_args={'q': 'sha1:%s' % content['sha1']}, + request=rv.wsgi_request) expected_data = indexer_data.content_get_language(content['sha1']) expected_data['content_url'] = content_url assert rv.data == expected_data @@ -106,7 +108,8 @@ ('language_url', 'api-1-content-language'), ('filetype_url', 'api-1-content-filetype')): expected_entry[key] = reverse( - view_name, url_args={'q': 'sha1:%s' % content_sha1}) + view_name, url_args={'q': 'sha1:%s' % content_sha1}, + request=rv.wsgi_request) expected_entry['sha1'] = content_sha1 del expected_entry['id'] assert entry == expected_entry @@ -117,11 +120,11 @@ query_params={'per_page': 2}) rv = api_client.get(url) - next_url = rv.wsgi_request.build_absolute_uri( - reverse('api-1-content-symbol', - url_args={'q': contents_with_ctags['symbol_name']}, - query_params={'last_sha1': rv.data[1]['sha1'], - 'per_page': 2})) + next_url = reverse('api-1-content-symbol', + url_args={'q': contents_with_ctags['symbol_name']}, + query_params={'last_sha1': rv.data[1]['sha1'], + 'per_page': 2}, + request=rv.wsgi_request), assert rv['Link'] == '<%s>; rel="next"' % next_url @@ -150,7 +153,8 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' content_url = reverse('api-1-content', - url_args={'q': 'sha1:%s' % content['sha1']}) + url_args={'q': 'sha1:%s' % content['sha1']}, + request=rv.wsgi_request) expected_data = list(indexer_data.content_get_ctags(content['sha1'])) for e in expected_data: e['content_url'] = content_url @@ -169,7 +173,8 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' content_url = reverse('api-1-content', - url_args={'q': 'sha1:%s' % content['sha1']}) + url_args={'q': 'sha1:%s' % content['sha1']}, + request=rv.wsgi_request) expected_data = indexer_data.content_get_license(content['sha1']) expected_data['content_url'] = content_url assert rv.data == expected_data @@ -204,7 +209,8 @@ ('language_url', 'api-1-content-language'), ('filetype_url', 'api-1-content-filetype')): expected_data[key] = reverse( - view_name, url_args={'q': 'sha1:%s' % content['sha1']}) + view_name, url_args={'q': 'sha1:%s' % content['sha1']}, + request=rv.wsgi_request) assert rv.data == expected_data diff --git a/swh/web/tests/api/views/test_directory.py b/swh/web/tests/api/views/test_directory.py --- a/swh/web/tests/api/views/test_directory.py +++ b/swh/web/tests/api/views/test_directory.py @@ -7,6 +7,7 @@ from hypothesis import given +from swh.web.api.utils import enrich_directory from swh.web.common.utils import reverse from swh.web.tests.data import random_sha1 from swh.web.tests.strategies import directory @@ -21,8 +22,10 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' - expected_data = list(map(_enrich_dir_data, - archive_data.directory_ls(directory))) + dir_content = list(archive_data.directory_ls(directory)) + expected_data = list(map(enrich_directory, + dir_content, + [rv.wsgi_request] * len(dir_content))) assert rv.data == expected_data @@ -54,7 +57,7 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' - assert rv.data == _enrich_dir_data(path) + assert rv.data == enrich_directory(path, rv.wsgi_request) @given(directory()) @@ -85,19 +88,3 @@ redirect_url = reverse('api-1-directory', url_args={'sha1_git': directory}) assert resp['location'] == redirect_url - - -def _enrich_dir_data(dir_data): - if dir_data['type'] == 'file': - dir_data['target_url'] = reverse( - 'api-1-content', - url_args={'q': 'sha1_git:%s' % dir_data['target']}) - elif dir_data['type'] == 'dir': - dir_data['target_url'] = reverse( - 'api-1-directory', - url_args={'sha1_git': dir_data['target']}) - elif dir_data['type'] == 'rev': - dir_data['target_url'] = reverse( - 'api-1-revision', - url_args={'sha1_git': dir_data['target']}) - return dir_data diff --git a/swh/web/tests/api/views/test_identifiers.py b/swh/web/tests/api/views/test_identifiers.py --- a/swh/web/tests/api/views/test_identifiers.py +++ b/swh/web/tests/api/views/test_identifiers.py @@ -43,7 +43,8 @@ browse_rev_url = reverse('browse-%s' % obj_type, url_args=url_args, - query_params={'origin': origin['url']}) + query_params={'origin': origin['url']}, + request=resp.wsgi_request) expected_result = { 'browse_url': browse_rev_url, diff --git a/swh/web/tests/api/views/test_origin.py b/swh/web/tests/api/views/test_origin.py --- a/swh/web/tests/api/views/test_origin.py +++ b/swh/web/tests/api/views/test_origin.py @@ -9,6 +9,7 @@ from swh.storage.exc import StorageDBError, StorageAPIError +from swh.web.api.utils import enrich_origin_visit, enrich_origin from swh.web.common.exc import BadInputExc from swh.web.common.utils import reverse from swh.web.common.origin_visits import get_origin_visits @@ -119,7 +120,7 @@ for last_visit, expected_visits in ( (None, all_visits[:2]), - (all_visits[1]['visit'], all_visits[2:4])): + (all_visits[1]['visit'], all_visits[2:])): url = reverse('api-1-origin-visits', url_args={'origin_url': new_origin['url']}, @@ -131,17 +132,10 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' - for expected_visit in expected_visits: - origin_visit_url = reverse( - 'api-1-origin-visit', - url_args={'origin_url': new_origin['url'], - 'visit_id': expected_visit['visit']}) - snapshot_url = reverse( - 'api-1-snapshot', - url_args={'snapshot_id': expected_visit['snapshot']}) - expected_visit['origin'] = new_origin['url'] - expected_visit['origin_visit_url'] = origin_visit_url - expected_visit['snapshot_url'] = snapshot_url + for i in range(len(expected_visits)): + expected_visits[i] = enrich_origin_visit( + expected_visits[i], with_origin_link=False, + with_origin_visit_link=True, request=rv.wsgi_request) assert rv.data == expected_visits @@ -174,17 +168,10 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' - for expected_visit in expected_visits: - origin_visit_url = reverse( - 'api-1-origin-visit', - url_args={'origin_url': new_origin['url'], - 'visit_id': expected_visit['visit']}) - snapshot_url = reverse( - 'api-1-snapshot', - url_args={'snapshot_id': expected_visit['snapshot']}) - expected_visit['origin'] = new_origin['url'] - expected_visit['origin_visit_url'] = origin_visit_url - expected_visit['snapshot_url'] = snapshot_url + for i in range(len(expected_visits)): + expected_visits[i] = enrich_origin_visit( + expected_visits[i], with_origin_link=False, + with_origin_visit_link=True, request=rv.wsgi_request) assert rv.data == expected_visits @@ -212,15 +199,9 @@ expected_visit = archive_data.origin_visit_get_by( new_origin['url'], visit_id) - origin_url = reverse('api-1-origin', - url_args={'origin_url': new_origin['url']}) - snapshot_url = reverse( - 'api-1-snapshot', - url_args={'snapshot_id': expected_visit['snapshot']}) - - expected_visit['origin'] = new_origin['url'] - expected_visit['origin_url'] = origin_url - expected_visit['snapshot_url'] = snapshot_url + expected_visit = enrich_origin_visit( + expected_visit, with_origin_link=True, + with_origin_visit_link=False, request=rv.wsgi_request) assert rv.data == expected_visit @@ -261,18 +242,16 @@ url_args={'origin_url': new_origin['url']}) rv = api_client.get(url) + assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' expected_visit = archive_data.origin_visit_get_by( new_origin['url'], visit_ids[1]) - origin_url = reverse('api-1-origin', - url_args={'origin_url': new_origin['url']}) - - expected_visit['origin'] = new_origin['url'] - expected_visit['origin_url'] = origin_url - expected_visit['snapshot_url'] = None + expected_visit = enrich_origin_visit( + expected_visit, with_origin_link=True, + with_origin_visit_link=False, request=rv.wsgi_request) assert rv.data == expected_visit @@ -295,25 +274,20 @@ snapshot=new_snapshots[0]['id']) url = reverse('api-1-origin-visit-latest', - url_args={'origin_url': new_origin['url']}) - url += '?require_snapshot=true' + url_args={'origin_url': new_origin['url']}, + query_params={'require_snapshot': True}) rv = api_client.get(url) + assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' expected_visit = archive_data.origin_visit_get_by( new_origin['url'], visit_ids[0]) - origin_url = reverse('api-1-origin', - url_args={'origin_url': new_origin['url']}) - snapshot_url = reverse( - 'api-1-snapshot', - url_args={'snapshot_id': expected_visit['snapshot']}) - - expected_visit['origin'] = new_origin['url'] - expected_visit['origin_url'] = origin_url - expected_visit['snapshot_url'] = snapshot_url + expected_visit = enrich_origin_visit( + expected_visit, with_origin_link=True, + with_origin_visit_link=False, request=rv.wsgi_request) assert rv.data == expected_visit @@ -394,10 +368,7 @@ expected_origin = archive_data.origin_get(origin) - origin_visits_url = reverse('api-1-origin-visits', - url_args={'origin_url': origin['url']}) - - expected_origin['origin_visits_url'] = origin_visits_url + expected_origin = enrich_origin(expected_origin, rv.wsgi_request) assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' diff --git a/swh/web/tests/api/views/test_release.py b/swh/web/tests/api/views/test_release.py --- a/swh/web/tests/api/views/test_release.py +++ b/swh/web/tests/api/views/test_release.py @@ -23,7 +23,8 @@ expected_release = archive_data.release_get(release) target_revision = expected_release['target'] target_url = reverse('api-1-revision', - url_args={'sha1_git': target_revision}) + url_args={'sha1_git': target_revision}, + request=rv.wsgi_request) expected_release['target_url'] = target_url assert rv.status_code == 200, rv.data @@ -77,7 +78,8 @@ url_args = {'sha1_git': target} target_url = reverse('api-1-%s' % target_type, - url_args=url_args) + url_args=url_args, + request=rv.wsgi_request) expected_release['target_url'] = target_url assert rv.status_code == 200, rv.data diff --git a/swh/web/tests/api/views/test_revision.py b/swh/web/tests/api/views/test_revision.py --- a/swh/web/tests/api/views/test_revision.py +++ b/swh/web/tests/api/views/test_revision.py @@ -5,8 +5,8 @@ from hypothesis import given +from swh.web.api.utils import enrich_revision from swh.web.common.exc import NotFoundExc - from swh.web.common.utils import reverse from swh.web.tests.data import random_sha1 from swh.web.tests.strategies import revision @@ -19,7 +19,7 @@ expected_revision = archive_data.revision_get(revision) - _enrich_revision(expected_revision) + enrich_revision(expected_revision, rv.wsgi_request) assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' @@ -79,7 +79,8 @@ rv = api_client.get(url) expected_log = archive_data.revision_log(revision, limit=per_page+1) - expected_log = list(map(_enrich_revision, expected_log)) + expected_log = list(map(enrich_revision, expected_log, + [rv.wsgi_request] * len(expected_log))) has_next = len(expected_log) > per_page @@ -132,7 +133,8 @@ expected_log = archive_data.revision_log(rev, limit=per_page) prev_revision = archive_data.revision_get(prev_rev) expected_log.insert(0, prev_revision) - expected_log = list(map(_enrich_revision, expected_log)) + expected_log = list(map(enrich_revision, expected_log, + [rv.wsgi_request] * len(expected_log))) assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' @@ -193,6 +195,15 @@ rv = api_client.get('/api/1/revision/999/directory/some/path/') + stub_dir['content'][0]['target_url'] = rv.wsgi_request.build_absolute_uri( + stub_dir['content'][0]['target_url']) + stub_dir['content'][0]['file_url'] = rv.wsgi_request.build_absolute_uri( + stub_dir['content'][0]['file_url']) + stub_dir['content'][1]['target_url'] = rv.wsgi_request.build_absolute_uri( + stub_dir['content'][1]['target_url']) + stub_dir['content'][1]['dir_url'] = rv.wsgi_request.build_absolute_uri( + stub_dir['content'][1]['dir_url']) + assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' assert rv.data == stub_dir @@ -223,6 +234,9 @@ url = '/api/1/revision/666/directory/some/other/path/' rv = api_client.get(url) + stub_content['content']['data_url'] = rv.wsgi_request.build_absolute_uri( + stub_content['content']['data_url']) + assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' assert rv.data == stub_content @@ -243,29 +257,3 @@ url_args={'sha1_git': revision}) assert resp['location'] == redirect_url - - -def _enrich_revision(revision): - directory_url = reverse( - 'api-1-directory', - url_args={'sha1_git': revision['directory']}) - - history_url = reverse('api-1-revision-log', - url_args={'sha1_git': revision['id']}) - - parents_id_url = [] - for p in revision['parents']: - parents_id_url.append({ - 'id': p, - 'url': reverse('api-1-revision', url_args={'sha1_git': p}) - }) - - revision_url = reverse('api-1-revision', - url_args={'sha1_git': revision['id']}) - - revision['directory_url'] = directory_url - revision['history_url'] = history_url - revision['url'] = revision_url - revision['parents'] = parents_id_url - - return revision diff --git a/swh/web/tests/api/views/test_snapshot.py b/swh/web/tests/api/views/test_snapshot.py --- a/swh/web/tests/api/views/test_snapshot.py +++ b/swh/web/tests/api/views/test_snapshot.py @@ -8,6 +8,7 @@ from hypothesis import given from swh.model.hashutil import hash_to_hex +from swh.web.api.utils import enrich_snapshot from swh.web.common.utils import reverse from swh.web.tests.data import random_sha1 from swh.web.tests.strategies import ( @@ -25,7 +26,7 @@ assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' expected_data = archive_data.snapshot_get(snapshot) - expected_data = _enrich_snapshot(archive_data, expected_data) + expected_data = enrich_snapshot(expected_data, rv.wsgi_request) assert rv.data == expected_data @@ -59,7 +60,7 @@ expected_data = archive_data.snapshot_get_branches( snapshot, branches_from, branches_count) - expected_data = _enrich_snapshot(archive_data, expected_data) + expected_data = enrich_snapshot(expected_data, rv.wsgi_request) branches_offset += branches_count if branches_offset < len(snapshot_branches): @@ -112,7 +113,7 @@ expected_data = archive_data.snapshot_get_branches( snapshot, target_types=target_type) - expected_data = _enrich_snapshot(archive_data, expected_data) + expected_data = enrich_snapshot(expected_data, rv.wsgi_request) assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' @@ -159,36 +160,3 @@ url_args={'snapshot_id': snp_id}) rv = api_client.get(url) assert rv.status_code == 200, rv.data - - -def _enrich_snapshot(archive_data, snapshot): - def _get_branch_url(target_type, target): - url = None - if target_type == 'revision': - url = reverse('api-1-revision', url_args={'sha1_git': target}) - if target_type == 'release': - url = reverse('api-1-release', url_args={'sha1_git': target}) - return url - - for branch in snapshot['branches'].keys(): - target = snapshot['branches'][branch]['target'] - target_type = snapshot['branches'][branch]['target_type'] - snapshot['branches'][branch]['target_url'] = \ - _get_branch_url(target_type, target) - for branch in snapshot['branches'].keys(): - target = snapshot['branches'][branch]['target'] - target_type = snapshot['branches'][branch]['target_type'] - if target_type == 'alias': - if target in snapshot['branches']: - snapshot['branches'][branch]['target_url'] = \ - snapshot['branches'][target]['target_url'] - else: - snp = archive_data.snapshot_get_branches(snapshot['id'], - branches_from=target, - branches_count=1) - alias_target = snp['branches'][target]['target'] - alias_target_type = snp['branches'][target]['target_type'] - snapshot['branches'][branch]['target_url'] = \ - _get_branch_url(alias_target_type, alias_target) - - return snapshot diff --git a/swh/web/tests/api/views/test_vault.py b/swh/web/tests/api/views/test_vault.py --- a/swh/web/tests/api/views/test_vault.py +++ b/swh/web/tests/api/views/test_vault.py @@ -4,45 +4,51 @@ # See top-level LICENSE file for more information from swh.model import hashutil +from swh.web.common.utils import reverse TEST_OBJ_ID = 'd4905454cc154b492bd6afed48694ae3c579345e' -OBJECT_TYPES = {'directory': ('directory', None), - 'revision_gitfast': ('revision', 'gitfast')} +OBJECT_TYPES = ('directory', 'revision_gitfast') def test_api_vault_cook(api_client, mocker): mock_service = mocker.patch('swh.web.api.views.vault.service') - stub_cook = { - 'fetch_url': ('http://127.0.0.1:5004/api/1/vault/directory/{}/raw/' - .format(TEST_OBJ_ID)), - 'obj_id': TEST_OBJ_ID, - 'obj_type': 'test_type', - 'progress_message': None, - 'status': 'done', - 'task_uuid': 'de75c902-5ee5-4739-996e-448376a93eff', - } - stub_fetch = b'content' - - mock_service.vault_cook.return_value = stub_cook - mock_service.vault_fetch.return_value = stub_fetch - - for obj_type, (obj_type_name, obj_type_format) in OBJECT_TYPES.items(): - url = '/api/1/vault/{}/{}/'.format(obj_type_name, TEST_OBJ_ID) - if obj_type_format: - url += '{}/'.format(obj_type_format) + + for obj_type in OBJECT_TYPES: + + fetch_url = reverse(f'api-1-vault-fetch-{obj_type}', + url_args={f'{obj_type[:3]}_id': TEST_OBJ_ID}) + stub_cook = { + 'fetch_url': fetch_url, + 'obj_id': TEST_OBJ_ID, + 'obj_type': obj_type, + 'progress_message': None, + 'status': 'done', + 'task_uuid': 'de75c902-5ee5-4739-996e-448376a93eff', + } + stub_fetch = b'content' + + mock_service.vault_cook.return_value = stub_cook + mock_service.vault_fetch.return_value = stub_fetch + + url = reverse(f'api-1-vault-cook-{obj_type}', + url_args={f'{obj_type[:3]}_id': TEST_OBJ_ID}) + rv = api_client.post(url, {'email': 'test@test.mail'}) assert rv.status_code == 200, rv.data assert rv['Content-Type'] == 'application/json' + stub_cook['fetch_url'] = rv.wsgi_request.build_absolute_uri( + stub_cook['fetch_url']) + assert rv.data == stub_cook mock_service.vault_cook.assert_called_with( obj_type, hashutil.hash_to_bytes(TEST_OBJ_ID), 'test@test.mail') - rv = api_client.get(url + 'raw/') + rv = api_client.get(fetch_url) assert rv.status_code == 200 assert rv['Content-Type'] == 'application/gzip' @@ -51,44 +57,33 @@ obj_type, hashutil.hash_to_bytes(TEST_OBJ_ID)) -def test_api_vault_cook_uppercase_hash(api_client, mocker): - mock_service = mocker.patch('swh.web.api.views.vault.service') - stub_cook = { - 'fetch_url': ('http://127.0.0.1:5004/api/1/vault/directory/{}/raw/' - .format(TEST_OBJ_ID.upper())), - 'obj_id': TEST_OBJ_ID.upper(), - 'obj_type': 'test_type', - 'progress_message': None, - 'status': 'done', - 'task_uuid': 'de75c902-5ee5-4739-996e-448376a93eff', - } - stub_fetch = b'content' - - mock_service.vault_cook.return_value = stub_cook - mock_service.vault_fetch.return_value = stub_fetch - - for obj_type, (obj_type_name, obj_type_format) in OBJECT_TYPES.items(): - url = '/api/1/vault/{}/{}/'.format(obj_type_name, TEST_OBJ_ID) - if obj_type_format: - url += '{}/'.format(obj_type_format) +def test_api_vault_cook_uppercase_hash(api_client): + + for obj_type in OBJECT_TYPES: + + url = reverse(f'api-1-vault-cook-{obj_type}-uppercase-checksum', + url_args={f'{obj_type[:3]}_id': TEST_OBJ_ID.upper()}) rv = api_client.post(url, {'email': 'test@test.mail'}) - assert rv.status_code == 200, rv.data - assert rv['Content-Type'] == 'application/json' + assert rv.status_code == 302 - assert rv.data == stub_cook - mock_service.vault_cook.assert_called_with( - obj_type, - hashutil.hash_to_bytes(TEST_OBJ_ID), - 'test@test.mail') + redirect_url = reverse(f'api-1-vault-cook-{obj_type}', + url_args={f'{obj_type[:3]}_id': TEST_OBJ_ID}) - rv = api_client.get(url + 'raw/') + assert rv['location'] == redirect_url - assert rv.status_code == 200 - assert rv['Content-Type'] == 'application/gzip' - assert rv.content == stub_fetch - mock_service.vault_fetch.assert_called_with( - obj_type, hashutil.hash_to_bytes(TEST_OBJ_ID)) + fetch_url = reverse( + f'api-1-vault-fetch-{obj_type}-uppercase-checksum', + url_args={f'{obj_type[:3]}_id': TEST_OBJ_ID.upper()}) + + rv = api_client.get(fetch_url) + + assert rv.status_code == 302 + + redirect_url = reverse(f'api-1-vault-fetch-{obj_type}', + url_args={f'{obj_type[:3]}_id': TEST_OBJ_ID}) + + assert rv['location'] == redirect_url def test_api_vault_cook_notfound(api_client, mocker): @@ -96,10 +91,9 @@ mock_service.vault_cook.return_value = None mock_service.vault_fetch.return_value = None - for obj_type, (obj_type_name, obj_type_format) in OBJECT_TYPES.items(): - url = '/api/1/vault/{}/{}/'.format(obj_type_name, TEST_OBJ_ID) - if obj_type_format: - url += '{}/'.format(obj_type_format) + for obj_type in OBJECT_TYPES: + url = reverse(f'api-1-vault-cook-{obj_type}', + url_args={f'{obj_type[:3]}_id': TEST_OBJ_ID}) rv = api_client.post(url) assert rv.status_code == 404, rv.data @@ -109,7 +103,10 @@ mock_service.vault_cook.assert_called_with( obj_type, hashutil.hash_to_bytes(TEST_OBJ_ID), None) - rv = api_client.get(url + 'raw/') + fetch_url = reverse(f'api-1-vault-fetch-{obj_type}', + url_args={f'{obj_type[:3]}_id': TEST_OBJ_ID}) + + rv = api_client.get(fetch_url) assert rv.status_code == 404, rv.data assert rv['Content-Type'] == 'application/json' diff --git a/swh/web/tests/common/test_templatetags.py b/swh/web/tests/common/test_templatetags.py --- a/swh/web/tests/common/test_templatetags.py +++ b/swh/web/tests/common/test_templatetags.py @@ -10,28 +10,30 @@ ) -def test_urlize_api_links_api(): - # update api link with html links content with links - content = '{"url": "/api/1/abc/"}' - expected_content = ('{"url": "/api/1/abc/"}') +def test_urlize_http_link(): + link = 'https://example.com/api/1/abc/' + expected_content = f'{link}' + + assert urlize_links_and_mails(link) == expected_content - assert urlize_links_and_mails(content) == expected_content +def test_urlize_email(): + email = 'someone@example.com' + expected_content = f'{email}' -def test_urlize_api_links_browse(): - # update /browse link with html links content with links - content = '{"url": "/browse/def/"}' - expected_content = ('{"url": "' - '/browse/def/"}') - assert urlize_links_and_mails(content) == expected_content + assert urlize_links_and_mails(email) == expected_content def test_urlize_header_links(): - # update api link with html links content with links - content = '; rel="next"\n; rel="prev"' - expected_content = ('</api/1/abc/>; rel="next"\n' - '</api/1/def/>; rel="prev"') + next_link = 'https://example.com/api/1/abc/' + prev_link = 'https://example.com/api/1/def/' + + content = f'<{next_link}>; rel="next"\n<{prev_link}>; rel="prev"' + + expected_content = ( + f'<{next_link}>; rel="next"\n' + f'<{prev_link}>; rel="prev"') assert urlize_header_links(content) == expected_content diff --git a/swh/web/tests/conftest.py b/swh/web/tests/conftest.py --- a/swh/web/tests/conftest.py +++ b/swh/web/tests/conftest.py @@ -83,6 +83,11 @@ cache.clear() +# Alias rf fixture from pytest-django +@pytest.fixture +def request_factory(rf): + return rf + # Fixture to get test client from Django REST Framework @pytest.fixture(scope='module') def api_client():