diff --git a/cypress/integration/origin-save.spec.js b/cypress/integration/origin-save.spec.js new file mode 100644 index 00000000..22718ac4 --- /dev/null +++ b/cypress/integration/origin-save.spec.js @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2019 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 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.', + 'unknownErr': 'An unexpected error happened when submitting the "save code now request' +}; + +function makeOriginSaveRequest(originType, originUrl) { + cy.get('#swh-input-origin-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) { + cy.route({ + method: 'POST', + status: responseStatus, + url: requestUrl, + response: genOriginSaveResponse(objectType, status, originUrl, Date().toString(), taskStatus) + }).as('saveRequest'); +} + +// Mocks API response : /save/(:object_type)/(:origin_url) +// object_type : {'git', 'hg', 'svn'} +function genOriginSaveResponse(objectType, saveRequestStatus, originUrl, saveRequestDate, saveTaskStatus) { + return { + 'origin_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 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 origin is rejected (status: 403)', function() { + stubSaveRequest(this.originSaveUrl, origin.type, 'rejected', + origin.url, 'not created', 403); + + 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['unknownErr']); + }); + }); + +}); diff --git a/cypress/support/index.js b/cypress/support/index.js index a0a6910d..5e096be2 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,124 +1,127 @@ /** * Copyright (C) 2019 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/code-coverage/support'; import {httpGetJson} from '../utils'; Cypress.Screenshot.defaults({ screenshotOnRunFailure: false }); before(function() { this.unarchivedRepo = { url: 'https://github.com/SoftwareHeritage/swh-web', + type: 'git', revision: '7bf1b2f489f16253527807baead7957ca9e8adde', snapshot: 'd9829223095de4bb529790de8ba4e4813e38672d', rootDirectory: '7d887d96c0047a77e2e8c4ee9bb1528463677663', content: [{ sha1git: 'b203ec39300e5b7e97b6e20986183cbd0b797859' }] }; this.origin = [{ url: 'https://github.com/memononen/libtess2', + type: 'git', content: [{ path: 'Source/tess.h' }, { path: 'premake4.lua' }], directory: [{ path: 'Source', id: 'cd19126d815470b28919d64b2a8e6a3e37f900dd' }], revisions: [], invalidSubDir: 'Source1' }, { url: 'https://github.com/wcoder/highlightjs-line-numbers.js', + type: 'git', content: [], directory: [], revisions: ['1c480a4573d2a003fc2630c21c2b25829de49972'] }]; const getMetadataForOrigin = async originUrl => { const originVisitsApiUrl = this.Urls.api_1_origin_visits(originUrl); const originVisits = await httpGetJson(originVisitsApiUrl); const lastVisit = originVisits[0]; const snapshotApiUrl = this.Urls.api_1_snapshot(lastVisit.snapshot); const lastOriginSnapshot = await httpGetJson(snapshotApiUrl); const revisionApiUrl = this.Urls.api_1_revision(lastOriginSnapshot.branches.HEAD.target); const lastOriginHeadRevision = await httpGetJson(revisionApiUrl); return { 'directory': lastOriginHeadRevision.directory, 'revision': lastOriginHeadRevision.id, 'snapshot': lastOriginSnapshot.id }; }; cy.visit('/').window().then(async win => { this.Urls = win.Urls; for (let origin of this.origin) { const metadata = await getMetadataForOrigin(origin.url); const directoryApiUrl = this.Urls.api_1_directory(metadata.directory); origin.dirContent = await httpGetJson(directoryApiUrl); origin.rootDirectory = metadata.directory; origin.revisions.push(metadata.revision); origin.snapshot = metadata.snapshot; for (let content of origin.content) { cy.visit(this.Urls.browse_origin_content(origin.url, content.path)) .window().then(win => { const contentMetaData = win.swh.webapp.getBrowsedSwhObjectMetadata(); content.name = contentMetaData.filename; content.sha1git = contentMetaData.sha1_git; content.directory = contentMetaData.directory; content.rawFilePath = this.Urls.browse_content_raw(`sha1_git:${content.sha1git}`) + `?filename=${encodeURIComponent(content.name)}`; cy.request(content.rawFilePath) .then((response) => { const fileText = response.body; const fileLines = fileText.split('\n'); content.numberLines = fileLines.length; // If last line is empty its not shown if (!fileLines[content.numberLines - 1]) content.numberLines -= 1; }); }); } } }); }); // force the use of fetch polyfill wrapping XmlHttpRequest // in order for cypress to be able to intercept and stub them Cypress.on('window:before:load', win => { win.fetch = null; }); // Ensure code coverage data do not get lost each time a new // page is loaded during a single test execution let coverage = {}; Cypress.on('window:before:unload', e => { coverage = Object.assign(coverage, e.currentTarget.__coverage__); }); beforeEach(function() { coverage = {}; }); afterEach(function() { cy.task('combineCoverage', coverage); });