feat: Integrate MetadataHandler

This commit is contained in:
Joachim Van Herwegen 2020-10-06 16:14:18 +02:00
parent 7dcb3eaa84
commit 31844a4f40
15 changed files with 163 additions and 150 deletions

View File

@ -5,6 +5,7 @@
"files-scs:config/presets/http.json", "files-scs:config/presets/http.json",
"files-scs:config/presets/ldp.json", "files-scs:config/presets/ldp.json",
"files-scs:config/presets/ldp/credentials-extractor.json", "files-scs:config/presets/ldp/credentials-extractor.json",
"files-scs:config/presets/ldp/metadata-handler.json",
"files-scs:config/presets/ldp/operation-handler.json", "files-scs:config/presets/ldp/operation-handler.json",
"files-scs:config/presets/ldp/permissions-extractor.json", "files-scs:config/presets/ldp/permissions-extractor.json",
"files-scs:config/presets/ldp/request-parser.json", "files-scs:config/presets/ldp/request-parser.json",

View File

@ -0,0 +1,20 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"@id": "urn:solid-server:default:MetadataExtractor",
"@type": "BasicMetadataExtractor",
"BasicMetadataExtractor:_parsers": [
{
"@type": "ContentTypeParser"
},
{
"@type": "LinkTypeParser"
},
{
"@type": "SlugParser"
}
]
}
]
}

View File

@ -10,6 +10,9 @@
"BasicRequestParser:_preferenceParser": { "BasicRequestParser:_preferenceParser": {
"@type": "AcceptPreferenceParser" "@type": "AcceptPreferenceParser"
}, },
"BasicRequestParser:_metadataExtractor": {
"@id": "urn:solid-server:default:MetadataExtractor"
},
"BasicRequestParser:_bodyParser": { "BasicRequestParser:_bodyParser": {
"@type": "CompositeAsyncHandler", "@type": "CompositeAsyncHandler",
"CompositeAsyncHandler:_handlers": [ "CompositeAsyncHandler:_handlers": [

View File

@ -14,6 +14,14 @@ export * from './src/authorization/WebAclAuthorizer';
export * from './src/init/CliRunner'; export * from './src/init/CliRunner';
export * from './src/init/Setup'; export * from './src/init/Setup';
// LDP/HTTP/Metadata
export * from './src/ldp/http/metadata/BasicMetadataExtractor';
export * from './src/ldp/http/metadata/ContentTypeParser';
export * from './src/ldp/http/metadata/LinkTypeParser';
export * from './src/ldp/http/metadata/MetadataExtractor';
export * from './src/ldp/http/metadata/MetadataParser';
export * from './src/ldp/http/metadata/SlugParser';
// LDP/HTTP // LDP/HTTP
export * from './src/ldp/http/AcceptPreferenceParser'; export * from './src/ldp/http/AcceptPreferenceParser';
export * from './src/ldp/http/BasicRequestParser'; export * from './src/ldp/http/BasicRequestParser';

View File

@ -1,6 +1,7 @@
import type { HttpRequest } from '../../server/HttpRequest'; import type { HttpRequest } from '../../server/HttpRequest';
import type { Operation } from '../operations/Operation'; import type { Operation } from '../operations/Operation';
import type { BodyParser } from './BodyParser'; import type { BodyParser } from './BodyParser';
import type { MetadataExtractor } from './metadata/MetadataExtractor';
import type { PreferenceParser } from './PreferenceParser'; import type { PreferenceParser } from './PreferenceParser';
import { RequestParser } from './RequestParser'; import { RequestParser } from './RequestParser';
import type { TargetExtractor } from './TargetExtractor'; import type { TargetExtractor } from './TargetExtractor';
@ -11,16 +12,18 @@ import type { TargetExtractor } from './TargetExtractor';
export interface SimpleRequestParserArgs { export interface SimpleRequestParserArgs {
targetExtractor: TargetExtractor; targetExtractor: TargetExtractor;
preferenceParser: PreferenceParser; preferenceParser: PreferenceParser;
metadataExtractor: MetadataExtractor;
bodyParser: BodyParser; bodyParser: BodyParser;
} }
/** /**
* Creates an {@link Operation} from an incoming {@link HttpRequest} by aggregating the results * Creates an {@link Operation} from an incoming {@link HttpRequest} by aggregating the results
* of a {@link TargetExtractor}, {@link PreferenceParser}, and {@link BodyParser}. * of a {@link TargetExtractor}, {@link PreferenceParser}, {@link MetadataExtractor}, and {@link BodyParser}.
*/ */
export class BasicRequestParser extends RequestParser { export class BasicRequestParser extends RequestParser {
private readonly targetExtractor!: TargetExtractor; private readonly targetExtractor!: TargetExtractor;
private readonly preferenceParser!: PreferenceParser; private readonly preferenceParser!: PreferenceParser;
private readonly metadataExtractor!: MetadataExtractor;
private readonly bodyParser!: BodyParser; private readonly bodyParser!: BodyParser;
public constructor(args: SimpleRequestParserArgs) { public constructor(args: SimpleRequestParserArgs) {
@ -38,7 +41,8 @@ export class BasicRequestParser extends RequestParser {
} }
const target = await this.targetExtractor.handleSafe(input); const target = await this.targetExtractor.handleSafe(input);
const preferences = await this.preferenceParser.handleSafe(input); const preferences = await this.preferenceParser.handleSafe(input);
const body = await this.bodyParser.handleSafe(input); const metadata = await this.metadataExtractor.handleSafe(input);
const body = await this.bodyParser.handleSafe({ request: input, metadata });
return { method: input.method, target, preferences, body }; return { method: input.method, target, preferences, body };
} }

View File

@ -1,8 +1,22 @@
import type { HttpRequest } from '../../server/HttpRequest'; import type { HttpRequest } from '../../server/HttpRequest';
import { AsyncHandler } from '../../util/AsyncHandler'; import { AsyncHandler } from '../../util/AsyncHandler';
import type { Representation } from '../representation/Representation'; import type { Representation } from '../representation/Representation';
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
export interface BodyParserArgs {
/**
* Request that contains the (potential) body.
*/
request: HttpRequest;
/**
* Metadata that has already been parsed from the request.
* Can be updated by the BodyParser with extra metadata.
*/
metadata: RepresentationMetadata;
}
/** /**
* Parses the body of an incoming {@link HttpRequest} and converts it to a {@link Representation}. * Parses the body of an incoming {@link HttpRequest} and converts it to a {@link Representation}.
*/ */
export abstract class BodyParser extends AsyncHandler<HttpRequest, Representation | undefined> {} export abstract class BodyParser extends
AsyncHandler<BodyParserArgs, Representation | undefined> {}

View File

@ -1,14 +1,10 @@
import type { HttpRequest } from '../../server/HttpRequest';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { CONTENT_TYPE, HTTP, RDF } from '../../util/UriConstants';
import type { Representation } from '../representation/Representation'; import type { Representation } from '../representation/Representation';
import { RepresentationMetadata } from '../representation/RepresentationMetadata'; import type { BodyParserArgs } from './BodyParser';
import { BodyParser } from './BodyParser'; import { BodyParser } from './BodyParser';
/** /**
* Converts incoming {@link HttpRequest} to a Representation without any further parsing. * Converts incoming {@link HttpRequest} to a Representation without any further parsing.
* Naively parses the mediatype from the content-type header.
* Some other metadata is also generated, but this should probably be done in an external handler.
*/ */
export class RawBodyParser extends BodyParser { export class RawBodyParser extends BodyParser {
public async canHandle(): Promise<void> { public async canHandle(): Promise<void> {
@ -17,56 +13,23 @@ export class RawBodyParser extends BodyParser {
// Note that the only reason this is a union is in case the body is empty. // Note that the only reason this is a union is in case the body is empty.
// If this check gets moved away from the BodyParsers this union could be removed // If this check gets moved away from the BodyParsers this union could be removed
public async handle(input: HttpRequest): Promise<Representation | undefined> { public async handle({ request, metadata }: BodyParserArgs): Promise<Representation | undefined> {
// RFC7230, §3.3: The presence of a message body in a request // RFC7230, §3.3: The presence of a message body in a request
// is signaled by a Content-Length or Transfer-Encoding header field. // is signaled by a Content-Length or Transfer-Encoding header field.
if (!input.headers['content-length'] && !input.headers['transfer-encoding']) { if (!request.headers['content-length'] && !request.headers['transfer-encoding']) {
return; return;
} }
// While RFC7231 allows treating a body without content type as an octet stream, // While RFC7231 allows treating a body without content type as an octet stream,
// such an omission likely signals a mistake, so force clients to make this explicit. // such an omission likely signals a mistake, so force clients to make this explicit.
if (!input.headers['content-type']) { if (!request.headers['content-type']) {
throw new UnsupportedHttpError('An HTTP request body was passed without Content-Type header'); throw new UnsupportedHttpError('An HTTP request body was passed without Content-Type header');
} }
return { return {
binary: true, binary: true,
data: input, data: request,
metadata: this.parseMetadata(input), metadata,
}; };
} }
private parseMetadata(input: HttpRequest): RepresentationMetadata {
const contentType = /^[^;]*/u.exec(input.headers['content-type']!)![0];
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: contentType });
const { link, slug } = input.headers;
if (slug) {
if (Array.isArray(slug)) {
throw new UnsupportedHttpError('At most 1 slug header is allowed.');
}
metadata.set(HTTP.slug, slug);
}
// There are similarities here to Accept header parsing so that library should become more generic probably
if (link) {
const linkArray = Array.isArray(link) ? link : [ link ];
const parsedLinks = linkArray.map((entry): { url: string; rel: string } => {
const [ , url, rest ] = /^<([^>]*)>(.*)$/u.exec(entry) ?? [];
const [ , rel ] = /^ *; *rel="(.*)"$/u.exec(rest) ?? [];
return { url, rel };
});
for (const entry of parsedLinks) {
if (entry.rel === 'type') {
metadata.set(RDF.type, entry.url);
break;
}
}
}
return metadata;
}
} }

View File

@ -1,53 +1,50 @@
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import type { Algebra } from 'sparqlalgebrajs';
import { translate } from 'sparqlalgebrajs'; import { translate } from 'sparqlalgebrajs';
import type { HttpRequest } from '../../server/HttpRequest';
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes'; import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
import { CONTENT_TYPE } from '../../util/UriConstants'; import { pipeStreamsAndErrors, readableToString } from '../../util/Util';
import { readableToString } from '../../util/Util'; import type { BodyParserArgs } from './BodyParser';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { BodyParser } from './BodyParser'; import { BodyParser } from './BodyParser';
import type { SparqlUpdatePatch } from './SparqlUpdatePatch'; import type { SparqlUpdatePatch } from './SparqlUpdatePatch';
/** /**
* {@link BodyParser} that supports `application/sparql-update` content. * {@link BodyParser} that supports `application/sparql-update` content.
* Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}. * Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}.
* Still needs access to a handler for parsing metadata.
*/ */
export class SparqlUpdateBodyParser extends BodyParser { export class SparqlUpdateBodyParser extends BodyParser {
public async canHandle(input: HttpRequest): Promise<void> { public async canHandle({ request }: BodyParserArgs): Promise<void> {
if (input.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) { if (request.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) {
throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.'); throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.');
} }
} }
public async handle(input: HttpRequest): Promise<SparqlUpdatePatch> { public async handle({ request, metadata }: BodyParserArgs): Promise<SparqlUpdatePatch> {
// Note that readableObjectMode is only defined starting from Node 12
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
const options = { objectMode: request.readableObjectMode };
const toAlgebraStream = new PassThrough(options);
const dataCopy = new PassThrough(options);
pipeStreamsAndErrors(request, toAlgebraStream);
pipeStreamsAndErrors(request, dataCopy);
let algebra: Algebra.Operation;
try { try {
// Note that readableObjectMode is only defined starting from Node 12
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
const options = { objectMode: input.readableObjectMode };
const toAlgebraStream = new PassThrough(options);
const dataCopy = new PassThrough(options);
input.pipe(toAlgebraStream);
input.pipe(dataCopy);
const sparql = await readableToString(toAlgebraStream); const sparql = await readableToString(toAlgebraStream);
const algebra = translate(sparql, { quads: true }); algebra = translate(sparql, { quads: true });
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_SPARQL_UPDATE });
// Prevent body from being requested again
return {
algebra,
binary: true,
data: dataCopy,
metadata,
};
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
throw new UnsupportedHttpError(error.message); throw new UnsupportedHttpError(error.message);
} }
throw new UnsupportedHttpError(); throw new UnsupportedHttpError();
} }
// Prevent body from being requested again
return {
algebra,
binary: true,
data: dataCopy,
metadata,
};
} }
} }

View File

@ -15,10 +15,13 @@ import {
} from '../../index'; } from '../../index';
import type { ServerConfig } from './ServerConfig'; import type { ServerConfig } from './ServerConfig';
import { getInMemoryResourceStore, import {
getInMemoryResourceStore,
getOperationHandler, getOperationHandler,
getConvertingStore, getConvertingStore,
getPatchingStore, getBasicRequestParser } from './Util'; getPatchingStore,
getBasicRequestParser,
} from './Util';
/** /**
* BasicHandlersConfig works with * BasicHandlersConfig works with

View File

@ -12,7 +12,12 @@ import {
UnsecureWebIdExtractor, UnsecureWebIdExtractor,
} from '../../index'; } from '../../index';
import type { ServerConfig } from './ServerConfig'; import type { ServerConfig } from './ServerConfig';
import { getFileResourceStore, getOperationHandler, getConvertingStore, getBasicRequestParser } from './Util'; import {
getFileResourceStore,
getOperationHandler,
getConvertingStore,
getBasicRequestParser,
} from './Util';
/** /**
* FileResourceStoreConfig works with * FileResourceStoreConfig works with

View File

@ -1,22 +1,23 @@
import { join } from 'path'; import { join } from 'path';
import type { BodyParser, import type { BodyParser,
HttpRequest,
Operation, Operation,
Representation,
RepresentationConverter, RepresentationConverter,
ResourceStore, ResourceStore,
ResponseDescription } from '../../index'; ResponseDescription } from '../../index';
import { import {
AcceptPreferenceParser, AcceptPreferenceParser,
BasicMetadataExtractor,
BasicRequestParser, BasicRequestParser,
BasicTargetExtractor, BasicTargetExtractor,
CompositeAsyncHandler, CompositeAsyncHandler,
ContentTypeParser,
DeleteOperationHandler, DeleteOperationHandler,
FileResourceStore, FileResourceStore,
GetOperationHandler, GetOperationHandler,
HeadOperationHandler, HeadOperationHandler,
InMemoryResourceStore, InMemoryResourceStore,
InteractionController, InteractionController,
LinkTypeParser,
MetadataController, MetadataController,
PatchingStore, PatchingStore,
PatchOperationHandler, PatchOperationHandler,
@ -25,6 +26,7 @@ import {
RawBodyParser, RawBodyParser,
RepresentationConvertingStore, RepresentationConvertingStore,
SingleThreadedResourceLocker, SingleThreadedResourceLocker,
SlugParser,
SparqlUpdatePatchHandler, SparqlUpdatePatchHandler,
UrlBasedAclManager, UrlBasedAclManager,
UrlContainerManager, UrlContainerManager,
@ -104,6 +106,15 @@ export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler
return new CompositeAsyncHandler<Operation, ResponseDescription>(handlers); return new CompositeAsyncHandler<Operation, ResponseDescription>(handlers);
}; };
/**
* Creates a BasicMetadataExtractor with parsers for content-type, slugs and link types.
*/
export const getBasicMetadataExtractor = (): BasicMetadataExtractor => new BasicMetadataExtractor([
new ContentTypeParser(),
new SlugParser(),
new LinkTypeParser(),
]);
/** /**
* Gives a basic request parser based on some body parses. * Gives a basic request parser based on some body parses.
* @param bodyParsers - Optional list of body parsers, default is RawBodyParser. * @param bodyParsers - Optional list of body parsers, default is RawBodyParser.
@ -118,11 +129,12 @@ export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequ
// If no body parser is given (array is empty), default to RawBodyParser // If no body parser is given (array is empty), default to RawBodyParser
bodyParser = new RawBodyParser(); bodyParser = new RawBodyParser();
} else { } else {
bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>(bodyParsers); bodyParser = new CompositeAsyncHandler(bodyParsers);
} }
return new BasicRequestParser({ return new BasicRequestParser({
targetExtractor: new BasicTargetExtractor(), targetExtractor: new BasicTargetExtractor(),
preferenceParser: new AcceptPreferenceParser(), preferenceParser: new AcceptPreferenceParser(),
metadataExtractor: getBasicMetadataExtractor(),
bodyParser, bodyParser,
}); });
}; };

View File

@ -4,15 +4,18 @@ import streamifyArray from 'streamify-array';
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser'; import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser'; import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser';
import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor'; import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor';
import { BasicMetadataExtractor } from '../../src/ldp/http/metadata/BasicMetadataExtractor';
import { ContentTypeParser } from '../../src/ldp/http/metadata/ContentTypeParser';
import { RawBodyParser } from '../../src/ldp/http/RawBodyParser'; import { RawBodyParser } from '../../src/ldp/http/RawBodyParser';
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
import type { HttpRequest } from '../../src/server/HttpRequest'; import type { HttpRequest } from '../../src/server/HttpRequest';
describe('A BasicRequestParser with simple input parsers', (): void => { describe('A BasicRequestParser with simple input parsers', (): void => {
const targetExtractor = new BasicTargetExtractor(); const targetExtractor = new BasicTargetExtractor();
const bodyParser = new RawBodyParser();
const preferenceParser = new AcceptPreferenceParser(); const preferenceParser = new AcceptPreferenceParser();
const requestParser = new BasicRequestParser({ targetExtractor, bodyParser, preferenceParser }); const metadataExtractor = new BasicMetadataExtractor([ new ContentTypeParser() ]);
const bodyParser = new RawBodyParser();
const requestParser = new BasicRequestParser({ targetExtractor, preferenceParser, metadataExtractor, bodyParser });
it('can parse an incoming request.', async(): Promise<void> => { it('can parse an incoming request.', async(): Promise<void> => {
const request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest; const request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;

View File

@ -1,20 +1,23 @@
import { BasicRequestParser } from '../../../../src/ldp/http/BasicRequestParser'; import { BasicRequestParser } from '../../../../src/ldp/http/BasicRequestParser';
import type { BodyParser } from '../../../../src/ldp/http/BodyParser'; import type { BodyParser } from '../../../../src/ldp/http/BodyParser';
import type { MetadataExtractor } from '../../../../src/ldp/http/metadata/MetadataExtractor';
import type { PreferenceParser } from '../../../../src/ldp/http/PreferenceParser'; import type { PreferenceParser } from '../../../../src/ldp/http/PreferenceParser';
import type { TargetExtractor } from '../../../../src/ldp/http/TargetExtractor'; import type { TargetExtractor } from '../../../../src/ldp/http/TargetExtractor';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler'; import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('A BasicRequestParser', (): void => { describe('A BasicRequestParser', (): void => {
let targetExtractor: TargetExtractor; let targetExtractor: TargetExtractor;
let bodyParser: BodyParser;
let preferenceParser: PreferenceParser; let preferenceParser: PreferenceParser;
let metadataExtractor: MetadataExtractor;
let bodyParser: BodyParser;
let requestParser: BasicRequestParser; let requestParser: BasicRequestParser;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
targetExtractor = new StaticAsyncHandler(true, 'target' as any); targetExtractor = new StaticAsyncHandler(true, 'target' as any);
bodyParser = new StaticAsyncHandler(true, 'body' as any);
preferenceParser = new StaticAsyncHandler(true, 'preference' as any); preferenceParser = new StaticAsyncHandler(true, 'preference' as any);
requestParser = new BasicRequestParser({ targetExtractor, bodyParser, preferenceParser }); metadataExtractor = new StaticAsyncHandler(true, 'metadata' as any);
bodyParser = new StaticAsyncHandler(true, 'body' as any);
requestParser = new BasicRequestParser({ targetExtractor, preferenceParser, metadataExtractor, bodyParser });
}); });
it('can handle any input.', async(): Promise<void> => { it('can handle any input.', async(): Promise<void> => {
@ -26,11 +29,12 @@ describe('A BasicRequestParser', (): void => {
}); });
it('returns the output of all input parsers after calling handle.', async(): Promise<void> => { it('returns the output of all input parsers after calling handle.', async(): Promise<void> => {
bodyParser.handle = ({ metadata }): any => ({ data: 'body', metadata });
await expect(requestParser.handle({ url: 'url', method: 'GET' } as any)).resolves.toEqual({ await expect(requestParser.handle({ url: 'url', method: 'GET' } as any)).resolves.toEqual({
method: 'GET', method: 'GET',
target: 'target', target: 'target',
preferences: 'preference', preferences: 'preference',
body: 'body', body: { data: 'body', metadata: 'metadata' },
}); });
}); });
}); });

View File

@ -1,87 +1,54 @@
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import type { BodyParserArgs } from '../../../../src/ldp/http/BodyParser';
import { RawBodyParser } from '../../../../src/ldp/http/RawBodyParser'; import { RawBodyParser } from '../../../../src/ldp/http/RawBodyParser';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../../src/server/HttpRequest'; import type { HttpRequest } from '../../../../src/server/HttpRequest';
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
import 'jest-rdf'; import 'jest-rdf';
import { HTTP, RDF } from '../../../../src/util/UriConstants';
describe('A RawBodyparser', (): void => { describe('A RawBodyparser', (): void => {
const bodyParser = new RawBodyParser(); const bodyParser = new RawBodyParser();
let input: BodyParserArgs;
beforeEach(async(): Promise<void> => {
input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata() };
});
it('accepts all input.', async(): Promise<void> => { it('accepts all input.', async(): Promise<void> => {
await expect(bodyParser.canHandle()).resolves.toBeUndefined(); await expect(bodyParser.canHandle()).resolves.toBeUndefined();
}); });
it('returns empty output if there was no content length or transfer encoding.', async(): Promise<void> => { it('returns empty output if there was no content length or transfer encoding.', async(): Promise<void> => {
const input = streamifyArray([ '' ]) as HttpRequest; input.request = streamifyArray([ '' ]) as HttpRequest;
input.headers = {}; input.request.headers = {};
await expect(bodyParser.handle(input)).resolves.toBeUndefined(); await expect(bodyParser.handle(input)).resolves.toBeUndefined();
}); });
it('errors when a content length was specified without content type.', async(): Promise<void> => { it('errors when a content length was specified without content type.', async(): Promise<void> => {
const input = streamifyArray([ 'abc' ]) as HttpRequest; input.request = streamifyArray([ 'abc' ]) as HttpRequest;
input.headers = { 'content-length': '0' }; input.request.headers = { 'content-length': '0' };
await expect(bodyParser.handle(input)).rejects await expect(bodyParser.handle(input)).rejects
.toThrow('An HTTP request body was passed without Content-Type header'); .toThrow('An HTTP request body was passed without Content-Type header');
}); });
it('errors when a transfer encoding was specified without content type.', async(): Promise<void> => { it('errors when a transfer encoding was specified without content type.', async(): Promise<void> => {
const input = streamifyArray([ 'abc' ]) as HttpRequest; input.request = streamifyArray([ 'abc' ]) as HttpRequest;
input.headers = { 'transfer-encoding': 'chunked' }; input.request.headers = { 'transfer-encoding': 'chunked' };
await expect(bodyParser.handle(input)).rejects await expect(bodyParser.handle(input)).rejects
.toThrow('An HTTP request body was passed without Content-Type header'); .toThrow('An HTTP request body was passed without Content-Type header');
}); });
it('returns a Representation if there was data.', async(): Promise<void> => { it('returns a Representation if there was data.', async(): Promise<void> => {
const input = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest; input.request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
input.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' }; input.request.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' };
const result = (await bodyParser.handle(input))!; const result = (await bodyParser.handle(input))!;
expect(result).toEqual({ expect(result).toEqual({
binary: true, binary: true,
data: input, data: input.request,
metadata: expect.any(RepresentationMetadata), metadata: input.metadata,
}); });
expect(result.metadata.contentType).toEqual('text/turtle');
await expect(arrayifyStream(result.data)).resolves.toEqual( await expect(arrayifyStream(result.data)).resolves.toEqual(
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ], [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
); );
}); });
it('adds the slug header to the metadata.', async(): Promise<void> => {
const input = {} as HttpRequest;
input.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle', slug: 'slugText' };
const result = (await bodyParser.handle(input))!;
expect(result.metadata.contentType).toEqual('text/turtle');
expect(result.metadata.get(HTTP.slug)?.value).toEqual('slugText');
});
it('errors if there are multiple slugs.', async(): Promise<void> => {
const input = {} as HttpRequest;
input.headers = { 'transfer-encoding': 'chunked',
'content-type': 'text/turtle',
slug: [ 'slugTextA', 'slugTextB' ]};
await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError);
});
it('adds the link type headers to the metadata.', async(): Promise<void> => {
const input = {} as HttpRequest;
input.headers = { 'transfer-encoding': 'chunked',
'content-type': 'text/turtle',
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"' };
const result = (await bodyParser.handle(input))!;
expect(result.metadata.contentType).toEqual('text/turtle');
expect(result.metadata.get(RDF.type)?.value).toEqual('http://www.w3.org/ns/ldp#Container');
});
it('ignores unknown link headers.', async(): Promise<void> => {
const input = {} as HttpRequest;
input.headers = { 'transfer-encoding': 'chunked',
'content-type': 'text/turtle',
link: [ '<unrelatedLink>', 'badLink' ]};
const result = (await bodyParser.handle(input))!;
expect(result.metadata.quads()).toHaveLength(1);
expect(result.metadata.contentType).toEqual('text/turtle');
});
}); });

View File

@ -1,43 +1,52 @@
import { namedNode, quad } from '@rdfjs/data-model'; import { namedNode, quad } from '@rdfjs/data-model';
import arrayifyStream from 'arrayify-stream';
import { Algebra } from 'sparqlalgebrajs'; import { Algebra } from 'sparqlalgebrajs';
import * as algebra from 'sparqlalgebrajs'; import * as algebra from 'sparqlalgebrajs';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import type { BodyParserArgs } from '../../../../src/ldp/http/BodyParser';
import { SparqlUpdateBodyParser } from '../../../../src/ldp/http/SparqlUpdateBodyParser'; import { SparqlUpdateBodyParser } from '../../../../src/ldp/http/SparqlUpdateBodyParser';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../../src/server/HttpRequest'; import type { HttpRequest } from '../../../../src/server/HttpRequest';
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError'; import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { readableToString } from '../../../../src/util/Util';
describe('A SparqlUpdateBodyParser', (): void => { describe('A SparqlUpdateBodyParser', (): void => {
const bodyParser = new SparqlUpdateBodyParser(); const bodyParser = new SparqlUpdateBodyParser();
let input: BodyParserArgs;
beforeEach(async(): Promise<void> => {
input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata() };
});
it('only accepts application/sparql-update content.', async(): Promise<void> => { it('only accepts application/sparql-update content.', async(): Promise<void> => {
await expect(bodyParser.canHandle({ headers: {}} as HttpRequest)).rejects.toThrow(UnsupportedMediaTypeHttpError); await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
await expect(bodyParser.canHandle({ headers: { 'content-type': 'text/plain' }} as HttpRequest)) input.request.headers = { 'content-type': 'text/plain' };
.rejects.toThrow(UnsupportedMediaTypeHttpError); await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
await expect(bodyParser.canHandle({ headers: { 'content-type': 'application/sparql-update' }} as HttpRequest)) input.request.headers = { 'content-type': 'application/sparql-update' };
.resolves.toBeUndefined(); await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
}); });
it('errors when handling invalid SPARQL updates.', async(): Promise<void> => { it('errors when handling invalid SPARQL updates.', async(): Promise<void> => {
await expect(bodyParser.handle(streamifyArray([ 'VERY INVALID UPDATE' ]) as HttpRequest)) input.request = streamifyArray([ 'VERY INVALID UPDATE' ]) as HttpRequest;
.rejects.toThrow(UnsupportedHttpError); await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError);
}); });
it('errors when receiving an unexpected error.', async(): Promise<void> => { it('errors when receiving an unexpected error.', async(): Promise<void> => {
const mock = jest.spyOn(algebra, 'translate').mockImplementationOnce((): any => { const mock = jest.spyOn(algebra, 'translate').mockImplementationOnce((): any => {
throw 'apple'; throw 'apple';
}); });
await expect(bodyParser.handle(streamifyArray( input.request = streamifyArray(
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}' ], [ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }' ],
) as HttpRequest)).rejects.toThrow(UnsupportedHttpError); ) as HttpRequest;
await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError);
mock.mockRestore(); mock.mockRestore();
}); });
it('converts SPARQL updates to algebra.', async(): Promise<void> => { it('converts SPARQL updates to algebra.', async(): Promise<void> => {
const result = await bodyParser.handle(streamifyArray( input.request = streamifyArray(
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}' ], [ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }' ],
) as HttpRequest); ) as HttpRequest;
const result = await bodyParser.handle(input);
expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT); expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT);
expect((result.algebra as Algebra.DeleteInsert).delete).toBeRdfIsomorphic([ quad( expect((result.algebra as Algebra.DeleteInsert).delete).toBeRdfIsomorphic([ quad(
namedNode('http://test.com/s'), namedNode('http://test.com/s'),
@ -45,11 +54,11 @@ describe('A SparqlUpdateBodyParser', (): void => {
namedNode('http://test.com/o'), namedNode('http://test.com/o'),
) ]); ) ]);
expect(result.binary).toBe(true); expect(result.binary).toBe(true);
expect(result.metadata.contentType).toEqual('application/sparql-update'); expect(result.metadata).toBe(input.metadata);
// Workaround for Node 10 not exposing objectMode // Workaround for Node 10 not exposing objectMode
expect((await arrayifyStream(result.data)).join('')).toEqual( expect(await readableToString(result.data)).toEqual(
'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}', 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }',
); );
}); });
}); });