diff --git a/cypress/integration/deposit-admin.spec.js b/cypress/integration/deposit-admin.spec.js index 4b308e4a..c8949e39 100644 --- a/cypress/integration/deposit-admin.spec.js +++ b/cypress/integration/deposit-admin.spec.js @@ -1,164 +1,208 @@ /** * Copyright (C) 2020-2022 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 */ // data to use as request query response let responseDeposits; let expectedOrigins; +let depositModerationUrl; +let depositListUrl; + +describe('Test moderation deposit Login/logout', function() { + before(function() { + depositModerationUrl = this.Urls.admin_deposit(); + }); + + it('should not display deposit moderation link in sidebar when anonymous', function() { + cy.visit(depositModerationUrl); + cy.get(`.sidebar a[href="${depositModerationUrl}"]`) + .should('not.exist'); + }); + + it('should not display deposit moderation link when connected as unprivileged user', function() { + cy.userLogin(); + cy.visit(depositModerationUrl); + + cy.get(`.sidebar a[href="${depositModerationUrl}"]`) + .should('not.exist'); + + }); + + it('should display deposit moderation link in sidebar when connected as privileged user', function() { + cy.depositLogin(); + cy.visit(depositModerationUrl); + + cy.get(`.sidebar a[href="${depositModerationUrl}"]`) + .should('exist'); + }); + + it('should display deposit moderation link in sidebar when connected as staff member', function() { + cy.adminLogin(); + cy.visit(depositModerationUrl); + + cy.get(`.sidebar a[href="${depositModerationUrl}"]`) + .should('exist'); + }); + +}); describe('Test admin deposit page', function() { + before(function() { + depositModerationUrl = this.Urls.admin_deposit(); + depositListUrl = this.Urls.admin_deposit_list(); + }); + beforeEach(() => { responseDeposits = [ { 'id': 614, 'type': 'code', 'external_id': 'ch-de-1', 'reception_date': '2020-05-18T13:48:27Z', 'status': 'done', 'status_detail': null, 'swhid': 'swh:1:dir:ef04a768', 'swhid_context': 'swh:1:dir:ef04a768;origin=https://w.s.o/c-d-1;visit=swh:1:snp:b234be1e;anchor=swh:1:rev:d24a75c9;path=/', 'uri': 'https://w.s.o/c-d-1' }, { 'id': 613, 'type': 'code', 'external_id': 'ch-de-2', 'reception_date': '2020-05-18T11:20:16Z', 'status': 'done', 'status_detail': null, 'swhid': 'swh:1:dir:181417fb', 'swhid_context': 'swh:1:dir:181417fb;origin=https://w.s.o/c-d-2;visit=swh:1:snp:8c32a2ef;anchor=swh:1:rev:3d1eba04;path=/', 'uri': 'https://w.s.o/c-d-2' }, { 'id': 612, 'type': 'code', 'external_id': 'ch-de-3', 'reception_date': '2020-05-18T11:20:16Z', 'status': 'rejected', 'status_detail': 'incomplete deposit!', 'swhid': null, 'swhid_context': null, 'uri': null } ]; // those are computed from the expectedOrigins = { 614: 'https://w.s.o/c-d-1', 613: 'https://w.s.o/c-d-2', 612: '' }; }); it('Should display properly entries', function() { cy.adminLogin(); const testDeposits = responseDeposits; - cy.intercept(`${this.Urls.admin_deposit_list()}**`, { + cy.intercept(`${depositListUrl}**`, { body: { 'draw': 10, 'recordsTotal': testDeposits.length, 'recordsFiltered': testDeposits.length, 'data': testDeposits } }).as('listDeposits'); - cy.visit(this.Urls.admin_deposit()); + cy.visit(depositModerationUrl); cy.location('pathname') - .should('be.equal', this.Urls.admin_deposit()); - cy.url().should('include', '/admin/deposit'); + .should('be.equal', depositModerationUrl); cy.get('#swh-admin-deposit-list') .should('exist'); cy.wait('@listDeposits').then((xhr) => { cy.log('response:', xhr.response); cy.log(xhr.response.body); const deposits = xhr.response.body.data; cy.log('Deposits: ', deposits); expect(deposits.length).to.equal(testDeposits.length); cy.get('#swh-admin-deposit-list').find('tbody > tr').as('rows'); // only 2 entries cy.get('@rows').each((row, idx, collection) => { const deposit = deposits[idx]; const responseDeposit = testDeposits[idx]; assert.isNotNull(deposit); assert.isNotNull(responseDeposit); expect(deposit.id).to.be.equal(responseDeposit['id']); expect(deposit.uri).to.be.equal(responseDeposit['uri']); expect(deposit.type).to.be.equal(responseDeposit['type']); expect(deposit.external_id).to.be.equal(responseDeposit['external_id']); expect(deposit.status).to.be.equal(responseDeposit['status']); expect(deposit.status_detail).to.be.equal(responseDeposit['status_detail']); expect(deposit.swhid).to.be.equal(responseDeposit['swhid']); expect(deposit.swhid_context).to.be.equal(responseDeposit['swhid_context']); const expectedOrigin = expectedOrigins[deposit.id]; // ensure it's in the dom cy.contains(deposit.id).should('be.visible'); if (deposit.status !== 'rejected') { expect(row).to.not.contain(deposit.external_id); cy.contains(expectedOrigin).should('be.visible'); } cy.contains(deposit.status).should('be.visible'); // those are hidden by default, so now visible if (deposit.status_detail !== null) { cy.contains(deposit.status_detail).should('not.exist'); } // those are hidden by default if (deposit.swhid !== null) { cy.contains(deposit.swhid).should('not.exist'); cy.contains(deposit.swhid_context).should('not.exist'); } }); // toggling all links and ensure, the previous checks are inverted cy.get('a.toggle-col').click({'multiple': true}).then(() => { cy.get('#swh-admin-deposit-list').find('tbody > tr').as('rows'); cy.get('@rows').each((row, idx, collection) => { const deposit = deposits[idx]; const expectedOrigin = expectedOrigins[deposit.id]; // ensure it's in the dom cy.contains(deposit.id).should('not.exist'); if (deposit.status !== 'rejected') { expect(row).to.not.contain(deposit.external_id); expect(row).to.contain(expectedOrigin); } expect(row).to.not.contain(deposit.status); // those are hidden by default, so now visible if (deposit.status_detail !== null) { cy.contains(deposit.status_detail).should('be.visible'); } // those are hidden by default, so now they should be visible if (deposit.swhid !== null) { cy.contains(deposit.swhid).should('be.visible'); cy.contains(deposit.swhid_context).should('be.visible'); // check SWHID link text formatting cy.contains(deposit.swhid_context).then(elt => { expect(elt[0].innerHTML).to.equal(deposit.swhid_context.replace(/;/g, ';
')); }); } }); }); cy.get('#swh-admin-deposit-list-error') .should('not.contain', 'An error occurred while retrieving the list of deposits'); }); }); }); diff --git a/cypress/support/index.js b/cypress/support/index.js index e85132b0..486209c6 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,95 +1,99 @@ /** * Copyright (C) 2019-2022 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 'cypress-hmr-restarter'; import '@cypress/code-coverage/support'; Cypress.Screenshot.defaults({ screenshotOnRunFailure: false }); Cypress.Commands.add('xhrShouldBeCalled', (alias, timesCalled) => { const testRoutes = cy.state('routes'); const aliasRoute = Cypress._.find(testRoutes, {alias}); expect(Object.keys(aliasRoute.requests || {})).to.have.length(timesCalled); }); function loginUser(username, password) { const url = '/admin/login/'; return cy.request({ url: url, method: 'GET' }).then(() => { cy.getCookie('sessionid').should('not.exist'); cy.getCookie('csrftoken').its('value').then((token) => { cy.request({ url: url, method: 'POST', form: true, followRedirect: false, body: { username: username, password: password, csrfmiddlewaretoken: token } }).then(() => { cy.getCookie('sessionid').should('exist'); return cy.getCookie('csrftoken').its('value'); }); }); }); } Cypress.Commands.add('adminLogin', () => { return loginUser('admin', 'admin'); }); Cypress.Commands.add('userLogin', () => { return loginUser('user', 'user'); }); Cypress.Commands.add('ambassadorLogin', () => { return loginUser('ambassador', 'ambassador'); }); +Cypress.Commands.add('depositLogin', () => { + return loginUser('deposit', 'deposit'); +}); + function mockCostlyRequests() { cy.intercept('https://status.softwareheritage.org/**', { body: { 'result': { 'status': [ { 'id': '5f7c4c567f50b304c1e7bd5f', 'name': 'Save Code Now', 'updated': '2020-11-30T13:51:21.151Z', 'status': 'Operational', 'status_code': 100 } ] } }}).as('swhPlatformStatus'); cy.intercept('/coverage', { body: '' }).as('swhCoverageWidget'); } before(function() { mockCostlyRequests(); cy.task('getSwhTestsData').then(testsData => { Object.assign(this, testsData); }); cy.visit('/').window().then(async win => { this.Urls = win.Urls; }); }); beforeEach(function() { mockCostlyRequests(); }); diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html index 1a682e2a..1ac28157 100644 --- a/swh/web/templates/layout.html +++ b/swh/web/templates/layout.html @@ -1,293 +1,295 @@ {% comment %} Copyright (C) 2015-2021 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 js_reverse %} {% load static %} {% load render_bundle from webpack_loader %} {% load swh_templatetags %} {% block title %}{% endblock %} {% render_bundle 'vendors' %} {% render_bundle 'webapp' %} {% render_bundle 'guided_tour' %} {{ request.user.is_authenticated|json_script:"swh_user_logged_in" }} {% block header %}{% endblock %} {% if not swh_web_dev and not swh_web_staging %} {% endif %}
{% comment %} {% endcomment %}
{% if swh_web_staging %}
Staging
v{{ swh_web_version }}
{% elif swh_web_dev %}
Development
v{{ swh_web_version|split:"+"|first }}
{% endif %} {% block content %}{% endblock %}
{% include "includes/global-modals.html" %}
back to top
diff --git a/swh/web/tests/create_test_admin.py b/swh/web/tests/create_test_admin.py index ca123dbf..b7af12e9 100644 --- a/swh/web/tests/create_test_admin.py +++ b/swh/web/tests/create_test_admin.py @@ -1,16 +1,20 @@ -# Copyright (C) 2019 The Software Heritage developers +# Copyright (C) 2019-2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from django.contrib.auth import get_user_model username = "admin" password = "admin" email = "admin@swh-web.org" User = get_user_model() -if not User.objects.filter(username=username).exists(): - User.objects.create_superuser(username, email, password) +try: + user = User.objects.filter(username=username).get() +except User.DoesNotExist: + user = User.objects.create_superuser(username, email, password) + +assert user.is_staff is True diff --git a/swh/web/tests/create_test_users.py b/swh/web/tests/create_test_users.py index f92d3604..b6668a72 100644 --- a/swh/web/tests/create_test_users.py +++ b/swh/web/tests/create_test_users.py @@ -1,29 +1,35 @@ -# Copyright (C) 2021 The Software Heritage developers +# Copyright (C) 2021-2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from typing import Dict, List, Tuple from django.contrib.auth import get_user_model -from swh.web.auth.utils import SWH_AMBASSADOR_PERMISSION +from swh.web.auth.utils import ADMIN_LIST_DEPOSIT_PERMISSION, SWH_AMBASSADOR_PERMISSION from swh.web.tests.utils import create_django_permission User = get_user_model() users: Dict[str, Tuple[str, str, List[str]]] = { - "user": ("user", "user@swh-web.org", []), - "ambassador": ("ambassador", "ambassador@swh-web.org", [SWH_AMBASSADOR_PERMISSION]), + "user": ("user", "user@example.org", []), + "ambassador": ( + "ambassador", + "ambassador@example.org", + [SWH_AMBASSADOR_PERMISSION], + ), + "deposit": ("deposit", "deposit@example.org", [ADMIN_LIST_DEPOSIT_PERMISSION],), } + for username, (password, email, permissions) in users.items(): if not User.objects.filter(username=username).exists(): user = User.objects.create_user(username, email, password) if permissions: for perm_name in permissions: permission = create_django_permission(perm_name) user.user_permissions.add(permission) user.save() diff --git a/swh/web/tests/utils.py b/swh/web/tests/utils.py index 216b2f1a..8d00afed 100644 --- a/swh/web/tests/utils.py +++ b/swh/web/tests/utils.py @@ -1,235 +1,239 @@ # Copyright (C) 2020-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from typing import Any, Dict, Optional, cast from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.http import HttpResponse, StreamingHttpResponse from django.test.client import Client from rest_framework.response import Response from rest_framework.test import APIClient from swh.web.tests.django_asserts import assert_template_used def _assert_http_response( response: HttpResponse, status_code: int, content_type: str ) -> HttpResponse: if isinstance(response, Response): drf_response = cast(Response, response) error_context = ( drf_response.data.pop("traceback") if isinstance(drf_response.data, dict) and "traceback" in drf_response.data else drf_response.data ) elif isinstance(response, StreamingHttpResponse): error_context = getattr(response, "traceback", response.streaming_content) else: error_context = getattr(response, "traceback", response.content) assert response.status_code == status_code, error_context if content_type != "*/*": assert response["Content-Type"].startswith(content_type) return response def check_http_get_response( client: Client, url: str, status_code: int, content_type: str = "*/*", http_origin: Optional[str] = None, server_name: Optional[str] = None, ) -> HttpResponse: """Helper function to check HTTP response for a GET request. Args: client: Django test client url: URL to check response status_code: expected HTTP status code content_type: expected response content type http_origin: optional HTTP_ORIGIN header value Returns: The HTTP response """ return _assert_http_response( response=client.get( url, HTTP_ACCEPT=content_type, HTTP_ORIGIN=http_origin, SERVER_NAME=server_name if server_name else "testserver", ), status_code=status_code, content_type=content_type, ) def check_http_post_response( client: Client, url: str, status_code: int, content_type: str = "*/*", request_content_type="application/json", data: Optional[Dict[str, Any]] = None, http_origin: Optional[str] = None, ) -> HttpResponse: """Helper function to check HTTP response for a POST request. Args: client: Django test client url: URL to check response status_code: expected HTTP status code content_type: expected response content type request_content_type: content type of request body data: optional POST data Returns: The HTTP response """ return _assert_http_response( response=client.post( url, data=data, content_type=request_content_type, HTTP_ACCEPT=content_type, HTTP_ORIGIN=http_origin, ), status_code=status_code, content_type=content_type, ) def check_api_get_responses( api_client: APIClient, url: str, status_code: int ) -> Response: """Helper function to check Web API responses for GET requests for all accepted content types (JSON, YAML, HTML). Args: api_client: DRF test client url: Web API URL to check responses status_code: expected HTTP status code Returns: The Web API JSON response """ # check JSON response response_json = check_http_get_response( api_client, url, status_code, content_type="application/json" ) # check HTML response (API Web UI) check_http_get_response(api_client, url, status_code, content_type="text/html") # check YAML response check_http_get_response( api_client, url, status_code, content_type="application/yaml" ) return cast(Response, response_json) def check_api_post_response( api_client: APIClient, url: str, status_code: int, content_type: str = "*/*", data: Optional[Dict[str, Any]] = None, ) -> HttpResponse: """Helper function to check Web API response for a POST request for all accepted content types. Args: api_client: DRF test client url: Web API URL to check response status_code: expected HTTP status code Returns: The HTTP response """ return _assert_http_response( response=api_client.post( url, data=data, format="json", HTTP_ACCEPT=content_type, ), status_code=status_code, content_type=content_type, ) def check_api_post_responses( api_client: APIClient, url: str, status_code: int, data: Optional[Dict[str, Any]] = None, ) -> Response: """Helper function to check Web API responses for POST requests for all accepted content types (JSON, YAML). Args: api_client: DRF test client url: Web API URL to check responses status_code: expected HTTP status code Returns: The Web API JSON response """ # check JSON response response_json = check_api_post_response( api_client, url, status_code, content_type="application/json", data=data ) # check YAML response check_api_post_response( api_client, url, status_code, content_type="application/yaml", data=data ) return cast(Response, response_json) def check_html_get_response( client: Client, url: str, status_code: int, template_used: Optional[str] = None ) -> HttpResponse: """Helper function to check HTML responses for a GET request. Args: client: Django test client url: URL to check responses status_code: expected HTTP status code template_used: optional used Django template to check Returns: The HTML response """ response = check_http_get_response( client, url, status_code, content_type="text/html" ) if template_used is not None: assert_template_used(response, template_used) return response def create_django_permission(perm_name: str) -> Permission: """Create permission out of a permission name string Args: perm_name: Permission name (e.g. swh.web.api.throttling_exempted, swh.ambassador, ...) Returns: The persisted permission """ perm_splitted = perm_name.split(".") app_label = ".".join(perm_splitted[:-1]) perm_name = perm_splitted[-1] content_type = ContentType.objects.create( - id=1000, app_label=app_label, model="dummy" + id=1000 + ContentType.objects.count(), app_label=app_label, model="dummy" ) + return Permission.objects.create( - codename=perm_name, name=perm_name, content_type=content_type, id=1000 + codename=perm_name, + name=perm_name, + content_type=content_type, + id=1000 + Permission.objects.count(), )