diff --git a/cypress/integration/origin-browse.spec.js b/cypress/integration/origin-browse.spec.js new file mode 100644 index 00000000..30f4b02a --- /dev/null +++ b/cypress/integration/origin-browse.spec.js @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2020 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 + */ + +describe('Test origin browse', function() { + beforeEach(function() { + const url = `${this.Urls.browse_origin()}?origin_url=${this.origin[1].url}`; + cy.visit(url); + }); + + it('should have code tab active by default', function() { + cy.get('#swh-browse-code-nav-link') + .should('have.class', 'active'); + }); + + it('should load branches view when clicking on the Branches tab', function() { + cy.get('#swh-browse-snapshot-branches-nav-link') + .click(); + + cy.location('pathname') + .should('eq', this.Urls.browse_origin_branches()); + + cy.location('search') + .should('eq', `?origin_url=${this.origin[1].url}`); + + cy.get('#swh-browse-snapshot-branches-nav-link') + .should('have.class', 'active'); + }); + + it('should load releases view when clicking on the Releases tab', function() { + cy.get('#swh-browse-snapshot-releases-nav-link') + .click(); + + cy.location('pathname') + .should('eq', this.Urls.browse_origin_releases()); + + cy.location('search') + .should('eq', `?origin_url=${this.origin[1].url}`); + + cy.get('#swh-browse-snapshot-releases-nav-link') + .should('have.class', 'active'); + }); + + it('should load visits view when clicking on the Visits tab', function() { + cy.get('#swh-browse-origin-visits-nav-link') + .click(); + + cy.location('pathname') + .should('eq', this.Urls.browse_origin_visits()); + + cy.location('search') + .should('eq', `?origin_url=${this.origin[1].url}`); + + cy.get('#swh-browse-origin-visits-nav-link') + .should('have.class', 'active'); + }); + + it('should load code view when clicking on the Code tab', function() { + cy.get('#swh-browse-origin-visits-nav-link') + .click(); + + cy.get('#swh-browse-code-nav-link') + .click(); + + cy.location('pathname') + .should('eq', this.Urls.browse_origin_directory()); + + cy.location('search') + .should('eq', `?origin_url=${this.origin[1].url}`); + + cy.get('#swh-browse-code-nav-link') + .should('have.class', 'active'); + + }); + + it('should have Releases tab link disabled when there is no releases', function() { + const url = `${this.Urls.browse_origin()}?origin_url=${this.origin[0].url}`; + cy.visit(url); + + cy.get('#swh-browse-snapshot-releases-nav-link') + .should('have.class', 'disabled'); + }); + +}); diff --git a/swh/web/assets/src/bundles/browse/browse-utils.js b/swh/web/assets/src/bundles/browse/browse-utils.js index 0dd6f8fa..a7bb7ca9 100644 --- a/swh/web/assets/src/bundles/browse/browse-utils.js +++ b/swh/web/assets/src/bundles/browse/browse-utils.js @@ -1,72 +1,86 @@ /** - * Copyright (C) 2018-2019 The Software Heritage developers + * Copyright (C) 2018-2020 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 {BREAKPOINT_SM} from 'utils/constants'; $(document).ready(() => { $('.dropdown-submenu a.dropdown-item').on('click', e => { $(e.target).next('div').toggle(); if ($(e.target).next('div').css('display') !== 'none') { $(e.target).focus(); } else { $(e.target).blur(); } e.stopPropagation(); e.preventDefault(); }); $('.swh-popover-toggler').popover({ boundary: 'viewport', container: 'body', html: true, placement: function() { const width = $(window).width(); if (width < BREAKPOINT_SM) { return 'top'; } else { return 'right'; } }, template: `<div class="popover" role="tooltip"> <div class="arrow"></div> <h3 class="popover-header"></h3> <div class="popover-body swh-popover"></div> </div>`, content: function() { var content = $(this).attr('data-popover-content'); return $(content).children('.popover-body').remove().html(); }, title: function() { var title = $(this).attr('data-popover-content'); return $(title).children('.popover-heading').html(); }, offset: '50vh', sanitize: false }); $('.swh-vault-menu a.dropdown-item').on('click', e => { $('.swh-popover-toggler').popover('hide'); }); $('.swh-popover-toggler').on('show.bs.popover', (e) => { $(`.swh-popover-toggler:not(#${e.currentTarget.id})`).popover('hide'); $('.swh-vault-menu .dropdown-menu').hide(); }); $('.swh-actions-dropdown').on('hide.bs.dropdown', () => { $('.swh-vault-menu .dropdown-menu').hide(); $('.swh-popover-toggler').popover('hide'); }); $('body').on('click', e => { if ($(e.target).parents('.swh-popover').length) { e.stopPropagation(); } }); }); + +export function initBrowseNavbar() { + if (window.location.pathname === Urls.browse_origin_visits()) { + $('#swh-browse-origin-visits-nav-link').addClass('active'); + } else if (window.location.pathname === Urls.browse_origin_branches() || + window.location.pathname === Urls.browse_snapshot_branches()) { + $('#swh-browse-snapshot-branches-nav-link').addClass('active'); + } else if (window.location.pathname === Urls.browse_origin_releases() || + window.location.pathname === Urls.browse_snapshot_releases()) { + $('#swh-browse-snapshot-releases-nav-link').addClass('active'); + } else { + $('#swh-browse-code-nav-link').addClass('active'); + } +} diff --git a/swh/web/assets/src/bundles/webapp/webapp.css b/swh/web/assets/src/bundles/webapp/webapp.css index 91073170..6b252251 100644 --- a/swh/web/assets/src/bundles/webapp/webapp.css +++ b/swh/web/assets/src/bundles/webapp/webapp.css @@ -1,679 +1,675 @@ /** - * Copyright (C) 2018-2019 The Software Heritage developers + * Copyright (C) 2018-2020 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 */ html { height: 100%; overflow-x: hidden; scroll-behavior: auto !important; } body { min-height: 100%; margin: 0; position: relative; padding-bottom: 120px; } a:active, a.active { outline: none; } code { background-color: #f9f2f4; } pre code { background-color: transparent; } footer { background-color: #262626; color: #fff; font-size: 0.8rem; position: absolute; bottom: 0; width: 100%; padding-top: 20px; padding-bottom: 20px; } footer a, footer a:visited, footer a:hover { color: #fecd1b; } footer a:hover { text-decoration: underline; } .link-color { color: #fecd1b; } pre { background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; padding: 9.5px; font-size: 0.8rem; } .btn.active { background-color: #e7e7e7; } .card { margin-bottom: 5px !important; overflow-x: auto; } .navbar-brand { padding: 5px; margin-right: 0; } .table { margin-bottom: 0; } .swh-table thead { background-color: #f2f4f5; border-top: 1px solid rgba(0, 0, 0, 0.2); font-weight: normal; } .swh-table-striped th { border-top: none; } .swh-table-striped tbody tr:nth-child(even) { background-color: #f2f4f5; } .swh-table-striped tbody tr:nth-child(odd) { background-color: #fff; } .swh-web-app-link a { text-decoration: none; border: none; } .swh-web-app-link:hover { background-color: #efeff2; } .table > thead > tr > th { border-top: none; border-bottom: 1px solid #e20026; } .table > tbody > tr > td { border-style: none; } .sitename .first-word, .sitename .second-word { color: rgba(0, 0, 0, 0.75); font-weight: normal; font-size: 1.2rem; } .sitename .first-word { font-family: 'Alegreya Sans', sans-serif; } .sitename .second-word { font-family: 'Alegreya', serif; } .swh-counter { font-size: 150%; } @media (max-width: 600px) { .swh-counter-container { margin-top: 1rem; } } .swh-http-error { margin: 0 auto; text-align: center; } .swh-http-error-head { color: #2d353c; font-size: 30px; } .swh-http-error-code { bottom: 60%; color: #2d353c; font-size: 96px; line-height: 80px; margin-bottom: 10px !important; } .swh-http-error-desc { font-size: 12px; color: #647788; text-align: center; } .swh-http-error-desc pre { display: inline-block; text-align: left; max-width: 800px; white-space: pre-wrap; } .swh-list-unstyled { list-style: none; } .popover { max-width: 97%; z-index: 40000; } .modal { text-align: center; padding: 0 !important; z-index: 50000; } .modal::before { content: ''; display: inline-block; height: 100%; vertical-align: middle; margin-right: -4px; } .modal-dialog { display: inline-block; text-align: left; vertical-align: middle; } .dropdown-submenu { position: relative; } .dropdown-submenu .dropdown-menu { top: 0; left: -100%; margin-top: -5px; margin-left: -2px; } .dropdown-item:hover, .dropdown-item:focus { background-color: rgba(0, 0, 0, 0.1); } a.dropdown-left::before { content: "\f035e"; font-family: 'Material Design Icons'; display: block; width: 20px; height: 20px; float: left; margin-left: 0; } #swh-navbar { border-top-style: none; border-left-style: none; border-right-style: none; border-bottom-style: solid; border-bottom-width: 5px; border-image: linear-gradient(to right, rgb(226, 0, 38) 0%, rgb(254, 205, 27) 100%) 1 1 1 1; width: 100%; padding: 5px; margin-bottom: 10px; margin-top: 30px; justify-content: normal; flex-wrap: nowrap; height: 72px; overflow: hidden; } #back-to-top { display: none; position: fixed; bottom: 30px; right: 30px; z-index: 10; } #back-to-top a img { display: block; width: 32px; height: 32px; background-size: 32px 32px; text-indent: -999px; overflow: hidden; } .swh-top-bar { direction: ltr; height: 30px; position: fixed; top: 0; left: 0; width: 100%; z-index: 99999; background-color: #262626; color: #fff; text-align: center; font-size: 14px; } .swh-top-bar ul { margin-top: 4px; padding-left: 0; white-space: nowrap; } .swh-top-bar li { display: inline-block; margin-left: 10px; margin-right: 10px; } .swh-top-bar a, .swh-top-bar a:visited { color: white; } .swh-top-bar a.swh-current-site, .swh-top-bar a.swh-current-site:visited { color: #fecd1b; } .swh-position-left { position: absolute; left: 0; } .swh-position-right { position: absolute; right: 0; } .swh-background-gray { background: #efeff2; } .swh-donate-link { border: 1px solid #fecd1b; background-color: #e20026; color: white !important; padding: 3px; border-radius: 3px; } .swh-navbar-content h4 { padding-top: 7px; } .swh-navbar-content .bread-crumbs { display: block; margin-left: -40px; } .swh-navbar-content .bread-crumbs li.bc-no-root { padding-top: 7px; } .main-sidebar { margin-top: 30px; } .content-wrapper { background: none; } .brand-image { max-height: 40px; } .brand-link { padding-top: 18.5px; padding-bottom: 18px; padding-left: 4px; border-bottom: 5px solid #e20026 !important; } .navbar-header a, ul.dropdown-menu a, ul.navbar-nav a, ul.nav-sidebar a { border-bottom-style: none; color: #323232; } .swh-sidebar .nav-link.active { color: #323232 !important; background-color: #e7e7e7 !important; } .nav-tabs .nav-link.active { border-top: 3px solid #e20026; } .swh-image-error { width: 80px; height: auto; } @media (max-width: 600px) { .card { min-width: 80%; } .swh-image-error { width: 40px; height: auto; } - .swh-navbar-content h4 { - font-size: 1rem; - } - .swh-donate-link { display: none; } } .form-check-label { padding-top: 4px; } .swh-id { white-space: pre-wrap; } .swh-id .swh-id-option { display: inline-block; margin-right: 5px; line-height: 1rem; } .nav-pills .nav-link:not(.active):hover { color: rgba(0, 0, 0, 0.55); } .swh-heading-color { color: #e20026 !important; } .sidebar-mini.sidebar-collapse .main-sidebar:hover { width: 4.6rem; } .sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel > .info, .sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p, .sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-text { visibility: hidden !important; } .sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info { transition: none; } .sidebar-mini.sidebar-mini.sidebar-collapse .sidebar { padding-right: 0; } .swh-words-logo { position: absolute; top: 0; left: 0; width: 73px; height: 73px; text-align: center; font-size: 10pt; color: rgba(0, 0, 0, 0.75); } .swh-words-logo:hover { text-decoration: none; } .swh-words-logo-swh { line-height: 1; padding-top: 13px; visibility: hidden; } hr.swh-faded-line { border: 0; height: 1px; background-image: linear-gradient(to left, #f0f0f0, #8c8b8b, #f0f0f0); } /* Ensure that section title with link is colored like standard section title */ .swh-readme h1 a, .swh-readme h2 a, .swh-readme h3 a, .swh-readme h4 a, .swh-readme h5 a, .swh-readme h6 a { color: #e20026; } /* Make list compact in reStructuredText rendering */ .swh-rst li p { margin-bottom: 0; } .swh-readme-txt pre { background: none; border: none; } .swh-coverage-col { padding-left: 10px; padding-right: 10px; } .swh-coverage { height: calc(65px + 1em); padding-top: 0.3rem; border: none; } .swh-coverage a { text-decoration: none; } .swh-coverage-logo { display: block; width: 100%; height: 50px; margin-left: auto; margin-right: auto; object-fit: contain; /* polyfill for old browsers, see https://github.com/bfred-it/object-fit-images */ font-family: 'object-fit: contain;'; } .swh-coverage-list { width: 100%; height: 320px; border: none; } tr.swh-tr-hover-highlight:hover td { background: #ededed; } tr.swh-api-doc-route a { text-decoration: none; } .swh-apidoc .col { margin: 10px; } .swh-apidoc .swh-rst blockquote { border: 0; margin: 0; padding: 0; } a.toggle-col { text-decoration: none; } a.toggle-col.col-hidden { text-decoration: line-through; } .admonition.warning { background: #fcf8e3; border: 1px solid #faebcc; padding: 15px; border-radius: 4px; } .admonition.warning p { margin-bottom: 0; } .admonition.warning .first { font-size: 1.5rem; } .swh-popover { max-height: 50vh; overflow-y: auto; overflow-x: auto; padding: 0; padding-right: 1.4em; } @media screen and (min-width: 768px) { .swh-popover { max-width: 50vw; } } .swh-metadata-table-row { border-top: 1px solid #ddd !important; } .swh-metadata-table-key { min-width: 200px; max-width: 200px; width: 200px; } .swh-metadata-table-value pre { white-space: pre-wrap; } .d3-wrapper { position: relative; height: 0; width: 100%; padding: 0; /* padding-bottom will be overwritten by JavaScript later */ padding-bottom: 100%; } .d3-wrapper > svg { position: absolute; height: 100%; width: 100%; left: 0; top: 0; } div.d3-tooltip { position: absolute; text-align: center; width: auto; height: auto; padding: 2px; font: 12px sans-serif; background: white; border: 1px solid black; border-radius: 4px; pointer-events: none; } .page-link { cursor: pointer; } .wrapper { overflow: hidden; } .swh-badge { padding-bottom: 1rem; cursor: pointer; } .swh-badge-html, .swh-badge-md, .swh-badge-rst { white-space: pre-wrap; } /* Material Design icons alignment tweaks */ .mdi { display: inline-block; } .mdi-camera { transform: translateY(1px); } .mdi-source-commit { transform: translateY(2px); } /* To set icons at a fixed width. Great to use when different icon widths throw off alignment. Courtesy of Font Awesome. */ .mdi-fw { text-align: center; width: 1.25em; } .main-header .nav-link { height: inherit; } .nav-sidebar .nav-header:not(:first-of-type) { padding-top: 1rem; } .nav-sidebar .nav-link { padding-top: 0; padding-bottom: 0; } .nav-sidebar > .nav-item .nav-icon { vertical-align: sub; } .swh-search-icon { line-height: 1rem; vertical-align: middle; } diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py index d28a2a74..9ff3fb7e 100644 --- a/swh/web/common/utils.py +++ b/swh/web/common/utils.py @@ -1,364 +1,365 @@ # Copyright (C) 2017-2020 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 re from datetime import datetime, timezone from dateutil import parser as date_parser from dateutil import tz from typing import Optional, Dict, Any import docutils.parsers.rst import docutils.utils from bs4 import BeautifulSoup from docutils.core import publish_parts from docutils.writers.html5_polyglot import Writer, HTMLTranslator from django.urls import reverse as django_reverse from django.http import QueryDict, HttpRequest from prometheus_client.registry import CollectorRegistry from rest_framework.authentication import SessionAuthentication from swh.web.common.exc import BadInputExc from swh.web.common.typing import QueryParameters from swh.web.config import get_config SWH_WEB_METRICS_REGISTRY = CollectorRegistry(auto_describe=True) swh_object_icons = { "branch": "mdi mdi-source-branch", "branches": "mdi mdi-source-branch", "content": "mdi mdi-file-document", "directory": "mdi mdi-folder", + "origin": "mdi mdi-source-repository", "person": "mdi mdi-account", "revisions history": "mdi mdi-history", "release": "mdi mdi-tag", "releases": "mdi mdi-tag", "revision": "mdi mdi-rotate-90 mdi-source-commit", "snapshot": "mdi mdi-camera", "visits": "mdi mdi-calendar-month", } def reverse( viewname: str, url_args: Optional[Dict[str, Any]] = None, query_params: Optional[QueryParameters] = 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: 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: 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 query parameters """ if url_args: url_args = {k: v for k, v in url_args.items() if v is not None} url = django_reverse( viewname, urlconf=urlconf, kwargs=url_args, current_app=current_app ) if query_params: query_params = {k: v for k, v in query_params.items() if v} if query_params and len(query_params) > 0: query_dict = QueryDict("", mutable=True) for k in sorted(query_params.keys()): query_dict[k] = query_params[k] url += "?" + query_dict.urlencode(safe="/;:") if request is not None: url = request.build_absolute_uri(url) return url def datetime_to_utc(date): """Returns datetime in UTC without timezone info Args: date (datetime.datetime): input datetime with timezone info Returns: datetime.datetime: datetime in UTC without timezone info """ if date.tzinfo: return date.astimezone(tz.gettz("UTC")).replace(tzinfo=timezone.utc) else: return date def parse_timestamp(timestamp): """Given a time or timestamp (as string), parse the result as UTC datetime. Returns: datetime.datetime: a timezone-aware datetime representing the parsed value or None if the parsing fails. Samples: - 2016-01-12 - 2016-01-12T09:19:12+0100 - Today is January 1, 2047 at 8:21:00AM - 1452591542 """ if not timestamp: return None try: date = date_parser.parse(timestamp, ignoretz=False, fuzzy=True) return datetime_to_utc(date) except Exception: try: return datetime.utcfromtimestamp(float(timestamp)).replace( tzinfo=timezone.utc ) except (ValueError, OverflowError) as e: raise BadInputExc(e) def shorten_path(path): """Shorten the given path: for each hash present, only return the first 8 characters followed by an ellipsis""" sha256_re = r"([0-9a-f]{8})[0-9a-z]{56}" sha1_re = r"([0-9a-f]{8})[0-9a-f]{32}" ret = re.sub(sha256_re, r"\1...", path) return re.sub(sha1_re, r"\1...", ret) def format_utc_iso_date(iso_date, fmt="%d %B %Y, %H:%M UTC"): """Turns a string representation of an ISO 8601 date string to UTC and format it into a more human readable one. For instance, from the following input string: '2017-05-04T13:27:13+02:00' the following one is returned: '04 May 2017, 11:27 UTC'. Custom format string may also be provided as parameter Args: iso_date (str): a string representation of an ISO 8601 date fmt (str): optional date formatting string Returns: str: a formatted string representation of the input iso date """ if not iso_date: return iso_date date = parse_timestamp(iso_date) return date.strftime(fmt) def gen_path_info(path): """Function to generate path data navigation for use with a breadcrumb in the swh web ui. For instance, from a path /folder1/folder2/folder3, it returns the following list:: [{'name': 'folder1', 'path': 'folder1'}, {'name': 'folder2', 'path': 'folder1/folder2'}, {'name': 'folder3', 'path': 'folder1/folder2/folder3'}] Args: path: a filesystem path Returns: list: a list of path data for navigation as illustrated above. """ path_info = [] if path: sub_paths = path.strip("/").split("/") path_from_root = "" for p in sub_paths: path_from_root += "/" + p path_info.append({"name": p, "path": path_from_root.strip("/")}) return path_info def parse_rst(text, report_level=2): """ Parse a reStructuredText string with docutils. Args: text (str): string with reStructuredText markups in it report_level (int): level of docutils report messages to print (1 info 2 warning 3 error 4 severe 5 none) Returns: docutils.nodes.document: a parsed docutils document """ parser = docutils.parsers.rst.Parser() components = (docutils.parsers.rst.Parser,) settings = docutils.frontend.OptionParser( components=components ).get_default_values() settings.report_level = report_level document = docutils.utils.new_document("rst-doc", settings=settings) parser.parse(text, document) return document def get_client_ip(request): """ Return the client IP address from an incoming HTTP request. Args: request (django.http.HttpRequest): the incoming HTTP request Returns: str: The client IP address """ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") if x_forwarded_for: ip = x_forwarded_for.split(",")[0] else: ip = request.META.get("REMOTE_ADDR") return ip browsers_supported_image_mimes = set( [ "image/gif", "image/png", "image/jpeg", "image/bmp", "image/webp", "image/svg", "image/svg+xml", ] ) def context_processor(request): """ Django context processor used to inject variables in all swh-web templates. """ config = get_config() if ( hasattr(request, "user") and request.user.is_authenticated and not hasattr(request.user, "backend") ): # To avoid django.template.base.VariableDoesNotExist errors # when rendering templates when standard Django user is logged in. request.user.backend = "django.contrib.auth.backends.ModelBackend" return { "swh_object_icons": swh_object_icons, "available_languages": None, "swh_client_config": config["client_config"], "oidc_enabled": bool(config["keycloak"]["server_url"]), "browsers_supported_image_mimes": browsers_supported_image_mimes, } class EnforceCSRFAuthentication(SessionAuthentication): """ Helper class to enforce CSRF validation on a DRF view when a user is not authenticated. """ def authenticate(self, request): 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 class _NoHeaderHTMLTranslator(HTMLTranslator): """ Docutils translator subclass to customize the generation of HTML from reST-formatted docstrings """ def __init__(self, document): super().__init__(document) self.body_prefix = [] self.body_suffix = [] _HTML_WRITER = Writer() _HTML_WRITER.translator_class = _NoHeaderHTMLTranslator def rst_to_html(rst: str) -> str: """ Convert reStructuredText document into HTML. Args: rst: A string containing a reStructuredText document Returns: Body content of the produced HTML conversion. """ settings = { "initial_header_level": 2, } pp = publish_parts(rst, writer=_HTML_WRITER, settings_overrides=settings) return f'<div class="swh-rst">{pp["html_body"]}</div>' def prettify_html(html: str) -> str: """ Prettify an HTML document. Args: html: Input HTML document Returns: The prettified HTML document """ return BeautifulSoup(html, "lxml").prettify() diff --git a/swh/web/templates/browse/browse.html b/swh/web/templates/browse/browse.html index 184a4846..1e98e7cc 100644 --- a/swh/web/templates/browse/browse.html +++ b/swh/web/templates/browse/browse.html @@ -1,65 +1,38 @@ {% extends "./layout.html" %} {% comment %} Copyright (C) 2017-2020 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 {% endcomment %} {% load swh_templatetags %} {% block title %}{{ heading }} – Software Heritage archive{% endblock %} {% block navbar-content %} -{% if snapshot_context %} - <h4> - <i class="{{ swh_object_icons|key_value:swh_object_name.lower }} mdi-fw" aria-hidden="true"></i> - - {% if snapshot_context.origin_info %} - Browse archived {{ swh_object_name.lower }} for origin - <a href="{% url 'browse-origin' %}?origin_url={{ snapshot_context.origin_info.url }}"> - {{ snapshot_context.origin_info.url }} - </a> - {% if snapshot_context.origin_info.url|slice:"0:4" == "http" %} - <a href="{{ snapshot_context.origin_info.url }}" title="Go to origin"> - <i class="mdi mdi-open-in-new" aria-hidden="true"></i> - </a> - {% endif %} - {% else %} - Browse archived {{ swh_object_name.lower }} for snapshot - <a href="{% url 'browse-swh-id' snapshot_context.snapshot_swhid %}"> - {{ snapshot_context.snapshot_swhid }} - </a> - {% endif %} - </h4> -{% else %} - <h4> - <i class="{{ swh_object_icons|key_value:swh_object_name.lower }} mdi-fw" aria-hidden="true"></i> - Browse archived {{ swh_object_name.lower }} - <a href="{% url 'browse-swh-id' swh_object_id %}"> - {{ swh_object_id }} - </a> - </h4> -{% endif %} +<h4> + Browse the archive +</h4> {% endblock %} {% block browse-content %} {% block swh-browse-before-content %} {% if snapshot_context %} {% include "includes/snapshot-context.html" %} {% endif %} {% endblock %} {% block swh-browse-content %}{% endblock %} {% block swh-browse-after-content %}{% endblock %} <script> swh.webapp.initPage('browse'); </script> {% endblock %} diff --git a/swh/web/templates/browse/origin-visits.html b/swh/web/templates/browse/origin-visits.html index 324c76f6..7ac93ebc 100644 --- a/swh/web/templates/browse/origin-visits.html +++ b/swh/web/templates/browse/origin-visits.html @@ -1,82 +1,85 @@ {% extends "./browse.html" %} {% comment %} -Copyright (C) 2017-2018 The Software Heritage developers +Copyright (C) 2017-2020 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 {% endcomment %} {% load static %} {% load swh_templatetags %} {% load render_bundle from webpack_loader %} {% block header %} {{ block.super }} {% render_bundle 'origin' %} {% endblock %} {% block swh-browse-content %} -<h4>Overview</h4> - -<ul> - <li class="d-inline-block"> - <b>Total number of visits: </b>{{ origin_visits|length }} - <i class="mdi mdi-fw" aria-hidden="true"></i> - </li> - <li class="d-inline-block"> - <b>Last full visit: </b><span style="margin-left: 20px;" id="swh-last-full-visit"></span> - <i class="mdi mdi-fw" aria-hidden="true"></i> - </li> - <li class="d-inline-block"> - <b>First full visit: </b><span style="margin-left: 20px;" id="swh-first-full-visit"></span> - <i class="mdi mdi-fw" aria-hidden="true"></i> - </li> - <li class="d-inline-block"> - <b>Last visit: </b><span style="margin-left: 20px;" id="swh-last-visit"></span> - <i class="mdi mdi-fw" aria-hidden="true"></i> - </li> -</ul> - -<h4>History</h4> - -<form class="text-center"> - <div class="custom-control custom-radio custom-control-inline"> - <input class="custom-control-input" type="radio" id="swh-different-snapshot-visits" name="swh-visits" value="option1" checked> - <label class="custom-control-label font-weight-normal" for="swh-different-snapshot-visits" onclick="swh.origin.showFullVisitsDifferentSnapshots(event)"> - Show full visits with different snapshots - </label> - </div> - <div class="custom-control custom-radio custom-control-inline"> - <input class="custom-control-input" type="radio" id="swh-full-visits" name="swh-visits" value="option2"> - <label class="custom-control-label font-weight-normal" for="swh-full-visits" onclick="swh.origin.showFullVisits(event)"> - Show all full visits - </label> - </div> - <div class="custom-control custom-radio custom-control-inline"> - <input class="custom-control-input" type="radio" id="swh-all-visits" name="swh-visits" value="option3"> - <label class="custom-control-label font-weight-normal" for="swh-all-visits" onclick="swh.origin.showAllVisits(event)"> - Show all visits - </label> - </div> -</form> - -<h5>Calendar</h5> - -<div id="swh-visits-calendar"></div> - -<h5>List</h5> - -<div id="swh-visits-list"></div> - -<h5>Timeline</h5> - -<div id="swh-visits-timeline" class="d3-wrapper"></div> +<div class="p-3"> + + <h4>Overview</h4> + + <ul> + <li class="d-inline-block"> + <b>Total number of visits: </b>{{ origin_visits|length }} + <i class="mdi mdi-fw" aria-hidden="true"></i> + </li> + <li class="d-inline-block"> + <b>Last full visit: </b><span style="margin-left: 20px;" id="swh-last-full-visit"></span> + <i class="mdi mdi-fw" aria-hidden="true"></i> + </li> + <li class="d-inline-block"> + <b>First full visit: </b><span style="margin-left: 20px;" id="swh-first-full-visit"></span> + <i class="mdi mdi-fw" aria-hidden="true"></i> + </li> + <li class="d-inline-block"> + <b>Last visit: </b><span style="margin-left: 20px;" id="swh-last-visit"></span> + <i class="mdi mdi-fw" aria-hidden="true"></i> + </li> + </ul> + + <h4>History</h4> + + <form class="text-center"> + <div class="custom-control custom-radio custom-control-inline"> + <input class="custom-control-input" type="radio" id="swh-different-snapshot-visits" name="swh-visits" value="option1" checked> + <label class="custom-control-label font-weight-normal" for="swh-different-snapshot-visits" onclick="swh.origin.showFullVisitsDifferentSnapshots(event)"> + Show full visits with different snapshots + </label> + </div> + <div class="custom-control custom-radio custom-control-inline"> + <input class="custom-control-input" type="radio" id="swh-full-visits" name="swh-visits" value="option2"> + <label class="custom-control-label font-weight-normal" for="swh-full-visits" onclick="swh.origin.showFullVisits(event)"> + Show all full visits + </label> + </div> + <div class="custom-control custom-radio custom-control-inline"> + <input class="custom-control-input" type="radio" id="swh-all-visits" name="swh-visits" value="option3"> + <label class="custom-control-label font-weight-normal" for="swh-all-visits" onclick="swh.origin.showAllVisits(event)"> + Show all visits + </label> + </div> + </form> + + <h5>Calendar</h5> + + <div id="swh-visits-calendar"></div> + + <h5>List</h5> + + <div id="swh-visits-list"></div> + + <h5>Timeline</h5> + + <div id="swh-visits-timeline" class="d3-wrapper"></div> +</div> <script> // all origin visits var visits = {{ origin_visits|jsonify }}; swh.origin.initVisitsReporting(visits); </script> {% endblock %} diff --git a/swh/web/templates/includes/snapshot-context.html b/swh/web/templates/includes/snapshot-context.html index 7f736393..741fea20 100644 --- a/swh/web/templates/includes/snapshot-context.html +++ b/swh/web/templates/includes/snapshot-context.html @@ -1,31 +1,86 @@ {% comment %} -Copyright (C) 2017-2018 The Software Heritage developers +Copyright (C) 2017-2020 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 {% endcomment %} {% load swh_templatetags %} -<hr class="mt-1 mb-1"> -<div class="swh-origin-visit-details"> - <ul> - {% if snapshot_context.origin_info %} - <li><i class="{{ swh_object_icons.visits }} mdi-fw"></i><a href="{{ snapshot_context.origin_visits_url }}">Visits</a></li> - {% endif %} - {% if snapshot_context.visit_info %} - <li><i class="{{ swh_object_icons.snapshot }} mdi-fw" aria-hidden="true"></i> Snapshot date: <a href="{{ snapshot_context.visit_info.url }}">{{ snapshot_context.visit_info.formatted_date }}</a></li> - {% endif %} - {% if not snapshot_context.snapshot_sizes.revision %} - <li><i class="{{ swh_object_icons.branches }} mdi-fw" aria-hidden="true"></i> Branches (0)</li> - {% else %} - <li><i class="{{ swh_object_icons.branches }} mdi-fw" aria-hidden="true"></i> <a href="{{ snapshot_context.branches_url }}">Branches ({{ snapshot_context.snapshot_sizes.revision}})</a></li> - {% endif %} - {% if not snapshot_context.snapshot_sizes.release %} - <li><i class="{{ swh_object_icons.releases }} mdi-fw" aria-hidden="true"></i> Releases (0)</li> - {% else %} - <li><i class="{{ swh_object_icons.releases }} mdi-fw" aria-hidden="true"></i> <a href="{{ snapshot_context.releases_url }}">Releases ({{ snapshot_context.snapshot_sizes.release }})</a></li> - {% endif %} - </ul> -</div> -<hr class="mt-1 mb-1"> \ No newline at end of file +<h5> +{% if snapshot_context.origin_info %} + <i class="{{ swh_object_icons.origin }} mdi-fw" aria-hidden="true" title="Origin"></i> + <a class="swh-heading-color" href="{% url 'browse-origin' %}?origin_url={{ snapshot_context.origin_info.url }}"> + {{ snapshot_context.origin_info.url }} + </a> + {% if snapshot_context.origin_info.url|slice:"0:4" == "http" %} + <a href="{{ snapshot_context.origin_info.url }}" title="Go to origin"> + <i class="mdi mdi-open-in-new" aria-hidden="true"></i> + </a> + {% endif %} +{% else %} + <i class="{{ swh_object_icons.snapshot }} mdi-fw" aria-hidden="true" title="Snapshot"></i> + <a class="swh-heading-color" href="{% url 'browse-swh-id' snapshot_context.snapshot_swhid %}"> + {{ snapshot_context.snapshot_swhid }} + </a> +{% endif %} +</h5> +{% if snapshot_context.visit_info %} + <div class="mb-1 pl-1"> + <i class="{{ swh_object_icons.snapshot }} mdi-fw" aria-hidden="true" title="Snapshot date"></i> + <a href="{{ snapshot_context.visit_info.url }}"> + {{ snapshot_context.visit_info.formatted_date }} + </a> + </div> +{% endif %} + +<ul class="nav nav-tabs" id="swh-snapshot-context-nav" style="padding-left: 5px;"> + <li class="nav-item"> + <a class="nav-link" id="swh-browse-code-nav-link" href="{{ snapshot_context.visit_info.url }}"> + <i class="mdi mdi-code-tags mdi-fw" aria-hidden="true"></i> + Code + </a> + </li> + {% if not snapshot_context.snapshot_sizes.revision %} + <li class="nav-item"> + <a class="nav-link disabled" id="swh-browse-snapshot-branches-nav-link" href="#"> + <i class="{{ swh_object_icons.branches }} mdi-fw" aria-hidden="true"></i> + Branches (0) + </a> + </li> + {% else %} + <li class="nav-item"> + <a class="nav-link" id="swh-browse-snapshot-branches-nav-link" href="{{ snapshot_context.branches_url }}"> + <i class="{{ swh_object_icons.branches }} mdi-fw" aria-hidden="true"></i> + Branches ({{ snapshot_context.snapshot_sizes.revision}}) + </a> + </li> + {% endif %} + {% if not snapshot_context.snapshot_sizes.release %} + <li class="nav-item"> + <a class="nav-link disabled" id="swh-browse-snapshot-releases-nav-link" href="#"> + <i class="{{ swh_object_icons.releases }} mdi-fw" aria-hidden="true"></i> + Releases (0) + </a> + </li> + {% else %} + <li class="nav-item"> + <a class="nav-link" id="swh-browse-snapshot-releases-nav-link" href="{{ snapshot_context.releases_url }}"> + <i class="{{ swh_object_icons.releases }} mdi-fw" aria-hidden="true"></i> + Releases ({{ snapshot_context.snapshot_sizes.release }}) + </a> + </li> + {% endif %} + {% if snapshot_context.origin_info %} + <li class="nav-item"> + <a class="nav-link" id="swh-browse-origin-visits-nav-link" href="{{ snapshot_context.origin_visits_url }}"> + <i class="{{ swh_object_icons.visits }} mdi-fw"></i> + Visits + </a> + </li> + {% endif %} +</ul> + +<script> + swh.browse.initBrowseNavbar(); +</script> \ No newline at end of file diff --git a/swh/web/templates/includes/top-navigation.html b/swh/web/templates/includes/top-navigation.html index 2e66468e..d961e927 100644 --- a/swh/web/templates/includes/top-navigation.html +++ b/swh/web/templates/includes/top-navigation.html @@ -1,141 +1,141 @@ {% comment %} -Copyright (C) 2017-2019 The Software Heritage developers +Copyright (C) 2017-2020 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 {% endcomment %} {% load swh_templatetags %} -<div class="swh-browse-top-navigation d-flex align-items-start justify-content-between flex-wrap"> +<div class="swh-browse-top-navigation d-flex align-items-start justify-content-between flex-wrap mt-1"> {% if snapshot_context %} {% if snapshot_context.branch or snapshot_context.release %} <div class="dropdown" id="swh-branches-releases-dd"> <button class="btn btn-block btn-default btn-sm dropdown-toggle" type="button" data-toggle="dropdown"> {% if snapshot_context.branch %} <i class="{{ swh_object_icons.branch }} mdi-fw" aria-hidden="true"></i> Branch: <strong>{{ snapshot_context.branch }}</strong> {% else %} <i class="{{ swh_object_icons.release }} mdi-fw" aria-hidden="true"></i> Release: <strong>{{ snapshot_context.release }}</strong> {% endif %} <span class="caret"></span> </button> <ul class="scrollable-menu dropdown-menu swh-branches-releases"> <ul class="nav nav-tabs"> <li class="nav-item"><a class="nav-link active swh-branches-switch" data-toggle="tab">Branches</a></li> <li class="nav-item"><a class="nav-link swh-releases-switch" data-toggle="tab">Releases</a></li> </ul> <div class="tab-content"> <div class="tab-pane active" id="swh-tab-branches"> {% for b in snapshot_context.branches %} <li class="swh-branch"> <a href="{{ b.url | safe }}"> <i class="{{ swh_object_icons.branch }} mdi-fw" aria-hidden="true"></i> {% if b.name == snapshot_context.branch %} <i class="mdi mdi-check-bold mdi-fw" aria-hidden="true"></i> {% else %} <i class="mdi mdi-fw" aria-hidden="true"></i> {% endif %} {{ b.name }} </a> </li> {% endfor %} {% if snapshot_context.branches|length < snapshot_context.snapshot_sizes.revision %} <li> <i class="mdi mdi-alert mdi-fw" aria-hidden="true"></i> Branches list truncated to {{ snapshot_context.branches|length }} entries, {{ snapshot_context.branches|length|mul:-1|add:snapshot_context.snapshot_sizes.revision }} were omitted. </li> {% endif %} </div> <div class="tab-pane" id="swh-tab-releases"> {% if snapshot_context.releases %} {% for r in snapshot_context.releases %} {% if r.target_type == 'revision' %} <li class="swh-release"> <a href="{{ r.url | safe }}"> <i class="{{ swh_object_icons.release }} mdi-fw" aria-hidden="true"></i> {% if r.name == snapshot_context.release %} <i class="mdi mdi-check-bold mdi-fw" aria-hidden="true"></i> {% else %} <i class="mdi mdi-fw" aria-hidden="true"></i> {% endif %} {{ r.name }} </a> </li> {% endif %} {% endfor %} {% if snapshot_context.releases|length < snapshot_context.snapshot_sizes.release %} <li> <i class="mdi mdi-alert mdi-fw" aria-hidden="true"></i> Releases list truncated to {{ snapshot_context.releases|length }} entries, {{ snapshot_context.releases|length|mul:-1|add:snapshot_context.snapshot_sizes.release }} were omitted. </li> {% endif %} {% else %} <span>No releases to show</span> {% endif %} </div> </div> </ul> </div> {% endif %} {% endif %} <div class="flex-grow-1"> {% include "includes/breadcrumbs.html" %} </div> <div class="btn-group swh-actions-dropdown ml-auto"> {% if top_right_link %} <a href="{{ top_right_link.url | safe }}" class="btn btn-default btn-sm swh-tr-link" role="button"> {% if top_right_link.icon %} <i class="{{ top_right_link.icon }} mdi-fw" aria-hidden="true"></i> {% endif %} {{ top_right_link.text }} </a> {% endif %} {% if available_languages %} <select data-placeholder="Select Language" class="language-select chosen-select"> <option value=""></option> {% for lang in available_languages %} <option value="{{ lang }}">{{ lang }}</option> {% endfor %} </select> {% endif %} {% if show_actions_menu %} <button class="btn btn-default btn-sm dropdown-toggle" type="button" data-toggle="dropdown"> <i class="mdi mdi-menu mdi-fw" aria-hidden="true"></i>Actions <span class="caret"></span> </button> <ul class="dropdown-menu dropdown-menu-right swh-browse-actions-menu"> {% if not snapshot_context or not snapshot_context.is_empty %} {% include "includes/vault-create-tasks.html" %} {% endif %} {% include "includes/show-metadata.html" %} {% include "includes/take-new-snapshot.html" %} </ul> {% endif %} </div> </div> {% include "includes/show-swh-ids.html" %} <script> var snapshotContext = false; var branch = false; {% if snapshot_context %} snapshotContext = true; branch = "{{ snapshot_context.branch|escape }}"; {% endif %} {% if available_languages %} $(".chosen-select").val("{{ language }}"); $(".chosen-select").chosen().change(function(event, params) { updateLanguage(params.selected); }); {% endif %} swh.browse.initSnapshotNavigation(snapshotContext, branch !== "None"); </script> diff --git a/swh/web/tests/browse/views/test_content.py b/swh/web/tests/browse/views/test_content.py index d5545215..a4c9fe1b 100644 --- a/swh/web/tests/browse/views/test_content.py +++ b/swh/web/tests/browse/views/test_content.py @@ -1,635 +1,607 @@ # Copyright (C) 2017-2020 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 random -import textwrap from django.utils.html import escape from hypothesis import given from swh.model.identifiers import CONTENT, DIRECTORY, RELEASE, REVISION, SNAPSHOT from swh.web.browse.snapshot_context import process_snapshot_branches from swh.web.browse.utils import ( get_mimetype_and_encoding_for_content, prepare_content_for_display, _re_encode_content, ) from swh.web.common.exc import NotFoundExc from swh.web.common.identifiers import get_swh_persistent_id from swh.web.common.utils import gen_path_info, reverse from swh.web.tests.django_asserts import ( assert_contains, assert_not_contains, assert_template_used, ) from swh.web.tests.strategies import ( content, content_text_non_utf8, content_text_no_highlight, content_image_type, content_unsupported_image_type_rendering, content_text, invalid_sha1, unknown_content, content_utf8_detected_as_binary, origin_with_multiple_visits, ) @given(content_text()) def test_content_view_text(client, archive_data, content): sha1_git = content["sha1_git"] url = reverse( "browse-content", url_args={"query_string": content["sha1"]}, query_params={"path": content["path"]}, ) url_raw = reverse("browse-content-raw", url_args={"query_string": content["sha1"]}) resp = client.get(url) content_display = _process_content_for_display(archive_data, content) mimetype = content_display["mimetype"] assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") if mimetype.startswith("text/"): assert_contains(resp, '<code class="%s">' % content_display["language"]) assert_contains(resp, escape(content_display["content_data"])) assert_contains(resp, url_raw) swh_cnt_id = get_swh_persistent_id(CONTENT, sha1_git) swh_cnt_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_cnt_id}) assert_contains(resp, swh_cnt_id) assert_contains(resp, swh_cnt_id_url) - assert_contains( - resp, - textwrap.indent( - ( - f"Browse archived content\n" - f'<a href="{swh_cnt_id_url}">\n' - f" {swh_cnt_id}\n" - f"</a>" - ), - " " * 4, - ), - ) @given(content_text_no_highlight()) def test_content_view_text_no_highlight(client, archive_data, content): sha1_git = content["sha1_git"] url = reverse("browse-content", url_args={"query_string": content["sha1"]}) url_raw = reverse("browse-content-raw", url_args={"query_string": content["sha1"]}) resp = client.get(url) content_display = _process_content_for_display(archive_data, content) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") assert_contains(resp, '<code class="nohighlight">') assert_contains(resp, escape(content_display["content_data"])) assert_contains(resp, url_raw) swh_cnt_id = get_swh_persistent_id(CONTENT, sha1_git) swh_cnt_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_cnt_id}) assert_contains(resp, swh_cnt_id) assert_contains(resp, swh_cnt_id_url) @given(content_text_non_utf8()) def test_content_view_no_utf8_text(client, archive_data, content): sha1_git = content["sha1_git"] url = reverse("browse-content", url_args={"query_string": content["sha1"]}) resp = client.get(url) content_display = _process_content_for_display(archive_data, content) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") swh_cnt_id = get_swh_persistent_id(CONTENT, sha1_git) swh_cnt_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_cnt_id}) assert_contains(resp, swh_cnt_id_url) assert_contains(resp, escape(content_display["content_data"])) @given(content_image_type()) def test_content_view_image(client, archive_data, content): url = reverse("browse-content", url_args={"query_string": content["sha1"]}) url_raw = reverse("browse-content-raw", url_args={"query_string": content["sha1"]}) resp = client.get(url) content_display = _process_content_for_display(archive_data, content) mimetype = content_display["mimetype"] content_data = content_display["content_data"] assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") assert_contains(resp, '<img src="data:%s;base64,%s"/>' % (mimetype, content_data)) assert_contains(resp, url_raw) @given(content_unsupported_image_type_rendering()) def test_content_view_image_no_rendering(client, archive_data, content): url = reverse("browse-content", url_args={"query_string": content["sha1"]}) resp = client.get(url) mimetype = content["mimetype"] encoding = content["encoding"] assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") assert_contains( resp, ( f"Content with mime type {mimetype} and encoding {encoding} " "cannot be displayed." ), ) @given(content_text()) def test_content_view_text_with_path(client, archive_data, content): path = content["path"] url = reverse( "browse-content", url_args={"query_string": content["sha1"]}, query_params={"path": path}, ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") assert_contains(resp, '<nav class="bread-crumbs') content_display = _process_content_for_display(archive_data, content) mimetype = content_display["mimetype"] if mimetype.startswith("text/"): hljs_language = content["hljs_language"] assert_contains(resp, '<code class="%s">' % hljs_language) assert_contains(resp, escape(content_display["content_data"])) split_path = path.split("/") root_dir_sha1 = split_path[0] filename = split_path[-1] path = path.replace(root_dir_sha1 + "/", "").replace(filename, "") swhid_context = { "anchor": get_swh_persistent_id(DIRECTORY, root_dir_sha1), "path": f"/{path}{filename}", } swh_cnt_id = get_swh_persistent_id( CONTENT, content["sha1_git"], metadata=swhid_context ) swh_cnt_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_cnt_id}) assert_contains(resp, swh_cnt_id) assert_contains(resp, swh_cnt_id_url) path_info = gen_path_info(path) root_dir_url = reverse("browse-directory", url_args={"sha1_git": root_dir_sha1}) assert_contains(resp, '<li class="swh-path">', count=len(path_info) + 1) assert_contains( resp, '<a href="' + root_dir_url + '">' + root_dir_sha1[:7] + "</a>" ) for p in path_info: dir_url = reverse( "browse-directory", url_args={"sha1_git": root_dir_sha1}, query_params={"path": p["path"]}, ) assert_contains(resp, '<a href="' + dir_url + '">' + p["name"] + "</a>") assert_contains(resp, "<li>" + filename + "</li>") url_raw = reverse( "browse-content-raw", url_args={"query_string": content["sha1"]}, query_params={"filename": filename}, ) assert_contains(resp, url_raw) url = reverse( "browse-content", url_args={"query_string": content["sha1"]}, query_params={"path": filename}, ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") assert_not_contains(resp, '<nav class="bread-crumbs') invalid_path = "%s/foo/bar/baz" % root_dir_sha1 url = reverse( "browse-content", url_args={"query_string": content["sha1"]}, query_params={"path": invalid_path}, ) resp = client.get(url) assert resp.status_code == 404 @given(content_text()) def test_content_raw_text(client, archive_data, content): url = reverse("browse-content-raw", url_args={"query_string": content["sha1"]}) resp = client.get(url) content_data = archive_data.content_get(content["sha1"])["data"] assert resp.status_code == 200 assert resp["Content-Type"] == "text/plain" assert resp["Content-disposition"] == ("filename=%s_%s" % ("sha1", content["sha1"])) assert resp.content == content_data filename = content["path"].split("/")[-1] url = reverse( "browse-content-raw", url_args={"query_string": content["sha1"]}, query_params={"filename": filename}, ) resp = client.get(url) assert resp.status_code == 200 assert resp["Content-Type"] == "text/plain" assert resp["Content-disposition"] == "filename=%s" % filename assert resp.content == content_data @given(content_text_non_utf8()) def test_content_raw_no_utf8_text(client, content): url = reverse("browse-content-raw", url_args={"query_string": content["sha1"]}) resp = client.get(url) assert resp.status_code == 200 _, encoding = get_mimetype_and_encoding_for_content(resp.content) assert encoding == content["encoding"] @given(content_image_type()) def test_content_raw_bin(client, archive_data, content): url = reverse("browse-content-raw", url_args={"query_string": content["sha1"]}) resp = client.get(url) filename = content["path"].split("/")[-1] content_data = archive_data.content_get(content["sha1"])["data"] assert resp.status_code == 200 assert resp["Content-Type"] == "application/octet-stream" assert resp["Content-disposition"] == "attachment; filename=%s_%s" % ( "sha1", content["sha1"], ) assert resp.content == content_data url = reverse( "browse-content-raw", url_args={"query_string": content["sha1"]}, query_params={"filename": filename}, ) resp = client.get(url) assert resp.status_code == 200 assert resp["Content-Type"] == "application/octet-stream" assert resp["Content-disposition"] == "attachment; filename=%s" % filename assert resp.content == content_data @given(invalid_sha1(), unknown_content()) def test_content_request_errors(client, invalid_sha1, unknown_content): url = reverse("browse-content", url_args={"query_string": invalid_sha1}) resp = client.get(url) assert resp.status_code == 400 assert_template_used(resp, "error.html") url = reverse("browse-content", url_args={"query_string": unknown_content["sha1"]}) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") @given(content()) def test_content_bytes_missing(client, archive_data, mocker, content): mock_service = mocker.patch("swh.web.browse.utils.service") content_data = archive_data.content_get_metadata(content["sha1"]) content_data["data"] = None mock_service.lookup_content.return_value = content_data mock_service.lookup_content_filetype.side_effect = Exception() mock_service.lookup_content_raw.side_effect = NotFoundExc( "Content bytes not available!" ) url = reverse("browse-content", url_args={"query_string": content["sha1"]}) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "browse/content.html") def test_content_too_large(client, mocker): mock_request_content = mocker.patch("swh.web.browse.views.content.request_content") stub_content_too_large_data = { "checksums": { "sha1": "8624bcdae55baeef00cd11d5dfcfa60f68710a02", "sha1_git": "94a9ed024d3859793618152ea559a168bbcbb5e2", "sha256": ( "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b16" "5a1dcd80c7c545eb65b903" ), "blake2s256": ( "38702b7168c7785bfe748b51b45d9856070ba90" "f9dc6d90f2ea75d4356411ffe" ), }, "length": 30000000, "raw_data": None, "mimetype": "text/plain", "encoding": "us-ascii", "language": "not detected", "licenses": "GPL", "error_code": 200, "error_message": "", "error_description": "", } content_sha1 = stub_content_too_large_data["checksums"]["sha1"] mock_request_content.return_value = stub_content_too_large_data url = reverse("browse-content", url_args={"query_string": content_sha1}) url_raw = reverse("browse-content-raw", url_args={"query_string": content_sha1}) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") assert_contains(resp, "Content is too large to be displayed") assert_contains(resp, url_raw) @given(content()) def test_content_uppercase(client, content): url = reverse( "browse-content-uppercase-checksum", url_args={"query_string": content["sha1"].upper()}, ) resp = client.get(url) assert resp.status_code == 302 redirect_url = reverse("browse-content", url_args={"query_string": content["sha1"]}) assert resp["location"] == redirect_url @given(content_utf8_detected_as_binary()) def test_content_utf8_detected_as_binary_display(client, archive_data, content): url = reverse("browse-content", url_args={"query_string": content["sha1"]}) resp = client.get(url) content_display = _process_content_for_display(archive_data, content) assert_contains(resp, escape(content_display["content_data"])) @given(origin_with_multiple_visits()) def test_content_origin_snapshot_branch_browse(client, archive_data, origin): visits = archive_data.origin_visit_get(origin["url"]) visit = random.choice(visits) snapshot = archive_data.snapshot_get(visit["snapshot"]) branches, releases = process_snapshot_branches(snapshot) branch_info = random.choice(branches) directory = archive_data.revision_get(branch_info["revision"])["directory"] directory_content = archive_data.directory_ls(directory) directory_file = random.choice( [e for e in directory_content if e["type"] == "file"] ) url = reverse( "browse-content", url_args={"query_string": directory_file["checksums"]["sha1"]}, query_params={ "origin_url": origin["url"], "snapshot": snapshot["id"], "branch": branch_info["name"], "path": directory_file["name"], }, ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") _check_origin_snapshot_related_html(resp, origin, snapshot, branches, releases) assert_contains(resp, directory_file["name"]) assert_contains(resp, f"Branch: <strong>{branch_info['name']}</strong>") cnt_swhid = get_swh_persistent_id( CONTENT, directory_file["checksums"]["sha1_git"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), "anchor": get_swh_persistent_id(REVISION, branch_info["revision"]), "path": f"/{directory_file['name']}", }, ) assert_contains(resp, cnt_swhid) dir_swhid = get_swh_persistent_id( DIRECTORY, directory, metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), "anchor": get_swh_persistent_id(REVISION, branch_info["revision"]), "path": "/", }, ) assert_contains(resp, dir_swhid) rev_swhid = get_swh_persistent_id( REVISION, branch_info["revision"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), }, ) assert_contains(resp, rev_swhid) snp_swhid = get_swh_persistent_id( SNAPSHOT, snapshot, metadata={"origin": origin["url"],}, ) assert_contains(resp, snp_swhid) @given(origin_with_multiple_visits()) def test_content_origin_snapshot_release_browse(client, archive_data, origin): visits = archive_data.origin_visit_get(origin["url"]) visit = random.choice(visits) snapshot = archive_data.snapshot_get(visit["snapshot"]) branches, releases = process_snapshot_branches(snapshot) release_info = random.choice(releases) directory_content = archive_data.directory_ls(release_info["directory"]) directory_file = random.choice( [e for e in directory_content if e["type"] == "file"] ) url = reverse( "browse-content", url_args={"query_string": directory_file["checksums"]["sha1"]}, query_params={ "origin_url": origin["url"], "snapshot": snapshot["id"], "release": release_info["name"], "path": directory_file["name"], }, ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") _check_origin_snapshot_related_html(resp, origin, snapshot, branches, releases) assert_contains(resp, directory_file["name"]) assert_contains(resp, f"Release: <strong>{release_info['name']}</strong>") cnt_swhid = get_swh_persistent_id( CONTENT, directory_file["checksums"]["sha1_git"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), "anchor": get_swh_persistent_id(RELEASE, release_info["id"]), "path": f"/{directory_file['name']}", }, ) assert_contains(resp, cnt_swhid) dir_swhid = get_swh_persistent_id( DIRECTORY, release_info["directory"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), "anchor": get_swh_persistent_id(RELEASE, release_info["id"]), "path": "/", }, ) assert_contains(resp, dir_swhid) rev_swhid = get_swh_persistent_id( REVISION, release_info["target"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), }, ) assert_contains(resp, rev_swhid) rel_swhid = get_swh_persistent_id( RELEASE, release_info["id"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), }, ) assert_contains(resp, rel_swhid) snp_swhid = get_swh_persistent_id( SNAPSHOT, snapshot, metadata={"origin": origin["url"],}, ) assert_contains(resp, snp_swhid) def _check_origin_snapshot_related_html(resp, origin, snapshot, branches, releases): browse_origin_url = reverse( "browse-origin", query_params={"origin_url": origin["url"]} ) - assert_contains( - resp, - textwrap.indent( - ( - "Browse archived content for origin\n" - f'<a href="{browse_origin_url}">\n' - f" {origin['url']}\n" - f"</a>" - ), - " " * 6, - ), - ) + assert_contains(resp, f'href="{browse_origin_url}"') origin_branches_url = reverse( "browse-origin-branches", query_params={"origin_url": origin["url"], "snapshot": snapshot["id"]}, ) - assert_contains( - resp, - '<a href="%s">Branches (%s)</a>' % (escape(origin_branches_url), len(branches)), - ) + assert_contains(resp, f'href="{escape(origin_branches_url)}"') + assert_contains(resp, f"Branches ({len(branches)})") origin_releases_url = reverse( "browse-origin-releases", query_params={"origin_url": origin["url"], "snapshot": snapshot["id"]}, ) - assert_contains( - resp, - '<a href="%s">Releases (%s)</a>' % (escape(origin_releases_url), len(releases)), - ) + assert_contains(resp, f'href="{escape(origin_releases_url)}"') + assert_contains(resp, f"Releases ({len(releases)})") assert_contains(resp, '<li class="swh-branch">', count=len(branches)) assert_contains(resp, '<li class="swh-release">', count=len(releases)) def _process_content_for_display(archive_data, content): content_data = archive_data.content_get(content["sha1"]) mime_type, encoding = get_mimetype_and_encoding_for_content(content_data["data"]) mime_type, encoding, content_data = _re_encode_content( mime_type, encoding, content_data["data"] ) content_display = prepare_content_for_display( content_data, mime_type, content["path"] ) assert type(content_display["content_data"]) == str return content_display diff --git a/swh/web/tests/browse/views/test_directory.py b/swh/web/tests/browse/views/test_directory.py index 1a023417..65a4c4fd 100644 --- a/swh/web/tests/browse/views/test_directory.py +++ b/swh/web/tests/browse/views/test_directory.py @@ -1,358 +1,330 @@ # Copyright (C) 2017-2020 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 random -import textwrap from django.utils.html import escape from hypothesis import given from swh.model.identifiers import DIRECTORY, RELEASE, REVISION, SNAPSHOT from swh.web.browse.snapshot_context import process_snapshot_branches from swh.web.common.identifiers import get_swh_persistent_id from swh.web.common.utils import gen_path_info, reverse from swh.web.tests.django_asserts import assert_contains, assert_template_used from swh.web.tests.strategies import ( directory, directory_with_subdirs, invalid_sha1, unknown_directory, origin_with_multiple_visits, ) @given(directory()) def test_root_directory_view(client, archive_data, directory): _directory_view_checks(client, directory, archive_data.directory_ls(directory)) @given(directory_with_subdirs()) def test_sub_directory_view(client, archive_data, directory): dir_content = archive_data.directory_ls(directory) subdir = random.choice([e for e in dir_content if e["type"] == "dir"]) subdir_content = archive_data.directory_ls(subdir["target"]) _directory_view_checks(client, directory, subdir_content, subdir["name"]) @given(invalid_sha1(), unknown_directory()) def test_directory_request_errors(client, invalid_sha1, unknown_directory): dir_url = reverse("browse-directory", url_args={"sha1_git": invalid_sha1}) resp = client.get(dir_url) assert resp.status_code == 400 assert_template_used(resp, "error.html") dir_url = reverse("browse-directory", url_args={"sha1_git": unknown_directory}) resp = client.get(dir_url) assert resp.status_code == 404 assert_template_used(resp, "error.html") @given(directory()) def test_directory_uppercase(client, directory): url = reverse( "browse-directory-uppercase-checksum", url_args={"sha1_git": directory.upper()} ) resp = client.get(url) assert resp.status_code == 302 redirect_url = reverse("browse-directory", url_args={"sha1_git": directory}) assert resp["location"] == redirect_url @given(directory()) def test_permalink_box_context(client, tests_data, directory): origin_url = random.choice(tests_data["origins"])["url"] url = reverse( "browse-directory", url_args={"sha1_git": directory}, query_params={"origin_url": origin_url}, ) resp = client.get(url) assert resp.status_code == 200 assert_contains(resp, 'id="swh-id-context-option-directory"') @given(origin_with_multiple_visits()) def test_directory_origin_snapshot_branch_browse(client, archive_data, origin): visits = archive_data.origin_visit_get(origin["url"]) visit = random.choice(visits) snapshot = archive_data.snapshot_get(visit["snapshot"]) branches, releases = process_snapshot_branches(snapshot) branch_info = random.choice(branches) directory = archive_data.revision_get(branch_info["revision"])["directory"] directory_content = archive_data.directory_ls(directory) directory_subdir = random.choice( [e for e in directory_content if e["type"] == "dir"] ) url = reverse( "browse-directory", url_args={"sha1_git": directory}, query_params={ "origin_url": origin["url"], "snapshot": snapshot["id"], "branch": branch_info["name"], "path": directory_subdir["name"], }, ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/directory.html") _check_origin_snapshot_related_html(resp, origin, snapshot, branches, releases) assert_contains(resp, directory_subdir["name"]) assert_contains(resp, f"Branch: <strong>{branch_info['name']}</strong>") dir_swhid = get_swh_persistent_id( DIRECTORY, directory_subdir["target"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), "anchor": get_swh_persistent_id(REVISION, branch_info["revision"]), "path": "/", }, ) assert_contains(resp, dir_swhid) rev_swhid = get_swh_persistent_id( REVISION, branch_info["revision"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), }, ) assert_contains(resp, rev_swhid) snp_swhid = get_swh_persistent_id( SNAPSHOT, snapshot, metadata={"origin": origin["url"],}, ) assert_contains(resp, snp_swhid) @given(origin_with_multiple_visits()) def test_content_origin_snapshot_release_browse(client, archive_data, origin): visits = archive_data.origin_visit_get(origin["url"]) visit = random.choice(visits) snapshot = archive_data.snapshot_get(visit["snapshot"]) branches, releases = process_snapshot_branches(snapshot) release_info = random.choice(releases) directory = release_info["directory"] directory_content = archive_data.directory_ls(directory) directory_subdir = random.choice( [e for e in directory_content if e["type"] == "dir"] ) url = reverse( "browse-directory", url_args={"sha1_git": directory}, query_params={ "origin_url": origin["url"], "snapshot": snapshot["id"], "release": release_info["name"], "path": directory_subdir["name"], }, ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/directory.html") _check_origin_snapshot_related_html(resp, origin, snapshot, branches, releases) assert_contains(resp, directory_subdir["name"]) assert_contains(resp, f"Release: <strong>{release_info['name']}</strong>") dir_swhid = get_swh_persistent_id( DIRECTORY, directory_subdir["target"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), "anchor": get_swh_persistent_id(RELEASE, release_info["id"]), "path": "/", }, ) assert_contains(resp, dir_swhid) rev_swhid = get_swh_persistent_id( REVISION, release_info["target"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), }, ) assert_contains(resp, rev_swhid) rel_swhid = get_swh_persistent_id( RELEASE, release_info["id"], metadata={ "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot), }, ) assert_contains(resp, rel_swhid) snp_swhid = get_swh_persistent_id( SNAPSHOT, snapshot, metadata={"origin": origin["url"],}, ) assert_contains(resp, snp_swhid) def _check_origin_snapshot_related_html(resp, origin, snapshot, branches, releases): browse_origin_url = reverse( "browse-origin", query_params={"origin_url": origin["url"]} ) - assert_contains( - resp, - textwrap.indent( - ( - "Browse archived directory for origin\n" - f'<a href="{browse_origin_url}">\n' - f" {origin['url']}\n" - f"</a>" - ), - " " * 6, - ), - ) + + assert_contains(resp, f'href="{browse_origin_url}"') origin_branches_url = reverse( "browse-origin-branches", query_params={"origin_url": origin["url"], "snapshot": snapshot["id"]}, ) - assert_contains( - resp, - '<a href="%s">Branches (%s)</a>' % (escape(origin_branches_url), len(branches)), - ) + assert_contains(resp, f'href="{escape(origin_branches_url)}"') + assert_contains(resp, f"Branches ({len(branches)})") origin_releases_url = reverse( "browse-origin-releases", query_params={"origin_url": origin["url"], "snapshot": snapshot["id"]}, ) - assert_contains( - resp, - '<a href="%s">Releases (%s)</a>' % (escape(origin_releases_url), len(releases)), - ) + assert_contains(resp, f'href="{escape(origin_releases_url)}"') + assert_contains(resp, f"Releases ({len(releases)})") assert_contains(resp, '<li class="swh-branch">', count=len(branches)) assert_contains(resp, '<li class="swh-release">', count=len(releases)) def _directory_view_checks( client, root_directory_sha1, directory_entries, path=None, origin_url=None, snapshot_id=None, ): dirs = [e for e in directory_entries if e["type"] in ("dir", "rev")] files = [e for e in directory_entries if e["type"] == "file"] url_args = {"sha1_git": root_directory_sha1} query_params = {"path": path, "origin_url": origin_url, "snapshot": snapshot_id} url = reverse("browse-directory", url_args=url_args, query_params=query_params) root_dir_url = reverse( "browse-directory", url_args={"sha1_git": root_directory_sha1} ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/directory.html") assert_contains( resp, '<a href="' + root_dir_url + '">' + root_directory_sha1[:7] + "</a>" ) assert_contains(resp, '<td class="swh-directory">', count=len(dirs)) assert_contains(resp, '<td class="swh-content">', count=len(files)) for d in dirs: if d["type"] == "rev": dir_url = reverse("browse-revision", url_args={"sha1_git": d["target"]}) else: dir_path = d["name"] if path: dir_path = "%s/%s" % (path, d["name"]) dir_url = reverse( "browse-directory", url_args={"sha1_git": root_directory_sha1}, query_params={"path": dir_path}, ) assert_contains(resp, dir_url) for f in files: file_path = "%s/%s" % (root_directory_sha1, f["name"]) if path: file_path = "%s/%s/%s" % (root_directory_sha1, path, f["name"]) query_string = "sha1_git:" + f["target"] file_url = reverse( "browse-content", url_args={"query_string": query_string}, query_params={"path": file_path}, ) assert_contains(resp, file_url) path_info = gen_path_info(path) assert_contains(resp, '<li class="swh-path">', count=len(path_info) + 1) assert_contains( resp, '<a href="%s">%s</a>' % (root_dir_url, root_directory_sha1[:7]) ) for p in path_info: dir_url = reverse( "browse-directory", url_args={"sha1_git": root_directory_sha1}, query_params={"path": p["path"]}, ) assert_contains(resp, '<a href="%s">%s</a>' % (dir_url, p["name"])) assert_contains(resp, "vault-cook-directory") swh_dir_id = get_swh_persistent_id(DIRECTORY, directory_entries[0]["dir_id"]) swh_dir_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_dir_id}) - assert_contains( - resp, - textwrap.indent( - ( - f"Browse archived directory\n" - f'<a href="{swh_dir_id_url}">\n' - f" {swh_dir_id}\n" - f"</a>" - ), - " " * 4, - ), - ) - swhid_context = {} if root_directory_sha1 != directory_entries[0]["dir_id"]: swhid_context["anchor"] = get_swh_persistent_id(DIRECTORY, root_directory_sha1) swhid_context["path"] = f"/{path}/" if path else "/" if root_directory_sha1 != directory_entries[0]["dir_id"]: swhid_context["anchor"] = get_swh_persistent_id(DIRECTORY, root_directory_sha1) swh_dir_id = get_swh_persistent_id( DIRECTORY, directory_entries[0]["dir_id"], metadata=swhid_context ) swh_dir_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_dir_id}) assert_contains(resp, swh_dir_id) assert_contains(resp, swh_dir_id_url) diff --git a/swh/web/tests/browse/views/test_origin.py b/swh/web/tests/browse/views/test_origin.py index ce8e65c2..fa160eb4 100644 --- a/swh/web/tests/browse/views/test_origin.py +++ b/swh/web/tests/browse/views/test_origin.py @@ -1,1351 +1,1314 @@ # Copyright (C) 2017-2020 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 random import re import string -import textwrap from django.utils.html import escape from hypothesis import given from swh.storage.utils import now from swh.model.hashutil import hash_to_bytes from swh.model.identifiers import CONTENT, DIRECTORY, RELEASE, REVISION, SNAPSHOT from swh.model.model import ( Snapshot, SnapshotBranch, TargetType, OriginVisit, OriginVisitStatus, ) from swh.web.browse.snapshot_context import process_snapshot_branches from swh.web.common.exc import NotFoundExc from swh.web.common.identifiers import get_swh_persistent_id from swh.web.common.utils import ( reverse, gen_path_info, format_utc_iso_date, parse_timestamp, ) from swh.web.tests.data import get_content, random_sha1 from swh.web.tests.django_asserts import assert_contains, assert_template_used from swh.web.tests.strategies import ( origin, origin_with_multiple_visits, new_origin, new_snapshot, visit_dates, revisions, origin_with_releases, release as existing_release, unknown_revision, ) @given(origin_with_multiple_visits()) def test_origin_visits_browse(client, archive_data, origin): url = reverse("browse-origin-visits", query_params={"origin_url": origin["url"]}) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/origin-visits.html") url = reverse("browse-origin-visits", query_params={"origin_url": origin["url"]}) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/origin-visits.html") visits = archive_data.origin_visit_get(origin["url"]) for v in visits: vdate = format_utc_iso_date(v["date"], "%Y-%m-%dT%H:%M:%SZ") browse_dir_url = reverse( "browse-origin-directory", query_params={"origin_url": origin["url"], "timestamp": vdate}, ) assert_contains(resp, browse_dir_url) - _check_origin_view_title(resp, origin["url"], "visits") + _check_origin_link(resp, origin["url"]) @given(origin_with_multiple_visits()) def test_origin_content_view(client, archive_data, origin): origin_visits = archive_data.origin_visit_get(origin["url"]) def _get_archive_data(visit_idx): snapshot = archive_data.snapshot_get(origin_visits[visit_idx]["snapshot"]) head_rev_id = archive_data.snapshot_get_head(snapshot) head_rev = archive_data.revision_get(head_rev_id) dir_content = archive_data.directory_ls(head_rev["directory"]) dir_files = [e for e in dir_content if e["type"] == "file"] dir_file = random.choice(dir_files) branches, releases = process_snapshot_branches(snapshot) return { "branches": branches, "releases": releases, "root_dir_sha1": head_rev["directory"], "content": get_content(dir_file["checksums"]["sha1"]), "visit": origin_visits[visit_idx], } tdata = _get_archive_data(-1) _origin_content_view_test_helper( client, archive_data, origin, origin_visits[-1], tdata["branches"], tdata["releases"], tdata["root_dir_sha1"], tdata["content"], ) _origin_content_view_test_helper( client, archive_data, origin, origin_visits[-1], tdata["branches"], tdata["releases"], tdata["root_dir_sha1"], tdata["content"], timestamp=tdata["visit"]["date"], ) visit_unix_ts = parse_timestamp(tdata["visit"]["date"]).timestamp() visit_unix_ts = int(visit_unix_ts) _origin_content_view_test_helper( client, archive_data, origin, origin_visits[-1], tdata["branches"], tdata["releases"], tdata["root_dir_sha1"], tdata["content"], timestamp=visit_unix_ts, ) _origin_content_view_test_helper( client, archive_data, origin, origin_visits[-1], tdata["branches"], tdata["releases"], tdata["root_dir_sha1"], tdata["content"], snapshot_id=tdata["visit"]["snapshot"], ) tdata = _get_archive_data(0) _origin_content_view_test_helper( client, archive_data, origin, origin_visits[0], tdata["branches"], tdata["releases"], tdata["root_dir_sha1"], tdata["content"], visit_id=tdata["visit"]["visit"], ) _origin_content_view_test_helper( client, archive_data, origin, origin_visits[0], tdata["branches"], tdata["releases"], tdata["root_dir_sha1"], tdata["content"], snapshot_id=tdata["visit"]["snapshot"], ) @given(origin()) def test_origin_root_directory_view(client, archive_data, origin): origin_visits = archive_data.origin_visit_get(origin["url"]) visit = origin_visits[-1] snapshot = archive_data.snapshot_get(visit["snapshot"]) head_rev_id = archive_data.snapshot_get_head(snapshot) head_rev = archive_data.revision_get(head_rev_id) root_dir_sha1 = head_rev["directory"] dir_content = archive_data.directory_ls(root_dir_sha1) branches, releases = process_snapshot_branches(snapshot) visit_unix_ts = parse_timestamp(visit["date"]).timestamp() visit_unix_ts = int(visit_unix_ts) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, visit_id=visit["visit"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, timestamp=visit_unix_ts, ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, timestamp=visit["date"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, snapshot_id=visit["snapshot"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, visit_id=visit["visit"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, timestamp=visit_unix_ts, ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, timestamp=visit["date"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, dir_content, snapshot_id=visit["snapshot"], ) @given(origin()) def test_origin_sub_directory_view(client, archive_data, origin): origin_visits = archive_data.origin_visit_get(origin["url"]) visit = origin_visits[-1] snapshot = archive_data.snapshot_get(visit["snapshot"]) head_rev_id = archive_data.snapshot_get_head(snapshot) head_rev = archive_data.revision_get(head_rev_id) root_dir_sha1 = head_rev["directory"] subdirs = [ e for e in archive_data.directory_ls(root_dir_sha1) if e["type"] == "dir" ] branches, releases = process_snapshot_branches(snapshot) visit_unix_ts = parse_timestamp(visit["date"]).timestamp() visit_unix_ts = int(visit_unix_ts) if len(subdirs) == 0: return subdir = random.choice(subdirs) subdir_content = archive_data.directory_ls(subdir["target"]) subdir_path = subdir["name"] _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, visit_id=visit["visit"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, timestamp=visit_unix_ts, ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, timestamp=visit["date"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, snapshot_id=visit["snapshot"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, visit_id=visit["visit"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, timestamp=visit_unix_ts, ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, timestamp=visit["date"], ) _origin_directory_view_test_helper( client, archive_data, origin, visit, branches, releases, root_dir_sha1, subdir_content, path=subdir_path, snapshot_id=visit["snapshot"], ) @given(origin()) def test_origin_branches(client, archive_data, origin): origin_visits = archive_data.origin_visit_get(origin["url"]) visit = origin_visits[-1] snapshot = archive_data.snapshot_get(visit["snapshot"]) snapshot_content = process_snapshot_branches(snapshot) _origin_branches_test_helper(client, origin, snapshot_content) _origin_branches_test_helper( client, origin, snapshot_content, snapshot_id=visit["snapshot"] ) @given(origin()) def test_origin_releases(client, archive_data, origin): origin_visits = archive_data.origin_visit_get(origin["url"]) visit = origin_visits[-1] snapshot = archive_data.snapshot_get(visit["snapshot"]) snapshot_content = process_snapshot_branches(snapshot) _origin_releases_test_helper(client, origin, snapshot_content) _origin_releases_test_helper( client, origin, snapshot_content, snapshot_id=visit["snapshot"] ) @given( new_origin(), new_snapshot(min_size=4, max_size=4), visit_dates(), revisions(min_size=3, max_size=3), ) def test_origin_snapshot_null_branch( client, archive_data, new_origin, new_snapshot, visit_dates, revisions ): snp_dict = new_snapshot.to_dict() new_origin = archive_data.origin_add([new_origin])[0] for i, branch in enumerate(snp_dict["branches"].keys()): if i == 0: snp_dict["branches"][branch] = None else: snp_dict["branches"][branch] = { "target_type": "revision", "target": hash_to_bytes(revisions[i - 1]), } archive_data.snapshot_add([Snapshot.from_dict(snp_dict)]) visit = archive_data.origin_visit_add( [ OriginVisit( origin=new_origin["url"], date=visit_dates[0], type="git", status="ongoing", snapshot=None, ) ] )[0] visit_status = OriginVisitStatus( origin=new_origin["url"], visit=visit.visit, date=now(), status="partial", snapshot=snp_dict["id"], ) archive_data.origin_visit_status_add([visit_status]) url = reverse( "browse-origin-directory", query_params={"origin_url": new_origin["url"]} ) rv = client.get(url) assert rv.status_code == 200 @given( new_origin(), new_snapshot(min_size=4, max_size=4), visit_dates(), revisions(min_size=4, max_size=4), ) def test_origin_snapshot_invalid_branch( client, archive_data, new_origin, new_snapshot, visit_dates, revisions ): snp_dict = new_snapshot.to_dict() new_origin = archive_data.origin_add([new_origin])[0] for i, branch in enumerate(snp_dict["branches"].keys()): snp_dict["branches"][branch] = { "target_type": "revision", "target": hash_to_bytes(revisions[i]), } archive_data.snapshot_add([Snapshot.from_dict(snp_dict)]) visit = archive_data.origin_visit_add( [ OriginVisit( origin=new_origin["url"], date=visit_dates[0], type="git", status="ongoing", snapshot=None, ) ] )[0] visit_status = OriginVisitStatus( origin=new_origin["url"], visit=visit.visit, date=now(), status="full", snapshot=snp_dict["id"], ) archive_data.origin_visit_status_add([visit_status]) url = reverse( "browse-origin-directory", query_params={"origin_url": new_origin["url"], "branch": "invalid_branch"}, ) rv = client.get(url) assert rv.status_code == 404 @given(new_origin()) def test_browse_visits_origin_not_found(client, new_origin): url = reverse("browse-origin-visits", query_params={"origin_url": new_origin.url}) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert_contains( resp, f"Origin with url {new_origin.url} not found", status_code=404 ) @given(origin()) def test_browse_origin_directory_no_visit(client, mocker, origin): mock_get_origin_visits = mocker.patch( "swh.web.common.origin_visits.get_origin_visits" ) mock_get_origin_visits.return_value = [] url = reverse("browse-origin-directory", query_params={"origin_url": origin["url"]}) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert_contains(resp, "No visit", status_code=404) assert mock_get_origin_visits.called @given(origin()) def test_browse_origin_directory_unknown_visit(client, mocker, origin): mock_get_origin_visits = mocker.patch( "swh.web.common.origin_visits.get_origin_visits" ) mock_get_origin_visits.return_value = [{"visit": 1}] url = reverse( "browse-origin-directory", query_params={"origin_url": origin["url"], "visit_id": 2}, ) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert re.search("Visit.*not found", resp.content.decode("utf-8")) assert mock_get_origin_visits.called @given(origin()) def test_browse_origin_directory_not_found(client, origin): url = reverse( "browse-origin-directory", query_params={"origin_url": origin["url"], "path": "/invalid/dir/path/"}, ) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert re.search("Directory.*not found", resp.content.decode("utf-8")) @given(origin()) def test_browse_origin_content_no_visit(client, mocker, origin): mock_get_origin_visits = mocker.patch( "swh.web.common.origin_visits.get_origin_visits" ) mock_get_origin_visits.return_value = [] url = reverse( "browse-origin-content", query_params={"origin_url": origin["url"], "path": "foo"}, ) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert_contains(resp, "No visit", status_code=404) assert mock_get_origin_visits.called @given(origin()) def test_browse_origin_content_unknown_visit(client, mocker, origin): mock_get_origin_visits = mocker.patch( "swh.web.common.origin_visits.get_origin_visits" ) mock_get_origin_visits.return_value = [{"visit": 1}] url = reverse( "browse-origin-content", query_params={"origin_url": origin["url"], "path": "foo", "visit_id": 2}, ) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert re.search("Visit.*not found", resp.content.decode("utf-8")) assert mock_get_origin_visits.called @given(origin()) def test_browse_origin_content_directory_empty_snapshot(client, mocker, origin): mock_snapshot_service = mocker.patch("swh.web.browse.snapshot_context.service") mock_get_origin_visit_snapshot = mocker.patch( "swh.web.browse.snapshot_context.get_origin_visit_snapshot" ) mock_get_origin_visit_snapshot.return_value = ([], []) mock_snapshot_service.lookup_origin.return_value = origin mock_snapshot_service.lookup_snapshot_sizes.return_value = { "revision": 0, "release": 0, } for browse_context in ("content", "directory"): url = reverse( f"browse-origin-{browse_context}", query_params={"origin_url": origin["url"], "path": "baz"}, ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, f"browse/{browse_context}.html") assert re.search("snapshot.*is empty", resp.content.decode("utf-8")) assert mock_get_origin_visit_snapshot.called assert mock_snapshot_service.lookup_origin.called assert mock_snapshot_service.lookup_snapshot_sizes.called @given(origin()) def test_browse_origin_content_not_found(client, origin): url = reverse( "browse-origin-content", query_params={"origin_url": origin["url"], "path": "/invalid/file/path"}, ) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert re.search("Directory entry.*not found", resp.content.decode("utf-8")) @given(origin()) def test_browse_directory_snapshot_not_found(client, mocker, origin): mock_get_snapshot_context = mocker.patch( "swh.web.browse.snapshot_context.get_snapshot_context" ) mock_get_snapshot_context.side_effect = NotFoundExc("Snapshot not found") url = reverse("browse-origin-directory", query_params={"origin_url": origin["url"]}) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert_contains(resp, "Snapshot not found", status_code=404) assert mock_get_snapshot_context.called @given(origin()) def test_origin_empty_snapshot(client, mocker, origin): mock_service = mocker.patch("swh.web.browse.snapshot_context.service") mock_get_origin_visit_snapshot = mocker.patch( "swh.web.browse.snapshot_context.get_origin_visit_snapshot" ) mock_get_origin_visit_snapshot.return_value = ([], []) mock_service.lookup_snapshot_sizes.return_value = { "revision": 0, "release": 0, } mock_service.lookup_origin.return_value = origin url = reverse("browse-origin-directory", query_params={"origin_url": origin["url"]}) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/directory.html") resp_content = resp.content.decode("utf-8") assert re.search("snapshot.*is empty", resp_content) assert not re.search("swh-tr-link", resp_content) assert mock_get_origin_visit_snapshot.called assert mock_service.lookup_snapshot_sizes.called @given(origin_with_releases()) def test_origin_release_browse(client, archive_data, origin): snapshot = archive_data.snapshot_get_latest(origin["url"]) release = [ b for b in snapshot["branches"].values() if b["target_type"] == "release" ][-1] release_data = archive_data.release_get(release["target"]) revision_data = archive_data.revision_get(release_data["target"]) url = reverse( "browse-origin-directory", query_params={"origin_url": origin["url"], "release": release_data["name"]}, ) resp = client.get(url) assert resp.status_code == 200 assert_contains(resp, release_data["name"]) assert_contains(resp, release["target"]) swhid_context = { "origin": origin["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot["id"]), "anchor": get_swh_persistent_id(RELEASE, release_data["id"]), "path": "/", } swh_dir_id = get_swh_persistent_id( DIRECTORY, revision_data["directory"], metadata=swhid_context ) swh_dir_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_dir_id}) assert_contains(resp, swh_dir_id) assert_contains(resp, swh_dir_id_url) @given(origin_with_releases()) def test_origin_release_browse_not_found(client, origin): invalid_release_name = "swh-foo-bar" url = reverse( "browse-origin-directory", query_params={"origin_url": origin["url"], "release": invalid_release_name}, ) resp = client.get(url) assert resp.status_code == 404 assert re.search( f"Release {invalid_release_name}.*not found", resp.content.decode("utf-8") ) @given(new_origin(), unknown_revision()) def test_origin_browse_directory_branch_with_non_resolvable_revision( client, archive_data, new_origin, unknown_revision ): branch_name = "master" snapshot = Snapshot( branches={ branch_name.encode(): SnapshotBranch( target=hash_to_bytes(unknown_revision), target_type=TargetType.REVISION, ) } ) new_origin = archive_data.origin_add([new_origin])[0] archive_data.snapshot_add([snapshot]) visit = archive_data.origin_visit_add( [ OriginVisit( origin=new_origin["url"], date=now(), type="git", status="ongoing", snapshot=None, ) ] )[0] visit_status = OriginVisitStatus( origin=new_origin["url"], visit=visit.visit, date=now(), status="partial", snapshot=snapshot.id, ) archive_data.origin_visit_status_add([visit_status]) url = reverse( "browse-origin-directory", query_params={"origin_url": new_origin["url"], "branch": branch_name}, ) resp = client.get(url) assert resp.status_code == 200 assert_contains( resp, f"Revision {unknown_revision } could not be found in the archive." ) @given(origin()) def test_origin_content_no_path(client, origin): url = reverse("browse-origin-content", query_params={"origin_url": origin["url"]}) resp = client.get(url) assert resp.status_code == 400 assert_contains( resp, "The path of a content must be given as query parameter.", status_code=400 ) def test_origin_views_no_url_query_parameter(client): for browse_context in ( "content", "directory", "log", "branches", "releases", "visits", ): url = reverse(f"browse-origin-{browse_context}") resp = client.get(url) assert resp.status_code == 400 assert_contains( resp, "An origin URL must be provided as query parameter.", status_code=400 ) def _origin_content_view_test_helper( client, archive_data, origin_info, origin_visit, origin_branches, origin_releases, root_dir_sha1, content, visit_id=None, timestamp=None, snapshot_id=None, ): content_path = "/".join(content["path"].split("/")[1:]) if not visit_id and not snapshot_id: visit_id = origin_visit["visit"] query_params = {"origin_url": origin_info["url"], "path": content_path} if timestamp: query_params["timestamp"] = timestamp if visit_id: query_params["visit_id"] = visit_id elif snapshot_id: query_params["snapshot"] = snapshot_id url = reverse("browse-origin-content", query_params=query_params) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") assert type(content["data"]) == str assert_contains(resp, '<code class="%s">' % content["hljs_language"]) assert_contains(resp, escape(content["data"])) split_path = content_path.split("/") filename = split_path[-1] path = content_path.replace(filename, "")[:-1] path_info = gen_path_info(path) del query_params["path"] if timestamp: query_params["timestamp"] = format_utc_iso_date( parse_timestamp(timestamp).isoformat(), "%Y-%m-%dT%H:%M:%SZ" ) root_dir_url = reverse("browse-origin-directory", query_params=query_params) assert_contains(resp, '<li class="swh-path">', count=len(path_info) + 1) assert_contains(resp, '<a href="%s">%s</a>' % (root_dir_url, root_dir_sha1[:7])) for p in path_info: query_params["path"] = p["path"] dir_url = reverse("browse-origin-directory", query_params=query_params) assert_contains(resp, '<a href="%s">%s</a>' % (dir_url, p["name"])) assert_contains(resp, "<li>%s</li>" % filename) query_string = "sha1_git:" + content["sha1_git"] url_raw = reverse( "browse-content-raw", url_args={"query_string": query_string}, query_params={"filename": filename}, ) assert_contains(resp, url_raw) if "path" in query_params: del query_params["path"] origin_branches_url = reverse("browse-origin-branches", query_params=query_params) - assert_contains( - resp, - '<a href="%s">Branches (%s)</a>' - % (escape(origin_branches_url), len(origin_branches)), - ) + assert_contains(resp, f'href="{escape(origin_branches_url)}"') + assert_contains(resp, f"Branches ({len(origin_branches)})") origin_releases_url = reverse("browse-origin-releases", query_params=query_params) - assert_contains( - resp, - '<a href="%s">Releases (%s)</a>' - % (escape(origin_releases_url), len(origin_releases)), - ) + assert_contains(resp, f'href="{escape(origin_releases_url)}">') + assert_contains(resp, f"Releases ({len(origin_releases)})") assert_contains(resp, '<li class="swh-branch">', count=len(origin_branches)) query_params["path"] = content_path for branch in origin_branches: root_dir_branch_url = reverse( "browse-origin-content", query_params={"branch": branch["name"], **query_params}, ) assert_contains(resp, '<a href="%s">' % root_dir_branch_url) assert_contains(resp, '<li class="swh-release">', count=len(origin_releases)) query_params["branch"] = None for release in origin_releases: root_dir_release_url = reverse( "browse-origin-content", query_params={"release": release["name"], **query_params}, ) assert_contains(resp, '<a href="%s">' % root_dir_release_url) url = reverse("browse-origin-content", query_params=query_params) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/content.html") snapshot = archive_data.snapshot_get(origin_visit["snapshot"]) head_rev_id = archive_data.snapshot_get_head(snapshot) swhid_context = { "origin": origin_info["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot["id"]), "anchor": get_swh_persistent_id(REVISION, head_rev_id), "path": f"/{content_path}", } swh_cnt_id = get_swh_persistent_id( CONTENT, content["sha1_git"], metadata=swhid_context ) swh_cnt_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_cnt_id}) assert_contains(resp, swh_cnt_id) assert_contains(resp, swh_cnt_id_url) assert_contains(resp, "swh-take-new-snapshot") - _check_origin_view_title(resp, origin_info["url"], "content") + _check_origin_link(resp, origin_info["url"]) def _origin_directory_view_test_helper( client, archive_data, origin_info, origin_visit, origin_branches, origin_releases, root_directory_sha1, directory_entries, visit_id=None, timestamp=None, snapshot_id=None, path=None, ): dirs = [e for e in directory_entries if e["type"] in ("dir", "rev")] files = [e for e in directory_entries if e["type"] == "file"] if not visit_id and not snapshot_id: visit_id = origin_visit["visit"] query_params = {"origin_url": origin_info["url"]} if timestamp: query_params["timestamp"] = timestamp elif visit_id: query_params["visit_id"] = visit_id else: query_params["snapshot"] = snapshot_id if path: query_params["path"] = path url = reverse("browse-origin-directory", query_params=query_params) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/directory.html") assert resp.status_code == 200 assert_template_used(resp, "browse/directory.html") assert_contains(resp, '<td class="swh-directory">', count=len(dirs)) assert_contains(resp, '<td class="swh-content">', count=len(files)) if timestamp: query_params["timestamp"] = format_utc_iso_date( parse_timestamp(timestamp).isoformat(), "%Y-%m-%dT%H:%M:%SZ" ) for d in dirs: if d["type"] == "rev": dir_url = reverse("browse-revision", url_args={"sha1_git": d["target"]}) else: dir_path = d["name"] if path: dir_path = "%s/%s" % (path, d["name"]) query_params["path"] = dir_path dir_url = reverse("browse-origin-directory", query_params=query_params,) assert_contains(resp, dir_url) for f in files: file_path = f["name"] if path: file_path = "%s/%s" % (path, f["name"]) query_params["path"] = file_path file_url = reverse("browse-origin-content", query_params=query_params) assert_contains(resp, file_url) if "path" in query_params: del query_params["path"] root_dir_branch_url = reverse("browse-origin-directory", query_params=query_params) nb_bc_paths = 1 if path: nb_bc_paths = len(path.split("/")) + 1 assert_contains(resp, '<li class="swh-path">', count=nb_bc_paths) assert_contains( resp, '<a href="%s">%s</a>' % (root_dir_branch_url, root_directory_sha1[:7]) ) origin_branches_url = reverse("browse-origin-branches", query_params=query_params) - assert_contains( - resp, - '<a href="%s">Branches (%s)</a>' - % (escape(origin_branches_url), len(origin_branches)), - ) + assert_contains(resp, f'href="{escape(origin_branches_url)}"') + assert_contains(resp, f"Branches ({len(origin_branches)})") origin_releases_url = reverse("browse-origin-releases", query_params=query_params) nb_releases = len(origin_releases) if nb_releases > 0: - assert_contains( - resp, - '<a href="%s">Releases (%s)</a>' - % (escape(origin_releases_url), nb_releases), - ) + assert_contains(resp, f'href="{escape(origin_releases_url)}"') + assert_contains(resp, f"Releases ({nb_releases})") if path: query_params["path"] = path assert_contains(resp, '<li class="swh-branch">', count=len(origin_branches)) for branch in origin_branches: query_params["branch"] = branch["name"] root_dir_branch_url = reverse( "browse-origin-directory", query_params=query_params ) assert_contains(resp, '<a href="%s">' % root_dir_branch_url) assert_contains(resp, '<li class="swh-release">', count=len(origin_releases)) query_params["branch"] = None for release in origin_releases: query_params["release"] = release["name"] root_dir_release_url = reverse( "browse-origin-directory", query_params=query_params ) - assert_contains(resp, '<a href="%s">' % root_dir_release_url) + assert_contains(resp, 'href="%s"' % root_dir_release_url) assert_contains(resp, "vault-cook-directory") assert_contains(resp, "vault-cook-revision") snapshot = archive_data.snapshot_get(origin_visit["snapshot"]) head_rev_id = archive_data.snapshot_get_head(snapshot) swhid_context = { "origin": origin_info["url"], "visit": get_swh_persistent_id(SNAPSHOT, snapshot["id"]), "anchor": get_swh_persistent_id(REVISION, head_rev_id), "path": f"/{path}" if path else "/", } swh_dir_id = get_swh_persistent_id( DIRECTORY, directory_entries[0]["dir_id"], metadata=swhid_context ) swh_dir_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_dir_id}) assert_contains(resp, swh_dir_id) assert_contains(resp, swh_dir_id_url) assert_contains(resp, "swh-take-new-snapshot") - _check_origin_view_title(resp, origin_info["url"], "directory") + _check_origin_link(resp, origin_info["url"]) def _origin_branches_test_helper( client, origin_info, origin_snapshot, snapshot_id=None ): query_params = {"origin_url": origin_info["url"], "snapshot": snapshot_id} url = reverse("browse-origin-branches", query_params=query_params) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/branches.html") origin_branches = origin_snapshot[0] origin_releases = origin_snapshot[1] origin_branches_url = reverse("browse-origin-branches", query_params=query_params) - assert_contains( - resp, - '<a href="%s">Branches (%s)</a>' - % (escape(origin_branches_url), len(origin_branches)), - ) + assert_contains(resp, f'href="{escape(origin_branches_url)}"') + assert_contains(resp, f"Branches ({len(origin_branches)})") origin_releases_url = reverse("browse-origin-releases", query_params=query_params) nb_releases = len(origin_releases) if nb_releases > 0: - assert_contains( - resp, - '<a href="%s">Releases (%s)</a>' - % (escape(origin_releases_url), nb_releases), - ) + assert_contains(resp, f'href="{escape(origin_releases_url)}">') + assert_contains(resp, f"Releases ({nb_releases})") assert_contains(resp, '<tr class="swh-branch-entry', count=len(origin_branches)) for branch in origin_branches: browse_branch_url = reverse( "browse-origin-directory", query_params={"branch": branch["name"], **query_params}, ) assert_contains(resp, '<a href="%s">' % escape(browse_branch_url)) browse_revision_url = reverse( "browse-revision", url_args={"sha1_git": branch["revision"]}, query_params=query_params, ) assert_contains(resp, '<a href="%s">' % escape(browse_revision_url)) - _check_origin_view_title(resp, origin_info["url"], "branches") + _check_origin_link(resp, origin_info["url"]) def _origin_releases_test_helper( client, origin_info, origin_snapshot, snapshot_id=None ): query_params = {"origin_url": origin_info["url"], "snapshot": snapshot_id} url = reverse("browse-origin-releases", query_params=query_params) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/releases.html") origin_branches = origin_snapshot[0] origin_releases = origin_snapshot[1] origin_branches_url = reverse("browse-origin-branches", query_params=query_params) - assert_contains( - resp, - '<a href="%s">Branches (%s)</a>' - % (escape(origin_branches_url), len(origin_branches)), - ) + assert_contains(resp, f'href="{escape(origin_branches_url)}"') + assert_contains(resp, f"Branches ({len(origin_branches)})") origin_releases_url = reverse("browse-origin-releases", query_params=query_params) nb_releases = len(origin_releases) if nb_releases > 0: - assert_contains( - resp, - '<a href="%s">Releases (%s)</a>' - % (escape(origin_releases_url), nb_releases), - ) + assert_contains(resp, f'href="{escape(origin_releases_url)}"') + assert_contains(resp, f"Releases ({nb_releases})") assert_contains(resp, '<tr class="swh-release-entry', count=nb_releases) for release in origin_releases: browse_release_url = reverse( "browse-release", url_args={"sha1_git": release["id"]}, query_params=query_params, ) browse_revision_url = reverse( "browse-revision", url_args={"sha1_git": release["target"]}, query_params=query_params, ) assert_contains(resp, '<a href="%s">' % escape(browse_release_url)) assert_contains(resp, '<a href="%s">' % escape(browse_revision_url)) - _check_origin_view_title(resp, origin_info["url"], "releases") + _check_origin_link(resp, origin_info["url"]) @given( new_origin(), visit_dates(), revisions(min_size=10, max_size=10), existing_release() ) def test_origin_branches_pagination_with_alias( client, archive_data, mocker, new_origin, visit_dates, revisions, existing_release ): """ When a snapshot contains a branch or a release alias, pagination links in the branches / releases view should be displayed. """ mocker.patch("swh.web.browse.snapshot_context.PER_PAGE", len(revisions) / 2) snp_dict = {"branches": {}, "id": hash_to_bytes(random_sha1())} for i in range(len(revisions)): branch = "".join(random.choices(string.ascii_lowercase, k=8)) snp_dict["branches"][branch.encode()] = { "target_type": "revision", "target": hash_to_bytes(revisions[i]), } release = "".join(random.choices(string.ascii_lowercase, k=8)) snp_dict["branches"][b"RELEASE_ALIAS"] = { "target_type": "alias", "target": release.encode(), } snp_dict["branches"][release.encode()] = { "target_type": "release", "target": hash_to_bytes(existing_release), } new_origin = archive_data.origin_add([new_origin])[0] archive_data.snapshot_add([Snapshot.from_dict(snp_dict)]) visit = archive_data.origin_visit_add( [ OriginVisit( origin=new_origin["url"], date=visit_dates[0], type="git", status="ongoing", snapshot=None, ) ] )[0] visit_status = OriginVisitStatus( origin=new_origin["url"], visit=visit.visit, date=now(), status="full", snapshot=snp_dict["id"], ) archive_data.origin_visit_status_add([visit_status]) url = reverse( "browse-origin-branches", query_params={"origin_url": new_origin["url"]} ) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/branches.html") assert_contains(resp, '<ul class="pagination') -def _check_origin_view_title(resp, origin_url, object_type): +def _check_origin_link(resp, origin_url): browse_origin_url = reverse( "browse-origin", query_params={"origin_url": origin_url} ) - - assert_contains( - resp, - textwrap.indent( - ( - f"Browse archived {object_type} for origin\n" - f'<a href="{browse_origin_url}">\n' - f" {origin_url}\n" - f"</a>" - ), - " " * 6, - ), - ) + assert_contains(resp, f'href="{browse_origin_url}"') diff --git a/swh/web/tests/browse/views/test_release.py b/swh/web/tests/browse/views/test_release.py index a2e65241..077619d7 100644 --- a/swh/web/tests/browse/views/test_release.py +++ b/swh/web/tests/browse/views/test_release.py @@ -1,169 +1,144 @@ # Copyright (C) 2018-2020 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 random -import textwrap from django.utils.html import escape from hypothesis import given from swh.web.common.identifiers import get_swh_persistent_id from swh.web.common.utils import reverse, format_utc_iso_date from swh.web.tests.django_asserts import assert_contains, assert_template_used from swh.web.tests.strategies import release, origin_with_releases, unknown_release @given(release()) def test_release_browse(client, archive_data, release): _release_browse_checks(client, release, archive_data) @given(origin_with_releases()) def test_release_browse_with_origin_snapshot(client, archive_data, origin): snapshot = archive_data.snapshot_get_latest(origin["url"]) release = random.choice( [ b["target"] for b in snapshot["branches"].values() if b["target_type"] == "release" ] ) _release_browse_checks(client, release, archive_data, origin_url=origin["url"]) _release_browse_checks(client, release, archive_data, snapshot_id=snapshot["id"]) _release_browse_checks( client, release, archive_data, origin_url=origin["url"], snapshot_id=snapshot["id"], ) @given(unknown_release()) def test_release_browse_not_found(client, archive_data, unknown_release): url = reverse("browse-release", url_args={"sha1_git": unknown_release}) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") err_msg = "Release with sha1_git %s not found" % unknown_release assert_contains(resp, err_msg, status_code=404) @given(release()) def test_release_uppercase(client, release): url = reverse( "browse-release-uppercase-checksum", url_args={"sha1_git": release.upper()} ) resp = client.get(url) assert resp.status_code == 302 redirect_url = reverse("browse-release", url_args={"sha1_git": release}) assert resp["location"] == redirect_url def _release_browse_checks( client, release, archive_data, origin_url=None, snapshot_id=None ): query_params = {"origin_url": origin_url, "snapshot": snapshot_id} url = reverse( "browse-release", url_args={"sha1_git": release}, query_params=query_params ) release_data = archive_data.release_get(release) resp = client.get(url) release_id = release_data["id"] release_name = release_data["name"] author_name = release_data["author"]["name"] release_date = release_data["date"] message = release_data["message"] target_type = release_data["target_type"] target = release_data["target"] target_url = reverse( "browse-revision", url_args={"sha1_git": target}, query_params=query_params ) message_lines = message.split("\n") assert resp.status_code == 200 assert_template_used(resp, "browse/release.html") assert_contains(resp, author_name) assert_contains(resp, format_utc_iso_date(release_date)) assert_contains( resp, "<h6>%s</h6>%s" % (message_lines[0] or "None", "\n".join(message_lines[1:])), ) assert_contains(resp, release_id) assert_contains(resp, release_name) assert_contains(resp, target_type) assert_contains(resp, '<a href="%s">%s</a>' % (escape(target_url), target)) swh_rel_id = get_swh_persistent_id("release", release_id) swh_rel_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_rel_id}) assert_contains(resp, swh_rel_id) assert_contains(resp, swh_rel_id_url) if origin_url: browse_origin_url = reverse( "browse-origin", query_params={"origin_url": origin_url} ) - title = ( - f"Browse archived release for origin\n" - f'<a href="{browse_origin_url}">\n' - f" {origin_url}\n" - f"</a>" - ) - indent = " " * 6 + assert_contains(resp, f'href="{browse_origin_url}"') elif snapshot_id: swh_snp_id = get_swh_persistent_id("snapshot", snapshot_id) swh_snp_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_snp_id}) - title = ( - f"Browse archived release for snapshot\n" - f'<a href="{swh_snp_id_url}">\n' - f" {swh_snp_id}\n" - f"</a>" - ) - indent = " " * 6 - else: - title = ( - f"Browse archived release\n" - f'<a href="{swh_rel_id_url}">\n' - f" {swh_rel_id}\n" - f"</a>" - ) - indent = " " * 4 - - assert_contains( - resp, textwrap.indent(title, indent), - ) + assert_contains(resp, f'href="{swh_snp_id_url}"') if release_data["target_type"] == "revision": if origin_url: directory_url = reverse( "browse-origin-directory", query_params={ "origin_url": origin_url, "release": release_data["name"], "snapshot": snapshot_id, }, ) elif snapshot_id: directory_url = reverse( "browse-snapshot-directory", url_args={"snapshot_id": snapshot_id}, query_params={"release": release_data["name"],}, ) else: rev = archive_data.revision_get(release_data["target"]) directory_url = reverse( "browse-directory", url_args={"sha1_git": rev["directory"]} ) assert_contains(resp, escape(directory_url)) diff --git a/swh/web/tests/browse/views/test_revision.py b/swh/web/tests/browse/views/test_revision.py index a5b97602..baf7e116 100644 --- a/swh/web/tests/browse/views/test_revision.py +++ b/swh/web/tests/browse/views/test_revision.py @@ -1,335 +1,294 @@ # Copyright (C) 2017-2020 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 random -import textwrap from django.utils.html import escape from hypothesis import given from swh.model.identifiers import DIRECTORY, REVISION, SNAPSHOT from swh.web.common.identifiers import get_swh_persistent_id from swh.web.common.utils import reverse, format_utc_iso_date, parse_timestamp from swh.web.tests.django_asserts import assert_contains, assert_template_used from swh.web.tests.strategies import origin, revision, unknown_revision, new_origin @given(revision()) def test_revision_browse(client, archive_data, revision): _revision_browse_checks(client, archive_data, revision) @given(origin()) def test_revision_origin_snapshot_browse(client, archive_data, origin): snapshot = archive_data.snapshot_get_latest(origin["url"]) revision = archive_data.snapshot_get_head(snapshot) _revision_browse_checks(client, archive_data, revision, origin_url=origin["url"]) _revision_browse_checks(client, archive_data, revision, snapshot=snapshot) _revision_browse_checks( client, archive_data, revision, origin_url=origin["url"], snapshot=snapshot, ) revision = random.choice(archive_data.revision_log(revision))["id"] _revision_browse_checks(client, archive_data, revision, origin_url=origin["url"]) @given(revision()) def test_revision_log_browse(client, archive_data, revision): per_page = 10 revision_log = archive_data.revision_log(revision) revision_log_sorted = sorted( revision_log, key=lambda rev: -parse_timestamp(rev["committer_date"]).timestamp(), ) url = reverse( "browse-revision-log", url_args={"sha1_git": revision}, query_params={"per_page": per_page}, ) resp = client.get(url) next_page_url = reverse( "browse-revision-log", url_args={"sha1_git": revision}, query_params={"offset": per_page, "per_page": per_page}, ) nb_log_entries = per_page if len(revision_log_sorted) < per_page: nb_log_entries = len(revision_log_sorted) assert resp.status_code == 200 assert_template_used(resp, "browse/revision-log.html") assert_contains(resp, '<tr class="swh-revision-log-entry', count=nb_log_entries) assert_contains(resp, '<a class="page-link">Newer</a>') if len(revision_log_sorted) > per_page: assert_contains( resp, '<a class="page-link" href="%s">Older</a>' % escape(next_page_url), ) for log in revision_log_sorted[:per_page]: revision_url = reverse("browse-revision", url_args={"sha1_git": log["id"]}) assert_contains(resp, log["id"][:7]) assert_contains(resp, log["author"]["name"]) assert_contains(resp, format_utc_iso_date(log["date"])) assert_contains(resp, escape(log["message"])) assert_contains(resp, format_utc_iso_date(log["committer_date"])) assert_contains(resp, revision_url) if len(revision_log_sorted) <= per_page: return resp = client.get(next_page_url) prev_page_url = reverse( "browse-revision-log", url_args={"sha1_git": revision}, query_params={"per_page": per_page}, ) next_page_url = reverse( "browse-revision-log", url_args={"sha1_git": revision}, query_params={"offset": 2 * per_page, "per_page": per_page}, ) nb_log_entries = len(revision_log_sorted) - per_page if nb_log_entries > per_page: nb_log_entries = per_page assert resp.status_code == 200 assert_template_used(resp, "browse/revision-log.html") assert_contains(resp, '<tr class="swh-revision-log-entry', count=nb_log_entries) assert_contains( resp, '<a class="page-link" href="%s">Newer</a>' % escape(prev_page_url) ) if len(revision_log_sorted) > 2 * per_page: assert_contains( resp, '<a class="page-link" href="%s">Older</a>' % escape(next_page_url), ) if len(revision_log_sorted) <= 2 * per_page: return resp = client.get(next_page_url) prev_page_url = reverse( "browse-revision-log", url_args={"sha1_git": revision}, query_params={"offset": per_page, "per_page": per_page}, ) next_page_url = reverse( "browse-revision-log", url_args={"sha1_git": revision}, query_params={"offset": 3 * per_page, "per_page": per_page}, ) nb_log_entries = len(revision_log_sorted) - 2 * per_page if nb_log_entries > per_page: nb_log_entries = per_page assert resp.status_code == 200 assert_template_used(resp, "browse/revision-log.html") assert_contains(resp, '<tr class="swh-revision-log-entry', count=nb_log_entries) assert_contains( resp, '<a class="page-link" href="%s">Newer</a>' % escape(prev_page_url) ) if len(revision_log_sorted) > 3 * per_page: assert_contains( resp, '<a class="page-link" href="%s">Older</a>' % escape(next_page_url), ) - swh_rev_id = get_swh_persistent_id(REVISION, revision) - swh_rev_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_rev_id}) - - assert_contains( - resp, - textwrap.indent( - ( - f"Browse archived revisions history\n" - f'<a href="{swh_rev_id_url}">\n' - f" {swh_rev_id}\n" - f"</a>" - ), - " " * 4, - ), - ) - @given(revision(), unknown_revision(), new_origin()) def test_revision_request_errors(client, revision, unknown_revision, new_origin): url = reverse("browse-revision", url_args={"sha1_git": unknown_revision}) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert_contains( resp, "Revision with sha1_git %s not found" % unknown_revision, status_code=404 ) url = reverse( "browse-revision", url_args={"sha1_git": revision}, query_params={"origin_url": new_origin.url}, ) resp = client.get(url) assert resp.status_code == 404 assert_template_used(resp, "error.html") assert_contains( resp, "the origin mentioned in your request" " appears broken", status_code=404 ) @given(revision()) def test_revision_uppercase(client, revision): url = reverse( "browse-revision-uppercase-checksum", url_args={"sha1_git": revision.upper()} ) resp = client.get(url) assert resp.status_code == 302 redirect_url = reverse("browse-revision", url_args={"sha1_git": revision}) assert resp["location"] == redirect_url def _revision_browse_checks( client, archive_data, revision, origin_url=None, snapshot=None ): query_params = {} if origin_url: query_params["origin_url"] = origin_url if snapshot: query_params["snapshot"] = snapshot["id"] url = reverse( "browse-revision", url_args={"sha1_git": revision}, query_params=query_params ) revision_data = archive_data.revision_get(revision) author_name = revision_data["author"]["name"] committer_name = revision_data["committer"]["name"] dir_id = revision_data["directory"] if origin_url: snapshot = archive_data.snapshot_get_latest(origin_url) history_url = reverse( "browse-origin-log", query_params={"revision": revision, **query_params}, ) elif snapshot: history_url = reverse( "browse-snapshot-log", url_args={"snapshot_id": snapshot["id"]}, query_params={"revision": revision}, ) else: history_url = reverse("browse-revision-log", url_args={"sha1_git": revision}) resp = client.get(url) assert resp.status_code == 200 assert_template_used(resp, "browse/revision.html") assert_contains(resp, author_name) assert_contains(resp, committer_name) assert_contains(resp, history_url) for parent in revision_data["parents"]: parent_url = reverse( "browse-revision", url_args={"sha1_git": parent}, query_params=query_params ) assert_contains(resp, '<a href="%s">%s</a>' % (escape(parent_url), parent[:7])) author_date = revision_data["date"] committer_date = revision_data["committer_date"] message_lines = revision_data["message"].split("\n") assert_contains(resp, format_utc_iso_date(author_date)) assert_contains(resp, format_utc_iso_date(committer_date)) assert_contains(resp, escape(message_lines[0])) assert_contains(resp, escape("\n".join(message_lines[1:]))) assert_contains(resp, "vault-cook-directory") assert_contains(resp, "vault-cook-revision") swh_rev_id = get_swh_persistent_id("revision", revision) swh_rev_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_rev_id}) assert_contains(resp, swh_rev_id) assert_contains(resp, swh_rev_id_url) swh_dir_id = get_swh_persistent_id("directory", dir_id) swh_dir_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_dir_id}) assert_contains(resp, swh_dir_id) assert_contains(resp, swh_dir_id_url) if origin_url: assert_contains(resp, "swh-take-new-snapshot") swh_rev_id = get_swh_persistent_id(REVISION, revision) swh_rev_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_rev_id}) if origin_url: browse_origin_url = reverse( "browse-origin", query_params={"origin_url": origin_url} ) - title = ( - f"Browse archived revision for origin\n" - f'<a href="{browse_origin_url}">\n' - f" {origin_url}\n" - f"</a>" - ) - indent = " " * 6 + assert_contains(resp, f'href="{browse_origin_url}"') elif snapshot: swh_snp_id = get_swh_persistent_id("snapshot", snapshot["id"]) swh_snp_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_snp_id}) - title = ( - f"Browse archived revision for snapshot\n" - f'<a href="{swh_snp_id_url}">\n' - f" {swh_snp_id}\n" - f"</a>" - ) - indent = " " * 6 - else: - title = ( - f"Browse archived revision\n" - f'<a href="{swh_rev_id_url}">\n' - f" {swh_rev_id}\n" - f"</a>" - ) - indent = " " * 4 - - assert_contains( - resp, textwrap.indent(title, indent), - ) + assert_contains(resp, f'href="{swh_snp_id_url}"') swhid_context = {} if origin_url: swhid_context["origin"] = origin_url if snapshot: swhid_context["visit"] = get_swh_persistent_id(SNAPSHOT, snapshot["id"]) swh_rev_id = get_swh_persistent_id(REVISION, revision, metadata=swhid_context) swh_rev_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_rev_id}) assert_contains(resp, swh_rev_id) assert_contains(resp, swh_rev_id_url) swhid_context["anchor"] = get_swh_persistent_id(REVISION, revision) swhid_context["path"] = "/" swh_dir_id = get_swh_persistent_id(DIRECTORY, dir_id, metadata=swhid_context) swh_dir_id_url = reverse("browse-swh-id", url_args={"swh_id": swh_dir_id}) assert_contains(resp, swh_dir_id) assert_contains(resp, swh_dir_id_url)