import 'jest-rdf'; import type { Term } from '@rdfjs/types'; import { DataFactory } from 'n3'; import type { BodyParserArgs } from '../../../../../src/http/input/body/BodyParser'; import { N3PatchBodyParser } from '../../../../../src/http/input/body/N3PatchBodyParser'; import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata'; import type { HttpRequest } from '../../../../../src/server/HttpRequest'; import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError'; import { UnsupportedMediaTypeHttpError } from '../../../../../src/util/errors/UnsupportedMediaTypeHttpError'; import { guardedStreamFrom } from '../../../../../src/util/StreamUtil'; const { defaultGraph, literal, namedNode, quad, variable } = DataFactory; describe('An N3PatchBodyParser', (): void => { let input: BodyParserArgs; const parser = new N3PatchBodyParser(); beforeEach(async(): Promise => { input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata({ path: 'http://example.com/foo' }, 'text/n3'), }; }); it('can only handle N3 data.', async(): Promise => { input.metadata.contentType = 'text/plain'; await expect(parser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError); input.metadata.contentType = 'text/n3'; await expect(parser.canHandle(input)).resolves.toBeUndefined(); }); it('errors on invalid N3.', async(): Promise => { input.request = guardedStreamFrom([ 'invalid syntax' ]) as HttpRequest; await expect(parser.handle(input)).rejects.toThrow(BadRequestHttpError); }); it('extracts the patch quads from the request.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { ?person ex:givenName "Alex". }; solid:deletes { ?person ex:givenName "Claudia". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; const patch = await parser.handle(input); expect(patch.conditions).toBeRdfIsomorphic([ quad(variable('person'), namedNode('http://www.example.org/terms#familyName'), literal('Garcia')), quad(variable('person'), namedNode('http://www.example.org/terms#nickName'), literal('Garry')), ]); expect(patch.inserts).toBeRdfIsomorphic([ quad(variable('person'), namedNode('http://www.example.org/terms#givenName'), literal('Alex')), ]); expect(patch.deletes).toBeRdfIsomorphic([ quad(variable('person'), namedNode('http://www.example.org/terms#givenName'), literal('Claudia')), ]); }); it('strips the graph from the result quads.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { ?person ex:givenName "Alex". }; solid:deletes { ?person ex:givenName "Claudia". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; const patch = await parser.handle(input); const quads = [ ...patch.deletes, ...patch.inserts, ...patch.conditions ]; const uniqueGraphs = [ ...new Set(quads.map((entry): Term => entry.graph)) ]; expect(uniqueGraphs).toHaveLength(1); expect(uniqueGraphs[0]).toEqualRdfTerm(defaultGraph()); }); it('errors if no solid:InsertDeletePatch is found.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { ?person ex:givenName "Alex". }; solid:deletes { ?person ex:givenName "Claudia". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects.toThrow( 'This patcher only supports N3 Patch documents with exactly 1 solid:InsertDeletePatch entry, but received 0.', ); }); it('errors if multiple solid:InsertDeletePatch entries are found.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:other a solid:InsertDeletePatch. _:rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { ?person ex:givenName "Alex". }; solid:deletes { ?person ex:givenName "Claudia". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects.toThrow( 'This patcher only supports N3 Patch documents with exactly 1 solid:InsertDeletePatch entry, but received 2.', ); }); it('errors if the patch subject is not a blank or named node.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . ?rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { ?person ex:givenName "Alex". }; solid:deletes { ?person ex:givenName "Claudia". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects .toThrow('An N3 Patch subject needs to be a blank or named node.'); }); it('errors if there are multiple where entries.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:where { ?person ex:givenName "Alex". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects .toThrow('An N3 Patch can have at most 1 http://www.w3.org/ns/solid/terms#where.'); }); it('errors if there are multiple delete entries.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:deletes { ex:person ex:familyName "Garcia". }; solid:deletes { ex:person ex:givenName "Alex". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects .toThrow('An N3 Patch can have at most 1 http://www.w3.org/ns/solid/terms#deletes.'); }); it('errors if there are multiple insert entries.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:inserts { ex:person ex:familyName "Garcia". }; solid:inserts { ex:person ex:givenName "Alex". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects .toThrow('An N3 Patch can have at most 1 http://www.w3.org/ns/solid/terms#inserts.'); }); it('errors if there are blank nodes in the delete formula.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { ?person ex:givenName "Alex". }; solid:deletes { _:person ex:givenName "Claudia". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects .toThrow('An N3 Patch delete/insert formula can not contain blank nodes.'); }); it('errors if there are blank nodes in the insert formula.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { _:person ex:givenName "Alex". }; solid:deletes { ?person ex:givenName "Claudia". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects .toThrow('An N3 Patch delete/insert formula can not contain blank nodes.'); }); it('errors if there are unknown variables in the delete formula.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { ?person ex:givenName "Alex". }; solid:deletes { ?person ex:givenName ?name. }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects .toThrow('An N3 Patch delete/insert formula can only contain variables found in the conditions formula.'); }); it('errors if there are unknown variables in the insert formula.', async(): Promise => { const n3 = `@prefix solid: . @prefix ex: . _:rename a solid:InsertDeletePatch; solid:where { ?person ex:familyName "Garcia"; ex:nickName "Garry". }; solid:inserts { ?person ex:givenName ?name. }; solid:deletes { ?person ex:givenName "Claudia". }.`; input.request = guardedStreamFrom([ n3 ]) as HttpRequest; await expect(parser.handle(input)).rejects .toThrow('An N3 Patch delete/insert formula can only contain variables found in the conditions formula.'); }); });