fix: Make SimpleResourceStore behaviour closer to expected

This commit is contained in:
Joachim Van Herwegen 2020-08-07 11:51:54 +02:00
parent 4403421c49
commit 011822e859
4 changed files with 82 additions and 7 deletions

View File

@ -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<void> {
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<ResponseDescription> {
await this.store.setRepresentation(input.target, input.body!);
return { identifier: input.target };
}
}

View File

@ -42,10 +42,12 @@ export class SimpleResourceStore implements ResourceStore {
*/ */
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> { public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
const containerPath = this.parseIdentifier(container); 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.index += 1;
this.store[newPath] = await this.copyRepresentation(representation); 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<void> { public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
const path = this.parseIdentifier(identifier); const path = this.parseIdentifier(identifier);
this.checkPath(path);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.store[path]; delete this.store[path];
} }
@ -68,6 +71,7 @@ export class SimpleResourceStore implements ResourceStore {
*/ */
public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> { public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
const path = this.parseIdentifier(identifier); const path = this.parseIdentifier(identifier);
this.checkPath(path);
return this.generateRepresentation(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 identifier - Identifier to replace.
* @param representation - New Representation. * @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. * @param identifier - Incoming identifier.
* *
* @throws {@link NotFoundHttpError} * @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. * @returns A string representing the relative path.
*/ */
private parseIdentifier(identifier: ResourceIdentifier): string { private parseIdentifier(identifier: ResourceIdentifier): string {
const path = identifier.path.slice(this.base.length); 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(); throw new NotFoundHttpError();
} }
return path; 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. * Copies the Representation by draining the original data stream and creating a new one.
* *

View File

@ -11,7 +11,7 @@ describe('A SimpleDeleteOperationHandler', (): void => {
store.deleteResource = jest.fn(async(): Promise<void> => {}); store.deleteResource = jest.fn(async(): Promise<void> => {});
}); });
it('only supports GET operations.', async(): Promise<void> => { it('only supports DELETE operations.', async(): Promise<void> => {
await expect(handler.canHandle({ method: 'DELETE' } as Operation)).resolves.toBeUndefined(); await expect(handler.canHandle({ method: 'DELETE' } as Operation)).resolves.toBeUndefined();
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError); await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError);
}); });

View File

@ -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<void> => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
store.setRepresentation = jest.fn(async(): Promise<void> => {});
});
it('only supports PUT operations with a body.', async(): Promise<void> => {
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<void> => {
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' });
});
});