From 0471faef27a60fce763d11fc4dfc3d728aa68f04 Mon Sep 17 00:00:00 2001 From: Clayton Liddell Date: Wed, 26 Jan 2022 10:16:26 -0600 Subject: [PATCH] Add 'hidden' state to dkan_publishing workflow (round 2) (#3742) --- cypress/integration/01_metastore.spec.js | 302 ++++-------------- cypress/integration/08_admin_views.spec.js | 2 +- .../10_workflow_transitions.spec.js | 163 ++++++++++ cypress/plugins/index.js | 11 +- .../{commands.js => commands/drupal.js} | 10 +- cypress/support/helpers/dkan.js | 240 ++++++++++++++ cypress/support/index.js | 2 +- modules/common/src/DatasetInfo.php | 2 +- .../datastore/src/Service/ResourcePurger.php | 4 +- .../tests/src/Unit/Controller/MockStorage.php | 6 +- .../workflows.workflow.dkan_publishing.yml | 21 +- modules/metastore/metastore.install | 46 +++ .../config/install/search_api.index.dkan.yml | 2 + .../metastore_search/metastore_search.module | 85 +++-- .../DkanDatasetFilterProcessorBase.php | 81 +++++ .../DkanDatasetFilterProcessorInterface.php | 23 ++ .../search_api/datasource/DkanDataset.php | 50 ++- .../processor/DkanDatasetFilterHidden.php | 28 ++ .../DkanDatasetFilterUnpublished.php | 28 ++ .../DkanDatasetFilterProcessorBaseTest.php | 43 +++ .../search_api/datasource/DkanDatasetTest.php | 13 +- .../src/Controller/MetastoreController.php | 23 +- modules/metastore/src/LifeCycle/LifeCycle.php | 6 +- modules/metastore/src/Service.php | 38 +-- modules/metastore/src/Storage/Data.php | 66 ++-- .../src/Storage/MetastoreStorageInterface.php | 29 +- modules/metastore/src/Storage/NodeData.php | 4 +- .../src/Functional/Storage/NodeDataTest.php | 2 +- .../src/Unit/MetastoreControllerTest.php | 57 ++-- .../metastore/tests/src/Unit/ServiceTest.php | 38 +-- tests/src/Functional/DatasetTest.php | 7 + 31 files changed, 959 insertions(+), 473 deletions(-) create mode 100644 cypress/integration/10_workflow_transitions.spec.js rename cypress/support/{commands.js => commands/drupal.js} (74%) create mode 100644 cypress/support/helpers/dkan.js create mode 100644 modules/metastore/modules/metastore_search/src/Plugin/search_api/DkanDatasetFilterProcessorBase.php create mode 100644 modules/metastore/modules/metastore_search/src/Plugin/search_api/DkanDatasetFilterProcessorInterface.php create mode 100644 modules/metastore/modules/metastore_search/src/Plugin/search_api/processor/DkanDatasetFilterHidden.php create mode 100644 modules/metastore/modules/metastore_search/src/Plugin/search_api/processor/DkanDatasetFilterUnpublished.php create mode 100644 modules/metastore/modules/metastore_search/tests/src/Unit/Plugin/search_api/DkanDatasetFilterProcessorBaseTest.php diff --git a/cypress/integration/01_metastore.spec.js b/cypress/integration/01_metastore.spec.js index 1e2ba6d3d2..5e7ae56d4e 100644 --- a/cypress/integration/01_metastore.spec.js +++ b/cypress/integration/01_metastore.spec.js @@ -1,182 +1,12 @@ -context('Metastore', () => { - - const api_uri = Cypress.config('apiUri') - const user_credentials = Cypress.env('TEST_USER_CREDENTIALS') - const uuid_regex = new RegExp(Cypress.env('UUID_REGEX')) - const metastore_schemas = [ - 'dataset', - 'publisher', - 'distribution', - 'theme', - 'keyword', - 'data-dictionary', - ] - - function getMetastoreCreateEndpoint (schema_id) { - return `/${api_uri}/metastore/schemas/${schema_id}/items` - } - - function getMetastoreGetEndpoint (schema_id, identifier) { - return `/${api_uri}/metastore/schemas/${schema_id}/items/${identifier}` - } - - function getMetastorePutEndpoint (schema_id, identifier) { - return `/${api_uri}/metastore/schemas/${schema_id}/items/${identifier}` - } - - function getMetastorePatchEndpoint (schema_id, identifier) { - return `/${api_uri}/metastore/schemas/${schema_id}/items/${identifier}` - } - - function getMetastoreDeleteEndpoint (schema_id, identifier) { - return `/${api_uri}/metastore/schemas/${schema_id}/items/${identifier}` - } - - // Generate a random uuid. - // Credit: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript - function generateMetastoreIdentifier () { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8) - return v.toString(16) - }) - } - - function generateRandomString () { - return generateMetastoreIdentifier() - } - - function generateRandomDateString () { - const start = new Date('1970-01-01T00:00:00.000Z') - const end = new Date() - const date = new Date(+start + Math.random() * (end - start)) - - const year = date.getFullYear().toString() - const month = date.getMonth().toString().padStart(2, '0') - const day = date.getDate().toString().padStart(2, '0') - - return year + '-' + month + '-' + day - } - - // Create a metastore item via API. - function createMetastore (schema_id, item = null) { - item = item || generateMetastore(schema_id) - // Lookup the proper metastore creation procedure for the given schema ID. - return cy.request({ - method: 'POST', - url: `${api_uri}/metastore/schemas/${schema_id}/items`, - auth: user_credentials, - body: item - }) - } - - function generateMetastore (schema_id, identifier = null) { - // Generate a unique metastore identifier if one was not supplied. - identifier = identifier || generateMetastoreIdentifier() - // Lookup the proper metastore generation procedure for the given schema ID. - const metastore_generator_dictionary = { - "dataset": generateDataset, - "publisher": generatePublisher, - "distribution": generateDistribution, - "theme": generateTheme, - "keyword": generateKeyword, - "data-dictionary": generateDataDictionary, - } - return metastore_generator_dictionary[schema_id](identifier) - } - - // Generate a metastore dataset item object. - function generateDataset(uuid) { - return { - title: "Title for " + uuid, - description: "Description for " + uuid, - identifier: uuid, - accessLevel: "public", - bureauCode: ["1234:56"], - modified: generateRandomDateString(), - "@type": "dcat:Dataset", - distribution: [ - { - "@type": "dcat:Distribution", - downloadURL: "https://dkan-default-content-files.s3.amazonaws.com/phpunit/district_centerpoints_small.csv", - mediaType: "text/csv", - format: "csv", - description: `

${generateRandomString()}

`, - title: generateRandomString() - } - ], - keyword: [ - generateRandomString(), - generateRandomString(), - generateRandomString() - ], - contactPoint: { - "@type": "vcard:Contact", - fn: generateRandomString() + " " + generateRandomString(), - hasEmail: "mailto:first.last@example.com" - } - } - } - - // Generate a metastore publisher item object. - function generatePublisher(uuid) { - return { - "identifier": uuid, - "data": { - "@type": "org:Organization", - "name": generateRandomString(), - "subOrganizationOf": generateRandomString() - } - } - } - - function generateDistribution(uuid) { - return { - "identifier": uuid, - "data": { - "title": "Title for " + uuid, - "description": `

${generateRandomString()}

`, - "format": "csv", - "mediaType": "text/csv", - "downloadURL": "https://dkan-default-content-files.s3.amazonaws.com/phpunit/district_centerpoints_small.csv", - } - } - } - - function generateTheme(uuid) { - return { - "identifier": uuid, - "data": generateRandomString() - } - } - - function generateKeyword(uuid) { - return { - "identifier": uuid, - "data": generateRandomString() - } - } - - // Generate a metastore data-dictionary item object. - function generateDataDictionary(uuid) { - return { - "identifier": uuid, - "title": "Title for " + uuid, - "data": { - "fields": [ - { - "name": generateRandomString(), - "title": generateRandomString(), - "type": "string", - "format": "default" - } - ] - } - } - } +import * as dkan from '../support/helpers/dkan' +const api_uri = Cypress.config('apiUri') +const user_credentials = Cypress.env('TEST_USER_CREDENTIALS') +const uuid_regex = new RegExp(Cypress.env('UUID_REGEX')) +context('Metastore', () => { context('Catalog', () => { it('Should contain newly created datasets', () => { - createMetastore('dataset').then((response) => { + dkan.createMetastore('dataset').then((response) => { expect(response.status).eql(201) const identifier = response.body.identifier cy.request('data.json').then((response) => { @@ -206,27 +36,27 @@ context('Metastore', () => { const schema_id = 'dataset' it('bad query parameter', () => { - const item = generateMetastore(schema_id) - createMetastore(schema_id, item).then((response) => { - cy.request(getMetastoreGetEndpoint(schema_id, response.body.identifier) + '?view=foobar').then((response) => { + const item = dkan.generateMetastore(schema_id) + dkan.createMetastore(schema_id, item).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, response.body.identifier) + '?view=foobar').then((response) => { expect(response.body.keyword).eql(item.keyword) }) }) }) it('data (default)', () => { - const item = generateMetastore(schema_id) - createMetastore(schema_id, item).then((response) => { - cy.request(getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { + const item = dkan.generateMetastore(schema_id) + dkan.createMetastore(schema_id, item).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { expect(response.body.keyword).eql(item.keyword) }) }) }) it('data+identifier', () => { - const item = generateMetastore(schema_id) - createMetastore(schema_id, item).then((response) => { - cy.request(getMetastoreGetEndpoint(schema_id, item.identifier) + '?show-reference-ids').then((response) => { + const item = dkan.generateMetastore(schema_id) + dkan.createMetastore(schema_id, item).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, item.identifier) + '?show-reference-ids').then((response) => { expect(response.body.keyword).not.eql(item.keyword) expect(response.body.keyword.length).eql(item.keyword.length) expect(response.body.keyword[0].identifier).to.match(uuid_regex) @@ -240,10 +70,10 @@ context('Metastore', () => { }) }) - metastore_schemas.forEach((schema_id) => { + dkan.metastore_schemas.forEach((schema_id) => { context(`Metastore Item Creation (${schema_id})`, () => { it(`Create a ${schema_id}`, () => { - createMetastore(schema_id).then((response) => { + dkan.createMetastore(schema_id).then((response) => { expect(response.status).eql(201) expect(response.body.identifier).to.match(uuid_regex) }) @@ -252,7 +82,7 @@ context('Metastore', () => { it('Create request fails with an empty payload', () => { cy.request({ method: 'POST', - url: getMetastoreCreateEndpoint(schema_id), + url: dkan.getMetastoreCreateEndpoint(schema_id), auth: user_credentials, body: {}, failOnStatusCode: false @@ -264,7 +94,7 @@ context('Metastore', () => { it('Create request fails with no payload', () => { cy.request({ method: 'POST', - url: getMetastoreCreateEndpoint(schema_id), + url: dkan.getMetastoreCreateEndpoint(schema_id), auth: user_credentials, failOnStatusCode: false }).then((response) => { @@ -276,7 +106,7 @@ context('Metastore', () => { context(`Metastore Item Retrieval (${schema_id})`, () => { it(`GET a non-existent ${schema_id}`, () => { cy.request({ - url: getMetastoreGetEndpoint(schema_id, generateRandomString()), + url: dkan.getMetastoreGetEndpoint(schema_id, dkan.generateRandomString()), failOnStatusCode: false }).then((response) => { expect(response.status).eql(404) @@ -288,7 +118,7 @@ context('Metastore', () => { it('PUT fails with an empty payload', () => { cy.request({ method: 'PUT', - url: getMetastorePutEndpoint(schema_id, generateRandomString()), + url: dkan.getMetastorePutEndpoint(schema_id, dkan.generateRandomString()), auth: user_credentials, body: {}, failOnStatusCode: false @@ -300,7 +130,7 @@ context('Metastore', () => { it('PUT fails with no payload', () => { cy.request({ method: 'PUT', - url: getMetastorePutEndpoint(schema_id, generateRandomString()), + url: dkan.getMetastorePutEndpoint(schema_id, dkan.generateRandomString()), auth: user_credentials, failOnStatusCode: false }).then((response) => { @@ -309,13 +139,13 @@ context('Metastore', () => { }) it('PUT fails to modify the identifier', () => { - createMetastore(schema_id).then((response) => { + dkan.createMetastore(schema_id).then((response) => { expect(response.status).eql(201) cy.request({ method: 'PUT', - url: getMetastorePutEndpoint(schema_id, response.body.identifier), + url: dkan.getMetastorePutEndpoint(schema_id, response.body.identifier), auth: user_credentials, - body: generateMetastore(schema_id), + body: dkan.generateMetastore(schema_id), failOnStatusCode: false }).then((response) => { expect(response.status).eql(409) @@ -324,20 +154,20 @@ context('Metastore', () => { }) it('PUT updates an existing dataset', () => { - createMetastore(schema_id).then((response) => { + dkan.createMetastore(schema_id).then((response) => { expect(response.status).eql(201) - const new_item = generateMetastore(schema_id, response.body.identifier) + const new_item = dkan.generateMetastore(schema_id, response.body.identifier) cy.request({ method: 'PUT', - url: getMetastorePutEndpoint(schema_id, response.body.identifier), + url: dkan.getMetastorePutEndpoint(schema_id, response.body.identifier), auth: user_credentials, body: new_item }).then((response) => { expect(response.status).eql(200) - expect(response.body.endpoint).eql(getMetastoreGetEndpoint(schema_id, new_item.identifier)) + expect(response.body.endpoint).eql(dkan.getMetastoreGetEndpoint(schema_id, new_item.identifier)) expect(response.body.identifier).eql(response.body.identifier) // Verify item. - cy.request(getMetastoreGetEndpoint(schema_id, response.body.identifier)).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, response.body.identifier)).then((response) => { expect(response.status).eql(200) expect(response.body).eql(new_item) }) @@ -346,8 +176,8 @@ context('Metastore', () => { }) it('PUT creates a dataset, if non-existent', () => { - const item = generateMetastore(schema_id) - const endpoint = getMetastorePutEndpoint(schema_id, item.identifier) + const item = dkan.generateMetastore(schema_id) + const endpoint = dkan.getMetastorePutEndpoint(schema_id, item.identifier) cy.request({ method: 'PUT', url: endpoint, @@ -357,7 +187,7 @@ context('Metastore', () => { expect(response.status).eql(201) expect(response.body.endpoint).eql(endpoint) // Verify item. - cy.request(getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { expect(response.status).eql(200) expect(response.body).eql(item) }) @@ -365,18 +195,18 @@ context('Metastore', () => { }) it('PUT fails to modify if data is the same', () => { - const item = generateMetastore(schema_id) - createMetastore(schema_id, item).then((response) => { + const item = dkan.generateMetastore(schema_id) + dkan.createMetastore(schema_id, item).then((response) => { cy.request({ method: 'PUT', - url: getMetastorePutEndpoint(schema_id, item.identifier), + url: dkan.getMetastorePutEndpoint(schema_id, item.identifier), auth: user_credentials, body: item, failOnStatusCode: false }).then((response) => { expect(response.status).eql(403) // Verify data has not been modified. - cy.request(getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { expect(response.status).eql(200) expect(response.body).eql(item) }) @@ -387,12 +217,12 @@ context('Metastore', () => { context(`Metastore Item Replacement (${schema_id})`, () => { it('PATCH fails for non-existent dataset', () => { - const identifier = generateRandomString(schema_id) + const identifier = dkan.generateRandomString(schema_id) cy.request({ method: 'PATCH', - url: getMetastorePatchEndpoint(schema_id, identifier), + url: dkan.getMetastorePatchEndpoint(schema_id, identifier), auth: user_credentials, - body: generateMetastore(schema_id, identifier), + body: dkan.generateMetastore(schema_id, identifier), failOnStatusCode: false }).then((response) => { expect(response.status).eql(412) @@ -400,14 +230,14 @@ context('Metastore', () => { }) it('PATCH fails to modify the identifier', () => { - createMetastore(schema_id).then((response) => { + dkan.createMetastore(schema_id).then((response) => { expect(response.status).eql(201) cy.request({ method: 'PATCH', - url: getMetastorePatchEndpoint(schema_id, response.body.identifier), + url: dkan.getMetastorePatchEndpoint(schema_id, response.body.identifier), auth: user_credentials, body: { - identifier: generateRandomString(schema_id) + identifier: dkan.generateRandomString(schema_id) }, failOnStatusCode: false }).then((response) => { @@ -417,19 +247,19 @@ context('Metastore', () => { }) it('PATCH - empty payload', () => { - const item = generateMetastore(schema_id) - createMetastore(schema_id, item).then((response) => { + const item = dkan.generateMetastore(schema_id) + dkan.createMetastore(schema_id, item).then((response) => { expect(response.status).eql(201) cy.request({ method: 'PATCH', - url: getMetastorePatchEndpoint(schema_id, item.identifier), + url: dkan.getMetastorePatchEndpoint(schema_id, item.identifier), auth: user_credentials, body: {}, failOnStatusCode: false }).then((response) => { expect(response.status).eql(200) // Verify data. - cy.request(getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { expect(response.status).eql(200) expect(response.body).eql(item) }) @@ -438,18 +268,18 @@ context('Metastore', () => { }) it('PATCH - basic case', () => { - createMetastore(schema_id).then((response) => { + dkan.createMetastore(schema_id).then((response) => { expect(response.status).eql(201) - const item = generateMetastore(schema_id, response.body.identifier) + const item = dkan.generateMetastore(schema_id, response.body.identifier) cy.request({ method: 'PATCH', - url: getMetastorePatchEndpoint(schema_id, response.body.identifier), + url: dkan.getMetastorePatchEndpoint(schema_id, response.body.identifier), auth: user_credentials, body: item }).then((response) => { expect(response.status).eql(200) // Verify item. - cy.request(getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { expect(response.status).eql(200) expect(response.body).eql(item) }) @@ -460,16 +290,16 @@ context('Metastore', () => { context(`Metastore Item Deletion (${schema_id})`, () => { it('Delete existing datasets', () => { - createMetastore(schema_id).then((response) => { + dkan.createMetastore(schema_id).then((response) => { const identifier = response.body.identifier cy.request({ method: 'DELETE', - url: getMetastoreDeleteEndpoint(schema_id, identifier), + url: dkan.getMetastoreDeleteEndpoint(schema_id, identifier), auth: user_credentials }).then((response) => { expect(response.status).eql(200) cy.request({ - url: getMetastoreGetEndpoint(schema_id, identifier), + url: dkan.getMetastoreGetEndpoint(schema_id, identifier), failOnStatusCode: false }).then((response) => { expect(response.status).eql(404) @@ -483,16 +313,16 @@ context('Metastore', () => { context('Metastore Item Replacement (dataset; edge cases)', () => { const schema_id = 'dataset' it('PATCH modifies array elements (add, remove, edit)', () => { - const item = generateMetastore(schema_id) - createMetastore(schema_id, item).then((response) => { + const item = dkan.generateMetastore(schema_id) + dkan.createMetastore(schema_id, item).then((response) => { expect(response.status).eql(201) const new_keywords = [ - generateRandomString(), - generateRandomString() + dkan.generateRandomString(), + dkan.generateRandomString() ] cy.request({ method: 'PATCH', - url: getMetastorePatchEndpoint(schema_id, item.identifier), + url: dkan.getMetastorePatchEndpoint(schema_id, item.identifier), auth: user_credentials, body: { keyword: new_keywords @@ -500,7 +330,7 @@ context('Metastore', () => { }).then((response) => { expect(response.status).eql(200) // Verify expected data: added, removed, edited and left unchanged. - cy.request(getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { expect(response.status).eql(200) new_keywords.forEach((keyword) => expect(response.body.keyword).to.contain(keyword)) item.keyword.forEach((keyword) => expect(response.body.keyword).to.not.contain(keyword)) @@ -510,14 +340,14 @@ context('Metastore', () => { }) it('PATCH modifies object properties (add, remove, edit)', () => { - const item = generateMetastore(schema_id) - createMetastore(schema_id, item).then((response) => { + const item = dkan.generateMetastore(schema_id) + dkan.createMetastore(schema_id, item).then((response) => { expect(response.status).eql(201) const new_keywords = [ - generateRandomString(), - generateRandomString() + dkan.generateRandomString(), + dkan.generateRandomString() ] - const endpoint = getMetastorePatchEndpoint(schema_id, item.identifier) + const endpoint = dkan.getMetastorePatchEndpoint(schema_id, item.identifier) cy.request({ method: 'PATCH', url: endpoint, @@ -534,7 +364,7 @@ context('Metastore', () => { expect(response.body.endpoint).eql(endpoint) expect(response.body.identifier).eql(item.identifier) // Verify expected data: added, removed, edited and left unchanged. - cy.request(getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { + cy.request(dkan.getMetastoreGetEndpoint(schema_id, item.identifier)).then((response) => { expect(response.status).eql(200) expect(response.body.contactPoint["fn"]).eql("Contact's name updated by PATCH") expect(response.body.contactPoint["@type"]).to.be.undefined diff --git a/cypress/integration/08_admin_views.spec.js b/cypress/integration/08_admin_views.spec.js index a385311571..1479a4b77b 100644 --- a/cypress/integration/08_admin_views.spec.js +++ b/cypress/integration/08_admin_views.spec.js @@ -41,7 +41,7 @@ context('Admin content and dataset views', () => { cy.get('h1').should('have.text', 'Create Data') }) - it('User can archive, publish, and edit, and delete a dataset. The edit link on the admin view should go to the json form.', () => { + it('User can archive, publish, edit, and delete a dataset. The edit link on the admin view should go to the json form.', () => { // Create a dataset. cy.visit(baseurl + "/node/add/data") cy.wait(2000) diff --git a/cypress/integration/10_workflow_transitions.spec.js b/cypress/integration/10_workflow_transitions.spec.js new file mode 100644 index 0000000000..628eb71523 --- /dev/null +++ b/cypress/integration/10_workflow_transitions.spec.js @@ -0,0 +1,163 @@ +import * as dkan from '../support/helpers/dkan' + +context('Draft datasets', () => { + beforeEach(() => cy.drupalLogin('testeditor', 'testeditor')) + + it('Draft datasets are hidden from the catalog until published.', () => { + // Create draft dataset + const dataset_title = dkan.generateRandomString() + dkan.createDatasetWithModerationState(dataset_title, 'draft') + cy.get('@datasetId').then(datasetId => { + + // Ensure dataset is hidden from catalog + dkan.searchMetastore({fulltext: dataset_title, facets: ''}).then((response) => { + expect(response.status).to.eq(200) + expect(response.body.results).to.be.empty + }) + + // Ensure dataset details are not available when directly visited + cy.request({ + url: '/api/1/metastore/schemas/dataset/items/' + datasetId, + failOnStatusCode: false + }).should((response) => { + expect(response.status).to.eq(404) + expect(response.body.message).to.contain(datasetId + ' not found') + }) + + // Publish the draft dataset + cy.get('@nodeId').then((nodeId) => { + cy.visit('/node/' + nodeId + '/edit') + }) + cy.get('h1.page-title').should('contain', dataset_title) + cy.get('#edit-moderation-state-0-state').select('published') + cy.get('#edit-submit').click() + cy.get('.messages--status').should('contain', 'has been updated') + + // Ensure dataset is visible via public API with correct title + cy.request('/api/1/metastore/schemas/dataset/items/' + datasetId).should((response) => { + expect(response.status).to.eq(200) + expect(response.body.title).to.eq(dataset_title) + }) + + // Ensure dataset is present in catalog + dkan.searchMetastore({fulltext: dataset_title, facets: ''}).then((response) => { + expect(response.status).to.eq(200) + expect(response.body.total).to.eq('1') + }) + }) + }) +}) + +context('Archived datasets', () => { + beforeEach(() => cy.drupalLogin('testeditor', 'testeditor')) + + it('Existing datasets which are archived cannot be visited, and are hidden from the catalog.', () => { + // Create published dataset + const dataset_title = dkan.generateRandomString() + dkan.createDatasetWithModerationState(dataset_title, 'published') + cy.get('@datasetId').then(datasetId => { + + // Ensure dataset is visible via public API with correct title + cy.request('/api/1/metastore/schemas/dataset/items/' + datasetId).should((response) => { + expect(response.status).to.eq(200) + expect(response.body.title).to.eq(dataset_title) + }) + + // Ensure dataset is present in catalog + dkan.searchMetastore({fulltext: dataset_title, facets: ''}).then((response) => { + expect(response.status).to.eq(200) + expect(response.body.total).to.eq('1') + }) + + // Archive the published dataset + cy.get('@nodeId').then((nodeId) => { + cy.visit('/node/' + nodeId + '/edit') + }) + cy.get('h1.page-title').should('contain', dataset_title) + cy.get('#edit-moderation-state-0-state').select('archived') + cy.get('#edit-submit').click() + cy.get('.messages--status').should('contain', 'has been updated') + + // Ensure dataset is hidden from catalog + dkan.searchMetastore({fulltext: dataset_title, facets: ''}).then((response) => { + expect(response.status).to.eq(200) + expect(response.body.results).to.be.empty + }) + + // Ensure dataset details are not available when directly visited + cy.request({ + url: '/api/1/metastore/schemas/dataset/items/' + datasetId, + failOnStatusCode: false + }).should((response) => { + expect(response.status).to.eq(404) + expect(response.body.message).to.contain(datasetId + ' not found') + }) + }) + }) +}) + +context('Hidden datasets', () => { + beforeEach(() => cy.drupalLogin('testeditor', 'testeditor')) + + it('Newly created hidden datasets are visible when visited directly, but hidden from the catalog.', () => { + // create hidden dataset + const dataset_title = dkan.generateRandomString() + dkan.createDatasetWithModerationState(dataset_title, 'hidden') + cy.get('@datasetId').then(datasetId => { + + // Ensure dataset is visible via public API with correct title + cy.request('/api/1/metastore/schemas/dataset/items/' + datasetId).should((response) => { + expect(response.status).to.eq(200) + expect(response.body.title).to.eq(dataset_title) + }) + + // Ensure dataset is hidden from catalog + dkan.searchMetastore({fulltext: dataset_title, facets: ''}).then((response) => { + expect(response.status).to.eq(200) + expect(response.body.results).to.be.empty + }) + }) + }) + + it('Existing datasets which are transitioned to hidden are visible when visited directly, but hidden from the catalog.', () => { + // create published dataset + const dataset_title = dkan.generateRandomString() + // Create a new published dataset in UI and get the resulting UUID + dkan.createDatasetWithModerationState(dataset_title, 'published') + cy.get('@datasetId').then(datasetId => { + + // Ensure dataset is visible via public API with correct title + cy.request('/api/1/metastore/schemas/dataset/items/' + datasetId).should((response) => { + expect(response.status).to.eq(200) + expect(response.body.title).to.eq(dataset_title) + }) + + // Ensure dataset is visible in search + dkan.searchMetastore({fulltext: dataset_title, facets: ''}).then((response) => { + expect(response.status).to.eq(200) + expect(response.body.total).to.eq('1') + }) + + // Set the dataset workflow state to hidden + cy.get('@nodeId').then((nodeId) => { + cy.visit('/node/' + nodeId + '/edit') + }) + cy.get('h1.page-title').should('contain', dataset_title) + cy.get('#edit-moderation-state-0-state').select('hidden') + cy.get('#edit-submit').click() + cy.get('.messages--status').should('contain', 'has been updated') + + // Ensure dataset is now hidden from search + dkan.searchMetastore({fulltext: dataset_title, facets: ''}).then((response) => { + expect(response.status).to.eq(200) + expect(response.body.results).to.be.empty + }) + + // Ensure hidden dataset details are still available via public API + cy.request('/api/1/metastore/schemas/dataset/items/' + datasetId).should((response) => { + expect(response.status).to.eq(200) + expect(response.body.title).to.eq(dataset_title) + }) + }) + }) +}) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index c1a16f4a4b..7663c5fe3f 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -19,6 +19,13 @@ module.exports = (on, config) => { return launchOptions }) - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config + + // Provide log task for debugging. eg cy.task('log', myValue) + on('task', { + log(message) { + console.log(message) + + return null + }, + }) } diff --git a/cypress/support/commands.js b/cypress/support/commands/drupal.js similarity index 74% rename from cypress/support/commands.js rename to cypress/support/commands/drupal.js index 624c05683a..ed3cdd9131 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands/drupal.js @@ -1,7 +1,5 @@ -/** - * Drupal Collection - */ -Cypress.Commands.add("drupalLogin", (user, password) => { +// login as a given drupal user +Cypress.Commands.add('drupalLogin', (user, password) => { return cy.request({ method: 'POST', url: '/user/login', @@ -14,11 +12,13 @@ Cypress.Commands.add("drupalLogin", (user, password) => { }); }); +// logout of drupal Cypress.Commands.add('drupalLogout', () => { return cy.request('/user/logout'); }); -Cypress.Commands.add("drupalDrushCommand", (command) => { +// Run the supplied drush command +Cypress.Commands.add('drupalDrushCommand', (command) => { var cmd = Cypress.env('drupalDrushCmdLine'); if (cmd == null) { diff --git a/cypress/support/helpers/dkan.js b/cypress/support/helpers/dkan.js new file mode 100644 index 0000000000..f06e0e144b --- /dev/null +++ b/cypress/support/helpers/dkan.js @@ -0,0 +1,240 @@ +const api_uri = Cypress.config('apiUri') +const user_credentials = Cypress.env('TEST_USER_CREDENTIALS') + +export const metastore_schemas = [ + 'dataset', + 'publisher', + 'distribution', + 'theme', + 'keyword', + 'data-dictionary', +] + +export function getMetastoreCreateEndpoint (schema_id) { + return `/${api_uri}/metastore/schemas/${schema_id}/items` +} + +export function getMetastoreGetEndpoint (schema_id, identifier) { + return `/${api_uri}/metastore/schemas/${schema_id}/items/${identifier}` +} + +export function getMetastorePutEndpoint (schema_id, identifier) { + return `/${api_uri}/metastore/schemas/${schema_id}/items/${identifier}` +} + +export function getMetastorePatchEndpoint (schema_id, identifier) { + return `/${api_uri}/metastore/schemas/${schema_id}/items/${identifier}` +} + +export function getMetastoreDeleteEndpoint (schema_id, identifier) { + return `/${api_uri}/metastore/schemas/${schema_id}/items/${identifier}` +} + +function getMetastoreSearchEndpoint () { + return `/${api_uri}/search` +} + +// Generate a random uuid. +// Credit: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript +export function generateMetastoreIdentifier () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8) + return v.toString(16) + }) +} + +export function generateRandomString () { + return generateMetastoreIdentifier() +} + +export function generateRandomDateString () { + const start = new Date('1970-01-01T00:00:00.000Z') + const end = new Date() + const date = new Date(+start + Math.random() * (end - start)) + + const year = date.getFullYear().toString() + const month = date.getMonth().toString().padStart(2, '0') + const day = date.getDate().toString().padStart(2, '0') + + return year + '-' + month + '-' + day +} + +// Create a metastore item via API. +export function createMetastore (schema_id, item = null) { + item = item || generateMetastore(schema_id) + // Lookup the proper metastore creation procedure for the given schema ID. + return cy.request({ + method: 'POST', + url: getMetastoreCreateEndpoint(schema_id), + auth: user_credentials, + body: item + }) +} + +// search metastore +export function searchMetastore (params = {}) { + // build url + const param_string = (new URLSearchParams(params)).toString() + const url = getMetastoreSearchEndpoint() + '?' + param_string + // perform request + return cy.request('GET', url) +} + +export function generateMetastore (schema_id, identifier = null) { + // Generate a unique metastore identifier if one was not supplied. + identifier = identifier || generateMetastoreIdentifier() + // Lookup the proper metastore generation procedure for the given schema ID. + const metastore_generator_dictionary = { + "dataset": generateDataset, + "publisher": generatePublisher, + "distribution": generateDistribution, + "theme": generateTheme, + "keyword": generateKeyword, + "data-dictionary": generateDataDictionary, + } + return metastore_generator_dictionary[schema_id](identifier) +} + +// Generate a metastore dataset item object. +export function generateDataset(uuid) { + return { + title: "Title for " + uuid, + description: "Description for " + uuid, + identifier: uuid, + accessLevel: "public", + bureauCode: ["1234:56"], + modified: generateRandomDateString(), + "@type": "dcat:Dataset", + distribution: [ + { + "@type": "dcat:Distribution", + downloadURL: "https://dkan-default-content-files.s3.amazonaws.com/phpunit/district_centerpoints_small.csv", + mediaType: "text/csv", + format: "csv", + description: `

${generateRandomString()}

`, + title: generateRandomString() + } + ], + keyword: [ + generateRandomString(), + generateRandomString(), + generateRandomString() + ], + contactPoint: { + "@type": "vcard:Contact", + fn: generateRandomString() + " " + generateRandomString(), + hasEmail: "mailto:first.last@example.com" + } + } +} + +// Generate a metastore publisher item object. +export function generatePublisher(uuid) { + return { + "identifier": uuid, + "data": { + "@type": "org:Organization", + "name": generateRandomString(), + "subOrganizationOf": generateRandomString() + } + } +} + +export function generateDistribution(uuid) { + return { + "identifier": uuid, + "data": { + "title": "Title for " + uuid, + "description": `

${generateRandomString()}

`, + "format": "csv", + "mediaType": "text/csv", + "downloadURL": "https://dkan-default-content-files.s3.amazonaws.com/phpunit/district_centerpoints_small.csv", + } + } +} + +export function generateTheme(uuid) { + return { + "identifier": uuid, + "data": generateRandomString() + } +} + +export function generateKeyword(uuid) { + return { + "identifier": uuid, + "data": generateRandomString() + } +} + +// Generate a metastore data-dictionary item object. +export function generateDataDictionary(uuid) { + return { + "identifier": uuid, + "title": "Title for " + uuid, + "data": { + "fields": [ + { + "name": generateRandomString(), + "title": generateRandomString(), + "type": "string", + "format": "default" + } + ] + } + } +} + +// create dataset with a given moderation state +export function createDatasetWithModerationState(dataset_title, moderation_state) { + // Create a dataset. + cy.visit('/node/add/data') + cy.get('#edit-field-json-metadata-0-value-title') + .type(dataset_title, { force:true } ) + cy.get('#edit-field-json-metadata-0-value-description') + .type('DKANTEST dataset description.', { force:true } ) + cy.get('#edit-field-json-metadata-0-value-accesslevel') + .select('public', { force:true } ) + cy.get('#edit-field-json-metadata-0-value-modified-date') + .type('2020-02-02', { force:true } ) + // Fill select2 field for publisher. + cy.get('#edit-field-json-metadata-0-value-publisher-publisher-name + .select2') + .find('.select2-selection') + .click({ force:true }) + cy.get('input[aria-controls="select2-edit-field-json-metadata-0-value-publisher-publisher-name-results"]') + .type('DKANTEST Publisher{enter}') + // End filling up publisher. + cy.get('#edit-field-json-metadata-0-value-contactpoint-contactpoint-fn') + .type('DKANTEST Contact Name', { force:true } ) + cy.get('#edit-field-json-metadata-0-value-contactpoint-contactpoint-hasemail') + .type('dkantest@test.com', { force:true } ) + // Fill select2 field for keyword. + cy.get('#edit-field-json-metadata-0-value-keyword-keyword-0 + .select2') + .find('.select2-selection') + .click({ force: true }) + cy.get('input[aria-controls="select2-edit-field-json-metadata-0-value-keyword-keyword-0-results"]') + .type('open data{enter}') + cy.get('#edit-moderation-state-0-state') + .select(moderation_state, { force:true } ) + // End filling up keyword. + cy.get('#edit-submit') + .click({ force:true }) + cy.get('.messages--status') + .should('contain','has been created') + cy.get('.messages--status a').click() + // Visit the new dataset and retrieve the identifier. + // Alias the resulting dataset identifier so it can be retrieved + // in the test. + cy.get('.field--name-field-json-metadata .field__item').invoke('text') + .then((text) => { + const regexp = /"identifier":"([a-zA-Z0-9\-]+?)",/g + const result = regexp.exec(text) + cy.wrap(result[1]).as('datasetId') + }) + // Let's capture the node ID too + cy.url().then((url) => { + const regexp = /\/([0-9]+)$/g + const result = regexp.exec(url) + cy.wrap(result[1]).as('nodeId') + }) +} diff --git a/cypress/support/index.js b/cypress/support/index.js index d68db96df2..4a68bb7ef2 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -14,7 +14,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import './commands/drupal' // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/modules/common/src/DatasetInfo.php b/modules/common/src/DatasetInfo.php index 82562719bd..60189ee18d 100644 --- a/modules/common/src/DatasetInfo.php +++ b/modules/common/src/DatasetInfo.php @@ -127,7 +127,7 @@ public function gather(string $uuid) : array { $latestRevisionIsDraft = 'draft' === $latest->get('moderation_state')->getString(); $published = $this->storage->getEntityPublishedRevision($uuid); - if ($latestRevisionIsDraft && $published && 'published' === $published->get('moderation_state')->getString()) { + if ($latestRevisionIsDraft && isset($published)) { $info['published_revision'] = $this->getRevisionInfo($published); } diff --git a/modules/datastore/src/Service/ResourcePurger.php b/modules/datastore/src/Service/ResourcePurger.php index 2a7bae9479..a43f97f16d 100644 --- a/modules/datastore/src/Service/ResourcePurger.php +++ b/modules/datastore/src/Service/ResourcePurger.php @@ -316,7 +316,7 @@ private function getResourcesToKeep(string $uuid) : array { private function getRevisionData(string $vid) : array { $revision = $this->storage->getEntityStorage()->loadRevision($vid); return [ - $revision->get('moderation_state')->getString() == 'published', + $revision->status->value ?? FALSE, $this->getResources($revision), ]; } @@ -334,7 +334,7 @@ private function getRevisionData(string $vid) : array { private function getResources(NodeInterface $dataset) : array { $resources = []; $metadata = json_decode($dataset->get('field_json_metadata')->getString()); - $distributions = $metadata->{'%Ref:distribution'}; + $distributions = $metadata->{'%Ref:distribution'} ?? []; foreach ($distributions as $distribution) { // Retrieve and validate the resource for this distribution before adding diff --git a/modules/datastore/tests/src/Unit/Controller/MockStorage.php b/modules/datastore/tests/src/Unit/Controller/MockStorage.php index 6f09e12957..580a8f9937 100644 --- a/modules/datastore/tests/src/Unit/Controller/MockStorage.php +++ b/modules/datastore/tests/src/Unit/Controller/MockStorage.php @@ -18,11 +18,7 @@ public function retrieveByHash($hash, $schemaId) { return []; } - public function retrievePublished(string $uuid) : ?string { - throw new MissingObjectException("Error retrieving published dataset: distribution {$uuid} not found."); - } - - public function retrieve(string $uuid) : ?string { + public function retrieve(string $uuid, bool $published = FALSE) : ?string { throw new MissingObjectException("Error retrieving published dataset: distribution {$uuid} not found."); } diff --git a/modules/metastore/config/install/workflows.workflow.dkan_publishing.yml b/modules/metastore/config/install/workflows.workflow.dkan_publishing.yml index 522c26da53..8f5c12c7ff 100644 --- a/modules/metastore/config/install/workflows.workflow.dkan_publishing.yml +++ b/modules/metastore/config/install/workflows.workflow.dkan_publishing.yml @@ -1,4 +1,4 @@ -uuid: c6063248-bbd1-4f27-bdd6-4bb7f60a50e3 +uuid: f13dbb18-99d0-41b2-b046-061b9dd5992d langcode: en status: true dependencies: @@ -14,9 +14,9 @@ type: content_moderation type_settings: states: archived: + label: Archived published: false default_revision: true - label: Archived weight: 3 draft: label: Draft @@ -28,6 +28,11 @@ type_settings: default_revision: true label: Orphaned weight: 2 + hidden: + published: true + default_revision: true + label: 'Published (hidden)' + weight: 4 published: label: Published published: true @@ -37,6 +42,7 @@ type_settings: archive: label: Archive from: + - hidden - published to: archived weight: 3 @@ -46,11 +52,13 @@ type_settings: weight: 0 from: - draft + - hidden - published orphan: label: Orphan from: - draft + - hidden - published to: orphaned weight: 2 @@ -60,7 +68,16 @@ type_settings: weight: 1 from: - draft + - hidden + - published + hidden: + label: 'Remove from search indexing' + from: + - draft + - hidden - published + to: hidden + weight: 5 restore: label: Restore from: diff --git a/modules/metastore/metastore.install b/modules/metastore/metastore.install index c311fdaeae..6a064573be 100644 --- a/modules/metastore/metastore.install +++ b/modules/metastore/metastore.install @@ -60,3 +60,49 @@ function metastore_update_8004() { drupal_flush_all_caches(); } + +/** + * Add hidden state and transitions to dkan publishing workflow config. + */ +function metastore_update_8005() { + $workflow_settings = \Drupal::service('config.factory')->getEditable('workflows.workflow.dkan_publishing'); + + $states = $workflow_settings->get('type_settings.states'); + $states['hidden'] = [ + 'label' => 'Published (hidden)', + 'published' => TRUE, + 'default_revision' => TRUE, + 'weight' => 4 + ]; + + $transitions = $workflow_settings->get('type_settings.transitions'); + $hidable = ['archive', 'create_new_draft', 'orphan', 'publish']; + foreach ($hidable as $state) { + $transitions[$state]['from'][] = 'hidden'; + } + $transitions['hidden'] = [ + 'label' => 'Remove from search indexing', + 'from' => [ + 'draft', + 'hidden', + 'published', + ], + 'to' => 'hidden', + 'weight' => 5, + ]; + + $workflow_settings->set('type_settings.states', $states); + $workflow_settings->set('type_settings.transitions', $transitions); + $workflow_settings->save(); +} + +/** + * Add hidden and unpublished filters to the dkan search_api index. + */ +function metastore_update_8006() { + $index_settings = \Drupal::service('config.factory')->getEditable('search_api.index.dkan'); + + $index_settings->set('processor_settings.dkan_dataset_filter_hidden', []); + $index_settings->set('processor_settings.dkan_dataset_filter_unpublished', []); + $index_settings->save(); +} diff --git a/modules/metastore/modules/metastore_search/config/install/search_api.index.dkan.yml b/modules/metastore/modules/metastore_search/config/install/search_api.index.dkan.yml index ccf11e0a07..fa887572b3 100644 --- a/modules/metastore/modules/metastore_search/config/install/search_api.index.dkan.yml +++ b/modules/metastore/modules/metastore_search/config/install/search_api.index.dkan.yml @@ -46,6 +46,8 @@ datasource_settings: processor_settings: add_url: { } aggregated_field: { } + dkan_dataset_filter_hidden: { } + dkan_dataset_filter_unpublished: { } ignorecase: all_fields: false fields: diff --git a/modules/metastore/modules/metastore_search/metastore_search.module b/modules/metastore/modules/metastore_search/metastore_search.module index 646f7efcff..ce48ebdbe4 100644 --- a/modules/metastore/modules/metastore_search/metastore_search.module +++ b/modules/metastore/modules/metastore_search/metastore_search.module @@ -2,57 +2,74 @@ use Drupal\Core\Entity\EntityInterface; +use Drupal\common\Exception\DataNodeLifeCycleEntityValidationException; +use Drupal\metastore\NodeWrapper\Data; + /** - * Manage item tracking of search api items. + * Determine whether the supplied entity is a dataset. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity in question. + * + * @return bool + * Whether the entity is a dataset. */ -function metastore_search_search_api_tracking(EntityInterface $entity, string $operation) { - if ($entity->getEntityTypeId() != 'node') { - return; - } - - if ($entity->bundle() != 'data') { - return; - } - - $wrapper = new Drupal\metastore\NodeWrapper\Data($entity); - if ($wrapper->getDataType() != 'dataset') { - return; +function _metastore_search_is_dataset(EntityInterface $entity): bool { + try { + // Attempt to check this entity's data type. + return (new Data($entity))->getDataType() === 'dataset'; } - - $acceptableOperation = ['Inserted', 'Updated', 'Deleted']; - - if (!in_array($operation, $acceptableOperation)) { - return; + catch (DataNodeLifeCycleEntityValidationException $e) { + // If the data object fails validation, the given entity is not a dataset. + return FALSE; } - - $method = "trackItems{$operation}"; - - $storage = \Drupal::service('entity_type.manager') - ->getStorage('search_api_index'); - - /* @var $index \Drupal\search_api\Entity\Index */ - $index = $storage->load('dkan'); - - $index->{$method}('dkan_dataset', [$entity->uuid()]); } /** * Implements hook_entity_insert(). + * + * Track created datasets in the DKAN search api index. */ -function metastore_search_entity_insert(EntityInterface $entity) { - metastore_search_search_api_tracking($entity, 'Inserted'); +function metastore_search_entity_insert(EntityInterface $entity): void { + // Ensure the entity in question is a dataset and is not hidden or + // unpublished. + if (!_metastore_search_is_dataset($entity)) { + return; + } + + $storage = \Drupal::service('entity_type.manager')->getStorage('search_api_index'); + $index = $storage->load('dkan'); + $index->trackItemsInserted('dkan_dataset', [$entity->uuid()]); } /** * Implements hook_entity_update(). + * + * Track updated datasets in the DKAN search api index. */ -function metastore_search_entity_update(EntityInterface $entity) { - metastore_search_search_api_tracking($entity, 'Updated'); +function metastore_search_entity_update(EntityInterface $entity): void { + // Ensure the entity in question is a dataset. + if (!_metastore_search_is_dataset($entity)) { + return; + } + + $storage = \Drupal::service('entity_type.manager')->getStorage('search_api_index'); + $index = $storage->load('dkan'); + $index->trackItemsUpdated('dkan_dataset', [$entity->uuid()]); } /** * Implements hook_entity_delete(). + * + * Track deleted datasets in the DKAN search api index. */ -function metastore_search_entity_delete(EntityInterface $entity) { - metastore_search_search_api_tracking($entity, 'Deleted'); +function metastore_search_entity_delete(EntityInterface $entity): void { + // Ensure the entity in question is a dataset. + if (!_metastore_search_is_dataset($entity)) { + return; + } + + $storage = \Drupal::service('entity_type.manager')->getStorage('search_api_index'); + $index = $storage->load('dkan'); + $index->trackItemsDeleted('dkan_dataset', [$entity->uuid()]); } diff --git a/modules/metastore/modules/metastore_search/src/Plugin/search_api/DkanDatasetFilterProcessorBase.php b/modules/metastore/modules/metastore_search/src/Plugin/search_api/DkanDatasetFilterProcessorBase.php new file mode 100644 index 0000000000..d9a5f6e0a3 --- /dev/null +++ b/modules/metastore/modules/metastore_search/src/Plugin/search_api/DkanDatasetFilterProcessorBase.php @@ -0,0 +1,81 @@ +dataStorage = $configuration['dkan_data_storage']; + unset($configuration['dkan_data_storage']); + + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $dataStorageFactory = $container->get('dkan.metastore.storage'); + $configuration['dkan_data_storage'] = $dataStorageFactory->getInstance('dataset'); + + return parent::create($container, $configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function supportsIndex(IndexInterface $index) { + foreach ($index->getDatasources() as $datasource) { + $datasource_id = $datasource->getPluginId(); + // We only support indexes with the dkan_dataset datasource. + if ($datasource_id === 'dkan_dataset') { + return TRUE; + } + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function alterIndexedItems(array &$items) { + foreach (array_keys($items) as $item_id) { + // Retrieve item object. + $item_object = $items[$item_id]->getOriginalObject(); + // Extract dataset ID. + $id_parts = Utility::splitCombinedId($item_id); + $dataset_id = $id_parts[1]; + + // Filter out invalid datasets. + if ($item_object instanceof Dataset && !$this->isValid($dataset_id)) { + unset($items[$item_id]); + } + } + } + + /** + * {@inheritdoc} + */ + abstract public function isValid(string $dataset_id): bool; + +} diff --git a/modules/metastore/modules/metastore_search/src/Plugin/search_api/DkanDatasetFilterProcessorInterface.php b/modules/metastore/modules/metastore_search/src/Plugin/search_api/DkanDatasetFilterProcessorInterface.php new file mode 100644 index 0000000000..67f5221c8a --- /dev/null +++ b/modules/metastore/modules/metastore_search/src/Plugin/search_api/DkanDatasetFilterProcessorInterface.php @@ -0,0 +1,23 @@ +accessCheck(FALSE) - ->condition('status', 1) ->condition('type', 'data') ->condition('field_data_type', 'dataset'); - $total = $query->count()->execute(); - $pages = floor($total / $pageSize); - - if ($page <= $pages) { - - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('status', 1) - ->condition('type', 'data') - ->condition('field_data_type', 'dataset') - ->range($page * $pageSize, $pageSize); - $nids = $query->execute(); + if (isset($page)) { + $ids_query->range($page * self::PAGE_SIZE, self::PAGE_SIZE); + } - foreach ($nids as $id) { - $node = Node::load($id); - $ids[] = $node->uuid(); - } + $uuids = array_map(function ($node) { + return $node->uuid(); + }, Node::loadMultiple($ids_query->execute())); - return $ids; - } - return NULL; + return $uuids ?: NULL; } /** @@ -74,18 +66,12 @@ public function getItemIds($page = NULL) { public function loadMultiple(array $ids) { /* @var \Drupal\metastore\Storage\DataFactory $dataStorageFactory */ $dataStorageFactory = \Drupal::service("dkan.metastore.storage"); - /* @var \Drupal\metastore\Storage\Data $dataStorage */ $dataStorage = $dataStorageFactory->getInstance('dataset'); - $items = []; - foreach ($ids as $id) { - try { - $items[$id] = new Dataset($dataStorage->retrievePublished($id)); - } - catch (\Exception $e) { - } - } + $items = array_map(function ($id) use ($dataStorage) { + return new Dataset($dataStorage->retrieve($id)); + }, array_combine($ids, $ids)); return $items; } diff --git a/modules/metastore/modules/metastore_search/src/Plugin/search_api/processor/DkanDatasetFilterHidden.php b/modules/metastore/modules/metastore_search/src/Plugin/search_api/processor/DkanDatasetFilterHidden.php new file mode 100644 index 0000000000..8ced457686 --- /dev/null +++ b/modules/metastore/modules/metastore_search/src/Plugin/search_api/processor/DkanDatasetFilterHidden.php @@ -0,0 +1,28 @@ +dataStorage->isHidden($dataset_id); + } + +} diff --git a/modules/metastore/modules/metastore_search/src/Plugin/search_api/processor/DkanDatasetFilterUnpublished.php b/modules/metastore/modules/metastore_search/src/Plugin/search_api/processor/DkanDatasetFilterUnpublished.php new file mode 100644 index 0000000000..f49037b660 --- /dev/null +++ b/modules/metastore/modules/metastore_search/src/Plugin/search_api/processor/DkanDatasetFilterUnpublished.php @@ -0,0 +1,28 @@ +dataStorage->isPublished($dataset_id); + } + +} diff --git a/modules/metastore/modules/metastore_search/tests/src/Unit/Plugin/search_api/DkanDatasetFilterProcessorBaseTest.php b/modules/metastore/modules/metastore_search/tests/src/Unit/Plugin/search_api/DkanDatasetFilterProcessorBaseTest.php new file mode 100644 index 0000000000..2599488cb8 --- /dev/null +++ b/modules/metastore/modules/metastore_search/tests/src/Unit/Plugin/search_api/DkanDatasetFilterProcessorBaseTest.php @@ -0,0 +1,43 @@ +add(DatasourceInterface::class, 'getPluginId', 'dkan_dataset') + ->getMock(); + $index_with_dkan_datasource = (new Chain($this)) + ->add(IndexInterface::class, 'getDatasources', [$dkan_datasource]) + ->getMock(); + $this->assertTrue(DkanDatasetFilterProcessorBase::supportsIndex($index_with_dkan_datasource)); + + // Test filter processor doesn't support indexes without dkan datasources. + $non_dkan_datasource = (new Chain($this)) + ->add(DatasourceInterface::class, 'getPluginId', 'non_dkan_dataset') + ->getMock(); + $index_without_dkan_datasource = (new Chain($this)) + ->add(IndexInterface::class, 'getDatasources', [$non_dkan_datasource]) + ->getMock(); + $this->assertFalse(DkanDatasetFilterProcessorBase::supportsIndex($index_without_dkan_datasource)); + } + +} diff --git a/modules/metastore/modules/metastore_search/tests/src/Unit/Plugin/search_api/datasource/DkanDatasetTest.php b/modules/metastore/modules/metastore_search/tests/src/Unit/Plugin/search_api/datasource/DkanDatasetTest.php index b5a686cf65..ff469d8b90 100644 --- a/modules/metastore/modules/metastore_search/tests/src/Unit/Plugin/search_api/datasource/DkanDatasetTest.php +++ b/modules/metastore/modules/metastore_search/tests/src/Unit/Plugin/search_api/datasource/DkanDatasetTest.php @@ -12,7 +12,6 @@ use Drupal\node\NodeInterface; use MockChain\Chain; use MockChain\Options; -use MockChain\Sequence; use PHPUnit\Framework\TestCase; use Drupal\Core\Entity\EntityTypeRepository; use Drupal\metastore_search\ComplexData\Dataset; @@ -35,22 +34,22 @@ public function test() { ->add('dkan.metastore.storage', DataFactory::class) ->index(0); - $nids = [1, 2]; - $executeSequence = (new Sequence())->add(2)->add($nids); + $nodeMock = (new Chain($this)) + ->add(NodeInterface::class, 'uuid', 'xyz') + ->getMock(); $container = (new Chain($this)) ->add(Container::class, 'get', $containerOptions) ->add(EntityTypeManager::class, 'getStorage', EntityStorageInterface::class) ->add(EntityStorageInterface::class, 'getQuery', QueryInterface::class) - ->add(EntityStorageInterface::class, 'load', NodeInterface::class) - ->add(NodeInterface::class, 'uuid', 'xyz') + ->add(EntityStorageInterface::class, 'loadMultiple', [$nodeMock, $nodeMock]) ->add(QueryInterface::class, 'accessCheck', QueryInterface::class) ->add(QueryInterface::class, 'condition', QueryInterface::class) ->add(QueryInterface::class, 'count', QueryInterface::class) - ->add(QueryInterface::class, 'execute', $executeSequence) + ->add(QueryInterface::class, 'execute', [1, 2]) ->add(QueryInterface::class, 'range', QueryInterface::class) ->add(EntityTypeRepository::class, 'getEntityTypeFromClass', NULL) ->add(DataFactory::class, 'getInstance', Data::class) - ->add(Data::class, 'retrievePublished', '{}') + ->add(Data::class, 'retrieve', '{}') ->getMock(); \Drupal::setContainer($container); diff --git a/modules/metastore/src/Controller/MetastoreController.php b/modules/metastore/src/Controller/MetastoreController.php index e0f1e93d98..0c775e98e0 100644 --- a/modules/metastore/src/Controller/MetastoreController.php +++ b/modules/metastore/src/Controller/MetastoreController.php @@ -122,6 +122,9 @@ public function getAll(string $schema_id, Request $request) { * * @return \Symfony\Component\HttpFoundation\JsonResponse * The json response. + * + * @throws \InvalidArgumentException + * When an unpublished or invalid resource is requested. */ public function get(string $schema_id, string $identifier, Request $request) { try { @@ -158,26 +161,6 @@ private function wantObjectWithReferences(Request $request) { return TRUE; } - /** - * GET all resources associated with a dataset. - * - * @param string $schema_id - * The {schema_id} slug from the HTTP request. - * @param string $identifier - * Identifier. - * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * The json response. - */ - public function getResources(string $schema_id, string $identifier) { - try { - return $this->apiResponse->cachedJsonResponse($this->service->getResources($schema_id, $identifier)); - } - catch (\Exception $e) { - return $this->getResponseFromException($e, 404); - } - } - /** * Implements POST method. * diff --git a/modules/metastore/src/LifeCycle/LifeCycle.php b/modules/metastore/src/LifeCycle/LifeCycle.php index 960d475fbd..ca4cefc0dd 100644 --- a/modules/metastore/src/LifeCycle/LifeCycle.php +++ b/modules/metastore/src/LifeCycle/LifeCycle.php @@ -185,12 +185,12 @@ protected function distributionPredelete(MetastoreItemInterface $data) { $resource = $storage->retrieve($distributionUuid); $resource = json_decode($resource); - $id = $resource->data->{'%Ref:downloadURL'}[0]->data->identifier; - $perspective = $resource->data->{'%Ref:downloadURL'}[0]->data->perspective; - $version = $resource->data->{'%Ref:downloadURL'}[0]->data->version; + $id = $resource->data->{'%Ref:downloadURL'}[0]->data->identifier ?? NULL; // Ensure a valid resource ID was found since it's required. if (isset($id)) { + $perspective = $resource->data->{'%Ref:downloadURL'}[0]->data->perspective ?? NULL; + $version = $resource->data->{'%Ref:downloadURL'}[0]->data->version ?? NULL; $this->queueFactory->get('orphan_resource_remover')->createItem([ $id, $perspective, diff --git a/modules/metastore/src/Service.php b/modules/metastore/src/Service.php index 4236c019ec..836e755917 100644 --- a/modules/metastore/src/Service.php +++ b/modules/metastore/src/Service.php @@ -106,7 +106,7 @@ public function getSchema($identifier) { * @return \Drupal\metastore\Storage\MetastoreStorageInterface * Entity storage. */ - private function getStorage(string $schema_id): MetastoreStorageInterface { + protected function getStorage(string $schema_id): MetastoreStorageInterface { if (!isset($this->storages[$schema_id])) { $this->storages[$schema_id] = $this->storageFactory->getInstance($schema_id); } @@ -212,45 +212,37 @@ function ($jsonString) use ($schema_id) { } /** - * Implements GET method. + * Determine whether the given metastore item is published. * * @param string $schema_id - * The {schema_id} slug from the HTTP request. + * The metastore schema in question. * @param string $identifier - * Identifier. + * The ID of the metastore item in question. * - * @return \RootedData\RootedJsonData - * The json data. + * @return bool + * Whether the given metastore item is published. */ - public function get($schema_id, $identifier): RootedJsonData { - $json_string = $this->getStorage($schema_id)->retrievePublished($identifier); - $data = $this->validMetadataFactory->get($json_string, $schema_id); - - $data = $this->dispatchEvent(self::EVENT_DATA_GET, $data); - return $data; + public function isPublished(string $schema_id, string $identifier): bool { + return $this->getStorage($schema_id)->isPublished($identifier); } /** - * GET all resources associated with a dataset. + * Implements GET method. * * @param string $schema_id * The {schema_id} slug from the HTTP request. * @param string $identifier * Identifier. * - * @return array - * An array of resources. - * - * @todo Make this aware of revisions and moderation states. + * @return \RootedData\RootedJsonData + * The json data. */ - public function getResources($schema_id, $identifier): array { - $json_string = $this->getStorage($schema_id)->retrieve($identifier); + public function get(string $schema_id, string $identifier): RootedJsonData { + $json_string = $this->getStorage($schema_id)->retrieve($identifier, TRUE); $data = $this->validMetadataFactory->get($json_string, $schema_id); - /* @todo decouple from POD. */ - $resources = $data->{"$.distribution"}; - - return $resources; + $data = $this->dispatchEvent(self::EVENT_DATA_GET, $data); + return $data; } /** diff --git a/modules/metastore/src/Storage/Data.php b/modules/metastore/src/Storage/Data.php index 32c53fa865..9118a84aed 100644 --- a/modules/metastore/src/Storage/Data.php +++ b/modules/metastore/src/Storage/Data.php @@ -4,7 +4,7 @@ use Drupal\common\LoggerTrait; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Entity\RevisionLogInterface; use Drupal\metastore\Exception\MissingObjectException; @@ -23,7 +23,7 @@ abstract class Data implements MetastoreEntityStorageInterface { /** * Entity type manager. * - * @var \Drupal\Core\Entity\EntityTypeManager + * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; @@ -86,10 +86,10 @@ abstract class Data implements MetastoreEntityStorageInterface { /** * Constructor. */ - public function __construct(string $schemaId, EntityTypeManager $entityTypeManager) { + public function __construct(string $schemaId, EntityTypeManagerInterface $entityTypeManager) { $this->entityTypeManager = $entityTypeManager; $this->entityStorage = $this->entityTypeManager->getStorage($this->entityType); - $this->setSchema($schemaId); + $this->schemaId = $schemaId; } /** @@ -102,13 +102,6 @@ public function getEntityStorage() { return $this->entityStorage; } - /** - * Private. - */ - private function setSchema($schemaId) { - $this->schemaId = $schemaId; - } - /** * Create basic query for a list of metastore items. * @@ -170,14 +163,10 @@ public function retrieveIds(?int $start = NULL, ?int $length = NULL, bool $unpub * * {@inheritdoc}. */ - public function retrievePublished(string $uuid) : ?string { + public function isHidden(string $uuid): bool { $entity = $this->getEntityPublishedRevision($uuid); - if ($entity && $entity->get('moderation_state')->getString() == 'published') { - return $entity->get($this->metadataField)->getString(); - } - - throw new MissingObjectException("Error retrieving published dataset: {$this->schemaId} {$uuid} not found."); + return isset($entity) && ($entity->moderation_state->value ?? NULL) === 'hidden'; } /** @@ -185,20 +174,25 @@ public function retrievePublished(string $uuid) : ?string { * * {@inheritdoc}. */ - public function retrieve(string $uuid) : ?string { + public function isPublished(string $uuid): bool { + $entity = $this->getEntityPublishedRevision($uuid); - if ($this->getDefaultModerationState() === 'published') { - $entity = $this->getEntityPublishedRevision($uuid); - } - else { - $entity = $this->getEntityLatestRevision($uuid); - } + return isset($entity); + } - if ($entity) { - return $entity->get($this->metadataField)->getString(); + /** + * Inherited. + * + * {@inheritdoc}. + */ + public function retrieve(string $uuid, bool $published = FALSE) : ?string { + $entity = $published ? $this->getEntityPublishedRevision($uuid) : $this->getEntityLatestRevision($uuid); + + if (!isset($entity)) { + throw new MissingObjectException("Error retrieving metadata: {$this->schemaId} {$uuid} not found."); } - throw new MissingObjectException("Error retrieving metadata: {$this->schemaId} {$uuid} not found."); + return $entity->get($this->metadataField)->getString(); } /** @@ -249,13 +243,21 @@ protected function setWorkflowState(string $uuid, string $state): bool { * The dataset identifier. * * @return \Drupal\Core\Entity\ContentEntityInterface|null - * The entity's published revision, if found. + * The entity's published revision, if one is found. */ - public function getEntityPublishedRevision(string $uuid) { - + public function getEntityPublishedRevision(string $uuid): ?ContentEntityInterface { $entity_id = $this->getEntityIdFromUuid($uuid); - // @todo extract an actual published revision. - return $entity_id ? $this->entityStorage->load($entity_id) : NULL; + if (!isset($entity_id)) { + return NULL; + } + + $entity = $this->entityStorage->load($entity_id); + $published = $entity->status->value ?? FALSE; + if (!$published) { + return NULL; + } + + return $entity; } /** diff --git a/modules/metastore/src/Storage/MetastoreStorageInterface.php b/modules/metastore/src/Storage/MetastoreStorageInterface.php index acfe5eb98a..a7ca14ed5b 100644 --- a/modules/metastore/src/Storage/MetastoreStorageInterface.php +++ b/modules/metastore/src/Storage/MetastoreStorageInterface.php @@ -19,26 +19,20 @@ interface MetastoreStorageInterface { public function count(bool $unpublished = FALSE): int; /** - * Retrieve a metadata string by ID, regardless of whether it is published. + * Retrieve a metadata string by ID. * * @param string $id * The identifier for the data. + * @param bool $published + * Whether to retrieve the published revision of the metadata. * * @return string|HydratableInterface * The data or null if no data could be retrieved. - */ - public function retrieve(string $id); - - /** - * Retrieve the json metadata from an entity only if it is published. * - * @param string $uuid - * The identifier. - * - * @return string|null - * The entity's json metadata, or NULL if the entity was not found. + * @throws \Drupal\metastore\Exception\MissingObjectException + * When attempting to retrieve metadata fails. */ - public function retrievePublished(string $uuid) : ?string; + public function retrieve(string $id, bool $published = FALSE); /** * Retrieve all metadata items. @@ -87,6 +81,17 @@ public function retrieveIds(?int $start, ?int $length, bool $unpublished): array */ public function retrieveContains(string $string, bool $caseSensitive): array; + /** + * Determine whether the given metastore item is published. + * + * @param string $uuid + * The ID of the metastore item in question. + * + * @return bool + * Whether the given metastore item is published. + */ + public function isPublished(string $uuid) : bool; + /** * Publish the latest version of a data entity. * diff --git a/modules/metastore/src/Storage/NodeData.php b/modules/metastore/src/Storage/NodeData.php index ecd67abae6..d223346bc1 100644 --- a/modules/metastore/src/Storage/NodeData.php +++ b/modules/metastore/src/Storage/NodeData.php @@ -2,7 +2,7 @@ namespace Drupal\metastore\Storage; -use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Entity\EntityTypeManagerInterface; /** * Node Data. @@ -12,7 +12,7 @@ class NodeData extends Data { /** * NodeData constructor. */ - public function __construct(string $schemaId, EntityTypeManager $entityTypeManager) { + public function __construct(string $schemaId, EntityTypeManagerInterface $entityTypeManager) { $this->entityType = 'node'; $this->bundle = 'data'; $this->bundleKey = "type"; diff --git a/modules/metastore/tests/src/Functional/Storage/NodeDataTest.php b/modules/metastore/tests/src/Functional/Storage/NodeDataTest.php index 9852b21d95..f52617d32b 100644 --- a/modules/metastore/tests/src/Functional/Storage/NodeDataTest.php +++ b/modules/metastore/tests/src/Functional/Storage/NodeDataTest.php @@ -63,7 +63,7 @@ public function testStorageRetrieveMethods() { $this->assertEquals("456", $allIds[1]); $this->expectException(MissingObjectException::class); - $datasetStorage->retrievePublished('abc'); + $datasetStorage->retrieve('abc'); } diff --git a/modules/metastore/tests/src/Unit/MetastoreControllerTest.php b/modules/metastore/tests/src/Unit/MetastoreControllerTest.php index e88c7eb99a..76024b99ec 100644 --- a/modules/metastore/tests/src/Unit/MetastoreControllerTest.php +++ b/modules/metastore/tests/src/Unit/MetastoreControllerTest.php @@ -15,6 +15,11 @@ use Drupal\metastore\NodeWrapper\Data as NodeWrapperData; use Drupal\metastore\NodeWrapper\NodeDataFactory; use Drupal\metastore\SchemaRetriever; +use Drupal\metastore\Storage\NodeData; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\Query\QueryInterface; + use MockChain\Chain; use MockChain\Options; use PHPUnit\Framework\TestCase; @@ -87,14 +92,33 @@ public function testGetAllRefs() { * */ public function testGet() { + $schema_id = 'dataset'; + $identifier = 1; + $json = '{"name":"hello"}'; $jsonWithRefs = '{"name": "hello", "%Ref:name": {"identifier": "123", "data": []}}'; - $mockChain = $this->getCommonMockChain(); - $mockChain->add(Service::class, 'get', new RootedJsonData($jsonWithRefs)); + $mockChain = $this->getCommonMockChain() + ->add(Service::class, 'get', new RootedJsonData($jsonWithRefs)); $controller = MetastoreController::create($mockChain->getMock()); - $response = $controller->get('dataset', 1, new Request()); + $response = $controller->get($schema_id, $identifier, new Request()); $this->assertEquals($json, $response->getContent()); + + $entityTypeManagerMock = (new Chain($this)) + ->add(EntityTypeManagerInterface::class, 'getStorage', EntityStorageInterface::class) + ->add(EntityStorageInterface::class, 'getQuery', QueryInterface::class) + ->add(QueryInterface::class, 'accessCheck', QueryInterface::class) + ->add(QueryInterface::class, 'condition', QueryInterface::class) + ->add(QueryInterface::class, 'execute', NULL) + ->getMock(); + $nodeDataMock = new NodeData($schema_id, $entityTypeManagerMock); + $container = $this->getCommonMockChain() + ->add(Service::class, 'getStorage', $nodeDataMock) + ->getMock(); + $controller = MetastoreController::create($container); + $response = $controller->get($schema_id, $identifier, new Request()); + $json = json_decode($response->getContent()); + $this->assertEquals("Error retrieving metadata: {$schema_id} {$identifier} not found.", $json->message); } @@ -138,32 +162,6 @@ public function testGetReferences() { $this->assertEquals('{"name":{"identifier":"123","data":[]}}', $response->getContent()); } - - /** - * - */ - public function testGetResources() { - $mockChain = $this->getCommonMockChain(); - $distributions = [(object) ["title" => "Foo"], (object) ["title" => "Bar"]]; - $mockChain->add(Service::class, 'getResources', $distributions); - - $controller = MetastoreController::create($mockChain->getMock()); - $response = $controller->getResources(1, 'dataset'); - $this->assertEquals(json_encode($distributions), $response->getContent()); - } - - /** - * - */ - public function testGetResourcesException() { - $mockChain = $this->getCommonMockChain(); - $mockChain->add(Service::class, 'getResources', new \Exception("bad")); - - $controller = MetastoreController::create($mockChain->getMock()); - $response = $controller->getResources(1, 'dataset'); - $this->assertStringContainsString('"message":"bad","status":404', $response->getContent()); - } - /** * */ @@ -488,6 +486,7 @@ private function getCommonMockChain() { ->add(Service::class, 'getSchemas', ['dataset']) ->add(Service::class, 'getSchema', (object) ["id" => "http://schema"]) ->add(Service::class, 'getValidMetadataFactory', ValidMetadataFactory::class) + ->add(Service::class, 'isPublished', TRUE) ->add(MetastoreApiResponse::class, 'getMetastoreItemFactory', NodeDataFactory::class) ->add(MetastoreApiResponse::class, 'addReferenceDependencies', NULL) ->add(NodeDataFactory::class, 'getInstance', NodeWrapperData::class) diff --git a/modules/metastore/tests/src/Unit/ServiceTest.php b/modules/metastore/tests/src/Unit/ServiceTest.php index ace3674da5..58a6f2ced4 100644 --- a/modules/metastore/tests/src/Unit/ServiceTest.php +++ b/modules/metastore/tests/src/Unit/ServiceTest.php @@ -12,7 +12,9 @@ use Drupal\metastore\Service; use Drupal\metastore\SchemaRetriever; use Drupal\metastore\Storage\DataFactory; +use Drupal\metastore\Storage\MetastoreStorageInterface; use Drupal\metastore\Storage\NodeData; + use MockChain\Chain; use MockChain\Sequence; use PHPUnit\Framework\TestCase; @@ -36,6 +38,18 @@ protected function setUp(): void { $this->validMetadataFactory = self::getValidMetadataFactory($this); } + /** + * Test \Drupal\metastore\Service::isPublished() method. + */ + public function testIsPublished() { + $service = (new Chain($this)) + ->add(Service::class, 'getStorage', MetastoreStorageInterface::class) + ->add(MetastoreStorageInterface::class, 'isPublished', TRUE) + ->getMock(); + + $this->assertTrue($service->isPublished('dataset', 1)); + } + /** * Get a dataset. */ @@ -43,7 +57,7 @@ public function testGet() { $data = $this->validMetadataFactory->get(json_encode(['foo' => 'bar']), 'dataset'); $container = self::getCommonMockChain($this) - ->add(NodeData::class, "retrievePublished", json_encode(['foo' => 'bar'])) + ->add(NodeData::class, 'retrieve', json_encode(['foo' => 'bar'])) ->add(ValidMetadataFactory::class, 'get', $data); \Drupal::setContainer($container->getMock()); @@ -119,28 +133,6 @@ public function testGetAllException() { ); } - /** - * - */ - public function testGetResources() { - $dataset = [ - "identifier" => "1", - "distribution" => [ - ["title" => "hello"], - ], - ]; - $data = $this->validMetadataFactory->get(json_encode($dataset), 'dataset'); - - $container = self::getCommonMockChain($this) - ->add(Data::class, "retrieve", json_encode($dataset)) - ->add(ValidMetadataFactory::class, 'get', $data); - - $service = Service::create($container->getMock()); - - $this->assertEquals(json_encode([["title" => "hello"]]), - json_encode($service->getResources("dataset", "1"))); - } - /** * */ diff --git a/tests/src/Functional/DatasetTest.php b/tests/src/Functional/DatasetTest.php index 8cb4fb1cd2..adf164e3ef 100644 --- a/tests/src/Functional/DatasetTest.php +++ b/tests/src/Functional/DatasetTest.php @@ -329,6 +329,13 @@ private function getData(string $identifier, string $title, array $downloadUrls) $data->modified = "06-04-2020"; $data->keyword = ["some keyword"]; $data->distribution = []; + $data->publisher = (object) [ + 'name' => 'Test Publisher', + ]; + $data->contactPoint = (object) [ + 'fn' => 'Test Name', + 'hasEmail' => 'test@example.com', + ]; foreach ($downloadUrls as $key => $downloadUrl) { $distribution = new \stdClass();