import 'jest-rdf'; import arrayifyStream from 'arrayify-stream'; import { DataFactory } from 'n3'; import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation'; import type { N3Patch } from '../../../../src/http/representation/N3Patch'; import { N3Patcher } from '../../../../src/storage/patch/N3Patcher'; import type { RepresentationPatcherInput } from '../../../../src/storage/patch/RepresentationPatcher'; import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; const { namedNode, quad, variable } = DataFactory; describe('An N3Patcher', (): void => { let patch: N3Patch; let input: RepresentationPatcherInput; const patcher = new N3Patcher(); beforeEach(async(): Promise => { patch = new BasicRepresentation() as N3Patch; patch.deletes = []; patch.inserts = []; patch.conditions = []; input = { patch, identifier: { path: 'http://example.com/foo' }, }; }); it('can only handle N3 Patches.', async(): Promise => { await expect(patcher.canHandle(input)).resolves.toBeUndefined(); input.patch = new BasicRepresentation() as N3Patch; await expect(patcher.canHandle(input)).rejects.toThrow(NotImplementedHttpError); }); it('returns an empty representation for an empty patch for new resources.', async(): Promise => { patch.deletes = []; patch.inserts = []; patch.conditions = []; const result = await patcher.handle(input); expect(result.metadata.contentType).toBe('internal/quads'); await expect(arrayifyStream(result.data)).resolves.toEqual([]); }); it('returns the input representation for an empty patch.', async(): Promise => { patch.deletes = []; patch.inserts = []; patch.conditions = []; const representation = new BasicRepresentation([], 'internal/quads'); input.representation = representation; const result = await patcher.handle(input); expect(result).toBe(representation); }); it('errors if the input representation has the wrong content-type.', async(): Promise => { // Just need a non-empty patch patch.deletes = [ quad(namedNode('ex:s1'), namedNode('ex:p1'), namedNode('ex:o1')) ]; input.representation = new BasicRepresentation(); await expect(patcher.handle(input)).rejects.toThrow('Quad stream was expected for patching.'); }); it('can delete and insert triples.', async(): Promise => { patch.deletes = [ quad(namedNode('ex:s1'), namedNode('ex:p1'), namedNode('ex:o1')) ]; patch.inserts = [ quad(namedNode('ex:s2'), namedNode('ex:p2'), namedNode('ex:o2')) ]; input.representation = new BasicRepresentation([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), quad(namedNode('ex:s1'), namedNode('ex:p1'), namedNode('ex:o1')), ], 'internal/quads', false); const result = await patcher.handle(input); expect(result.metadata.contentType).toBe('internal/quads'); await expect(arrayifyStream(result.data)).resolves.toBeRdfIsomorphic([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), quad(namedNode('ex:s2'), namedNode('ex:p2'), namedNode('ex:o2')), ]); }); it('can create new representations using insert.', async(): Promise => { patch.inserts = [ quad(namedNode('ex:s2'), namedNode('ex:p2'), namedNode('ex:o2')) ]; const result = await patcher.handle(input); expect(result.metadata.contentType).toBe('internal/quads'); await expect(arrayifyStream(result.data)).resolves.toBeRdfIsomorphic([ quad(namedNode('ex:s2'), namedNode('ex:p2'), namedNode('ex:o2')), ]); }); it('can use conditions to target specific triples.', async(): Promise => { patch.conditions = [ quad(variable('v'), namedNode('ex:p1'), namedNode('ex:o1')) ]; patch.deletes = [ quad(variable('v'), namedNode('ex:p1'), namedNode('ex:o1')) ]; patch.inserts = [ quad(variable('v'), namedNode('ex:p2'), namedNode('ex:o2')) ]; input.representation = new BasicRepresentation([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), quad(namedNode('ex:s1'), namedNode('ex:p1'), namedNode('ex:o1')), ], 'internal/quads', false); const result = await patcher.handle(input); expect(result.metadata.contentType).toBe('internal/quads'); await expect(arrayifyStream(result.data)).resolves.toBeRdfIsomorphic([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), quad(namedNode('ex:s1'), namedNode('ex:p2'), namedNode('ex:o2')), ]); }); it('errors if the conditions find no match.', async(): Promise => { patch.conditions = [ quad(variable('v'), namedNode('ex:p3'), namedNode('ex:o3')) ]; input.representation = new BasicRepresentation([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), quad(namedNode('ex:s1'), namedNode('ex:p1'), namedNode('ex:o1')), ], 'internal/quads', false); const prom = patcher.handle(input); await expect(prom).rejects.toThrow(ConflictHttpError); await expect(prom).rejects.toThrow( 'The document does not contain any matches for the N3 Patch solid:where condition.', ); }); it('errors if the conditions find multiple matches.', async(): Promise => { patch.conditions = [ quad(variable('v'), namedNode('ex:p0'), namedNode('ex:o0')) ]; input.representation = new BasicRepresentation([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), quad(namedNode('ex:s1'), namedNode('ex:p0'), namedNode('ex:o0')), ], 'internal/quads', false); const prom = patcher.handle(input); await expect(prom).rejects.toThrow(ConflictHttpError); await expect(prom).rejects.toThrow( 'The document contains multiple matches for the N3 Patch solid:where condition, which is not allowed.', ); }); it('errors if the delete triples have no match.', async(): Promise => { patch.deletes = [ quad(namedNode('ex:s1'), namedNode('ex:p1'), namedNode('ex:o1')) ]; input.representation = new BasicRepresentation([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), ], 'internal/quads', false); const prom = patcher.handle(input); await expect(prom).rejects.toThrow(ConflictHttpError); await expect(prom).rejects.toThrow( 'The document does not contain all triples the N3 Patch requests to delete, which is required for patching.', ); }); it('works correctly if there are duplicate delete triples.', async(): Promise => { patch.conditions = [ quad(variable('v'), namedNode('ex:p1'), namedNode('ex:o1')) ]; patch.deletes = [ quad(variable('v'), namedNode('ex:p1'), namedNode('ex:o1')), quad(namedNode('ex:s1'), namedNode('ex:p1'), namedNode('ex:o1')), ]; input.representation = new BasicRepresentation([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), quad(namedNode('ex:s1'), namedNode('ex:p1'), namedNode('ex:o1')), ], 'internal/quads', false); const result = await patcher.handle(input); expect(result.metadata.contentType).toBe('internal/quads'); await expect(arrayifyStream(result.data)).resolves.toBeRdfIsomorphic([ quad(namedNode('ex:s0'), namedNode('ex:p0'), namedNode('ex:o0')), ]); }); });