mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Patch containers by recreating Representation from metadata
Also included is a change to the Patching architecture. Patching is now done by RepresentationPatchers that take a Representation as input.
This commit is contained in:
91
test/unit/storage/patch/ContainerPatcher.test.ts
Normal file
91
test/unit/storage/patch/ContainerPatcher.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { DataFactory } from 'n3';
|
||||
import type { Patch } from '../../../../src/ldp/http/Patch';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||
import { ContainerPatcher } from '../../../../src/storage/patch/ContainerPatcher';
|
||||
import type {
|
||||
RepresentationPatcherInput,
|
||||
RepresentationPatcher,
|
||||
} from '../../../../src/storage/patch/RepresentationPatcher';
|
||||
import { SOLID_META } from '../../../../src/util/Vocabularies';
|
||||
const { namedNode, quad } = DataFactory;
|
||||
|
||||
describe('A ContainerPatcher', (): void => {
|
||||
const identifier: ResourceIdentifier = { path: 'http://test.com/foo/' };
|
||||
const patch: Patch = new BasicRepresentation([], 'type/patch');
|
||||
let representation: Representation;
|
||||
let args: RepresentationPatcherInput;
|
||||
const patchResult = new BasicRepresentation([], 'internal/quads');
|
||||
let patcher: jest.Mocked<RepresentationPatcher>;
|
||||
let containerPatcher: ContainerPatcher;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
representation = new BasicRepresentation([], 'internal/quads');
|
||||
args = { patch, identifier, representation };
|
||||
|
||||
patcher = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn().mockResolvedValue(patchResult),
|
||||
} as any;
|
||||
|
||||
containerPatcher = new ContainerPatcher(patcher);
|
||||
});
|
||||
|
||||
it('can only handle container identifiers.', async(): Promise<void> => {
|
||||
args.identifier = { path: 'http://test.com/foo' };
|
||||
await expect(containerPatcher.canHandle(args)).rejects.toThrow('Only containers are supported.');
|
||||
});
|
||||
|
||||
it('checks if the patcher can handle the input if there is no representation.', async(): Promise<void> => {
|
||||
delete args.representation;
|
||||
|
||||
await expect(containerPatcher.canHandle(args)).resolves.toBeUndefined();
|
||||
|
||||
patcher.canHandle.mockRejectedValueOnce(new Error('unsupported patch'));
|
||||
await expect(containerPatcher.canHandle(args)).rejects.toThrow('unsupported patch');
|
||||
});
|
||||
|
||||
it('sends a mock representation with the correct type to the patcher to check support.', async(): Promise<void> => {
|
||||
await expect(containerPatcher.canHandle(args)).resolves.toBeUndefined();
|
||||
expect(patcher.canHandle).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.canHandle.mock.calls[0][0].representation?.metadata.contentType).toBe('internal/quads');
|
||||
});
|
||||
|
||||
it('passes the arguments to the patcher if there is no representation.', async(): Promise<void> => {
|
||||
delete args.representation;
|
||||
await expect(containerPatcher.handle(args)).resolves.toBe(patchResult);
|
||||
expect(patcher.handle).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.handle).toHaveBeenLastCalledWith(args);
|
||||
});
|
||||
|
||||
it('creates a new representation with all generated metadata removed.', async(): Promise<void> => {
|
||||
const triples = [
|
||||
quad(namedNode('a'), namedNode('real'), namedNode('triple')),
|
||||
quad(namedNode('a'), namedNode('generated'), namedNode('triple')),
|
||||
];
|
||||
const metadata = new RepresentationMetadata(identifier);
|
||||
metadata.addQuad(triples[0].subject as any, triples[0].predicate as any, triples[0].object as any);
|
||||
// Make one of the triples generated
|
||||
metadata.addQuad(triples[0].subject as any,
|
||||
triples[0].predicate as any,
|
||||
triples[0].object as any,
|
||||
SOLID_META.terms.ResponseMetadata);
|
||||
args.representation = new BasicRepresentation(triples, metadata);
|
||||
|
||||
await expect(containerPatcher.handle(args)).resolves.toBe(patchResult);
|
||||
expect(patcher.handle).toHaveBeenCalledTimes(1);
|
||||
const callArgs = patcher.handle.mock.calls[0][0];
|
||||
expect(callArgs.identifier).toBe(identifier);
|
||||
expect(callArgs.patch).toBe(patch);
|
||||
// Only content-type metadata
|
||||
expect(callArgs.representation?.metadata.quads()).toHaveLength(1);
|
||||
expect(callArgs.representation?.metadata.contentType).toBe('internal/quads');
|
||||
// Generated data got removed
|
||||
const data = await arrayifyStream(callArgs.representation!.data);
|
||||
expect(data).toHaveLength(1);
|
||||
expect(data[0].predicate.value).toBe('real');
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
import type { Patch } from '../../../../src/ldp/http/Patch';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||
import type {
|
||||
RepresentationConverter,
|
||||
RepresentationConverterArgs,
|
||||
} from '../../../../src/storage/conversion/RepresentationConverter';
|
||||
import { ConvertingPatchHandler } from '../../../../src/storage/patch/ConvertingPatchHandler';
|
||||
import type { PatchHandlerArgs } from '../../../../src/storage/patch/PatchHandler';
|
||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||
|
||||
class SimpleConvertingPatchHandler extends ConvertingPatchHandler {
|
||||
private readonly type: string;
|
||||
|
||||
public constructor(converter: RepresentationConverter, intermediateType: string, defaultType: string) {
|
||||
super(converter, intermediateType, defaultType);
|
||||
this.type = intermediateType;
|
||||
}
|
||||
|
||||
public async patch(input: PatchHandlerArgs, representation?: Representation): Promise<Representation> {
|
||||
return representation ?
|
||||
new BasicRepresentation('patched', representation.metadata) :
|
||||
new BasicRepresentation('patched', input.identifier, this.type);
|
||||
}
|
||||
}
|
||||
|
||||
describe('A ConvertingPatchHandler', (): void => {
|
||||
const intermediateType = 'internal/quads';
|
||||
const defaultType = 'text/turtle';
|
||||
const identifier: ResourceIdentifier = { path: 'http://test.com/foo' };
|
||||
const patch: Patch = new BasicRepresentation([], 'type/patch');
|
||||
const representation: Representation = new BasicRepresentation([], 'application/trig');
|
||||
let source: jest.Mocked<ResourceStore>;
|
||||
let args: PatchHandlerArgs;
|
||||
let converter: jest.Mocked<RepresentationConverter>;
|
||||
let handler: jest.Mocked<SimpleConvertingPatchHandler>;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
converter = {
|
||||
handleSafe: jest.fn(async({ preferences }: RepresentationConverterArgs): Promise<any> =>
|
||||
new BasicRepresentation('converted', Object.keys(preferences.type!)[0])),
|
||||
} as any;
|
||||
|
||||
source = {
|
||||
getRepresentation: jest.fn().mockResolvedValue(representation),
|
||||
setRepresentation: jest.fn(async(id: ResourceIdentifier): Promise<ResourceIdentifier[]> => [ id ]),
|
||||
} as any;
|
||||
|
||||
args = { patch, identifier, source };
|
||||
|
||||
handler = new SimpleConvertingPatchHandler(converter, intermediateType, defaultType) as any;
|
||||
jest.spyOn(handler, 'patch');
|
||||
});
|
||||
|
||||
it('converts the representation before calling the patch function.', async(): Promise<void> => {
|
||||
await expect(handler.handle(args)).resolves.toEqual([ identifier ]);
|
||||
|
||||
// Convert input
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.getRepresentation).toHaveBeenLastCalledWith(identifier, { });
|
||||
expect(converter.handleSafe).toHaveBeenCalledTimes(2);
|
||||
expect(converter.handleSafe).toHaveBeenCalledWith({
|
||||
representation: await source.getRepresentation.mock.results[0].value,
|
||||
identifier,
|
||||
preferences: { type: { [intermediateType]: 1 }},
|
||||
});
|
||||
|
||||
// Patch
|
||||
expect(handler.patch).toHaveBeenCalledTimes(1);
|
||||
expect(handler.patch).toHaveBeenLastCalledWith(args, await converter.handleSafe.mock.results[0].value);
|
||||
|
||||
// Convert back
|
||||
expect(converter.handleSafe).toHaveBeenLastCalledWith({
|
||||
representation: await handler.patch.mock.results[0].value,
|
||||
identifier,
|
||||
preferences: { type: { 'application/trig': 1 }},
|
||||
});
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.setRepresentation)
|
||||
.toHaveBeenLastCalledWith(identifier, await converter.handleSafe.mock.results[1].value);
|
||||
expect(source.setRepresentation.mock.calls[0][1].metadata.contentType).toBe(representation.metadata.contentType);
|
||||
});
|
||||
|
||||
it('expects the patch function to create a new representation if there is none.', async(): Promise<void> => {
|
||||
source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
|
||||
|
||||
await expect(handler.handle(args)).resolves.toEqual([ identifier ]);
|
||||
|
||||
// Try to get input
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.getRepresentation).toHaveBeenLastCalledWith(identifier, { });
|
||||
|
||||
// Patch
|
||||
expect(handler.patch).toHaveBeenCalledTimes(1);
|
||||
expect(handler.patch).toHaveBeenLastCalledWith(args, undefined);
|
||||
|
||||
// Convert new Representation to default type
|
||||
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(converter.handleSafe).toHaveBeenLastCalledWith({
|
||||
representation: await handler.patch.mock.results[0].value,
|
||||
identifier,
|
||||
preferences: { type: { [defaultType]: 1 }},
|
||||
});
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.setRepresentation)
|
||||
.toHaveBeenLastCalledWith(identifier, await converter.handleSafe.mock.results[0].value);
|
||||
expect(source.setRepresentation.mock.calls[0][1].metadata.contentType).toBe(defaultType);
|
||||
});
|
||||
|
||||
it('rethrows the error if something goes wrong getting the representation.', async(): Promise<void> => {
|
||||
const error = new Error('bad data');
|
||||
source.getRepresentation.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(handler.handle(args)).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
129
test/unit/storage/patch/ConvertingPatcher.test.ts
Normal file
129
test/unit/storage/patch/ConvertingPatcher.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { Patch } from '../../../../src/ldp/http/Patch';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||
import type {
|
||||
RepresentationConverter,
|
||||
RepresentationConverterArgs,
|
||||
} from '../../../../src/storage/conversion/RepresentationConverter';
|
||||
import { ConvertingPatcher } from '../../../../src/storage/patch/ConvertingPatcher';
|
||||
import type {
|
||||
RepresentationPatcher,
|
||||
RepresentationPatcherInput,
|
||||
} from '../../../../src/storage/patch/RepresentationPatcher';
|
||||
|
||||
describe('A ConvertingPatcher', (): void => {
|
||||
const intermediateType = 'internal/quads';
|
||||
const defaultType = 'text/turtle';
|
||||
const identifier: ResourceIdentifier = { path: 'http://test.com/foo' };
|
||||
const patch: Patch = new BasicRepresentation([], 'type/patch');
|
||||
const representation = new BasicRepresentation([], 'application/trig');
|
||||
const patchResult = new BasicRepresentation([], 'internal/quads');
|
||||
let args: RepresentationPatcherInput;
|
||||
let converter: jest.Mocked<RepresentationConverter>;
|
||||
let patcher: jest.Mocked<RepresentationPatcher>;
|
||||
let convertingPatcher: ConvertingPatcher;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
args = { patch, identifier, representation };
|
||||
|
||||
converter = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn(async({ preferences }: RepresentationConverterArgs): Promise<any> =>
|
||||
new BasicRepresentation('converted', Object.keys(preferences.type!)[0])),
|
||||
} as any;
|
||||
|
||||
patcher = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn().mockResolvedValue(patchResult),
|
||||
} as any;
|
||||
|
||||
convertingPatcher = new ConvertingPatcher(patcher, converter, intermediateType, defaultType);
|
||||
});
|
||||
|
||||
it('rejects requests the converter cannot handle.', async(): Promise<void> => {
|
||||
converter.canHandle.mockRejectedValueOnce(new Error('unsupported type'));
|
||||
await expect(convertingPatcher.canHandle(args)).rejects.toThrow('unsupported type');
|
||||
});
|
||||
|
||||
it('checks if the patcher can handle the input if there is no representation.', async(): Promise<void> => {
|
||||
delete args.representation;
|
||||
|
||||
await expect(convertingPatcher.canHandle(args)).resolves.toBeUndefined();
|
||||
|
||||
patcher.canHandle.mockRejectedValueOnce(new Error('unsupported patch'));
|
||||
await expect(convertingPatcher.canHandle(args)).rejects.toThrow('unsupported patch');
|
||||
});
|
||||
|
||||
it('sends a mock representation with the correct type to the patcher to check support.', async(): Promise<void> => {
|
||||
await expect(convertingPatcher.canHandle(args)).resolves.toBeUndefined();
|
||||
expect(patcher.canHandle).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.canHandle.mock.calls[0][0].representation?.metadata.contentType).toBe(intermediateType);
|
||||
});
|
||||
|
||||
it('converts the representation before calling the patcher.', async(): Promise<void> => {
|
||||
const result = await convertingPatcher.handle(args);
|
||||
expect(result.metadata.contentType).toBe('application/trig');
|
||||
|
||||
// Convert input
|
||||
expect(converter.handle).toHaveBeenCalledTimes(2);
|
||||
expect(converter.handle).toHaveBeenCalledWith({
|
||||
representation,
|
||||
identifier,
|
||||
preferences: { type: { [intermediateType]: 1 }},
|
||||
});
|
||||
|
||||
// Patch
|
||||
expect(patcher.handle).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.handle)
|
||||
.toHaveBeenLastCalledWith({ ...args, representation: await converter.handle.mock.results[0].value });
|
||||
|
||||
// Convert back
|
||||
expect(converter.handle).toHaveBeenLastCalledWith({
|
||||
representation: patchResult,
|
||||
identifier,
|
||||
preferences: { type: { 'application/trig': 1 }},
|
||||
});
|
||||
});
|
||||
|
||||
it('expects the patcher to create a new representation if there is none.', async(): Promise<void> => {
|
||||
delete args.representation;
|
||||
|
||||
const result = await convertingPatcher.handle(args);
|
||||
expect(result.metadata.contentType).toBe(defaultType);
|
||||
|
||||
// Patch
|
||||
expect(patcher.handle).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.handle).toHaveBeenLastCalledWith(args);
|
||||
|
||||
// Convert new Representation to default type
|
||||
expect(converter.handle).toHaveBeenCalledTimes(1);
|
||||
expect(converter.handle).toHaveBeenLastCalledWith({
|
||||
representation: patchResult,
|
||||
identifier,
|
||||
preferences: { type: { [defaultType]: 1 }},
|
||||
});
|
||||
});
|
||||
|
||||
it('does no conversion if there is no intermediate type.', async(): Promise<void> => {
|
||||
convertingPatcher = new ConvertingPatcher(patcher, converter);
|
||||
const result = await convertingPatcher.handle(args);
|
||||
expect(result.metadata.contentType).toBe(patchResult.metadata.contentType);
|
||||
|
||||
// Patch
|
||||
expect(converter.handle).toHaveBeenCalledTimes(0);
|
||||
expect(patcher.handle).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.handle).toHaveBeenLastCalledWith(args);
|
||||
});
|
||||
|
||||
it('does not convert to a default type if there is none.', async(): Promise<void> => {
|
||||
delete args.representation;
|
||||
convertingPatcher = new ConvertingPatcher(patcher, converter);
|
||||
const result = await convertingPatcher.handle(args);
|
||||
expect(result.metadata.contentType).toBe(patchResult.metadata.contentType);
|
||||
|
||||
// Patch
|
||||
expect(converter.handle).toHaveBeenCalledTimes(0);
|
||||
expect(patcher.handle).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.handle).toHaveBeenLastCalledWith(args);
|
||||
});
|
||||
});
|
||||
63
test/unit/storage/patch/RepresentationPatchHandler.test.ts
Normal file
63
test/unit/storage/patch/RepresentationPatchHandler.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { Patch } from '../../../../src/ldp/http/Patch';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { PatchHandlerInput } from '../../../../src/storage/patch/PatchHandler';
|
||||
import type { RepresentationPatcher } from '../../../../src/storage/patch/RepresentationPatcher';
|
||||
import { RepresentationPatchHandler } from '../../../../src/storage/patch/RepresentationPatchHandler';
|
||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||
|
||||
describe('A RepresentationPatchHandler', (): void => {
|
||||
const identifier = { path: 'http://test.com/foo' };
|
||||
const representation = new BasicRepresentation('', 'text/turtle');
|
||||
const patch: Patch = new BasicRepresentation('', 'application/sparql-update');
|
||||
const patchResult = new BasicRepresentation('', 'application/trig');
|
||||
let input: PatchHandlerInput;
|
||||
let source: jest.Mocked<ResourceStore>;
|
||||
let patcher: jest.Mocked<RepresentationPatcher>;
|
||||
let handler: RepresentationPatchHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
source = {
|
||||
getRepresentation: jest.fn().mockResolvedValue(representation),
|
||||
setRepresentation: jest.fn().mockResolvedValue([ identifier ]),
|
||||
} as any;
|
||||
|
||||
input = { source, identifier, patch };
|
||||
|
||||
patcher = {
|
||||
handleSafe: jest.fn().mockResolvedValue(patchResult),
|
||||
} as any;
|
||||
|
||||
handler = new RepresentationPatchHandler(patcher);
|
||||
});
|
||||
|
||||
it('calls the patcher with the representation from the store.', async(): Promise<void> => {
|
||||
await expect(handler.handle(input)).resolves.toEqual([ identifier ]);
|
||||
|
||||
expect(patcher.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.handleSafe).toHaveBeenLastCalledWith({ identifier, patch, representation });
|
||||
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.setRepresentation).toHaveBeenLastCalledWith(identifier, patchResult);
|
||||
});
|
||||
|
||||
it('calls the patcher with no representation if there is none.', async(): Promise<void> => {
|
||||
source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
|
||||
|
||||
await expect(handler.handle(input)).resolves.toEqual([ identifier ]);
|
||||
|
||||
expect(patcher.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(patcher.handleSafe).toHaveBeenLastCalledWith({ identifier, patch });
|
||||
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.setRepresentation).toHaveBeenLastCalledWith(identifier, patchResult);
|
||||
});
|
||||
|
||||
it('errors if the store throws a non-404 error.', async(): Promise<void> => {
|
||||
const error = new BadRequestHttpError();
|
||||
source.getRepresentation.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(handler.handle(input)).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
@@ -1,249 +0,0 @@
|
||||
import 'jest-rdf';
|
||||
import { namedNode, quad } from '@rdfjs/data-model';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import type { Quad } from 'rdf-js';
|
||||
import { translate } from 'sparqlalgebrajs';
|
||||
import type { SparqlUpdatePatch } from '../../../../src/ldp/http/SparqlUpdatePatch';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { RepresentationConverterArgs,
|
||||
RepresentationConverter } from '../../../../src/storage/conversion/RepresentationConverter';
|
||||
import { SparqlUpdatePatchHandler } from '../../../../src/storage/patch/SparqlUpdatePatchHandler';
|
||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
|
||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
|
||||
|
||||
describe('A SparqlUpdatePatchHandler', (): void => {
|
||||
let converter: jest.Mocked<RepresentationConverter>;
|
||||
let handler: SparqlUpdatePatchHandler;
|
||||
let source: jest.Mocked<ResourceStore>;
|
||||
let startQuads: Quad[];
|
||||
const defaultType = 'internal/not-quads';
|
||||
const identifier = { path: 'http://test.com/foo' };
|
||||
const fulfilledDataInsert = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 . }';
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
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'),
|
||||
) ];
|
||||
|
||||
converter = {
|
||||
handleSafe: jest.fn(async({ representation, preferences }: RepresentationConverterArgs): Promise<any> =>
|
||||
new BasicRepresentation(representation.data, Object.keys(preferences.type!)[0])),
|
||||
} as any;
|
||||
|
||||
source = {
|
||||
getRepresentation: jest.fn(async(): Promise<any> => new BasicRepresentation(startQuads, defaultType)),
|
||||
setRepresentation: jest.fn(),
|
||||
} as any;
|
||||
|
||||
handler = new SparqlUpdatePatchHandler(converter, defaultType);
|
||||
});
|
||||
|
||||
async function basicChecks(quads: Quad[]): Promise<boolean> {
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.getRepresentation).toHaveBeenLastCalledWith(identifier, { });
|
||||
expect(converter.handleSafe).toHaveBeenCalledTimes(2);
|
||||
expect(converter.handleSafe).toHaveBeenCalledWith({
|
||||
representation: await source.getRepresentation.mock.results[0].value,
|
||||
identifier,
|
||||
preferences: { type: { [INTERNAL_QUADS]: 1 }},
|
||||
});
|
||||
expect(converter.handleSafe).toHaveBeenLastCalledWith({
|
||||
representation: expect.objectContaining({ binary: false, metadata: expect.any(RepresentationMetadata) }),
|
||||
identifier,
|
||||
preferences: { type: { [defaultType]: 1 }},
|
||||
});
|
||||
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
const setParams = source.setRepresentation.mock.calls[0];
|
||||
expect(setParams[0]).toEqual(identifier);
|
||||
expect(setParams[1]).toEqual(expect.objectContaining({
|
||||
binary: true,
|
||||
metadata: expect.any(RepresentationMetadata),
|
||||
}));
|
||||
expect(setParams[1].metadata.contentType).toEqual(defaultType);
|
||||
await expect(arrayifyStream(setParams[1].data)).resolves.toBeRdfIsomorphic(quads);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handle(query: string): Promise<void> {
|
||||
const prefixedQuery = `prefix : <http://test.com/>\n${query}`;
|
||||
await handler.handle({
|
||||
source,
|
||||
identifier,
|
||||
patch: {
|
||||
algebra: translate(prefixedQuery, { quads: true }),
|
||||
data: guardedStreamFrom(prefixedQuery),
|
||||
metadata: new RepresentationMetadata(),
|
||||
binary: true,
|
||||
} as SparqlUpdatePatch,
|
||||
});
|
||||
}
|
||||
|
||||
it('only accepts SPARQL updates.', async(): Promise<void> => {
|
||||
const input = { source, identifier, patch: { algebra: {}} as SparqlUpdatePatch };
|
||||
await expect(handler.canHandle(input)).resolves.toBeUndefined();
|
||||
delete (input.patch as any).algebra;
|
||||
await expect(handler.canHandle(input)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('handles NOP operations by not doing anything.', async(): Promise<void> => {
|
||||
await handle('');
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(0);
|
||||
expect(converter.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('handles INSERT DATA updates.', async(): Promise<void> => {
|
||||
await handle(fulfilledDataInsert);
|
||||
expect(await basicChecks([ ...startQuads,
|
||||
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')),
|
||||
])).toBe(true);
|
||||
});
|
||||
|
||||
it('handles DELETE DATA updates.', async(): Promise<void> => {
|
||||
await handle('DELETE DATA { :startS1 :startP1 :startO1 }');
|
||||
expect(await basicChecks(
|
||||
[ quad(namedNode('http://test.com/startS2'),
|
||||
namedNode('http://test.com/startP2'),
|
||||
namedNode('http://test.com/startO2')) ],
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles DELETE WHERE updates with no variables.', async(): Promise<void> => {
|
||||
const query = 'DELETE WHERE { :startS1 :startP1 :startO1 }';
|
||||
await handle(query);
|
||||
expect(await basicChecks(
|
||||
[ quad(namedNode('http://test.com/startS2'),
|
||||
namedNode('http://test.com/startP2'),
|
||||
namedNode('http://test.com/startO2')) ],
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles DELETE WHERE updates with variables.', async(): Promise<void> => {
|
||||
const query = 'DELETE WHERE { :startS1 :startP1 ?o }';
|
||||
await handle(query);
|
||||
expect(await basicChecks(
|
||||
[ quad(namedNode('http://test.com/startS2'),
|
||||
namedNode('http://test.com/startP2'),
|
||||
namedNode('http://test.com/startO2')) ],
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles DELETE/INSERT updates with empty WHERE.', async(): Promise<void> => {
|
||||
const query = 'DELETE { :startS1 :startP1 :startO1 } INSERT { :s1 :p1 :o1 . } WHERE {}';
|
||||
await handle(query);
|
||||
expect(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')),
|
||||
])).toBe(true);
|
||||
});
|
||||
|
||||
it('handles composite INSERT/DELETE updates.', async(): Promise<void> => {
|
||||
const query = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 };' +
|
||||
'DELETE WHERE { :s1 :p1 :o1 . :startS1 :startP1 :startO1 }';
|
||||
await handle(query);
|
||||
expect(await basicChecks([
|
||||
quad(namedNode('http://test.com/startS2'),
|
||||
namedNode('http://test.com/startP2'),
|
||||
namedNode('http://test.com/startO2')),
|
||||
quad(namedNode('http://test.com/s2'),
|
||||
namedNode('http://test.com/p2'),
|
||||
namedNode('http://test.com/o2')),
|
||||
])).toBe(true);
|
||||
});
|
||||
|
||||
it('handles composite DELETE/INSERT updates.', async(): Promise<void> => {
|
||||
const query = 'DELETE DATA { :s1 :p1 :o1 . :startS1 :startP1 :startO1 } ;' +
|
||||
'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 }';
|
||||
await handle(query);
|
||||
expect(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')),
|
||||
quad(namedNode('http://test.com/s2'),
|
||||
namedNode('http://test.com/p2'),
|
||||
namedNode('http://test.com/o2')),
|
||||
])).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects GRAPH inserts.', async(): Promise<void> => {
|
||||
const query = 'INSERT DATA { GRAPH :graph { :s1 :p1 :o1 } }';
|
||||
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('rejects GRAPH deletes.', async(): Promise<void> => {
|
||||
const query = 'DELETE DATA { GRAPH :graph { :s1 :p1 :o1 } }';
|
||||
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('rejects DELETE/INSERT updates with non-BGP WHERE.', async(): Promise<void> => {
|
||||
const query = 'DELETE { :s1 :p1 :o1 } INSERT { :s1 :p1 :o1 } WHERE { ?s ?p ?o. FILTER (?o > 5) }';
|
||||
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('rejects INSERT WHERE updates with a UNION.', async(): Promise<void> => {
|
||||
const query = 'INSERT { :s1 :p1 :o1 . } WHERE { { :s1 :p1 :o1 } UNION { :s1 :p1 :o2 } }';
|
||||
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('rejects non-DELETE/INSERT updates.', async(): Promise<void> => {
|
||||
const query = 'MOVE DEFAULT TO GRAPH :newGraph';
|
||||
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('throws the error returned by the store if there is one.', async(): Promise<void> => {
|
||||
source.getRepresentation.mockRejectedValueOnce(new Error('error'));
|
||||
await expect(handle(fulfilledDataInsert)).rejects.toThrow('error');
|
||||
});
|
||||
|
||||
it('creates a new resource if it does not exist yet.', async(): Promise<void> => {
|
||||
startQuads = [];
|
||||
source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
|
||||
const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
|
||||
await handle(query);
|
||||
|
||||
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(converter.handleSafe).toHaveBeenLastCalledWith({
|
||||
representation: expect.objectContaining({ binary: false, metadata: expect.any(RepresentationMetadata) }),
|
||||
identifier,
|
||||
preferences: { type: { [defaultType]: 1 }},
|
||||
});
|
||||
|
||||
const quads =
|
||||
[ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')) ];
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
const setParams = source.setRepresentation.mock.calls[0];
|
||||
expect(setParams[1].metadata.contentType).toEqual(defaultType);
|
||||
await expect(arrayifyStream(setParams[1].data)).resolves.toBeRdfIsomorphic(quads);
|
||||
});
|
||||
|
||||
it('defaults to text/turtle if no default type was set.', async(): Promise<void> => {
|
||||
handler = new SparqlUpdatePatchHandler(converter);
|
||||
startQuads = [];
|
||||
source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
|
||||
const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
|
||||
await handle(query);
|
||||
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
const setParams = source.setRepresentation.mock.calls[0];
|
||||
expect(setParams[1].metadata.contentType).toEqual('text/turtle');
|
||||
});
|
||||
});
|
||||
228
test/unit/storage/patch/SparqlUpdatePatcher.test.ts
Normal file
228
test/unit/storage/patch/SparqlUpdatePatcher.test.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import 'jest-rdf';
|
||||
import { namedNode, quad } from '@rdfjs/data-model';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import type { Quad } from 'rdf-js';
|
||||
import { translate } from 'sparqlalgebrajs';
|
||||
import type { SparqlUpdatePatch } from '../../../../src/ldp/http/SparqlUpdatePatch';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { RepresentationPatcherInput } from '../../../../src/storage/patch/RepresentationPatcher';
|
||||
import { SparqlUpdatePatcher } from '../../../../src/storage/patch/SparqlUpdatePatcher';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
|
||||
|
||||
function getPatch(query: string): SparqlUpdatePatch {
|
||||
const prefixedQuery = `prefix : <http://test.com/>\n${query}`;
|
||||
return {
|
||||
algebra: translate(prefixedQuery, { quads: true }),
|
||||
data: guardedStreamFrom(prefixedQuery),
|
||||
metadata: new RepresentationMetadata(),
|
||||
binary: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe('A SparqlUpdatePatcher', (): void => {
|
||||
let patcher: SparqlUpdatePatcher;
|
||||
let startQuads: Quad[];
|
||||
let representation: Representation;
|
||||
const identifier = { path: 'http://test.com/foo' };
|
||||
const fulfilledDataInsert = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 . }';
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
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'),
|
||||
) ];
|
||||
|
||||
representation = new BasicRepresentation(startQuads, 'internal/quads');
|
||||
|
||||
patcher = new SparqlUpdatePatcher();
|
||||
});
|
||||
|
||||
it('only accepts SPARQL updates.', async(): Promise<void> => {
|
||||
const input = { identifier, patch: { algebra: {}} as SparqlUpdatePatch };
|
||||
await expect(patcher.canHandle(input)).resolves.toBeUndefined();
|
||||
delete (input.patch as any).algebra;
|
||||
await expect(patcher.canHandle(input)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('handles NOP operations by not doing anything.', async(): Promise<void> => {
|
||||
let patch = getPatch('');
|
||||
let input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
await expect(patcher.handle(input)).resolves.toBe(representation);
|
||||
|
||||
patch = getPatch('');
|
||||
input = { identifier, patch };
|
||||
const result = await patcher.handle(input);
|
||||
expect(result.metadata.contentType).toBe('internal/quads');
|
||||
await expect(arrayifyStream(result.data)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('handles INSERT DATA updates.', async(): Promise<void> => {
|
||||
const patch = getPatch(fulfilledDataInsert);
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
const result = await patcher.handle(input);
|
||||
expect(result.metadata.contentType).toBe('internal/quads');
|
||||
const resultQuads = await arrayifyStream(result.data);
|
||||
expect(resultQuads).toBeRdfIsomorphic([
|
||||
...startQuads,
|
||||
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<void> => {
|
||||
const patch = getPatch('DELETE DATA { :startS1 :startP1 :startO1 }');
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
const result = await patcher.handle(input);
|
||||
const resultQuads = await arrayifyStream(result.data);
|
||||
expect(resultQuads).toBeRdfIsomorphic([
|
||||
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<void> => {
|
||||
const patch = getPatch('DELETE WHERE { :startS1 :startP1 :startO1 }');
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
const result = await patcher.handle(input);
|
||||
const resultQuads = await arrayifyStream(result.data);
|
||||
expect(resultQuads).toBeRdfIsomorphic([
|
||||
quad(namedNode('http://test.com/startS2'),
|
||||
namedNode('http://test.com/startP2'),
|
||||
namedNode('http://test.com/startO2')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles DELETE WHERE updates with variables.', async(): Promise<void> => {
|
||||
const patch = getPatch('DELETE WHERE { :startS1 :startP1 ?o }');
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
const result = await patcher.handle(input);
|
||||
const resultQuads = await arrayifyStream(result.data);
|
||||
expect(resultQuads).toBeRdfIsomorphic([
|
||||
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<void> => {
|
||||
const patch = getPatch('DELETE { :startS1 :startP1 :startO1 } INSERT { :s1 :p1 :o1 . } WHERE {}');
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
const result = await patcher.handle(input);
|
||||
const resultQuads = await arrayifyStream(result.data);
|
||||
expect(resultQuads).toBeRdfIsomorphic([
|
||||
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('handles composite INSERT/DELETE updates.', async(): Promise<void> => {
|
||||
const query = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 };' +
|
||||
'DELETE WHERE { :s1 :p1 :o1 . :startS1 :startP1 :startO1 }';
|
||||
const patch = getPatch(query);
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
const result = await patcher.handle(input);
|
||||
const resultQuads = await arrayifyStream(result.data);
|
||||
expect(resultQuads).toBeRdfIsomorphic([
|
||||
quad(namedNode('http://test.com/startS2'),
|
||||
namedNode('http://test.com/startP2'),
|
||||
namedNode('http://test.com/startO2')),
|
||||
quad(namedNode('http://test.com/s2'),
|
||||
namedNode('http://test.com/p2'),
|
||||
namedNode('http://test.com/o2')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles composite DELETE/INSERT updates.', async(): Promise<void> => {
|
||||
const query = 'DELETE DATA { :s1 :p1 :o1 . :startS1 :startP1 :startO1 } ;' +
|
||||
'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 }';
|
||||
const patch = getPatch(query);
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
const result = await patcher.handle(input);
|
||||
const resultQuads = await arrayifyStream(result.data);
|
||||
expect(resultQuads).toBeRdfIsomorphic([
|
||||
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')),
|
||||
quad(namedNode('http://test.com/s2'),
|
||||
namedNode('http://test.com/p2'),
|
||||
namedNode('http://test.com/o2')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects GRAPH inserts.', async(): Promise<void> => {
|
||||
const query = 'INSERT DATA { GRAPH :graph { :s1 :p1 :o1 } }';
|
||||
const patch = getPatch(query);
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
|
||||
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('rejects GRAPH deletes.', async(): Promise<void> => {
|
||||
const query = 'DELETE DATA { GRAPH :graph { :s1 :p1 :o1 } }';
|
||||
const patch = getPatch(query);
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
|
||||
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('rejects DELETE/INSERT updates with non-BGP WHERE.', async(): Promise<void> => {
|
||||
const query = 'DELETE { :s1 :p1 :o1 } INSERT { :s1 :p1 :o1 } WHERE { ?s ?p ?o. FILTER (?o > 5) }';
|
||||
const patch = getPatch(query);
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
|
||||
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('rejects INSERT WHERE updates with a UNION.', async(): Promise<void> => {
|
||||
const query = 'INSERT { :s1 :p1 :o1 . } WHERE { { :s1 :p1 :o1 } UNION { :s1 :p1 :o2 } }';
|
||||
const patch = getPatch(query);
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
|
||||
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('rejects non-DELETE/INSERT updates.', async(): Promise<void> => {
|
||||
const query = 'MOVE DEFAULT TO GRAPH :newGraph';
|
||||
const patch = getPatch(query);
|
||||
const input: RepresentationPatcherInput = { identifier, patch, representation };
|
||||
|
||||
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('creates a new resource if it does not exist yet.', async(): Promise<void> => {
|
||||
const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
|
||||
const patch = getPatch(query);
|
||||
const input: RepresentationPatcherInput = { identifier, patch };
|
||||
const result = await patcher.handle(input);
|
||||
expect(result.metadata.contentType).toBe('internal/quads');
|
||||
expect(result.metadata.identifier.value).toBe(identifier.path);
|
||||
const resultQuads = await arrayifyStream(result.data);
|
||||
expect(resultQuads).toBeRdfIsomorphic([
|
||||
quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires the input body to contain quads.', async(): Promise<void> => {
|
||||
const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
|
||||
const patch = getPatch(query);
|
||||
representation.metadata.contentType = 'text/turtle';
|
||||
const input = { identifier, patch, representation };
|
||||
await expect(patcher.handle(input)).rejects.toThrow('Quad stream was expected for patching.');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user