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:
Joachim Van Herwegen
2021-09-24 15:49:56 +02:00
parent 8f5d61911d
commit bb7e88b137
28 changed files with 483 additions and 574 deletions

View File

@@ -0,0 +1,78 @@
import { CredentialGroup } from '../../../src/authentication/Credentials';
import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
import type { Authorizer } from '../../../src/authorization/Authorizer';
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
import type { Operation } from '../../../src/ldp/operations/Operation';
import type { ModesExtractor } from '../../../src/ldp/permissions/ModesExtractor';
import { AccessMode } from '../../../src/ldp/permissions/Permissions';
import { AuthorizingHttpHandler } from '../../../src/server/AuthorizingHttpHandler';
import type { HttpRequest } from '../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../src/server/HttpResponse';
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
describe('An AuthorizingHttpHandler', (): void => {
const credentials = { [CredentialGroup.public]: {}};
const modes = new Set([ AccessMode.read ]);
const permissionSet = { [CredentialGroup.public]: { read: true }};
const request: HttpRequest = {} as any;
const response: HttpResponse = {} as any;
let operation: Operation;
let credentialsExtractor: jest.Mocked<CredentialsExtractor>;
let modesExtractor: jest.Mocked<ModesExtractor>;
let permissionReader: jest.Mocked<PermissionReader>;
let authorizer: jest.Mocked<Authorizer>;
let source: jest.Mocked<OperationHttpHandler>;
let handler: AuthorizingHttpHandler;
beforeEach(async(): Promise<void> => {
operation = {
target: { path: 'http://test.com/foo' },
method: 'GET',
preferences: {},
};
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;
source = {
handleSafe: jest.fn(),
} as any;
handler = new AuthorizingHttpHandler(
{ credentialsExtractor, modesExtractor, permissionReader, authorizer, operationHandler: source },
);
});
it('goes through all the steps and calls the source.', async(): Promise<void> => {
await expect(handler.handle({ request, response, operation })).resolves.toBeUndefined();
expect(credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
expect(modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
expect(permissionReader.handleSafe).toHaveBeenCalledTimes(1);
expect(permissionReader.handleSafe).toHaveBeenLastCalledWith({ credentials, identifier: operation.target });
expect(authorizer.handleSafe).toHaveBeenCalledTimes(1);
expect(authorizer.handleSafe)
.toHaveBeenLastCalledWith({ credentials, identifier: operation.target, modes, permissionSet });
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(source.handleSafe).toHaveBeenLastCalledWith({ request, response, operation });
expect(operation.permissionSet).toBe(permissionSet);
});
it('errors if authorization fails.', async(): Promise<void> => {
const error = new ForbiddenHttpError();
authorizer.handleSafe.mockRejectedValueOnce(error);
await expect(handler.handle({ request, response, operation })).rejects.toThrow(error);
expect(source.handleSafe).toHaveBeenCalledTimes(0);
});
});

View File

@@ -1,65 +1,75 @@
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
import type { RequestParser } from '../../../src/ldp/http/RequestParser';
import { OkResponseDescription } from '../../../src/ldp/http/response/OkResponseDescription';
import { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
import type { OperationMetadataCollector } from '../../../src/ldp/operations/metadata/OperationMetadataCollector';
import type { Operation } from '../../../src/ldp/operations/Operation';
import type { BaseHttpHandlerArgs } from '../../../src/server/BaseHttpHandler';
import { BaseHttpHandler } from '../../../src/server/BaseHttpHandler';
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../src/server/HttpResponse';
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
import { ParsingHttpHandler } from '../../../src/server/ParsingHttpHandler';
class DummyHttpHandler extends BaseHttpHandler {
public constructor(args: BaseHttpHandlerArgs) {
super(args);
}
public async handleOperation(): Promise<ResponseDescription | undefined> {
return undefined;
}
}
describe('A BaseHttpHandler', (): void => {
describe('A ParsingHttpHandler', (): void => {
const request: HttpRequest = {} as any;
const response: HttpResponse = {} as any;
const preferences = { type: { 'text/html': 1 }};
const operation: Operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences };
const errorResponse = new ResponseDescription(400);
let requestParser: jest.Mocked<RequestParser>;
let metadataCollector: jest.Mocked<OperationMetadataCollector>;
let errorHandler: jest.Mocked<ErrorHandler>;
let responseWriter: jest.Mocked<ResponseWriter>;
let handler: jest.Mocked<DummyHttpHandler>;
let source: jest.Mocked<OperationHttpHandler>;
let handler: ParsingHttpHandler;
beforeEach(async(): Promise<void> => {
requestParser = { handleSafe: jest.fn().mockResolvedValue(operation) } as any;
metadataCollector = { handleSafe: jest.fn() } as any;
errorHandler = { handleSafe: jest.fn().mockResolvedValue(errorResponse) } as any;
responseWriter = { handleSafe: jest.fn() } as any;
handler = new DummyHttpHandler({ requestParser, errorHandler, responseWriter }) as any;
handler.handleOperation = jest.fn();
source = {
handleSafe: jest.fn(),
} as any;
handler = new ParsingHttpHandler(
{ requestParser, metadataCollector, errorHandler, responseWriter, operationHandler: source },
);
});
it('calls the handleOperation function with the generated operation.', async(): Promise<void> => {
it('calls the source with the generated operation.', async(): Promise<void> => {
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(handler.handleOperation).toHaveBeenCalledTimes(1);
expect(handler.handleOperation).toHaveBeenLastCalledWith(operation, request, response);
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(source.handleSafe).toHaveBeenLastCalledWith({ operation, request, response });
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0);
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
});
it('calls the responseWriter if there is a response.', async(): Promise<void> => {
const result = new ResponseDescription(200);
handler.handleOperation.mockResolvedValueOnce(result);
source.handleSafe.mockResolvedValueOnce(result);
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(handler.handleOperation).toHaveBeenCalledTimes(1);
expect(handler.handleOperation).toHaveBeenLastCalledWith(operation, request, response);
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(source.handleSafe).toHaveBeenLastCalledWith({ operation, request, response });
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0);
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(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);
source.handleSafe.mockResolvedValueOnce(okResult);
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(metadataCollector.handleSafe).toHaveBeenCalledTimes(1);
expect(metadataCollector.handleSafe).toHaveBeenLastCalledWith({ operation, metadata });
});
it('calls the error handler if something goes wrong.', async(): Promise<void> => {
const error = new Error('bad data');
handler.handleOperation.mockRejectedValueOnce(error);
source.handleSafe.mockRejectedValueOnce(error);
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });