From 011822e8593ca3079ab0d94273ff63a3b583c2de Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 7 Aug 2020 11:51:54 +0200 Subject: [PATCH] fix: Make SimpleResourceStore behaviour closer to expected --- .../operations/SimplePutOperationHandler.ts | 32 +++++++++++++++++++ src/storage/SimpleResourceStore.ts | 29 +++++++++++++---- .../SimpleDeleteOperationHandler.test.ts | 2 +- .../SimplePutOperationHandler.test.ts | 26 +++++++++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 src/ldp/operations/SimplePutOperationHandler.ts create mode 100644 test/unit/ldp/operations/SimplePutOperationHandler.test.ts diff --git a/src/ldp/operations/SimplePutOperationHandler.ts b/src/ldp/operations/SimplePutOperationHandler.ts new file mode 100644 index 000000000..3dc9bceb5 --- /dev/null +++ b/src/ldp/operations/SimplePutOperationHandler.ts @@ -0,0 +1,32 @@ +import { Operation } from './Operation'; +import { OperationHandler } from './OperationHandler'; +import { ResourceStore } from '../../storage/ResourceStore'; +import { ResponseDescription } from './ResponseDescription'; +import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; + +/** + * Handles PUT {@link Operation}s. + * Calls the setRepresentation function from a {@link ResourceStore}. + */ +export class SimplePutOperationHandler extends OperationHandler { + private readonly store: ResourceStore; + + public constructor(store: ResourceStore) { + super(); + this.store = store; + } + + public async canHandle(input: Operation): Promise { + if (input.method !== 'PUT') { + throw new UnsupportedHttpError('This handler only supports PUT operations.'); + } + if (typeof input.body !== 'object') { + throw new UnsupportedHttpError('PUT operations require a body.'); + } + } + + public async handle(input: Operation): Promise { + await this.store.setRepresentation(input.target, input.body!); + return { identifier: input.target }; + } +} diff --git a/src/storage/SimpleResourceStore.ts b/src/storage/SimpleResourceStore.ts index 57f547dd4..5ba58c7c6 100644 --- a/src/storage/SimpleResourceStore.ts +++ b/src/storage/SimpleResourceStore.ts @@ -42,10 +42,12 @@ export class SimpleResourceStore implements ResourceStore { */ public async addResource(container: ResourceIdentifier, representation: Representation): Promise { const containerPath = this.parseIdentifier(container); - const newPath = `${ensureTrailingSlash(containerPath)}${this.index}`; + this.checkPath(containerPath); + const newID = { path: `${ensureTrailingSlash(container.path)}${this.index}` }; + const newPath = this.parseIdentifier(newID); this.index += 1; this.store[newPath] = await this.copyRepresentation(representation); - return { path: `${this.base}${newPath}` }; + return newID; } /** @@ -54,6 +56,7 @@ export class SimpleResourceStore implements ResourceStore { */ public async deleteResource(identifier: ResourceIdentifier): Promise { const path = this.parseIdentifier(identifier); + this.checkPath(path); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.store[path]; } @@ -68,6 +71,7 @@ export class SimpleResourceStore implements ResourceStore { */ public async getRepresentation(identifier: ResourceIdentifier): Promise { const path = this.parseIdentifier(identifier); + this.checkPath(path); return this.generateRepresentation(path); } @@ -79,7 +83,7 @@ export class SimpleResourceStore implements ResourceStore { } /** - * Replaces the stored Representation with the new one for the given identifier. + * Puts the given data in the given location. * @param identifier - Identifier to replace. * @param representation - New Representation. */ @@ -89,22 +93,35 @@ export class SimpleResourceStore implements ResourceStore { } /** - * Strips the base from the identifier and checks if it is in the store. + * Strips the base from the identifier and checks if it is valid. * @param identifier - Incoming identifier. * * @throws {@link NotFoundHttpError} - * If the identifier is not in the store. + * If the identifier doesn't start with the base ID. * * @returns A string representing the relative path. */ private parseIdentifier(identifier: ResourceIdentifier): string { const path = identifier.path.slice(this.base.length); - if (!this.store[path] || !identifier.path.startsWith(this.base)) { + if (!identifier.path.startsWith(this.base)) { throw new NotFoundHttpError(); } return path; } + /** + * Checks if the relative path is in the store. + * @param identifier - Incoming identifier. + * + * @throws {@link NotFoundHttpError} + * If the path is not in the store. + */ + private checkPath(path: string): void { + if (!this.store[path]) { + throw new NotFoundHttpError(); + } + } + /** * Copies the Representation by draining the original data stream and creating a new one. * diff --git a/test/unit/ldp/operations/SimpleDeleteOperationHandler.test.ts b/test/unit/ldp/operations/SimpleDeleteOperationHandler.test.ts index 8c359289e..2fadd7160 100644 --- a/test/unit/ldp/operations/SimpleDeleteOperationHandler.test.ts +++ b/test/unit/ldp/operations/SimpleDeleteOperationHandler.test.ts @@ -11,7 +11,7 @@ describe('A SimpleDeleteOperationHandler', (): void => { store.deleteResource = jest.fn(async(): Promise => {}); }); - it('only supports GET operations.', async(): Promise => { + it('only supports DELETE operations.', async(): Promise => { await expect(handler.canHandle({ method: 'DELETE' } as Operation)).resolves.toBeUndefined(); await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError); }); diff --git a/test/unit/ldp/operations/SimplePutOperationHandler.test.ts b/test/unit/ldp/operations/SimplePutOperationHandler.test.ts new file mode 100644 index 000000000..2ddbf59a5 --- /dev/null +++ b/test/unit/ldp/operations/SimplePutOperationHandler.test.ts @@ -0,0 +1,26 @@ +import { Operation } from '../../../../src/ldp/operations/Operation'; +import { ResourceStore } from '../../../../src/storage/ResourceStore'; +import { SimplePutOperationHandler } from '../../../../src/ldp/operations/SimplePutOperationHandler'; +import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; + +describe('A SimplePutOperationHandler', (): void => { + const store = {} as unknown as ResourceStore; + const handler = new SimplePutOperationHandler(store); + beforeEach(async(): Promise => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + store.setRepresentation = jest.fn(async(): Promise => {}); + }); + + it('only supports PUT operations with a body.', async(): Promise => { + await expect(handler.canHandle({ method: 'PUT' } as Operation)).rejects.toThrow(UnsupportedHttpError); + await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError); + await expect(handler.canHandle({ method: 'PUT', body: { dataType: 'test' }} as Operation)).resolves.toBeUndefined(); + }); + + it('sets the representation in the store and returns its identifier.', async(): Promise => { + await expect(handler.handle({ target: { path: 'url' }, body: { dataType: 'test' }} as Operation)) + .resolves.toEqual({ identifier: { path: 'url' }}); + expect(store.setRepresentation).toHaveBeenCalledTimes(1); + expect(store.setRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, { dataType: 'test' }); + }); +});