diff --git a/cypress/integration/basics.js b/cypress/integration/basics.js index 1edc0a3..be34285 100644 --- a/cypress/integration/basics.js +++ b/cypress/integration/basics.js @@ -1,335 +1,350 @@ /** * 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 */ /* * Tests the basic features of the application. */ "use strict"; describe('JSON Generation', function() { beforeEach(function() { /* Clear the session storage, as it is used to restore field data; * and we don't want a test to load data from the previous test. */ cy.window().then((win) => { win.sessionStorage.clear() }) cy.visit('./index.html'); }); it('works just from the software name', function() { cy.get('#name').type('My Test Software'); cy.get('#generateCodemetaV2').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", }); }); it('works just from all main fields when using only one license', function() { cy.get('#name').type('My Test Software'); cy.get('#description').type('This is a\ngreat piece of software'); cy.get('#dateCreated').type('2019-10-02'); cy.get('#datePublished').type('2020-01-01'); cy.get('#license').type('AGPL-3.0'); cy.get("#license").type('{enter}'); cy.get('#generateCodemetaV2').click(); cy.get("#license").should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", }); }); it('works just from all main fields when using multiple licenses', function() { cy.get('#name').type('My Test Software'); cy.get('#description').type('This is a\ngreat piece of software'); cy.get('#dateCreated').type('2019-10-02'); cy.get('#datePublished').type('2020-01-01'); cy.get('#license').type('AGPL-3.0'); cy.get("#license").type('{enter}'); cy.get('#license').type('MIT'); cy.get("#license").type('{enter}'); cy.get('#generateCodemetaV2').click(); cy.get("#license").should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "license": ["https://spdx.org/licenses/AGPL-3.0", "https://spdx.org/licenses/MIT"], "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", }); }); it('works when choosing licenses without the keyboard', function() { cy.get('#name').type('My Test Software'); cy.get('#description').type('This is a\ngreat piece of software'); cy.get('#dateCreated').type('2019-10-02'); cy.get('#datePublished').type('2020-01-01'); cy.get('#license').type('AGPL-3.0'); // no cy.get("#license").type('{enter}'); here cy.get('#generateCodemetaV2').click(); cy.get("#license").should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", }); }); it('works for new codemeta terms in both versions', function() { cy.get('#name').type('My Test Software'); cy.get('#contIntegration').type('https://test-ci.org/my-software'); cy.get('#isSourceCodeOf').type('Bigger Application'); cy.get('#reviewAspect').type('Some software aspect'); cy.get('#reviewBody').type('Some review'); cy.get('#generateCodemetaV2').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", "contIntegration": "https://test-ci.org/my-software", "codemeta:continuousIntegration": { "id": "https://test-ci.org/my-software" }, "codemeta:isSourceCodeOf": { "id": "Bigger Application" }, "schema:review": { "type": "schema:Review", "schema:reviewAspect": "Some software aspect", "schema:reviewBody": "Some review" } }); cy.get('#generateCodemetaV3').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", "continuousIntegration": "https://test-ci.org/my-software", "codemeta:contIntegration": { "id": "https://test-ci.org/my-software" }, "isSourceCodeOf": "Bigger Application", "review": { "type": "Review", "reviewAspect": "Some software aspect", "reviewBody": "Some review" } }); }); }); describe('JSON Import', function() { it('works just from the software name', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); }); it('works just from all main fields when using license as string', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); cy.get('#description').should('have.value', 'This is a\ngreat piece of software'); cy.get('#dateCreated').should('have.value', '2019-10-02'); cy.get('#datePublished').should('have.value', '2020-01-01'); cy.get('#license').should('have.value', ''); cy.get("#selected-licenses").children().should('have.length', 1); cy.get("#selected-licenses").children().first().children().first().should('have.text', 'AGPL-3.0'); }); it('works just from all main fields when using license as array', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": ["https://spdx.org/licenses/AGPL-3.0", "https://spdx.org/licenses/MIT"], "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); cy.get('#description').should('have.value', 'This is a\ngreat piece of software'); cy.get('#dateCreated').should('have.value', '2019-10-02'); cy.get('#datePublished').should('have.value', '2020-01-01'); cy.get('#license').should('have.value', ''); cy.get("#selected-licenses").children().should('have.length', 2); cy.get("#selected-licenses").children().eq(0).children().first().should('have.text', 'AGPL-3.0'); cy.get("#selected-licenses").children().eq(1).children().first().should('have.text', 'MIT'); }); it('works with expanded document version', function () { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "http://schema.org/name": [ { "@value": "My Test Software" } ], "@type": [ "http://schema.org/SoftwareSourceCode" ] })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); }); it('errors on invalid type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "foo", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); // Should still be imported as much as possible cy.get('#name').should('have.value', 'My Test Software'); // But must display an error cy.get('#errorMessage').should('have.text', 'Wrong document type: must be "SoftwareSourceCode"/"SoftwareApplication", not "foo"'); }); it('allows singleton array as context', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": ["https://doi.org/10.5063/schema/codemeta-2.0"], "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); }); it('imports properties introduced in codemeta v3.0', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", "continuousIntegration": "https://test-ci.org/my-software", "isSourceCodeOf": "Bigger Application", "review": { "type": "Review", "reviewAspect": "Some software aspect", "reviewBody": "Some review" } })) ); cy.get('#importCodemeta').click(); cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); cy.get('#isSourceCodeOf').should('have.value', 'Bigger Application'); cy.get('#reviewAspect').should('have.value', 'Some software aspect'); cy.get('#reviewBody').should('have.value', 'Some review'); }); it('imports codemeta v2.0 properties from document with v3.0 context', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", - "continuousIntegration": "https://test-ci.org/my-software", "codemeta:contIntegration": { "id": "https://test-ci.org/my-software" - }, + } })) ); cy.get('#importCodemeta').click(); cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); }); - it('works for codemeta v3.0 terms in v2.0 version, and does not work for new terms', function() { + it('imports codemeta v3.0 properties from document with v2.0 context', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", - "contIntegration": "https://test-ci.org/my-software", "codemeta:continuousIntegration": { "id": "https://test-ci.org/my-software" }, "codemeta:isSourceCodeOf": { "id": "Bigger Application" }, "schema:review": { "type": "schema:Review", "schema:reviewAspect": "Some software aspect", "schema:reviewBody": "Some review" } })) ); cy.get('#importCodemeta').click(); cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); - cy.get('#isSourceCodeOf').should('have.value', ''); - cy.get('#reviewAspect').should('have.value', ''); - cy.get('#reviewBody').should('have.value', ''); + cy.get('#isSourceCodeOf').should('have.value', 'Bigger Application'); + cy.get('#reviewAspect').should('have.value', 'Some software aspect'); + cy.get('#reviewBody').should('have.value', 'Some review'); + }); + + it('imports newest version property when it is duplicate in multiple version context', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "contIntegration": "https://test-ci1.org/my-software", + "codemeta:continuousIntegration": { + "id": "https://test-ci2.org/my-software" + }, + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci2.org/my-software'); }); }); diff --git a/js/validation/index.js b/js/validation/index.js index 2f7d01f..4035f8c 100644 --- a/js/validation/index.js +++ b/js/validation/index.js @@ -1,99 +1,96 @@ /** * 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 */ /* * Reads a Codemeta file and shows human-friendly errors on it. * * This validator intentionaly does not use a schema, in order to show errors * that are easy to understand for users with no understanding of JSON-LD. */ function validateDocument(doc) { if (!Array.isArray(doc) && typeof doc != 'object') { setError("Document must be an object (starting and ending with { and }), not ${typeof doc}.") return false; } // TODO: validate id/@id - if (doc["@context"] === undefined) { - setError("Missing context (required to determine import version).") - return false; - } - // TODO: check there is either type or @type but not both var type = getDocumentType(doc); if (type === undefined) { setError("Missing type (must be SoftwareSourceCode or SoftwareApplication).") return false; } else if (!isCompactTypeEqual(type, "SoftwareSourceCode") && !isCompactTypeEqual(type, "SoftwareApplication")) { // Check this before other fields, as a wrong type error is more // understandable than "invalid field". setError(`Wrong document type: must be "SoftwareSourceCode"/"SoftwareApplication", not ${JSON.stringify(type)}`) return false; } else { return Object.entries(doc).every((entry) => { var fieldName = entry[0]; var subdoc = entry[1]; if (fieldName == "@context") { // Was checked before return true; } else if (fieldName == "type" || fieldName == "@type") { // Was checked before return true; } else if (isFieldFromOtherVersionToIgnore(fieldName)) { // Do not check fields from other versions FIXME return true; } else { var validator = softwareFieldValidators[fieldName]; if (validator === undefined) { // TODO: find if it's a field that belongs to another type, // and suggest that to the user setError(`Unknown field "${fieldName}".`) return false; } else { return validator(fieldName, subdoc); } } }); } } async function parseAndValidateCodemeta(showPopup) { var codemetaText = document.querySelector('#codemetaText').innerText; let parsed, doc; try { parsed = JSON.parse(codemetaText); } catch (e) { setError(`Could not read codemeta document because it is not valid JSON (${e}). Check for missing or extra quote, colon, or bracket characters.`); return; } setError(""); var isValid = validateDocument(parsed); if (showPopup) { if (isValid) { alert('Document is valid!') } else { alert('Document is invalid.'); } } - doc = await jsonld.compact(parsed, parsed["@context"]); + parsed["@context"] = LOCAL_CONTEXT_URL; + const expanded = await jsonld.expand(parsed); + doc = await jsonld.compact(expanded, LOCAL_CONTEXT_URL); return doc; }