mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Split HttpHandler behaviour over multiple classes
This allows easier reuse of certain reoccurring behaviours, such as authorization. The AuthenticatedLdpHandler is no longer required since it is a combination of parsing and authorization. This did require a small change to the OperationHandler interface.
This commit is contained in:
@@ -1,133 +0,0 @@
|
||||
import { CredentialGroup } from '../../../src/authentication/Credentials';
|
||||
import type { CredentialSet } from '../../../src/authentication/Credentials';
|
||||
import type { AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler';
|
||||
import { AuthenticatedLdpHandler } from '../../../src/ldp/AuthenticatedLdpHandler';
|
||||
import { OkResponseDescription } from '../../../src/ldp/http/response/OkResponseDescription';
|
||||
import { ResetResponseDescription } from '../../../src/ldp/http/response/ResetResponseDescription';
|
||||
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
||||
import type { Operation } from '../../../src/ldp/operations/Operation';
|
||||
import type { PermissionSet } from '../../../src/ldp/permissions/Permissions';
|
||||
import { AccessMode } from '../../../src/ldp/permissions/Permissions';
|
||||
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences';
|
||||
import * as LogUtil from '../../../src/logging/LogUtil';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||
|
||||
describe('An AuthenticatedLdpHandler', (): void => {
|
||||
const request: HttpRequest = {} as any;
|
||||
const response: HttpResponse = {} as any;
|
||||
const preferences: RepresentationPreferences = { type: { 'text/turtle': 0.9 }};
|
||||
let operation: Operation;
|
||||
const credentials: CredentialSet = {};
|
||||
const modes: Set<AccessMode> = new Set([ AccessMode.read ]);
|
||||
const permissionSet: PermissionSet = { [CredentialGroup.agent]: { read: true }};
|
||||
const result: ResponseDescription = new ResetResponseDescription();
|
||||
const errorResult: ResponseDescription = { statusCode: 500 };
|
||||
let args: AuthenticatedLdpHandlerArgs;
|
||||
let handler: AuthenticatedLdpHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = { target: { path: 'identifier' }, method: 'GET', preferences };
|
||||
args = {
|
||||
requestParser: {
|
||||
canHandle: jest.fn(),
|
||||
handleSafe: jest.fn().mockResolvedValue(operation),
|
||||
} as any,
|
||||
credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
|
||||
modesExtractor: { handleSafe: jest.fn().mockResolvedValue(modes) } as any,
|
||||
permissionReader: { handleSafe: jest.fn().mockResolvedValue(permissionSet) } as any,
|
||||
authorizer: { handleSafe: jest.fn() } as any,
|
||||
operationHandler: { handleSafe: jest.fn().mockResolvedValue(result) } as any,
|
||||
operationMetadataCollector: { handleSafe: jest.fn() } as any,
|
||||
errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any,
|
||||
responseWriter: { handleSafe: jest.fn() } as any,
|
||||
};
|
||||
handler = new AuthenticatedLdpHandler(args);
|
||||
});
|
||||
|
||||
it('can be created.', async(): Promise<void> => {
|
||||
expect(new AuthenticatedLdpHandler(args)).toBeInstanceOf(AuthenticatedLdpHandler);
|
||||
});
|
||||
|
||||
it('can not handle the input if the RequestParser rejects it.', async(): Promise<void> => {
|
||||
(args.requestParser.canHandle as jest.Mock).mockRejectedValueOnce(new Error('bad data!'));
|
||||
await expect(handler.canHandle({ request, response })).rejects.toThrow('bad data!');
|
||||
expect(args.requestParser.canHandle).toHaveBeenLastCalledWith(request);
|
||||
});
|
||||
|
||||
it('can handle the input if the RequestParser can handle it.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.requestParser.canHandle).toHaveBeenCalledTimes(1);
|
||||
expect(args.requestParser.canHandle).toHaveBeenLastCalledWith(request);
|
||||
});
|
||||
|
||||
it('can handle input.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.requestParser.handleSafe).toHaveBeenLastCalledWith(request);
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
|
||||
expect(args.modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||
expect(args.permissionReader.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.permissionReader.handleSafe).toHaveBeenLastCalledWith({ credentials, identifier: operation.target });
|
||||
expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.authorizer.handleSafe)
|
||||
.toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, modes, permissionSet });
|
||||
expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
|
||||
});
|
||||
|
||||
it('calls the operation metadata collector if there is response metadata.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata();
|
||||
const okResult = new OkResponseDescription(metadata);
|
||||
(args.operationHandler.handleSafe as jest.Mock).mockResolvedValueOnce(okResult);
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenLastCalledWith({ operation, metadata });
|
||||
});
|
||||
|
||||
it('sets preferences to text/plain in case of an error during request parsing.', async(): Promise<void> => {
|
||||
const error = new Error('bad request!');
|
||||
(args.requestParser.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
|
||||
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResult });
|
||||
});
|
||||
|
||||
it('sets preferences to the request preferences if they were parsed before the error.', async(): Promise<void> => {
|
||||
const error = new Error('bad request!');
|
||||
(args.credentialsExtractor.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
|
||||
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResult });
|
||||
});
|
||||
|
||||
it('logs an error if authorization failed.', async(): Promise<void> => {
|
||||
const logger = { verbose: jest.fn() };
|
||||
const mock = jest.spyOn(LogUtil, 'getLoggerFor');
|
||||
mock.mockReturnValueOnce(logger as any);
|
||||
handler = new AuthenticatedLdpHandler(args);
|
||||
(args.authorizer.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad auth!'));
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(logger.verbose).toHaveBeenLastCalledWith('Authorization failed: bad auth!');
|
||||
|
||||
mock.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -5,22 +5,25 @@ import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
|
||||
describe('A DeleteOperationHandler', (): void => {
|
||||
let operation: Operation;
|
||||
const conditions = new BasicConditions({});
|
||||
const store = {} as unknown as ResourceStore;
|
||||
const handler = new DeleteOperationHandler(store);
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = { method: 'DELETE', target: { path: 'http://test.com/foo' }, preferences: {}, conditions };
|
||||
store.deleteResource = jest.fn(async(): Promise<any> => undefined);
|
||||
});
|
||||
|
||||
it('only supports DELETE operations.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ method: 'DELETE' } as Operation)).resolves.toBeUndefined();
|
||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
||||
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||
operation.method = 'GET';
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
|
||||
const result = await handler.handle({ target: { path: 'url' }, conditions } as Operation);
|
||||
const result = await handler.handle({ operation });
|
||||
expect(store.deleteResource).toHaveBeenCalledTimes(1);
|
||||
expect(store.deleteResource).toHaveBeenLastCalledWith({ path: 'url' }, conditions);
|
||||
expect(store.deleteResource).toHaveBeenLastCalledWith(operation.target, conditions);
|
||||
expect(result.statusCode).toBe(205);
|
||||
expect(result.metadata).toBeUndefined();
|
||||
expect(result.data).toBeUndefined();
|
||||
|
||||
@@ -6,12 +6,14 @@ import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
|
||||
describe('A GetOperationHandler', (): void => {
|
||||
let operation: Operation;
|
||||
const conditions = new BasicConditions({});
|
||||
const preferences = {};
|
||||
let store: ResourceStore;
|
||||
let handler: GetOperationHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences, conditions };
|
||||
store = {
|
||||
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
||||
({ binary: false, data: 'data', metadata: 'metadata' } as any)),
|
||||
@@ -21,16 +23,17 @@ describe('A GetOperationHandler', (): void => {
|
||||
});
|
||||
|
||||
it('only supports GET operations.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).resolves.toBeUndefined();
|
||||
await expect(handler.canHandle({ method: 'POST' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
||||
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||
operation.method = 'POST';
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('returns the representation from the store with the correct response.', async(): Promise<void> => {
|
||||
const result = await handler.handle({ target: { path: 'url' }, preferences, conditions } as Operation);
|
||||
const result = await handler.handle({ operation });
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.metadata).toBe('metadata');
|
||||
expect(result.data).toBe('data');
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
||||
expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
|
||||
describe('A HeadOperationHandler', (): void => {
|
||||
let operation: Operation;
|
||||
const conditions = new BasicConditions({});
|
||||
const preferences = {};
|
||||
let store: ResourceStore;
|
||||
@@ -14,6 +15,7 @@ describe('A HeadOperationHandler', (): void => {
|
||||
let data: Readable;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = { method: 'HEAD', target: { path: 'http://test.com/foo' }, preferences, conditions };
|
||||
data = { destroy: jest.fn() } as any;
|
||||
store = {
|
||||
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
||||
@@ -24,18 +26,20 @@ describe('A HeadOperationHandler', (): void => {
|
||||
});
|
||||
|
||||
it('only supports HEAD operations.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ method: 'HEAD' } as Operation)).resolves.toBeUndefined();
|
||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
||||
await expect(handler.canHandle({ method: 'POST' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
||||
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||
operation.method = 'GET';
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||
operation.method = 'POST';
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('returns the representation from the store with the correct response.', async(): Promise<void> => {
|
||||
const result = await handler.handle({ target: { path: 'url' }, preferences, conditions } as Operation);
|
||||
const result = await handler.handle({ operation });
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.metadata).toBe('metadata');
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(data.destroy).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
||||
expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +1,41 @@
|
||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||
import { PatchOperationHandler } from '../../../../src/ldp/operations/PatchOperationHandler';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
|
||||
describe('A PatchOperationHandler', (): void => {
|
||||
let operation: Operation;
|
||||
let body: Representation;
|
||||
const conditions = new BasicConditions({});
|
||||
const store = {} as unknown as ResourceStore;
|
||||
const handler = new PatchOperationHandler(store);
|
||||
beforeEach(async(): Promise<void> => {
|
||||
body = new BasicRepresentation('', 'text/turtle');
|
||||
operation = { method: 'PATCH', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
||||
store.modifyResource = jest.fn(async(): Promise<any> => undefined);
|
||||
});
|
||||
|
||||
it('only supports PATCH operations.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ method: 'PATCH' } as Operation)).resolves.toBeUndefined();
|
||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
||||
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||
operation.method = 'GET';
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
|
||||
await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
|
||||
.rejects.toThrow(BadRequestHttpError);
|
||||
operation.body!.metadata.contentType = undefined;
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||
delete operation.body;
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||
});
|
||||
|
||||
it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata('text/turtle');
|
||||
const result = await handler.handle({ target: { path: 'url' }, body: { metadata }, conditions } as Operation);
|
||||
const result = await handler.handle({ operation });
|
||||
expect(store.modifyResource).toHaveBeenCalledTimes(1);
|
||||
expect(store.modifyResource).toHaveBeenLastCalledWith({ path: 'url' }, { metadata }, conditions);
|
||||
expect(store.modifyResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
||||
expect(result.statusCode).toBe(205);
|
||||
expect(result.metadata).toBeUndefined();
|
||||
expect(result.data).toBeUndefined();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||
import { PostOperationHandler } from '../../../../src/ldp/operations/PostOperationHandler';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
||||
@@ -9,11 +11,15 @@ import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplemen
|
||||
import { SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
||||
|
||||
describe('A PostOperationHandler', (): void => {
|
||||
let operation: Operation;
|
||||
let body: Representation;
|
||||
const conditions = new BasicConditions({});
|
||||
let store: ResourceStore;
|
||||
let handler: PostOperationHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
body = new BasicRepresentation('', 'text/turtle');
|
||||
operation = { method: 'POST', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
||||
store = {
|
||||
addResource: jest.fn(async(): Promise<ResourceIdentifier> => ({ path: 'newPath' } as ResourceIdentifier)),
|
||||
} as unknown as ResourceStore;
|
||||
@@ -21,28 +27,25 @@ describe('A PostOperationHandler', (): void => {
|
||||
});
|
||||
|
||||
it('only supports POST operations.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ method: 'POST', body: { }} as Operation))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(handler.canHandle({ method: 'GET', body: { }} as Operation))
|
||||
.rejects.toThrow(NotImplementedHttpError);
|
||||
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||
operation.method = 'GET';
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
|
||||
await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
|
||||
.rejects.toThrow(BadRequestHttpError);
|
||||
operation.body!.metadata.contentType = undefined;
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||
delete operation.body;
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||
});
|
||||
|
||||
it('adds the given representation to the store and returns the correct response.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata('text/turtle');
|
||||
const result = await handler.handle(
|
||||
{ method: 'POST', target: { path: 'url' }, body: { metadata }, conditions } as Operation,
|
||||
);
|
||||
const result = await handler.handle({ operation });
|
||||
expect(result.statusCode).toBe(201);
|
||||
expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
|
||||
expect(result.metadata?.get(SOLID_HTTP.location)?.value).toBe('newPath');
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(store.addResource).toHaveBeenCalledTimes(1);
|
||||
expect(store.addResource).toHaveBeenLastCalledWith({ path: 'url' }, { metadata }, conditions);
|
||||
expect(store.addResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||
import { PutOperationHandler } from '../../../../src/ldp/operations/PutOperationHandler';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
|
||||
describe('A PutOperationHandler', (): void => {
|
||||
let operation: Operation;
|
||||
let body: Representation;
|
||||
const conditions = new BasicConditions({});
|
||||
const store = {} as unknown as ResourceStore;
|
||||
const handler = new PutOperationHandler(store);
|
||||
beforeEach(async(): Promise<void> => {
|
||||
body = new BasicRepresentation('', 'text/turtle');
|
||||
operation = { method: 'PUT', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
store.setRepresentation = jest.fn(async(): Promise<any> => {});
|
||||
});
|
||||
|
||||
it('only supports PUT operations.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
||||
await expect(handler.canHandle({ method: 'PUT' } as Operation)).resolves.toBeUndefined();
|
||||
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||
operation.method = 'GET';
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
|
||||
await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
|
||||
.rejects.toThrow(BadRequestHttpError);
|
||||
operation.body!.metadata.contentType = undefined;
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||
delete operation.body;
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||
});
|
||||
|
||||
it('sets the representation in the store and returns the correct response.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata('text/turtle');
|
||||
const result = await handler.handle({ target: { path: 'url' }, body: { metadata }, conditions } as Operation);
|
||||
const result = await handler.handle({ operation });
|
||||
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.setRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, { metadata }, conditions);
|
||||
expect(store.setRepresentation).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
||||
expect(result.statusCode).toBe(205);
|
||||
expect(result.metadata).toBeUndefined();
|
||||
expect(result.data).toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user