diff --git a/src/storage/ShapeValidationStore.ts b/src/storage/ShapeValidationStore.ts index 96f4ece..46f7297 100644 --- a/src/storage/ShapeValidationStore.ts +++ b/src/storage/ShapeValidationStore.ts @@ -105,7 +105,6 @@ export class ShapeValidationStore extends PassthroughStore { const createdIdentifiers = Array.from(filter(updatedResources.keys(), (id: ResourceIdentifier) => id.path !== topIdentifier.path)); const sortedIdentifiers = createdIdentifiers.sort((a: ResourceIdentifier, b: ResourceIdentifier) => b.path.length - a.path.length); for (const sortedIdentifier of sortedIdentifiers) { - console.log(sortedIdentifier) await this.source.deleteResource(sortedIdentifier); } throw new BadRequestHttpError("Not allowed to create new containers within a constrained container"); diff --git a/src/storage/validators/ShaclValidator.ts b/src/storage/validators/ShaclValidator.ts index 76acda3..af2297e 100644 --- a/src/storage/validators/ShaclValidator.ts +++ b/src/storage/validators/ShaclValidator.ts @@ -1,20 +1,19 @@ -import type { AuxiliaryStrategy, RepresentationConverter } from '@solid/community-server'; +import type {AuxiliaryStrategy, RepresentationConverter} from '@solid/community-server'; import { BadRequestHttpError, + BasicRepresentation, cloneRepresentation, fetchDataset, getLoggerFor, - InternalServerError, INTERNAL_QUADS, NotImplementedHttpError, - readableToQuads, - BasicRepresentation + readableToQuads } from '@solid/community-server'; -import type { Store } from 'n3'; +import type {Store} from 'n3'; import SHACLValidator from 'rdf-validate-shacl'; -import { LDP, SH } from '../../util/Vocabularies'; -import type { ShapeValidatorInput } from './ShapeValidator'; -import { ShapeValidator } from './ShapeValidator'; +import {LDP, SH} from '../../util/Vocabularies'; +import type {ShapeValidatorInput} from './ShapeValidator'; +import {ShapeValidator} from './ShapeValidator'; /** * Validates a Representation against SHACL shapes using an external SHACL validator. @@ -39,14 +38,18 @@ export class ShaclValidator extends ShapeValidator { if (!shapeURL) { throw new NotImplementedHttpError('No ldp:constrainedBy predicate.'); } + + if (representation.isEmpty) { + throw new BadRequestHttpError('Data could not be validated as it could not be converted to rdf'); + } } public async handle(input: ShapeValidatorInput): Promise { const { parentRepresentation, representation } = input; - // Check if the parent has ldp:constrainedBy in the metadata const shapeURL = parentRepresentation.metadata.get(LDP.terms.constrainedBy)!.value; - let representationData: BasicRepresentation; + // Convert the RDF representation to a N3.Store + let representationData: BasicRepresentation; const preferences = { type: { [INTERNAL_QUADS]: 1 }}; try { // Creating a new representation as the data might be written later by DataAccessorBasedStore diff --git a/test/integration/LdpHandlerWithoutAuth.test.ts b/test/integration/LdpHandlerWithoutAuth.test.ts index 4e1b413..28fa23b 100644 --- a/test/integration/LdpHandlerWithoutAuth.test.ts +++ b/test/integration/LdpHandlerWithoutAuth.test.ts @@ -1,18 +1,8 @@ -import { createReadStream } from 'fs'; import fetch from 'cross-fetch'; -import type { Quad } from 'n3'; -import { DataFactory, Parser, Store } from 'n3'; -import { joinFilePath, PIM, RDF } from '@solid/community-server'; -import type { App } from '@solid/community-server'; -import { - deleteResource, - expectQuads, - getResource, - patchResource, - postResource, - putResource, -} from '../util/FetchUtil'; -import { getPort } from '../util/Util'; +import {DataFactory} from 'n3'; +import type {App} from '@solid/community-server'; +import {deleteResource, getResource, patchResource, postResource, putResource,} from '../util/FetchUtil'; +import {getPort} from '../util/Util'; import { getDefaultVariables, getPresetConfigPath, @@ -21,7 +11,6 @@ import { instantiateFromConfig, removeFolder, } from './Config'; -const { literal, namedNode, quad } = DataFactory; const port = getPort('LpdHandlerWithoutAuth'); const baseUrl = `http://localhost:${port}/`; @@ -76,10 +65,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC const conformingResource = ` a . "test".`; - it('can validate RDF resources using SHACL shapes.', async(): Promise => { - const shapeURL = `${baseUrl}shape`; - const constrainedContainerURL = `${baseUrl}constrained/`; - + async function initialiseShapeContainer(shapeURL: string, constrainedContainerURL: string): Promise { // PUT shape await putResource(shapeURL, { contentType: 'text/turtle', body: shape }); @@ -97,6 +83,18 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC const constrainedContainerResponse = await getResource(constrainedContainerURL); expect(constrainedContainerResponse.headers.get('link')) .toContain(`<${shapeURL}>; rel="http://www.w3.org/ns/ldp#constrainedBy"`); + } + + async function cleanupShapeContainer(shapeURL: string, constrainedContainerURL: string): Promise { + expect(await deleteResource(constrainedContainerURL)).toBeUndefined(); + expect(await deleteResource(shapeURL)).toBeUndefined(); + } + + it('can validate RDF resources using SHACL shapes.', async(): Promise => { + const shapeURL = `${baseUrl}shape`; + const constrainedContainerURL = `${baseUrl}constrained/`; + + await initialiseShapeContainer(shapeURL, constrainedContainerURL); // POST: Add resource to constrained container which is valid const postResponse = await postResource( @@ -120,42 +118,57 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC // DELETE expect(await deleteResource(postResponse.headers.get('location')!)).toBeUndefined(); - expect(await deleteResource(constrainedContainerURL)).toBeUndefined(); - expect(await deleteResource(shapeURL)).toBeUndefined(); + await cleanupShapeContainer(shapeURL, constrainedContainerURL); }); it('can not validate non-RDF resources using SHACL shapes.', async(): Promise => { const shapeURL = `${baseUrl}shape`; const constrainedContainerURL = `${baseUrl}constrained/`; - // PUT shape - await putResource(shapeURL, { contentType: 'text/turtle', body: shape }); - - // PUT container for constrained resources - await putResource(constrainedContainerURL, { contentType: 'text/turtle' }); - - // PATCH: Add shape constraint to container - await patchResource( - constrainedContainerURL + metaSuffix, - `INSERT DATA { <${constrainedContainerURL}> <${shapeURL}>}`, - 'sparql', - true, - ); - - const constrainedContainerResponse = await getResource(constrainedContainerURL); - expect(constrainedContainerResponse.headers.get('link')) - .toContain(`<${shapeURL}>; rel="http://www.w3.org/ns/ldp#constrainedBy"`); + await initialiseShapeContainer(shapeURL, constrainedContainerURL); // POST non-RDF resource - const response1 = await fetch(constrainedContainerURL, { + const response = await fetch(constrainedContainerURL, { method: 'POST', headers: { 'content-type': 'text/plain' }, body: 'Hello world!', }); - expect(response1.status).toBe(400); + expect(response.status).toBe(400); // DELETE - expect(await deleteResource(constrainedContainerURL)).toBeUndefined(); - expect(await deleteResource(shapeURL)).toBeUndefined(); + await cleanupShapeContainer(shapeURL, constrainedContainerURL); + }); + + it('can not created containers within a constrained container.', async (): Promise => { + const shapeURL = `${baseUrl}shape`; + const constrainedContainerURL = `${baseUrl}constrained/`; + + await initialiseShapeContainer(shapeURL, constrainedContainerURL); + + // new container directly + const newContainerURL = `${constrainedContainerURL}shouldNotBeAllowed/`; + + const containerCreateResponse = await fetch(newContainerURL,{ + method: "PUT" + }); + + expect(containerCreateResponse.status).toBe(400); + const containerGetResponse = await fetch(newContainerURL); + expect(containerGetResponse.status).toBe(404); + + // new container indirectly + const resourceURL = `${newContainerURL}resource`; + + const resourceCreateResponse = await fetch(resourceURL,{ + method: "PUT", + headers: {'content-type':'text/turtle'}, + body: ' .' + }); + expect(resourceCreateResponse.status).toBe(400); + const resourceGetResponse = await fetch(resourceURL); + expect(resourceGetResponse.status).toBe(404); + + // DELETE + await cleanupShapeContainer(shapeURL, constrainedContainerURL); }); }); diff --git a/test/unit/storage/validators/ShaclValidator.test.ts b/test/unit/storage/validators/ShaclValidator.test.ts index 63e5b38..dc19ab2 100644 --- a/test/unit/storage/validators/ShaclValidator.test.ts +++ b/test/unit/storage/validators/ShaclValidator.test.ts @@ -9,7 +9,6 @@ import { ShaclValidator } from '../../../../src/storage/validators/ShaclValidato import type { ShapeValidatorInput } from '../../../../src/storage/validators/ShapeValidator'; import { INTERNAL_QUADS } from '@solid/community-server'; import { BadRequestHttpError } from '@solid/community-server'; -import { InternalServerError } from '@solid/community-server'; import { NotImplementedHttpError } from '@solid/community-server'; import { fetchDataset } from '@solid/community-server'; import { guardedStreamFrom } from '@solid/community-server'; @@ -74,10 +73,6 @@ describe('ShaclValidator', (): void => { jest.clearAllMocks(); }); - it('throws error if the parent container is not constrained by a shape.', async(): Promise => { - input.parentRepresentation = new BasicRepresentation(); - await expect(validator.canHandle(input)).rejects.toThrow(Error); - }); it('does not validate when the parent container is not constrained by a shape.', async(): Promise => { input.parentRepresentation = new BasicRepresentation(); @@ -86,6 +81,13 @@ describe('ShaclValidator', (): void => { expect(fetchDataset).toHaveBeenCalledTimes(0); }); + it('does not validate when the representation is empty.', async(): Promise => { + input.representation = new BasicRepresentation() + await expect(validator.handleSafe(input)).resolves.toBeUndefined(); + expect(converter.handleSafe).toHaveBeenCalledTimes(0); + expect(fetchDataset).toHaveBeenCalledTimes(0); + }); + it('fetches the shape and validates the representation.', async(): Promise => { await expect(validator.handleSafe(input)).resolves.toBeUndefined(); expect(converter.handleSafe).toHaveBeenCalledTimes(1);