diff --git a/cypress/fixtures/origin-save.json b/cypress/fixtures/origin-save.json new file mode 100644 index 00000000..c74b6404 --- /dev/null +++ b/cypress/fixtures/origin-save.json @@ -0,0 +1,86 @@ +{ + "recordsTotal": 6839, + "draw": 2, + "recordsFiltered": 6839, + "data": [{ + "id": 6869, + "visit_type": "git", + "origin_url": "https://gitlab.inria.fr/solverstack/maphys/maphys/", + "save_request_date": "2020-03-25T15:57:21.725886+00:00", + "save_request_status": "accepted", + "save_task_status": "succeed", + "visit_date": "2020-03-25T16:23:15.309379+00:00" + }, { + "id": 6868, + "visit_type": "git", + "origin_url": "https://github.com/orthecreedence/cl-async.git", + "save_request_date": "2020-01-17T09:39:55.930715+00:00", + "save_request_status": "accepted", + "save_task_status": "succeed", + "visit_date": "2020-01-17T09:47:20.992279+00:00" + }, { + "id": 6867, + "visit_type": "git", + "origin_url": "https://forge.frm2.tum.de/cgit/cgit.cgi/frm2/mira/takin-data.git/", + "save_request_date": "2020-01-17T07:16:54.421290+00:00", + "save_request_status": "accepted", + "save_task_status": "failed", + "visit_date": "2020-01-17T07:46:35.223112+00:00" + }, { + "id": 6866, + "visit_type": "git", + "origin_url": "https://forge.frm2.tum.de/cgit/cgit.cgi/frm2/mira/mcstas.git/", + "save_request_date": "2020-01-17T07:16:43.411145+00:00", + "save_request_status": "accepted", + "save_task_status": "failed", + "visit_date": "2020-01-17T07:46:32.710716+00:00" + }, { + "id": 6865, + "visit_type": "git", + "origin_url": "https://forge.frm2.tum.de/cgit/cgit.cgi/frm2/mira/miezetools.git/", + "save_request_date": "2020-01-17T07:16:31.085132+00:00", + "save_request_status": "accepted", + "save_task_status": "failed", + "visit_date": "2020-01-17T07:46:32.634520+00:00" + }, { + "id": 6864, + "visit_type": "git", + "origin_url": "https://forge.frm2.tum.de/cgit/cgit.cgi/frm2/mira/tlibs.git/", + "save_request_date": "2020-01-17T07:16:21.708408+00:00", + "save_request_status": "accepted", + "save_task_status": "failed", + "visit_date": "2020-01-17T07:46:32.542717+00:00" + }, { + "id": 6863, + "visit_type": "git", + "origin_url": "https://forge.frm2.tum.de/cgit/cgit.cgi/frm2/mira/tastools.git/", + "save_request_date": "2020-01-17T07:15:58.076850+00:00", + "save_request_status": "accepted", + "save_task_status": "failed", + "visit_date": "2020-01-17T07:46:32.226750+00:00" + }, { + "id": 6862, + "visit_type": "git", + "origin_url": "https://github.com/openfun/flower-docker", + "save_request_date": "2020-01-16T15:26:05.406086+00:00", + "save_request_status": "accepted", + "save_task_status": "succeed", + "visit_date": "2020-01-16T15:47:50.200655+00:00" + }, { + "id": 6861, + "visit_type": "git", + "origin_url": "https://gitlab.inria.fr/dsi_public/depots_officiels_inria_gitlab", + "save_request_date": "2020-01-16T11:27:26.916797+00:00", + "save_request_status": "accepted", + "save_task_status": "succeed", + "visit_date": "2020-01-16T11:44:01.715665+00:00" + }, { + "id": 6860, + "visit_type": "git", + "origin_url": "https://github.com/SocialGouv/cdtn-run", + "save_request_date": "2020-01-16T11:23:24.933159+00:00", + "save_request_status": "accepted", + "save_task_status": "succeed", + "visit_date": "2020-01-16T11:39:50.882730+00:00" + }] +} \ No newline at end of file diff --git a/cypress/integration/origin-save.spec.js b/cypress/integration/origin-save.spec.js index 2692b79a..94a94da8 100644 --- a/cypress/integration/origin-save.spec.js +++ b/cypress/integration/origin-save.spec.js @@ -1,160 +1,192 @@ /** * Copyright (C) 2019-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 */ let url; let origin; +const $ = Cypress.$; const saveCodeMsg = { 'success': 'The "save code now" request has been accepted and will be processed as soon as possible.', 'warning': 'The "save code now" request has been put in pending state and may be accepted for processing after manual review.', 'rejected': 'The "save code now" request has been rejected because the provided origin url is blacklisted.', 'rateLimit': 'The rate limit for "save code now" requests has been reached. Please try again later.', 'unknownError': 'An unexpected error happened when submitting the "save code now request', 'csrfError': 'CSRF Failed: Referrer checking failed - no Referrer.' }; function makeOriginSaveRequest(originType, originUrl) { cy.get('#swh-input-visit-type') .select(originType) .get('#swh-input-origin-url') .type(originUrl) .get('#swh-save-origin-form') .submit(); } function checkAlertVisible(alertType, msg) { cy.get('#swh-origin-save-request-status') .should('be.visible') .find(`.alert-${alertType}`) .should('be.visible') .and('contain', msg); } // Stub requests to save an origin function stubSaveRequest(requestUrl, objectType, status, originUrl, taskStatus, responseStatus = 200, errorMessage = '') { let response; if (responseStatus !== 200 && errorMessage) { response = {'detail': errorMessage}; } else { response = genOriginSaveResponse(objectType, status, originUrl, Date().toString(), taskStatus); } cy.route({ method: 'POST', status: responseStatus, url: requestUrl, response: response }).as('saveRequest'); } // Mocks API response : /save/(:object_type)/(:origin_url) // object_type : {'git', 'hg', 'svn'} function genOriginSaveResponse(objectType, saveRequestStatus, originUrl, saveRequestDate, saveTaskStatus) { return { 'visit_type': objectType, 'save_request_status': saveRequestStatus, 'origin_url': originUrl, 'id': 1, 'save_request_date': saveRequestDate, 'save_task_status': saveTaskStatus, 'visit_date': null }; }; describe('Origin Save Tests', function() { before(function() { url = this.Urls.origin_save(); origin = this.origin[0]; this.originSaveUrl = this.Urls.origin_save_request(origin.type, origin.url); }); beforeEach(function() { cy.visit(url); cy.server(); }); it('should display accepted message when accepted', function() { stubSaveRequest(this.originSaveUrl, origin.type, 'accepted', origin.url, 'not yet scheduled'); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should validate gitlab subproject url', function() { const gitlabSubProjectUrl = 'https://gitlab.com/user/project/sub/'; const originSaveUrl = this.Urls.origin_save_request('git', gitlabSubProjectUrl); stubSaveRequest(originSaveUrl, 'git', 'accepted', gitlabSubProjectUrl, 'not yet scheduled'); makeOriginSaveRequest('git', gitlabSubProjectUrl); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should display warning message when pending', function() { stubSaveRequest(this.originSaveUrl, origin.type, 'pending', origin.url, 'not created'); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('warning', saveCodeMsg['warning']); }); }); it('should show error when csrf validation failed (status: 403)', function() { stubSaveRequest(this.originSaveUrl, origin.type, 'rejected', origin.url, 'not created', 403, saveCodeMsg['csrfError']); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['csrfError']); }); }); it('should show error when origin is rejected (status: 403)', function() { stubSaveRequest(this.originSaveUrl, origin.type, 'rejected', origin.url, 'not created', 403, saveCodeMsg['rejected']); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['rejected']); }); }); it('should show error when rate limited (status: 429)', function() { stubSaveRequest(this.originSaveUrl, origin.type, 'Request was throttled. Expected available in 60 seconds.', origin.url, 'not created', 429); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['rateLimit']); }); }); it('should show error when unknown error occurs (status other than 200, 403, 429)', function() { stubSaveRequest(this.originSaveUrl, origin.type, 'Error', origin.url, 'not created', 406); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['unknownError']); }); }); + it('should display origin save info in the requests table', function() { + cy.fixture('origin-save').then(originSaveJSON => { + cy.route('GET', '/save/requests/list/**', originSaveJSON); + cy.get('#swh-origin-save-requests-list-tab').click(); + cy.get('tbody tr').then(rows => { + let i = 0; + for (let row of rows) { + const cells = row.cells; + const requestDateStr = new Date(originSaveJSON.data[i].save_request_date).toLocaleString(); + const saveStatus = originSaveJSON.data[i].save_task_status; + assert.equal($(cells[0]).text(), requestDateStr); + assert.equal($(cells[1]).text(), originSaveJSON.data[i].visit_type); + let html = ''; + if (saveStatus === 'succeed') { + let browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${originSaveJSON.data[i].origin_url}`; + browseOriginUrl += `×tamp=${originSaveJSON.data[i].visit_date}`; + html += `${originSaveJSON.data[i].origin_url}`; + } else { + html += originSaveJSON.data[i].origin_url; + } + html += ` `; + html += ''; + assert.equal($(cells[2]).html(), html); + assert.equal($(cells[3]).text(), originSaveJSON.data[i].save_request_status); + assert.equal($(cells[4]).text(), saveStatus); + ++i; + } + }); + }); + }); + }); diff --git a/swh/web/assets/src/bundles/admin/origin-save.js b/swh/web/assets/src/bundles/admin/origin-save.js index fcbc58c9..b84f6332 100644 --- a/swh/web/assets/src/bundles/admin/origin-save.js +++ b/swh/web/assets/src/bundles/admin/origin-save.js @@ -1,450 +1,451 @@ /** * 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 {handleFetchError, csrfPost, htmlAlert} from 'utils/functions'; import {swhSpinnerSrc} from 'utils/constants'; let authorizedOriginTable; let unauthorizedOriginTable; let pendingSaveRequestsTable; let acceptedSaveRequestsTable; let rejectedSaveRequestsTable; function enableRowSelection(tableSel) { $(`${tableSel} tbody`).on('click', 'tr', function() { if ($(this).hasClass('selected')) { $(this).removeClass('selected'); $(tableSel).closest('.tab-pane').find('.swh-action-need-selection').prop('disabled', true); } else { $(`${tableSel} tr.selected`).removeClass('selected'); $(this).addClass('selected'); $(tableSel).closest('.tab-pane').find('.swh-action-need-selection').prop('disabled', false); } }); } export function initOriginSaveAdmin() { $(document).ready(() => { $.fn.dataTable.ext.errMode = 'throw'; authorizedOriginTable = $('#swh-authorized-origin-urls').DataTable({ serverSide: true, ajax: Urls.admin_origin_save_authorized_urls_list(), columns: [{data: 'url', name: 'url'}], scrollY: '50vh', scrollCollapse: true, info: false }); enableRowSelection('#swh-authorized-origin-urls'); swh.webapp.addJumpToPagePopoverToDataTable(authorizedOriginTable); unauthorizedOriginTable = $('#swh-unauthorized-origin-urls').DataTable({ serverSide: true, ajax: Urls.admin_origin_save_unauthorized_urls_list(), columns: [{data: 'url', name: 'url'}], scrollY: '50vh', scrollCollapse: true, info: false }); enableRowSelection('#swh-unauthorized-origin-urls'); swh.webapp.addJumpToPagePopoverToDataTable(unauthorizedOriginTable); let columnsData = [ { data: 'id', name: 'id', visible: false, searchable: false }, { data: 'save_request_date', name: 'request_date', render: (data, type, row) => { if (type === 'display') { let date = new Date(data); return date.toLocaleString(); } return data; } }, { data: 'visit_type', name: 'visit_type' }, { data: 'origin_url', name: 'origin_url', render: (data, type, row) => { if (type === 'display') { + let html = ''; const sanitizedURL = $.fn.dataTable.render.text().display(data); - return `${sanitizedURL}`; + if (row.save_task_status === 'succeed') { + let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${sanitizedURL}`; + browseOriginUrl += `×tamp=${row.visit_date}`; + html += `${sanitizedURL}`; + } else { + html += sanitizedURL; + } + html += ` `; + return html; } return data; } } ]; pendingSaveRequestsTable = $('#swh-origin-save-pending-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, ajax: Urls.origin_save_requests_list('pending'), searchDelay: 1000, columns: columnsData, scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']], responsive: { details: { type: 'none' } } }); enableRowSelection('#swh-origin-save-pending-requests'); swh.webapp.addJumpToPagePopoverToDataTable(pendingSaveRequestsTable); rejectedSaveRequestsTable = $('#swh-origin-save-rejected-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, ajax: Urls.origin_save_requests_list('rejected'), searchDelay: 1000, columns: columnsData, scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']], responsive: { details: { type: 'none' } } }); enableRowSelection('#swh-origin-save-rejected-requests'); swh.webapp.addJumpToPagePopoverToDataTable(rejectedSaveRequestsTable); columnsData.push({ data: 'save_task_status', - name: 'save_task_status', - render: (data, type, row) => { - if (data === 'succeed' && row.visit_date) { - let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${row.origin_url}`; - browseOriginUrl += `×tamp=${row.visit_date}`; - return `${data}`; - } - return data; - } + name: 'save_task_status' }); columnsData.push({ name: 'info', render: (data, type, row) => { if (row.save_task_status === 'succeed' || row.save_task_status === 'failed') { return '`; } else { return ''; } } }); acceptedSaveRequestsTable = $('#swh-origin-save-accepted-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, ajax: Urls.origin_save_requests_list('accepted'), searchDelay: 1000, columns: columnsData, scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']], responsive: { details: { type: 'none' } } }); enableRowSelection('#swh-origin-save-accepted-requests'); swh.webapp.addJumpToPagePopoverToDataTable(acceptedSaveRequestsTable); $('#swh-origin-save-requests-nav-item').on('shown.bs.tab', () => { pendingSaveRequestsTable.draw(); }); $('#swh-origin-save-url-filters-nav-item').on('shown.bs.tab', () => { authorizedOriginTable.draw(); }); $('#swh-authorized-origins-tab').on('shown.bs.tab', () => { authorizedOriginTable.draw(); }); $('#swh-unauthorized-origins-tab').on('shown.bs.tab', () => { unauthorizedOriginTable.draw(); }); $('#swh-save-requests-pending-tab').on('shown.bs.tab', () => { pendingSaveRequestsTable.draw(); }); $('#swh-save-requests-accepted-tab').on('shown.bs.tab', () => { acceptedSaveRequestsTable.draw(); }); $('#swh-save-requests-rejected-tab').on('shown.bs.tab', () => { rejectedSaveRequestsTable.draw(); }); $('#swh-save-requests-pending-tab').click(() => { pendingSaveRequestsTable.ajax.reload(null, false); }); $('#swh-save-requests-accepted-tab').click(() => { acceptedSaveRequestsTable.ajax.reload(null, false); }); $('#swh-save-requests-rejected-tab').click(() => { rejectedSaveRequestsTable.ajax.reload(null, false); }); $('body').on('click', e => { if ($(e.target).parents('.popover').length > 0) { event.stopPropagation(); } else if ($(e.target).parents('.swh-save-request-info').length === 0) { $('.swh-save-request-info').popover('dispose'); } }); }); } export function addAuthorizedOriginUrl() { let originUrl = $('#swh-authorized-url-prefix').val(); let addOriginUrl = Urls.admin_origin_save_add_authorized_url(originUrl); csrfPost(addOriginUrl) .then(handleFetchError) .then(() => { authorizedOriginTable.row.add({'url': originUrl}).draw(); $('.swh-add-authorized-origin-status').html( htmlAlert('success', 'The origin url prefix has been successfully added in the authorized list.', true) ); }) .catch(response => { $('.swh-add-authorized-origin-status').html( htmlAlert('warning', 'The provided origin url prefix is already registered in the authorized list.', true) ); }); } export function removeAuthorizedOriginUrl() { let originUrl = $('#swh-authorized-origin-urls tr.selected').text(); if (originUrl) { let removeOriginUrl = Urls.admin_origin_save_remove_authorized_url(originUrl); csrfPost(removeOriginUrl) .then(handleFetchError) .then(() => { authorizedOriginTable.row('.selected').remove().draw(); }) .catch(() => {}); } } export function addUnauthorizedOriginUrl() { let originUrl = $('#swh-unauthorized-url-prefix').val(); let addOriginUrl = Urls.admin_origin_save_add_unauthorized_url(originUrl); csrfPost(addOriginUrl) .then(handleFetchError) .then(() => { unauthorizedOriginTable.row.add({'url': originUrl}).draw(); $('.swh-add-unauthorized-origin-status').html( htmlAlert('success', 'The origin url prefix has been successfully added in the unauthorized list.', true) ); }) .catch(() => { $('.swh-add-unauthorized-origin-status').html( htmlAlert('warning', 'The provided origin url prefix is already registered in the unauthorized list.', true) ); }); } export function removeUnauthorizedOriginUrl() { let originUrl = $('#swh-unauthorized-origin-urls tr.selected').text(); if (originUrl) { let removeOriginUrl = Urls.admin_origin_save_remove_unauthorized_url(originUrl); csrfPost(removeOriginUrl) .then(handleFetchError) .then(() => { unauthorizedOriginTable.row('.selected').remove().draw(); }) .catch(() => {}); } } export function acceptOriginSaveRequest() { let selectedRow = pendingSaveRequestsTable.row('.selected'); if (selectedRow.length) { let acceptOriginSaveRequestCallback = () => { let rowData = selectedRow.data(); let acceptSaveRequestUrl = Urls.admin_origin_save_request_accept(rowData['visit_type'], rowData['origin_url']); csrfPost(acceptSaveRequestUrl) .then(() => { pendingSaveRequestsTable.ajax.reload(null, false); }); }; swh.webapp.showModalConfirm( 'Accept origin save request ?', 'Are you sure to accept this origin save request ?', acceptOriginSaveRequestCallback); } } export function rejectOriginSaveRequest() { let selectedRow = pendingSaveRequestsTable.row('.selected'); if (selectedRow.length) { let rejectOriginSaveRequestCallback = () => { let rowData = selectedRow.data(); let rejectSaveRequestUrl = Urls.admin_origin_save_request_reject(rowData['visit_type'], rowData['origin_url']); csrfPost(rejectSaveRequestUrl) .then(() => { pendingSaveRequestsTable.ajax.reload(null, false); }); }; swh.webapp.showModalConfirm( 'Reject origin save request ?', 'Are you sure to reject this origin save request ?', rejectOriginSaveRequestCallback); } } function removeOriginSaveRequest(requestTable) { let selectedRow = requestTable.row('.selected'); if (selectedRow.length) { let requestId = selectedRow.data()['id']; let removeOriginSaveRequestCallback = () => { let removeSaveRequestUrl = Urls.admin_origin_save_request_remove(requestId); csrfPost(removeSaveRequestUrl) .then(() => { requestTable.ajax.reload(null, false); }); }; swh.webapp.showModalConfirm( 'Remove origin save request ?', 'Are you sure to remove this origin save request ?', removeOriginSaveRequestCallback); } } export function removePendingOriginSaveRequest() { removeOriginSaveRequest(pendingSaveRequestsTable); } export function removeAcceptedOriginSaveRequest() { removeOriginSaveRequest(acceptedSaveRequestsTable); } export function removeRejectedOriginSaveRequest() { removeOriginSaveRequest(rejectedSaveRequestsTable); } export function displaySaveRequestInfo(event, saveRequestId) { event.stopPropagation(); const saveRequestTaskInfoUrl = Urls.admin_origin_save_task_info(saveRequestId); $('.swh-save-request-info').popover('dispose'); $(event.target).popover({ 'title': 'Save request task information', 'content': `
Fetching task information ...
${info.key} |
${info.value} |
---|