diff --git a/assets/src/bundles/add_forge/forge-admin-email.ejs b/assets/src/bundles/add_forge/forge-admin-email.ejs index 222b7346..4e8816a7 100644 --- a/assets/src/bundles/add_forge/forge-admin-email.ejs +++ b/assets/src/bundles/add_forge/forge-admin-email.ejs @@ -1,29 +1,33 @@ <%# Copyright (C) 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 %> Dear forge administrator, The mission of Software Heritage is to collect, preserve and share all the publicly available source code (see https://www.softwareheritage.org for more information). We just received a request to add the forge hosted at <%= forgeUrl %> to the list of software origins that are archived, and it is our understanding that you are the contact person for this forge. In order to archive the forge contents, we will have to periodically pull the public repositories it contains and clone them into the Software Heritage archive. -Would you be so kind as to reply to this message to acknowledge the -reception of this email and let us know if there are any special steps -we should take in order to properly archive the public repositories -hosted on your infrastructure? +Please let us know if there are any technical issues to consider before + we launch the archival of the public repositories hosted on your + infrastructure.(use "Reply all" to ensure our system will process + your answer properly) + +In the absence of an answer to this message, we will start to archive +your forge in the coming weeks (only the publicly accessible +repositories will be archived) Thank you in advance for your help. Kind regards, The Software Heritage team diff --git a/assets/src/bundles/add_forge/request-dashboard.js b/assets/src/bundles/add_forge/request-dashboard.js index 8594fe1f..fcb6219e 100644 --- a/assets/src/bundles/add_forge/request-dashboard.js +++ b/assets/src/bundles/add_forge/request-dashboard.js @@ -1,138 +1,138 @@ /** * Copyright (C) 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 {handleFetchError, csrfPost, getHumanReadableDate} from 'utils/functions'; import emailTempate from './forge-admin-email.ejs'; import requestHistoryItem from './add-request-history-item.ejs'; let forgeRequest; export function onRequestDashboardLoad(requestId) { $(document).ready(() => { populateRequestDetails(requestId); $('#contactForgeAdmin').click((event) => { contactForgeAdmin(event); }); $('#updateRequestForm').submit(async function(event) { event.preventDefault(); try { const response = await csrfPost($(this).attr('action'), {'Content-Type': 'application/x-www-form-urlencoded'}, $(this).serialize()); handleFetchError(response); $('#userMessage').text('The request status has been updated '); $('#userMessage').removeClass('badge-danger'); $('#userMessage').addClass('badge-success'); populateRequestDetails(requestId); } catch (response) { $('#userMessage').text('Sorry; Updating the request failed'); $('#userMessage').removeClass('badge-success'); $('#userMessage').addClass('badge-danger'); } }); }); } async function populateRequestDetails(requestId) { try { const response = await fetch(Urls.api_1_add_forge_request_get(requestId)); handleFetchError(response); const data = await response.json(); forgeRequest = data.request; $('#requestStatus').text(swh.add_forge.formatRequestStatusName(forgeRequest.status)); $('#requestType').text(forgeRequest.forge_type); $('#requestURL').text(forgeRequest.forge_url); $('#requestContactName').text(forgeRequest.forge_contact_name); $('#requestContactConsent').text(forgeRequest.submitter_forward_username); $('#requestContactEmail').text(forgeRequest.forge_contact_email); $('#submitterMessage').text(forgeRequest.forge_contact_comment); $('#updateComment').val(''); // Setting data for the email, now adding static data $('#contactForgeAdmin').attr('emailTo', forgeRequest.forge_contact_email); $('#contactForgeAdmin').attr('emailCc', forgeRequest.inbound_email_address); - $('#contactForgeAdmin').attr('emailSubject', `Software Heritage archival request for ${forgeRequest.forge_domain}`); + $('#contactForgeAdmin').attr('emailSubject', `Software Heritage archival notification for ${forgeRequest.forge_domain}`); populateRequestHistory(data.history); populateDecisionSelectOption(forgeRequest.status); } catch (e) { if (e instanceof Response) { // The fetch request failed (in handleFetchError), show the error message $('#fetchError').removeClass('d-none'); $('#requestDetails').addClass('d-none'); } else { // Unknown exception, pass it through throw e; } } } function populateRequestHistory(history) { $('#requestHistory').children().remove(); history.forEach((event, index) => { const historyEvent = requestHistoryItem({ 'event': event, 'index': index, 'getHumanReadableDate': getHumanReadableDate }); $('#requestHistory').append(historyEvent); }); } export function populateDecisionSelectOption(currentStatus) { const nextStatusesFor = { 'PENDING': ['WAITING_FOR_FEEDBACK', 'REJECTED', 'SUSPENDED'], 'WAITING_FOR_FEEDBACK': ['FEEDBACK_TO_HANDLE'], 'FEEDBACK_TO_HANDLE': [ 'WAITING_FOR_FEEDBACK', 'ACCEPTED', 'REJECTED', 'SUSPENDED' ], 'ACCEPTED': ['SCHEDULED'], 'SCHEDULED': [ 'FIRST_LISTING_DONE', 'FIRST_ORIGIN_LOADED' ], 'FIRST_LISTING_DONE': ['FIRST_ORIGIN_LOADED'], 'FIRST_ORIGIN_LOADED': [], 'REJECTED': [], 'SUSPENDED': ['PENDING'], 'DENIED': [] }; // Determine the possible next status out of the current one const nextStatuses = nextStatusesFor[currentStatus]; function addStatusOption(status, index) { // Push the next possible status options const label = swh.add_forge.formatRequestStatusName(status); $('#decisionOptions').append( `` ); } // Remove all the options and add new ones $('#decisionOptions').children().remove(); nextStatuses.forEach(addStatusOption); $('#decisionOptions').append( '' ); } function contactForgeAdmin(event) { // Open the mailclient with pre-filled text const mailTo = encodeURIComponent($('#contactForgeAdmin').attr('emailTo')); const mailCc = encodeURIComponent($('#contactForgeAdmin').attr('emailCc')); const subject = encodeURIComponent($('#contactForgeAdmin').attr('emailSubject')); const emailText = encodeURIComponent(emailTempate({'forgeUrl': forgeRequest.forge_url}).trim().replace(/\n/g, '\r\n')); const w = window.open('', '_blank', '', true); w.location.href = `mailto:${mailTo}?Cc=${mailCc}&Reply-To=${mailCc}&Subject=${subject}&body=${emailText}`; w.focus(); } diff --git a/cypress/e2e/add-forge-now-request-dashboard.cy.js b/cypress/e2e/add-forge-now-request-dashboard.cy.js index e2e8e859..c546f493 100644 --- a/cypress/e2e/add-forge-now-request-dashboard.cy.js +++ b/cypress/e2e/add-forge-now-request-dashboard.cy.js @@ -1,310 +1,315 @@ /** * Copyright (C) 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 */ let requestId; let requestForgeDomain; let requestInboundEmailAddress; const requestData = { forge_type: 'bitbucket', forge_url: 'test.example.com', forge_contact_email: 'test@example.com', forge_contact_name: 'test user', submitter_forward_username: true, forge_contact_comment: 'test comment' }; function createDummyRequest(urls) { cy.task('db:add_forge_now:delete'); cy.userLogin(); return cy.getCookie('csrftoken').its('value').then((token) => { cy.request({ method: 'POST', url: urls.api_1_add_forge_request_create(), body: requestData, headers: { 'X-CSRFToken': token } }).then((response) => { // setting requestId and forgeDomain from response requestId = response.body.id; requestForgeDomain = response.body.forge_domain; requestInboundEmailAddress = response.body.inbound_email_address; // logout the user cy.visit(urls.swh_web_homepage()); cy.contains('a', 'logout').click(); }); }); } function genEmailSrc() { return `Message-ID: Date: Tue, 19 Apr 2022 14:00:56 +0200 MIME-Version: 1.0 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Thunderbird/91.8.0 To: ${requestData.forge_contact_email} Cc: ${requestInboundEmailAddress} Reply-To: ${requestInboundEmailAddress} -Subject: Software Heritage archival request for test.example.com +Subject: Software Heritage archival notification for test.example.com Content-Language: en-US From: Test Admin Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit Dear forge administrator, -The mission of Software Heritage is to collect, preserve and share all the -publicly available source code (see https://www.softwareheritage.org for more -information). +The mission of Software Heritage is to collect, preserve and share all +the publicly available source code (see +https://www.softwareheritage.org for more information). -We just received a request to add the forge hosted at https://test.example.com to the -list of software origins that are archived, and it is our understanding that you -are the contact person for this forge. +We just received a request to add the forge hosted at <%= forgeUrl %> +to the list of software origins that are archived, and it is our +understanding that you are the contact person for this forge. -In order to archive the forge contents, we will have to periodically pull the -public repositories it contains and clone them into the +In order to archive the forge contents, we will have to periodically +pull the public repositories it contains and clone them into the Software Heritage archive. -Would you be so kind as to reply to this message to acknowledge the reception -of this email and let us know if there are any special steps we should take in -order to properly archive the public repositories hosted on your infrastructure? +Please let us know if there are any technical issues to consider before + we launch the archival of the public repositories hosted on your + infrastructure.(use "Reply all" to ensure our system will process + your answer properly) + +In the absence of an answer to this message, we will start to archive +your forge in the coming weeks (only the publicly accessible +repositories will be archived) Thank you in advance for your help. Kind regards, The Software Heritage team `; } describe('Test add forge now request dashboard load', function() { before(function() { // Create an add-forge-request object in the DB createDummyRequest(this.Urls); }); beforeEach(function() { const url = this.Urls.add_forge_now_request_dashboard(requestId); // request dashboard require admin permissions to view cy.adminLogin(); cy.intercept(`${this.Urls.api_1_add_forge_request_get(requestId)}**`).as('forgeRequestGet'); cy.visit(url); cy.get('.swh-add-forge-now-moderation-item') .should('have.class', 'active'); }); it('should load add forge request details', function() { cy.wait('@forgeRequestGet'); cy.get('#requestStatus') .should('contain', 'Pending'); cy.get('#requestType') .should('contain', 'bitbucket'); cy.get('#requestURL') .should('contain', 'test.example.com'); cy.get('#requestContactEmail') .should('contain', 'test@example.com'); cy.get('#requestContactName') .should('contain', 'test user'); cy.get('#requestContactEmail') .should('contain', 'test@example.com'); cy.get('#requestContactConsent') .should('contain', 'true'); cy.get('#submitterMessage') .should('contain', 'test comment'); }); it('should show send message link', function() { cy.wait('@forgeRequestGet'); cy.get('#contactForgeAdmin') .should('have.attr', 'emailto') .and('include', 'test@example.com'); cy.get('#contactForgeAdmin') .should('have.attr', 'emailsubject') - .and('include', `Software Heritage archival request for ${requestForgeDomain}`); + .and('include', `Software Heritage archival notification for ${requestForgeDomain}`); }); it('should not show any error message', function() { cy.wait('@forgeRequestGet'); cy.get('#fetchError') .should('have.class', 'd-none'); cy.get('#requestDetails') .should('not.have.class', 'd-none'); }); it('should show error message for an api error', function() { // requesting with a non existing request ID const invalidRequestId = requestId + 10; const url = this.Urls.add_forge_now_request_dashboard(invalidRequestId); cy.intercept(`${this.Urls.api_1_add_forge_request_get(invalidRequestId)}**`, {statusCode: 400}).as('forgeAddInvalidRequest'); cy.visit(url); cy.wait('@forgeAddInvalidRequest'); cy.get('#fetchError') .should('not.have.class', 'd-none'); cy.get('#requestDetails') .should('have.class', 'd-none'); }); it('should load add forge request history', function() { cy.wait('@forgeRequestGet'); cy.get('#requestHistory') .children() .should('have.length', 1); cy.get('#requestHistory') .children() .should('contain', 'New status: Pending'); cy.get('#requestHistory') .should('contain', 'From user (SUBMITTER)'); }); it('should load possible next status', function() { cy.wait('@forgeRequestGet'); // 3 possible next status and the comment option cy.get('#decisionOptions') .children() .should('have.length', 4); }); }); function populateAndSubmitForm() { cy.get('#decisionOptions').select('WAITING_FOR_FEEDBACK'); cy.get('#updateComment').type('This is an update comment'); cy.get('#updateRequestForm').submit(); } describe('Test add forge now request update', function() { beforeEach(function() { createDummyRequest(this.Urls).then(() => { this.url = this.Urls.add_forge_now_request_dashboard(requestId); cy.adminLogin(); // intercept GET API on page load cy.intercept(`${this.Urls.api_1_add_forge_request_get(requestId)}**`).as('forgeRequestGet'); // intercept update POST API cy.intercept('POST', `${this.Urls.api_1_add_forge_request_update(requestId)}**`).as('forgeRequestUpdate'); cy.visit(this.url).then(() => { cy.wait('@forgeRequestGet'); }); }); }); it('should submit correct details', function() { populateAndSubmitForm(); // Making sure posting the right data cy.wait('@forgeRequestUpdate').its('request.body') .should('include', 'new_status') .should('include', 'text') .should('include', 'WAITING_FOR_FEEDBACK'); }); it('should show success message', function() { populateAndSubmitForm(); // Making sure showing the success message cy.wait('@forgeRequestUpdate'); cy.get('#userMessage') .should('contain', 'The request status has been updated') .should('not.have.class', 'badge-danger') .should('have.class', 'badge-success'); }); it('should update the dashboard after submit', function() { populateAndSubmitForm(); // Making sure the UI is updated after the submit cy.wait('@forgeRequestGet'); cy.get('#requestStatus') .should('contain', 'Waiting for feedback'); cy.get('#requestHistory') .children() .should('have.length', 2); cy.get('#requestHistory') .children() .should('contain', 'New status: Waiting for feedback'); cy.get('#requestHistory') .children() .should('contain', 'This is an update comment'); cy.get('#requestHistory') .children() .should('contain', 'Status changed to: Waiting for feedback'); cy.get('#decisionOptions') .children() .should('have.length', 2); }); it('should update the dashboard after receiving email', function() { const emailSrc = genEmailSrc(); cy.task('processAddForgeNowInboundEmail', emailSrc); // Refresh page and wait for the async request to complete cy.visit(this.url); cy.wait('@forgeRequestGet'); cy.get('#requestHistory') .children() .should('have.length', 2); cy.get('#historyItem1') .click() .should('contain', 'New status: Waiting for feedback'); cy.get('#historyItemBody1') .find('a') .should('contain', 'Open original message in email client') .should('have.prop', 'href').and('contain', '/message-source/').then(function(href) { cy.request(href).then((response) => { expect(response.headers['content-type']) .to.equal('text/email'); expect(response.headers['content-disposition']) .to.match(/filename="add-forge-now-test.example.com-message\d+.eml"/); expect(response.body) .to.equal(emailSrc); }); }); }); it('should show an error on API failure', function() { cy.intercept('POST', `${this.Urls.api_1_add_forge_request_update(requestId)}**`, {forceNetworkError: true}) .as('updateFailedRequest'); cy.get('#updateComment').type('This is an update comment'); cy.get('#updateRequestForm').submit(); cy.wait('@updateFailedRequest'); cy.get('#userMessage') .should('contain', 'Sorry; Updating the request failed') .should('have.class', 'badge-danger') .should('not.have.class', 'badge-success'); }); });