From 4e4d7a7a399e2bc79dbf1dfff5a0a6abba2ede2a Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 27 Oct 2020 17:07:52 +0100 Subject: [PATCH] feat: Integrate MetadataSerializer --- config/presets/ldp/response-writer.json | 33 ++++++++++++++++++- index.ts | 3 ++ src/ldp/http/BasicResponseWriter.ts | 17 +++++----- test/configs/Util.ts | 26 +++++++++++---- .../AuthenticatedLdpHandler.test.ts | 4 +++ test/integration/FullConfig.acl.test.ts | 4 ++- test/integration/FullConfig.noAuth.test.ts | 11 +++++++ .../unit/ldp/http/BasicResponseWriter.test.ts | 25 ++++++++++++-- 8 files changed, 105 insertions(+), 18 deletions(-) diff --git a/config/presets/ldp/response-writer.json b/config/presets/ldp/response-writer.json index 6c26c73ed..778e23658 100644 --- a/config/presets/ldp/response-writer.json +++ b/config/presets/ldp/response-writer.json @@ -1,6 +1,34 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", "@graph": [ + { + "@id": "urn:solid-server:default:MetadataSerializer", + "@type": "AllVoidCompositeHandler", + "AllVoidCompositeHandler:_handlers": [ + { + "@type": "MappedMetadataWriter", + "MappedMetadataWriter:_headerMap": [ + { + "MappedMetadataWriter:_headerMap_key": "http://www.w3.org/ns/ma-ont#format", + "MappedMetadataWriter:_headerMap_value": "content-type" + }, + { + "MappedMetadataWriter:_headerMap_key": "urn:solid:http:location", + "MappedMetadataWriter:_headerMap_value": "location" + } + ] + }, + { + "@type": "LinkRelMetadataWriter", + "LinkRelMetadataWriter:_headerMap": [ + { + "LinkRelMetadataWriter:_headerMap_key": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + "LinkRelMetadataWriter:_headerMap_value": "type" + } + ] + } + ] + }, { "@id": "urn:solid-server:default:ResponseWriter", "@type": "FirstCompositeHandler", @@ -9,7 +37,10 @@ "@type": "ErrorResponseWriter" }, { - "@type": "BasicResponseWriter" + "@type": "BasicResponseWriter", + "BasicResponseWriter:_metadataWriter": { + "@id": "urn:solid-server:default:MetadataSerializer" + } } ] } diff --git a/index.ts b/index.ts index 880e77241..198988855 100644 --- a/index.ts +++ b/index.ts @@ -17,9 +17,12 @@ 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/LinkRelMetadataWriter'; export * from './src/ldp/http/metadata/LinkTypeParser'; +export * from './src/ldp/http/metadata/MappedMetadataWriter'; export * from './src/ldp/http/metadata/MetadataExtractor'; export * from './src/ldp/http/metadata/MetadataParser'; +export * from './src/ldp/http/metadata/MetadataWriter'; export * from './src/ldp/http/metadata/SlugParser'; // LDP/HTTP/Response diff --git a/src/ldp/http/BasicResponseWriter.ts b/src/ldp/http/BasicResponseWriter.ts index 06500ad23..7da7272b0 100644 --- a/src/ldp/http/BasicResponseWriter.ts +++ b/src/ldp/http/BasicResponseWriter.ts @@ -2,7 +2,7 @@ import { getLoggerFor } from '../../logging/LogUtil'; import type { HttpResponse } from '../../server/HttpResponse'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; -import { HTTP } from '../../util/UriConstants'; +import type { MetadataWriter } from './metadata/MetadataWriter'; import type { ResponseDescription } from './response/ResponseDescription'; import { ResponseWriter } from './ResponseWriter'; @@ -11,6 +11,12 @@ import { ResponseWriter } from './ResponseWriter'; */ export class BasicResponseWriter extends ResponseWriter { protected readonly logger = getLoggerFor(this); + private readonly metadataWriter: MetadataWriter; + + public constructor(metadataWriter: MetadataWriter) { + super(); + this.metadataWriter = metadataWriter; + } public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise { if (input.result instanceof Error || input.result.metadata?.contentType === INTERNAL_QUADS) { @@ -20,13 +26,8 @@ export class BasicResponseWriter extends ResponseWriter { } public async handle(input: { response: HttpResponse; result: ResponseDescription }): Promise { - const location = input.result.metadata?.get(HTTP.location); - if (location) { - input.response.setHeader('location', location.value); - } - if (input.result.data) { - const contentType = input.result.metadata?.contentType ?? 'text/plain'; - input.response.setHeader('content-type', contentType); + if (input.result.metadata) { + await this.metadataWriter.handleSafe({ response: input.response, metadata: input.result.metadata }); } input.response.writeHead(input.result.statusCode); diff --git a/test/configs/Util.ts b/test/configs/Util.ts index 5c48e82ac..c407fc90d 100644 --- a/test/configs/Util.ts +++ b/test/configs/Util.ts @@ -11,20 +11,22 @@ import type { OperationHandler, } from '../../index'; import { - AcceptPreferenceParser, + AcceptPreferenceParser, AllVoidCompositeHandler, BasicMetadataExtractor, BasicRequestParser, BasicResponseWriter, BasicTargetExtractor, - FirstCompositeHandler, ContentTypeParser, DataAccessorBasedStore, DeleteOperationHandler, ErrorResponseWriter, + FirstCompositeHandler, GetOperationHandler, HeadOperationHandler, InMemoryDataAccessor, + LinkRelMetadataWriter, LinkTypeParser, + MappedMetadataWriter, MetadataController, PatchingStore, PatchOperationHandler, @@ -39,6 +41,7 @@ import { UrlContainerManager, WebAclAuthorizer, } from '../../index'; +import { CONTENT_TYPE, HTTP, RDF } from '../../src/util/UriConstants'; export const BASE = 'http://test.com'; @@ -118,12 +121,23 @@ export const getOperationHandler = (store: ResourceStore): OperationHandler => { return new FirstCompositeHandler(handlers); }; -export const getResponseWriter = (): ResponseWriter => - new FirstCompositeHandler<{ response: HttpResponse; result: ResponseDescription | Error }, void>([ - new ErrorResponseWriter(), - new BasicResponseWriter(), +export const getResponseWriter = (): ResponseWriter => { + const serializer = new AllVoidCompositeHandler([ + new MappedMetadataWriter({ + [CONTENT_TYPE]: 'content-type', + [HTTP.location]: 'location', + }), + new LinkRelMetadataWriter({ + [RDF.type]: 'type', + }), ]); + return new FirstCompositeHandler<{ response: HttpResponse; result: ResponseDescription | Error }, void>([ + new ErrorResponseWriter(), + new BasicResponseWriter(serializer), + ]); +}; + /** * Creates a BasicMetadataExtractor with parsers for content-type, slugs and link types. */ diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts index c61fbf1f0..d956eb936 100644 --- a/test/integration/AuthenticatedLdpHandler.test.ts +++ b/test/integration/AuthenticatedLdpHandler.test.ts @@ -2,6 +2,7 @@ import * as url from 'url'; import { namedNode, quad } from '@rdfjs/data-model'; import { Parser } from 'n3'; import type { MockResponse } from 'node-mocks-http'; +import { LDP } from '../../src/util/UriConstants'; import { BasicConfig } from '../configs/BasicConfig'; import { BasicHandlersConfig } from '../configs/BasicHandlersConfig'; import { call } from '../util/Util'; @@ -38,6 +39,7 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { expect(response._getData()).toContain( ' .', ); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); // DELETE response = await call(handler, requestUrl, 'DELETE', {}, []); @@ -104,6 +106,7 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { expect(response._getBuffer().toString()).toContain( ' .', ); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); const parser = new Parser(); const triples = parser.parse(response._getBuffer().toString()); expect(triples).toBeRdfIsomorphic([ @@ -164,6 +167,7 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { [], ); expect(response.statusCode).toBe(200); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); const parser = new Parser(); const triples = parser.parse(response._getData()); expect(triples).toBeRdfIsomorphic([ diff --git a/test/integration/FullConfig.acl.test.ts b/test/integration/FullConfig.acl.test.ts index 5f7ecf430..44a28bc41 100644 --- a/test/integration/FullConfig.acl.test.ts +++ b/test/integration/FullConfig.acl.test.ts @@ -6,7 +6,7 @@ import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor'; import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor'; import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper'; import { MetadataController } from '../../src/util/MetadataController'; -import { CONTENT_TYPE } from '../../src/util/UriConstants'; +import { CONTENT_TYPE, LDP } from '../../src/util/UriConstants'; import { ensureTrailingSlash } from '../../src/util/Util'; import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig'; import type { ServerConfig } from '../configs/ServerConfig'; @@ -67,6 +67,7 @@ describe.each([ dataAccessorStore, inMemoryDataAccessorStore ])('A server using response = await fileHelper.getFile(id); expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain('TESTFILE2'); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); // DELETE file await fileHelper.deleteResource(id); @@ -95,6 +96,7 @@ describe.each([ dataAccessorStore, inMemoryDataAccessorStore ])('A server using // GET permanent file response = await fileHelper.getFile('http://test.com/permanent.txt'); expect(response._getBuffer().toString()).toContain('TEST'); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); // Try to delete permanent file response = await fileHelper.deleteResource('http://test.com/permanent.txt', true); diff --git a/test/integration/FullConfig.noAuth.test.ts b/test/integration/FullConfig.noAuth.test.ts index 7c972d2b0..677a921ee 100644 --- a/test/integration/FullConfig.noAuth.test.ts +++ b/test/integration/FullConfig.noAuth.test.ts @@ -5,6 +5,7 @@ import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor'; import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor'; import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper'; import { MetadataController } from '../../src/util/MetadataController'; +import { LDP } from '../../src/util/UriConstants'; import { DataAccessorBasedConfig } from '../configs/DataAccessorBasedConfig'; import type { ServerConfig } from '../configs/ServerConfig'; import { BASE, getRootFilePath } from '../configs/Util'; @@ -52,6 +53,7 @@ describe.each(configs)('A server using a %s', (name, configFn): void => { response = await fileHelper.getFile(id); expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain('TESTFILE0'); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); // DELETE await fileHelper.deleteResource(id); @@ -66,6 +68,7 @@ describe.each(configs)('A server using a %s', (name, configFn): void => { response = await fileHelper.getFile(id); expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain('TESTFILE0'); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); // PUT response = await fileHelper.overwriteFile('../assets/testfile1.txt', id, 'text/plain'); @@ -74,6 +77,7 @@ describe.each(configs)('A server using a %s', (name, configFn): void => { response = await fileHelper.getFile(id); expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain('TESTFILE1'); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); // DELETE await fileHelper.deleteResource(id); @@ -88,6 +92,9 @@ describe.each(configs)('A server using a %s', (name, configFn): void => { // GET response = await fileHelper.getFolder(id); expect(response.statusCode).toBe(200); + expect(response.getHeaders().link).toEqual( + [ `<${LDP.Container}>; rel="type"`, `<${LDP.BasicContainer}>; rel="type"`, `<${LDP.Resource}>; rel="type"` ], + ); // DELETE await fileHelper.deleteResource(id); @@ -105,6 +112,7 @@ describe.each(configs)('A server using a %s', (name, configFn): void => { // GET File response = await fileHelper.getFile(id); expect(response.statusCode).toBe(200); + expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); // DELETE await fileHelper.deleteResource(id); @@ -171,6 +179,9 @@ describe.each(configs)('A server using a %s', (name, configFn): void => { expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain(' .'); expect(response._getBuffer().toString()).toContain(' .'); + expect(response.getHeaders().link).toEqual( + [ `<${LDP.Container}>; rel="type"`, `<${LDP.BasicContainer}>; rel="type"`, `<${LDP.Resource}>; rel="type"` ], + ); // DELETE await fileHelper.deleteResource(fileId); diff --git a/test/unit/ldp/http/BasicResponseWriter.test.ts b/test/unit/ldp/http/BasicResponseWriter.test.ts index bbd139db4..90a0abf80 100644 --- a/test/unit/ldp/http/BasicResponseWriter.test.ts +++ b/test/unit/ldp/http/BasicResponseWriter.test.ts @@ -3,22 +3,33 @@ import type { MockResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http'; import streamifyArray from 'streamify-array'; import { BasicResponseWriter } from '../../../../src/ldp/http/BasicResponseWriter'; +import type { MetadataWriter } from '../../../../src/ldp/http/metadata/MetadataWriter'; import type { ResponseDescription } from '../../../../src/ldp/http/response/ResponseDescription'; +import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; +import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; +import { CONTENT_TYPE } from '../../../../src/util/UriConstants'; +import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler'; describe('A BasicResponseWriter', (): void => { - const writer = new BasicResponseWriter(); + let metadataWriter: MetadataWriter; + let writer: BasicResponseWriter; let response: MockResponse; let result: ResponseDescription; beforeEach(async(): Promise => { + metadataWriter = new StaticAsyncHandler(true, undefined); + writer = new BasicResponseWriter(metadataWriter); response = createResponse({ eventEmitter: EventEmitter }); result = { statusCode: 201 }; }); - it('requires the input to be a ResponseDescription.', async(): Promise => { + it('requires the input to be a binary ResponseDescription.', async(): Promise => { await expect(writer.canHandle({ response, result: new Error('error') })) .rejects.toThrow(UnsupportedHttpError); + const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: INTERNAL_QUADS }); + await expect(writer.canHandle({ response, result: { statusCode: 201, metadata }})) + .rejects.toThrow(UnsupportedHttpError); await expect(writer.canHandle({ response, result })) .resolves.toBeUndefined(); }); @@ -45,4 +56,14 @@ describe('A BasicResponseWriter', (): void => { await writer.handle({ response, result }); await end; }); + + it('serializes metadata if there is metadata.', async(): Promise => { + result = { statusCode: 201, metadata: new RepresentationMetadata() }; + metadataWriter.handle = jest.fn(); + await writer.handle({ response, result }); + expect(metadataWriter.handle).toHaveBeenCalledTimes(1); + expect(metadataWriter.handle).toHaveBeenLastCalledWith({ response, metadata: result.metadata }); + expect(response._isEndCalled()).toBeTruthy(); + expect(response._getStatusCode()).toBe(201); + }); });