From 13c231d04886f3792f1d73f2f24707a7c89c807d Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 6 Oct 2020 16:14:18 +0200 Subject: [PATCH] feat: Integrate MetadataHandler --- config/config-default.json | 1 + config/presets/ldp/metadata-handler.json | 20 ++++++ config/presets/ldp/request-parser.json | 3 + index.ts | 8 +++ src/ldp/http/BasicRequestParser.ts | 8 ++- src/ldp/http/BodyParser.ts | 16 ++++- src/ldp/http/RawBodyParser.ts | 49 ++------------ src/ldp/http/SparqlUpdateBodyParser.ts | 49 +++++++------- test/configs/BasicHandlersConfig.ts | 7 +- test/configs/FileResourceStoreConfig.ts | 7 +- test/configs/Util.ts | 18 ++++- test/integration/RequestParser.test.ts | 7 +- test/unit/ldp/http/BasicRequestParser.test.ts | 12 ++-- test/unit/ldp/http/RawBodyParser.test.ts | 65 +++++-------------- .../ldp/http/SparqlUpdateBodyParser.test.ts | 43 +++++++----- 15 files changed, 163 insertions(+), 150 deletions(-) create mode 100644 config/presets/ldp/metadata-handler.json diff --git a/config/config-default.json b/config/config-default.json index 905098c314..02e5320e29 100644 --- a/config/config-default.json +++ b/config/config-default.json @@ -5,6 +5,7 @@ "files-scs:config/presets/http.json", "files-scs:config/presets/ldp.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/permissions-extractor.json", "files-scs:config/presets/ldp/request-parser.json", diff --git a/config/presets/ldp/metadata-handler.json b/config/presets/ldp/metadata-handler.json new file mode 100644 index 0000000000..09e9247aff --- /dev/null +++ b/config/presets/ldp/metadata-handler.json @@ -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" + } + ] + } + ] +} diff --git a/config/presets/ldp/request-parser.json b/config/presets/ldp/request-parser.json index f3422cc132..027058807e 100644 --- a/config/presets/ldp/request-parser.json +++ b/config/presets/ldp/request-parser.json @@ -10,6 +10,9 @@ "BasicRequestParser:_preferenceParser": { "@type": "AcceptPreferenceParser" }, + "BasicRequestParser:_metadataExtractor": { + "@id": "urn:solid-server:default:MetadataExtractor" + }, "BasicRequestParser:_bodyParser": { "@type": "CompositeAsyncHandler", "CompositeAsyncHandler:_handlers": [ diff --git a/index.ts b/index.ts index 98e7446e3c..d8953a8154 100644 --- a/index.ts +++ b/index.ts @@ -14,6 +14,14 @@ export * from './src/authorization/WebAclAuthorizer'; export * from './src/init/CliRunner'; 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 export * from './src/ldp/http/AcceptPreferenceParser'; export * from './src/ldp/http/BasicRequestParser'; diff --git a/src/ldp/http/BasicRequestParser.ts b/src/ldp/http/BasicRequestParser.ts index 3b504ee602..5bffde4434 100644 --- a/src/ldp/http/BasicRequestParser.ts +++ b/src/ldp/http/BasicRequestParser.ts @@ -1,6 +1,7 @@ import type { HttpRequest } from '../../server/HttpRequest'; import type { Operation } from '../operations/Operation'; import type { BodyParser } from './BodyParser'; +import type { MetadataExtractor } from './metadata/MetadataExtractor'; import type { PreferenceParser } from './PreferenceParser'; import { RequestParser } from './RequestParser'; import type { TargetExtractor } from './TargetExtractor'; @@ -11,16 +12,18 @@ import type { TargetExtractor } from './TargetExtractor'; export interface SimpleRequestParserArgs { targetExtractor: TargetExtractor; preferenceParser: PreferenceParser; + metadataExtractor: MetadataExtractor; bodyParser: BodyParser; } /** * 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 { private readonly targetExtractor!: TargetExtractor; private readonly preferenceParser!: PreferenceParser; + private readonly metadataExtractor!: MetadataExtractor; private readonly bodyParser!: BodyParser; public constructor(args: SimpleRequestParserArgs) { @@ -38,7 +41,8 @@ export class BasicRequestParser extends RequestParser { } const target = await this.targetExtractor.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 }; } diff --git a/src/ldp/http/BodyParser.ts b/src/ldp/http/BodyParser.ts index f69e1705d2..0d0a774c41 100644 --- a/src/ldp/http/BodyParser.ts +++ b/src/ldp/http/BodyParser.ts @@ -1,8 +1,22 @@ import type { HttpRequest } from '../../server/HttpRequest'; import { AsyncHandler } from '../../util/AsyncHandler'; 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}. */ -export abstract class BodyParser extends AsyncHandler {} +export abstract class BodyParser extends + AsyncHandler {} diff --git a/src/ldp/http/RawBodyParser.ts b/src/ldp/http/RawBodyParser.ts index 9f652a86e9..0bb58ccf40 100644 --- a/src/ldp/http/RawBodyParser.ts +++ b/src/ldp/http/RawBodyParser.ts @@ -1,14 +1,10 @@ -import type { HttpRequest } from '../../server/HttpRequest'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE, HTTP, RDF } from '../../util/UriConstants'; import type { Representation } from '../representation/Representation'; -import { RepresentationMetadata } from '../representation/RepresentationMetadata'; +import type { BodyParserArgs } from './BodyParser'; import { BodyParser } from './BodyParser'; /** * 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 { public async canHandle(): Promise { @@ -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. // If this check gets moved away from the BodyParsers this union could be removed - public async handle(input: HttpRequest): Promise { + public async handle({ request, metadata }: BodyParserArgs): Promise { // RFC7230, ยง3.3: The presence of a message body in a request // 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; } // 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. - if (!input.headers['content-type']) { + if (!request.headers['content-type']) { throw new UnsupportedHttpError('An HTTP request body was passed without Content-Type header'); } return { binary: true, - data: input, - metadata: this.parseMetadata(input), + data: request, + 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; - } } diff --git a/src/ldp/http/SparqlUpdateBodyParser.ts b/src/ldp/http/SparqlUpdateBodyParser.ts index 6ead8058b9..ea550a2097 100644 --- a/src/ldp/http/SparqlUpdateBodyParser.ts +++ b/src/ldp/http/SparqlUpdateBodyParser.ts @@ -1,53 +1,50 @@ import { PassThrough } from 'stream'; +import type { Algebra } from 'sparqlalgebrajs'; import { translate } from 'sparqlalgebrajs'; -import type { HttpRequest } from '../../server/HttpRequest'; import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; -import { CONTENT_TYPE } from '../../util/UriConstants'; -import { readableToString } from '../../util/Util'; -import { RepresentationMetadata } from '../representation/RepresentationMetadata'; +import { pipeStreamsAndErrors, readableToString } from '../../util/Util'; +import type { BodyParserArgs } from './BodyParser'; import { BodyParser } from './BodyParser'; import type { SparqlUpdatePatch } from './SparqlUpdatePatch'; /** * {@link BodyParser} that supports `application/sparql-update` content. * 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 { - public async canHandle(input: HttpRequest): Promise { - if (input.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) { + public async canHandle({ request }: BodyParserArgs): Promise { + if (request.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) { throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.'); } } - public async handle(input: HttpRequest): Promise { + public async handle({ request, metadata }: BodyParserArgs): Promise { + // 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 { - // 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 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, - }; + algebra = translate(sparql, { quads: true }); } catch (error: unknown) { if (error instanceof Error) { throw new UnsupportedHttpError(error.message); } throw new UnsupportedHttpError(); } + + // Prevent body from being requested again + return { + algebra, + binary: true, + data: dataCopy, + metadata, + }; } } diff --git a/test/configs/BasicHandlersConfig.ts b/test/configs/BasicHandlersConfig.ts index a6d88d0252..c37e34d961 100644 --- a/test/configs/BasicHandlersConfig.ts +++ b/test/configs/BasicHandlersConfig.ts @@ -15,10 +15,13 @@ import { } from '../../index'; import type { ServerConfig } from './ServerConfig'; -import { getInMemoryResourceStore, +import { + getInMemoryResourceStore, getOperationHandler, getConvertingStore, - getPatchingStore, getBasicRequestParser } from './Util'; + getPatchingStore, + getBasicRequestParser, +} from './Util'; /** * BasicHandlersConfig works with diff --git a/test/configs/FileResourceStoreConfig.ts b/test/configs/FileResourceStoreConfig.ts index 5b091dd76f..fbfad6efbe 100644 --- a/test/configs/FileResourceStoreConfig.ts +++ b/test/configs/FileResourceStoreConfig.ts @@ -12,7 +12,12 @@ import { UnsecureWebIdExtractor, } from '../../index'; import type { ServerConfig } from './ServerConfig'; -import { getFileResourceStore, getOperationHandler, getConvertingStore, getBasicRequestParser } from './Util'; +import { + getFileResourceStore, + getOperationHandler, + getConvertingStore, + getBasicRequestParser, +} from './Util'; /** * FileResourceStoreConfig works with diff --git a/test/configs/Util.ts b/test/configs/Util.ts index e98e7178a0..ec8ec64a4c 100644 --- a/test/configs/Util.ts +++ b/test/configs/Util.ts @@ -1,21 +1,22 @@ import { join } from 'path'; import type { BodyParser, - HttpRequest, Operation, - Representation, RepresentationConverter, ResourceStore, ResponseDescription } from '../../index'; import { AcceptPreferenceParser, + BasicMetadataExtractor, BasicRequestParser, BasicTargetExtractor, CompositeAsyncHandler, + ContentTypeParser, DeleteOperationHandler, FileResourceStore, GetOperationHandler, InMemoryResourceStore, InteractionController, + LinkTypeParser, MetadataController, PatchingStore, PatchOperationHandler, @@ -24,6 +25,7 @@ import { RawBodyParser, RepresentationConvertingStore, SingleThreadedResourceLocker, + SlugParser, SparqlUpdatePatchHandler, UrlBasedAclManager, UrlContainerManager, @@ -102,6 +104,15 @@ export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler return new CompositeAsyncHandler(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. * @param bodyParsers - Optional list of body parsers, default is RawBodyParser. @@ -116,11 +127,12 @@ export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequ // If no body parser is given (array is empty), default to RawBodyParser bodyParser = new RawBodyParser(); } else { - bodyParser = new CompositeAsyncHandler(bodyParsers); + bodyParser = new CompositeAsyncHandler(bodyParsers); } return new BasicRequestParser({ targetExtractor: new BasicTargetExtractor(), preferenceParser: new AcceptPreferenceParser(), + metadataExtractor: getBasicMetadataExtractor(), bodyParser, }); }; diff --git a/test/integration/RequestParser.test.ts b/test/integration/RequestParser.test.ts index d5abb0f47c..9db393c339 100644 --- a/test/integration/RequestParser.test.ts +++ b/test/integration/RequestParser.test.ts @@ -4,15 +4,18 @@ import streamifyArray from 'streamify-array'; import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser'; import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser'; 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 { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata'; import type { HttpRequest } from '../../src/server/HttpRequest'; describe('A BasicRequestParser with simple input parsers', (): void => { const targetExtractor = new BasicTargetExtractor(); - const bodyParser = new RawBodyParser(); 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 => { const request = streamifyArray([ ' .' ]) as HttpRequest; diff --git a/test/unit/ldp/http/BasicRequestParser.test.ts b/test/unit/ldp/http/BasicRequestParser.test.ts index f63796199b..edc7035a4b 100644 --- a/test/unit/ldp/http/BasicRequestParser.test.ts +++ b/test/unit/ldp/http/BasicRequestParser.test.ts @@ -1,20 +1,23 @@ import { BasicRequestParser } from '../../../../src/ldp/http/BasicRequestParser'; 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 { TargetExtractor } from '../../../../src/ldp/http/TargetExtractor'; import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler'; describe('A BasicRequestParser', (): void => { let targetExtractor: TargetExtractor; - let bodyParser: BodyParser; let preferenceParser: PreferenceParser; + let metadataExtractor: MetadataExtractor; + let bodyParser: BodyParser; let requestParser: BasicRequestParser; beforeEach(async(): Promise => { targetExtractor = new StaticAsyncHandler(true, 'target' as any); - bodyParser = new StaticAsyncHandler(true, 'body' 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 => { @@ -26,11 +29,12 @@ describe('A BasicRequestParser', (): void => { }); it('returns the output of all input parsers after calling handle.', async(): Promise => { + bodyParser.handle = ({ metadata }): any => ({ data: 'body', metadata }); await expect(requestParser.handle({ url: 'url', method: 'GET' } as any)).resolves.toEqual({ method: 'GET', target: 'target', preferences: 'preference', - body: 'body', + body: { data: 'body', metadata: 'metadata' }, }); }); }); diff --git a/test/unit/ldp/http/RawBodyParser.test.ts b/test/unit/ldp/http/RawBodyParser.test.ts index 539a0ea7b7..6dbaa4b031 100644 --- a/test/unit/ldp/http/RawBodyParser.test.ts +++ b/test/unit/ldp/http/RawBodyParser.test.ts @@ -1,87 +1,54 @@ import arrayifyStream from 'arrayify-stream'; import streamifyArray from 'streamify-array'; +import type { BodyParserArgs } from '../../../../src/ldp/http/BodyParser'; import { RawBodyParser } from '../../../../src/ldp/http/RawBodyParser'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import type { HttpRequest } from '../../../../src/server/HttpRequest'; -import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; import 'jest-rdf'; -import { HTTP, RDF } from '../../../../src/util/UriConstants'; describe('A RawBodyparser', (): void => { const bodyParser = new RawBodyParser(); + let input: BodyParserArgs; + + beforeEach(async(): Promise => { + input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata() }; + }); it('accepts all input.', async(): Promise => { await expect(bodyParser.canHandle()).resolves.toBeUndefined(); }); it('returns empty output if there was no content length or transfer encoding.', async(): Promise => { - const input = streamifyArray([ '' ]) as HttpRequest; - input.headers = {}; + input.request = streamifyArray([ '' ]) as HttpRequest; + input.request.headers = {}; await expect(bodyParser.handle(input)).resolves.toBeUndefined(); }); it('errors when a content length was specified without content type.', async(): Promise => { - const input = streamifyArray([ 'abc' ]) as HttpRequest; - input.headers = { 'content-length': '0' }; + input.request = streamifyArray([ 'abc' ]) as HttpRequest; + input.request.headers = { 'content-length': '0' }; await expect(bodyParser.handle(input)).rejects .toThrow('An HTTP request body was passed without Content-Type header'); }); it('errors when a transfer encoding was specified without content type.', async(): Promise => { - const input = streamifyArray([ 'abc' ]) as HttpRequest; - input.headers = { 'transfer-encoding': 'chunked' }; + input.request = streamifyArray([ 'abc' ]) as HttpRequest; + input.request.headers = { 'transfer-encoding': 'chunked' }; await expect(bodyParser.handle(input)).rejects .toThrow('An HTTP request body was passed without Content-Type header'); }); it('returns a Representation if there was data.', async(): Promise => { - const input = streamifyArray([ ' .' ]) as HttpRequest; - input.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' }; + input.request = streamifyArray([ ' .' ]) as HttpRequest; + input.request.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' }; const result = (await bodyParser.handle(input))!; expect(result).toEqual({ binary: true, - data: input, - metadata: expect.any(RepresentationMetadata), + data: input.request, + metadata: input.metadata, }); - expect(result.metadata.contentType).toEqual('text/turtle'); await expect(arrayifyStream(result.data)).resolves.toEqual( [ ' .' ], ); }); - - it('adds the slug header to the metadata.', async(): Promise => { - 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 => { - 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 => { - const input = {} as HttpRequest; - input.headers = { 'transfer-encoding': 'chunked', - 'content-type': 'text/turtle', - link: '; 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 => { - const input = {} as HttpRequest; - input.headers = { 'transfer-encoding': 'chunked', - 'content-type': 'text/turtle', - link: [ '', 'badLink' ]}; - const result = (await bodyParser.handle(input))!; - expect(result.metadata.quads()).toHaveLength(1); - expect(result.metadata.contentType).toEqual('text/turtle'); - }); }); diff --git a/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts b/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts index bb1307e185..26d7b44cef 100644 --- a/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts +++ b/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts @@ -1,43 +1,52 @@ import { namedNode, quad } from '@rdfjs/data-model'; -import arrayifyStream from 'arrayify-stream'; import { Algebra } from 'sparqlalgebrajs'; import * as algebra from 'sparqlalgebrajs'; import streamifyArray from 'streamify-array'; +import type { BodyParserArgs } from '../../../../src/ldp/http/BodyParser'; import { SparqlUpdateBodyParser } from '../../../../src/ldp/http/SparqlUpdateBodyParser'; +import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import type { HttpRequest } from '../../../../src/server/HttpRequest'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError'; +import { readableToString } from '../../../../src/util/Util'; describe('A SparqlUpdateBodyParser', (): void => { const bodyParser = new SparqlUpdateBodyParser(); + let input: BodyParserArgs; + + beforeEach(async(): Promise => { + input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata() }; + }); it('only accepts application/sparql-update content.', async(): Promise => { - await expect(bodyParser.canHandle({ headers: {}} as HttpRequest)).rejects.toThrow(UnsupportedMediaTypeHttpError); - await expect(bodyParser.canHandle({ headers: { 'content-type': 'text/plain' }} as HttpRequest)) - .rejects.toThrow(UnsupportedMediaTypeHttpError); - await expect(bodyParser.canHandle({ headers: { 'content-type': 'application/sparql-update' }} as HttpRequest)) - .resolves.toBeUndefined(); + await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError); + input.request.headers = { 'content-type': 'text/plain' }; + await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError); + input.request.headers = { 'content-type': 'application/sparql-update' }; + await expect(bodyParser.canHandle(input)).resolves.toBeUndefined(); }); it('errors when handling invalid SPARQL updates.', async(): Promise => { - await expect(bodyParser.handle(streamifyArray([ 'VERY INVALID UPDATE' ]) as HttpRequest)) - .rejects.toThrow(UnsupportedHttpError); + input.request = streamifyArray([ 'VERY INVALID UPDATE' ]) as HttpRequest; + await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError); }); it('errors when receiving an unexpected error.', async(): Promise => { const mock = jest.spyOn(algebra, 'translate').mockImplementationOnce((): any => { throw 'apple'; }); - await expect(bodyParser.handle(streamifyArray( - [ 'DELETE DATA { }' ], - ) as HttpRequest)).rejects.toThrow(UnsupportedHttpError); + input.request = streamifyArray( + [ 'DELETE DATA { }' ], + ) as HttpRequest; + await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError); mock.mockRestore(); }); it('converts SPARQL updates to algebra.', async(): Promise => { - const result = await bodyParser.handle(streamifyArray( - [ 'DELETE DATA { }' ], - ) as HttpRequest); + input.request = streamifyArray( + [ 'DELETE DATA { }' ], + ) as HttpRequest; + const result = await bodyParser.handle(input); expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT); expect((result.algebra as Algebra.DeleteInsert).delete).toBeRdfIsomorphic([ quad( namedNode('http://test.com/s'), @@ -45,11 +54,11 @@ describe('A SparqlUpdateBodyParser', (): void => { namedNode('http://test.com/o'), ) ]); 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 - expect((await arrayifyStream(result.data)).join('')).toEqual( - 'DELETE DATA { }', + expect(await readableToString(result.data)).toEqual( + 'DELETE DATA { }', ); }); });