import { namedNode, quad } from '@rdfjs/data-model'; import arrayifyStream from 'arrayify-stream'; import { Quad } from 'rdf-js'; import { translate } from 'sparqlalgebrajs'; import streamifyArray from 'streamify-array'; import { SparqlUpdatePatch } from '../../../../src/ldp/http/SparqlUpdatePatch'; import { Lock } from '../../../../src/storage/Lock'; import { SparqlUpdatePatchHandler } from '../../../../src/storage/patch/SparqlUpdatePatchHandler'; import { ResourceLocker } from '../../../../src/storage/ResourceLocker'; import { ResourceStore } from '../../../../src/storage/ResourceStore'; import { CONTENT_TYPE_QUADS, DATA_TYPE_QUAD } from '../../../../src/util/ContentTypes'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; describe('A SparqlUpdatePatchHandler', (): void => { let handler: SparqlUpdatePatchHandler; let locker: ResourceLocker; let lock: Lock; let release: () => Promise; let source: ResourceStore; let startQuads: Quad[]; let order: string[]; beforeEach(async(): Promise => { order = []; startQuads = [ quad( namedNode('http://test.com/startS1'), namedNode('http://test.com/startP1'), namedNode('http://test.com/startO1'), ), quad( namedNode('http://test.com/startS2'), namedNode('http://test.com/startP2'), namedNode('http://test.com/startO2'), ) ]; source = { getRepresentation: jest.fn(async(): Promise => { order.push('getRepresentation'); return { dataType: 'quads', data: streamifyArray([ ...startQuads ]), metadata: null, }; }), setRepresentation: jest.fn(async(): Promise => { order.push('setRepresentation'); }), modifyResource: jest.fn(async(): Promise => { throw new Error('noModify'); }), } as unknown as ResourceStore; release = jest.fn(async(): Promise => order.push('release')); locker = { acquire: jest.fn(async(): Promise => { order.push('acquire'); lock = { release }; return lock; }), }; handler = new SparqlUpdatePatchHandler(source, locker); }); const basicChecks = async(quads: Quad[]): Promise => { expect(source.getRepresentation).toHaveBeenCalledTimes(1); expect(source.getRepresentation).toHaveBeenLastCalledWith( { path: 'path' }, { type: [{ value: CONTENT_TYPE_QUADS, weight: 1 }]}, ); expect(source.setRepresentation).toHaveBeenCalledTimes(1); expect(order).toEqual([ 'acquire', 'getRepresentation', 'setRepresentation', 'release' ]); const setParams = (source.setRepresentation as jest.Mock).mock.calls[0]; expect(setParams[0]).toEqual({ path: 'path' }); expect(setParams[1]).toEqual(expect.objectContaining({ dataType: DATA_TYPE_QUAD, metadata: { raw: [], profiles: [], contentType: CONTENT_TYPE_QUADS }, })); await expect(arrayifyStream(setParams[1].data)).resolves.toBeRdfIsomorphic(quads); }; it('only accepts SPARQL updates.', async(): Promise => { const input = { identifier: { path: 'path' }, patch: { algebra: {}} as SparqlUpdatePatch }; await expect(handler.canHandle(input)).resolves.toBeUndefined(); delete input.patch.algebra; await expect(handler.canHandle(input)).rejects.toThrow(UnsupportedHttpError); }); it('handles INSERT DATA updates.', async(): Promise => { await handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'INSERT DATA { . ' + ' }', { quads: true }, ) } as SparqlUpdatePatch }); await basicChecks(startQuads.concat( [ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')), quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')) ], )); }); it('handles DELETE DATA updates.', async(): Promise => { await handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'DELETE DATA { }', { quads: true }, ) } as SparqlUpdatePatch }); await basicChecks( [ quad(namedNode('http://test.com/startS2'), namedNode('http://test.com/startP2'), namedNode('http://test.com/startO2')) ], ); }); it('handles DELETE WHERE updates with no variables.', async(): Promise => { await handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'DELETE WHERE { }', { quads: true }, ) } as SparqlUpdatePatch }); await basicChecks( [ quad(namedNode('http://test.com/startS2'), namedNode('http://test.com/startP2'), namedNode('http://test.com/startO2')) ], ); }); it('handles DELETE/INSERT updates with empty WHERE.', async(): Promise => { await handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'DELETE { }\n' + 'INSERT { . }\n' + 'WHERE {}', { quads: true }, ) } as SparqlUpdatePatch }); await basicChecks([ quad(namedNode('http://test.com/startS2'), namedNode('http://test.com/startP2'), namedNode('http://test.com/startO2')), quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')), ]); }); it('rejects GRAPH inserts.', async(): Promise => { const handle = handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'INSERT DATA { GRAPH { ' + ' } }', { quads: true }, ) } as SparqlUpdatePatch }); await expect(handle).rejects.toThrow('GRAPH statements are not supported.'); expect(order).toEqual([]); }); it('rejects GRAPH deletes.', async(): Promise => { const handle = handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'DELETE DATA { GRAPH { ' + ' } }', { quads: true }, ) } as SparqlUpdatePatch }); await expect(handle).rejects.toThrow('GRAPH statements are not supported.'); expect(order).toEqual([]); }); it('rejects DELETE/INSERT updates with a non-empty WHERE.', async(): Promise => { const handle = handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'DELETE { }\n' + 'INSERT { . }\n' + 'WHERE { ?s ?p ?o }', { quads: true }, ) } as SparqlUpdatePatch }); await expect(handle).rejects.toThrow('WHERE statements are not supported.'); expect(order).toEqual([]); }); it('rejects DELETE WHERE updates with variables.', async(): Promise => { const handle = handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'DELETE WHERE { ?v }', { quads: true }, ) } as SparqlUpdatePatch }); await expect(handle).rejects.toThrow('WHERE statements are not supported.'); expect(order).toEqual([]); }); it('rejects non-DELETE/INSERT updates.', async(): Promise => { const handle = handler.handle({ identifier: { path: 'path' }, patch: { algebra: translate( 'MOVE DEFAULT TO GRAPH ', { quads: true }, ) } as SparqlUpdatePatch }); await expect(handle).rejects.toThrow('Only DELETE/INSERT SPARQL update operations are supported.'); expect(order).toEqual([]); }); });