Repository: solid/community-server
Pull request #39 (2020-07-24 07:42:19)

fix: Enable strict TypeScript settings
Sets TypeScript to strict

Biggest changes:
 * I made the `webId` field in `Credentials` optional. This way the `CredentialsExtractor` always returns a `Credentials` object, even if no credentials were found. (Instead of returning undefined in that case).
 * `BodyParser` now returns `Representation | undefined`. Not convinced that this is the best solution, but it is what is needed for now since the `SimpleBodyParser` also handles the case of there being no body. Perhaps this should be caught in the `RequestParser` instead. Related to #8 .
 * There are some cases where a check that was in the `canHandle` function was needed again in the `handle` function to prevent accessing potential `undefined` data since some input fields are optional. E.g. the method field in a `HttpRequest`. Another option would be to leave behind that duplicate check and tell TypeScript to trust us that the data is there (since it should have been checked in `canHandle`). Not that big of an issue but might become a problem in the future if more advanced checks are needed.
 * The `!` operator was used in some cases to convince TypeScript that it shouldn't worry. Specifically in these cases:
    - In constructors where `Object.assign` was used to assign values to the class variables. The only way to get around that would be to assign them all manually which can get ugly if there are many. Related TypeScript issue with people complaining about this [here](https://github.com/microsoft/TypeScript/issues/26792) (so far it has not been fixed).
    - One specific instance where an object was made right before the loop going over its keys to have cleaner code. Having a check there seems unnecessary.
    - Some tests since it just makes some things easier there.

RubenVerborgh: Excellent!

>     * `BodyParser` now returns `Representation | undefined`. Not convinced that this is the best solution, but it is what is needed for now since the `SimpleBodyParser` also handles the case of there being no body.

I wonder if we want an `EmptyRepresentation` singleton.

Patch:
diff --git a/bin/server.ts b/bin/server.ts
index cce5161ad..152943d45 100644
--- a/bin/server.ts
+++ b/bin/server.ts
@@ -37,7 +37,7 @@ const { argv } = yargs
 const { port } = argv;
 
 // This is instead of the dependency injection that still needs to be added
-const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation>([
+const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
   new SimpleBodyParser(),
   new SimpleSparqlUpdateBodyParser(),
 ]);
diff --git a/src/authentication/Credentials.ts b/src/authentication/Credentials.ts
index 696f56832..bae6d0ba9 100644
--- a/src/authentication/Credentials.ts
+++ b/src/authentication/Credentials.ts
@@ -2,5 +2,5 @@
  * Credentials identifying an entity accessing or owning data.
  */
 export interface Credentials {
-  webID: string;
+  webID?: string;
 }
diff --git a/src/authentication/SimpleCredentialsExtractor.ts b/src/authentication/SimpleCredentialsExtractor.ts
index 95379ee47..1909e60f7 100644
--- a/src/authentication/SimpleCredentialsExtractor.ts
+++ b/src/authentication/SimpleCredentialsExtractor.ts
@@ -14,5 +14,6 @@ export class SimpleCredentialsExtractor extends CredentialsExtractor {
     if (input.headers.authorization) {
       return { webID: input.headers.authorization };
     }
+    return {};
   }
 }
diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts
index 52aced3d8..fc4f162c7 100644
--- a/src/ldp/AuthenticatedLdpHandler.ts
+++ b/src/ldp/AuthenticatedLdpHandler.ts
@@ -46,12 +46,12 @@ export interface AuthenticatedLdpHandlerArgs {
  * The central manager that connects all the necessary handlers to go from an incoming request to an executed operation.
  */
 export class AuthenticatedLdpHandler extends HttpHandler {
-  private readonly requestParser: RequestParser;
-  private readonly credentialsExtractor: CredentialsExtractor;
-  private readonly permissionsExtractor: PermissionsExtractor;
-  private readonly authorizer: Authorizer;
-  private readonly operationHandler: OperationHandler;
-  private readonly responseWriter: ResponseWriter;
+  private readonly requestParser!: RequestParser;
+  private readonly credentialsExtractor!: CredentialsExtractor;
+  private readonly permissionsExtractor!: PermissionsExtractor;
+  private readonly authorizer!: Authorizer;
+  private readonly operationHandler!: OperationHandler;
+  private readonly responseWriter!: ResponseWriter;
 
   /**
    * Creates the handler.
@@ -87,17 +87,14 @@ export class AuthenticatedLdpHandler extends HttpHandler {
    * @returns A promise resolving when the handling is finished.
    */
   public async handle(input: { request: HttpRequest; response: HttpResponse }): Promise<void> {
-    let err: Error;
-    let description: ResponseDescription;
+    let writeData: { response: HttpResponse; result: ResponseDescription | Error };
 
     try {
-      description = await this.runHandlers(input.request);
+      writeData = { response: input.response, result: await this.runHandlers(input.request) };
     } catch (error) {
-      err = error;
+      writeData = { response: input.response, result: error };
     }
 
-    const writeData = { response: input.response, description, error: err };
-
     await this.responseWriter.handleSafe(writeData);
   }
 
diff --git a/src/ldp/http/AcceptPreferenceParser.ts b/src/ldp/http/AcceptPreferenceParser.ts
index 2f61a20c3..09912818d 100644
--- a/src/ldp/http/AcceptPreferenceParser.ts
+++ b/src/ldp/http/AcceptPreferenceParser.ts
@@ -25,14 +25,14 @@ export class AcceptPreferenceParser extends PreferenceParser {
 
   public async handle(input: HttpRequest): Promise<RepresentationPreferences> {
     const result: RepresentationPreferences = {};
-    const headers: { [T in keyof RepresentationPreferences]: { val: string; func: (input: string) => AcceptHeader[] }} = {
+    const headers: { [T in keyof RepresentationPreferences]: { val?: string; func: (input: string) => AcceptHeader[] }} = {
       type: { val: input.headers.accept, func: parseAccept },
       charset: { val: input.headers['accept-charset'] as string, func: parseAcceptCharset },
       encoding: { val: input.headers['accept-encoding'] as string, func: parseAcceptEncoding },
       language: { val: input.headers['accept-language'], func: parseAcceptLanguage },
     };
-    Object.keys(headers).forEach((key: keyof RepresentationPreferences): void => {
-      const preferences = this.parseHeader(headers[key].val, headers[key].func);
+    (Object.keys(headers) as (keyof RepresentationPreferences)[]).forEach((key): void => {
+      const preferences = this.parseHeader(headers[key]!.val, headers[key]!.func);
       if (preferences.length > 0) {
         result[key] = preferences;
       }
@@ -53,7 +53,7 @@ export class AcceptPreferenceParser extends PreferenceParser {
    *
    * @returns A list of {@link RepresentationPreference}. Returns an empty list if input was not defined.
    */
-  private parseHeader(input: string, parseFunction: (input: string) => AcceptHeader[]): RepresentationPreference[] {
+  private parseHeader(input: string | undefined, parseFunction: (input: string) => AcceptHeader[]): RepresentationPreference[] {
     if (!input) {
       return [];
     }
diff --git a/src/ldp/http/BodyParser.ts b/src/ldp/http/BodyParser.ts
index 95694f406..c810b354f 100644
--- a/src/ldp/http/BodyParser.ts
+++ b/src/ldp/http/BodyParser.ts
@@ -5,4 +5,4 @@ import { Representation } from '../representation/Representation';
 /**
  * Parses the body of an incoming {@link HttpRequest} and converts it to a {@link Representation}.
  */
-export abstract class BodyParser extends AsyncHandler<HttpRequest, Representation> {}
+export abstract class BodyParser extends AsyncHandler<HttpRequest, Representation | undefined> {}
diff --git a/src/ldp/http/ResponseWriter.ts b/src/ldp/http/ResponseWriter.ts
index ac5bed3d8..9dcee6c51 100644
--- a/src/ldp/http/ResponseWriter.ts
+++ b/src/ldp/http/ResponseWriter.ts
@@ -6,4 +6,4 @@ import { ResponseDescription } from '../operations/ResponseDescription';
  * Writes to the HttpResponse.
  * Response depends on the operation result and potentially which errors was thrown.
  */
-export abstract class ResponseWriter extends AsyncHandler<{ response: HttpResponse; description?: ResponseDescription; error?: Error }> {}
+export abstract class ResponseWriter extends AsyncHandler<{ response: HttpResponse; result: ResponseDescription | Error }> {}
diff --git a/src/ldp/http/SimpleBodyParser.ts b/src/ldp/http/SimpleBodyParser.ts
index dd4dd661f..fe07b85b8 100644
--- a/src/ldp/http/SimpleBodyParser.ts
+++ b/src/ldp/http/SimpleBodyParser.ts
@@ -28,7 +28,9 @@ export class SimpleBodyParser extends BodyParser {
     }
   }
 
-  public async handle(input: HttpRequest): Promise<QuadRepresentation> {
+  // Note that the only reason this is a union is in case the body is empty.
+  // If this check gets moved away from the BodyParsers this union could be removed
+  public async handle(input: HttpRequest): Promise<QuadRepresentation | undefined> {
     const contentType = input.headers['content-type'];
 
     if (!contentType) {
diff --git a/src/ldp/http/SimpleRequestParser.ts b/src/ldp/http/SimpleRequestParser.ts
index 07beeb3b3..0d6edc56e 100644
--- a/src/ldp/http/SimpleRequestParser.ts
+++ b/src/ldp/http/SimpleRequestParser.ts
@@ -19,9 +19,9 @@ export interface SimpleRequestParserArgs {
  * of a {@link TargetExtractor}, {@link PreferenceParser}, and {@link BodyParser}.
  */
 export class SimpleRequestParser extends RequestParser {
-  private readonly targetExtractor: TargetExtractor;
-  private readonly preferenceParser: PreferenceParser;
-  private readonly bodyParser: BodyParser;
+  private readonly targetExtractor!: TargetExtractor;
+  private readonly preferenceParser!: PreferenceParser;
+  private readonly bodyParser!: BodyParser;
 
   public constructor(args: SimpleRequestParserArgs) {
     super();
@@ -42,6 +42,9 @@ export class SimpleRequestParser extends RequestParser {
     const preferences = await this.preferenceParser.handleSafe(input);
     const body = await this.bodyParser.handleSafe(input);
 
+    if (!input.method) {
+      throw new Error('Missing method.');
+    }
     return { method: input.method, target, preferences, body };
   }
 }
diff --git a/src/ldp/http/SimpleResponseWriter.ts b/src/ldp/http/SimpleResponseWriter.ts
index 83b5da2c4..bd812d22d 100644
--- a/src/ldp/http/SimpleResponseWriter.ts
+++ b/src/ldp/http/SimpleResponseWriter.ts
@@ -8,40 +8,38 @@ import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
  * Writes to an {@link HttpResponse} based on the incoming {@link ResponseDescription} or error.
  */
 export class SimpleResponseWriter extends ResponseWriter {
-  public async canHandle(input: { response: HttpResponse; description?: ResponseDescription; error?: Error }): Promise<void> {
-    if (!input.description && !input.error) {
-      throw new UnsupportedHttpError('Either a description or an error is required for output.');
-    }
-    if (input.description && input.description.body) {
-      if (input.description.body.dataType !== 'binary' && input.description.body.dataType !== 'string') {
+  public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise<void> {
+    if (!(input.result instanceof Error)) {
+      const dataType = input.result.body?.dataType;
+      if (dataType && dataType !== 'binary' && dataType !== 'string') {
         throw new UnsupportedHttpError('Only string or binary results are supported.');
       }
     }
   }
 
-  public async handle(input: { response: HttpResponse; description?: ResponseDescription; error?: Error }): Promise<void> {
-    if (input.description) {
-      input.response.setHeader('location', input.description.identifier.path);
-      if (input.description.body) {
-        const contentType = input.description.body.metadata.contentType || 'text/plain';
+  public async handle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise<void> {
+    if (input.result instanceof Error) {
+      let code = 500;
+      if (input.result instanceof HttpError) {
+        code = input.result.statusCode;
+      }
+      input.response.setHeader('content-type', 'text/plain');
+      input.response.writeHead(code);
+      input.response.end(`${input.result.name}: ${input.result.message}\n${input.result.stack}`);
+    } else {
+      input.response.setHeader('location', input.result.identifier.path);
+      if (input.result.body) {
+        const contentType = input.result.body.metadata.contentType ?? 'text/plain';
         input.response.setHeader('content-type', contentType);
-        input.description.body.data.pipe(input.response);
+        input.result.body.data.pipe(input.response);
       }
 
       input.response.writeHead(200);
 
-      if (!input.description.body) {
+      if (!input.result.body) {
         // If there is an input body the response will end once the input stream ends
         input.response.end();
       }
-    } else {
-      let code = 500;
-      if (input.error instanceof HttpError) {
-        code = input.error.statusCode;
-      }
-      input.response.setHeader('content-type', 'text/plain');
-      input.response.writeHead(code);
-      input.response.end(`${input.error.name}: ${input.error.message}\n${input.error.stack}`);
     }
   }
 }
diff --git a/src/ldp/operations/SimplePostOperationHandler.ts b/src/ldp/operations/SimplePostOperationHandler.ts
index 33f7394b8..e09b2cde3 100644
--- a/src/ldp/operations/SimplePostOperationHandler.ts
+++ b/src/ldp/operations/SimplePostOperationHandler.ts
@@ -26,6 +26,9 @@ export class SimplePostOperationHandler extends OperationHandler {
   }
 
   public async handle(input: Operation): Promise<ResponseDescription> {
+    if (!input.body) {
+      throw new UnsupportedHttpError('POST operations require a body.');
+    }
     const identifier = await this.store.addResource(input.target, input.body);
     return { identifier };
   }
diff --git a/src/server/ExpressHttpServer.ts b/src/server/ExpressHttpServer.ts
index d7167b7ba..d5e5051b9 100644
--- a/src/server/ExpressHttpServer.ts
+++ b/src/server/ExpressHttpServer.ts
@@ -16,7 +16,7 @@ export class ExpressHttpServer {
     app.use(cors({
       // Based on https://github.com/solid/solid-spec/blob/master/recommendations-server.md#cors---cross-origin-resource-sharing
       // By default origin is always '*', this forces it to be the origin header if there is one
-      origin: (origin, callback): void => callback(null, (origin || '*') as any),
+      origin: (origin, callback): void => callback(null, (origin ?? '*') as any),
       methods: [ 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE' ],
     }));
 
diff --git a/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts b/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts
index b2111daaa..e65934b30 100644
--- a/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts
+++ b/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts
@@ -39,8 +39,8 @@ export class SimpleSparqlUpdatePatchHandler extends PatchHandler {
     }
 
     const def = defaultGraph();
-    const deletes = op.delete || [];
-    const inserts = op.insert || [];
+    const deletes = op.delete ?? [];
+    const inserts = op.insert ?? [];
 
     if (!deletes.every((pattern): boolean => pattern.graph.equals(def))) {
       throw new UnsupportedHttpError('GRAPH statements are not supported.');
@@ -48,7 +48,7 @@ export class SimpleSparqlUpdatePatchHandler extends PatchHandler {
     if (!inserts.every((pattern): boolean => pattern.graph.equals(def))) {
       throw new UnsupportedHttpError('GRAPH statements are not supported.');
     }
-    if (op.where || deletes.some((pattern): boolean => someTerms(pattern, (term): boolean => term.termType === 'Variable'))) {
+    if (op.where ?? deletes.some((pattern): boolean => someTerms(pattern, (term): boolean => term.termType === 'Variable'))) {
       throw new UnsupportedHttpError('WHERE statements are not supported.');
     }
 
diff --git a/src/util/errors/HttpError.ts b/src/util/errors/HttpError.ts
index c34c947c2..f940ac30e 100644
--- a/src/util/errors/HttpError.ts
+++ b/src/util/errors/HttpError.ts
@@ -11,7 +11,7 @@ export abstract class HttpError extends Error {
    * @param name - Error name. Useful for logging and stack tracing.
    * @param message - Message to be thrown.
    */
-  protected constructor(statusCode: number, name: string, message: string) {
+  protected constructor(statusCode: number, name: string, message?: string) {
     super(message);
     this.statusCode = statusCode;
     this.name = name;
diff --git a/src/util/errors/UnsupportedHttpError.ts b/src/util/errors/UnsupportedHttpError.ts
index e5fac8687..22ecb2207 100644
--- a/src/util/errors/UnsupportedHttpError.ts
+++ b/src/util/errors/UnsupportedHttpError.ts
@@ -10,6 +10,6 @@ export class UnsupportedHttpError extends HttpError {
    * @param message - Optional, more specific, message.
    */
   public constructor(message?: string) {
-    super(400, 'UnsupportedHttpError', message || 'The given input is not supported by the server configuration.');
+    super(400, 'UnsupportedHttpError', message ?? 'The given input is not supported by the server configuration.');
   }
 }
diff --git a/tsconfig.json b/tsconfig.json
index 2cc56f155..9520cceee 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,15 +3,13 @@
     "module": "commonjs",
     "target": "es2017",
     "newLine": "lf",
-    "alwaysStrict": true,
     "declaration": true,
     "esModuleInterop": true,
     "inlineSources": true,
-    "noImplicitAny": true,
-    "noImplicitThis": true,
     "noUnusedLocals": true,
     "preserveConstEnums": true,
     "sourceMap": true,
+    "strict": true,
     "stripInternal": true
   },
   "include": [


Test patch:
diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts
index 4ddb7037c..e5a89d530 100644
--- a/test/integration/AuthenticatedLdpHandler.test.ts
+++ b/test/integration/AuthenticatedLdpHandler.test.ts
@@ -118,7 +118,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
   });
 
   describe('with simple PATCH handlers', (): void => {
-    const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation>([
+    const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
       new SimpleBodyParser(),
       new SimpleSparqlUpdateBodyParser(),
     ]);
diff --git a/test/integration/RequestParser.test.ts b/test/integration/RequestParser.test.ts
index 469d2835b..173305d45 100644
--- a/test/integration/RequestParser.test.ts
+++ b/test/integration/RequestParser.test.ts
@@ -44,7 +44,7 @@ describe('A SimpleRequestParser with simple input parsers', (): void => {
       },
     });
 
-    await expect(arrayifyStream(result.body.data)).resolves.toEqualRdfQuadArray([ triple(
+    await expect(arrayifyStream(result.body!.data)).resolves.toEqualRdfQuadArray([ triple(
       namedNode('http://test.com/s'),
       namedNode('http://test.com/p'),
       namedNode('http://test.com/o'),
diff --git a/test/unit/authentication/SimpleCredentialsExtractor.test.ts b/test/unit/authentication/SimpleCredentialsExtractor.test.ts
index 6a59dff95..149c64976 100644
--- a/test/unit/authentication/SimpleCredentialsExtractor.test.ts
+++ b/test/unit/authentication/SimpleCredentialsExtractor.test.ts
@@ -9,7 +9,7 @@ describe('A SimpleCredentialsExtractor', (): void => {
   });
 
   it('returns undefined if there is no input.', async(): Promise<void> => {
-    await expect(extractor.handle({ headers: {}} as HttpRequest)).resolves.toBeUndefined();
+    await expect(extractor.handle({ headers: {}} as HttpRequest)).resolves.toEqual({});
   });
 
   it('returns the authorization header as webID if there is one.', async(): Promise<void> => {
diff --git a/test/unit/ldp/AuthenticatedLdpHandler.test.ts b/test/unit/ldp/AuthenticatedLdpHandler.test.ts
index f1e31b02a..50ecf1f46 100644
--- a/test/unit/ldp/AuthenticatedLdpHandler.test.ts
+++ b/test/unit/ldp/AuthenticatedLdpHandler.test.ts
@@ -1,5 +1,8 @@
 import { Authorizer } from '../../../src/authorization/Authorizer';
 import { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
+import { HttpRequest } from '../../../src/server/HttpRequest';
+import { HttpResponse } from '../../../src/server/HttpResponse';
+import { Operation } from '../../../src/ldp/operations/Operation';
 import { OperationHandler } from '../../../src/ldp/operations/OperationHandler';
 import { PermissionsExtractor } from '../../../src/ldp/permissions/PermissionsExtractor';
 import { RequestParser } from '../../../src/ldp/http/RequestParser';
@@ -36,30 +39,30 @@ describe('An AuthenticatedLdpHandler', (): void => {
   it('can check if it handles input.', async(): Promise<void> => {
     const handler = new AuthenticatedLdpHandler(args);
 
-    await expect(handler.canHandle({ request: null, response: null })).resolves.toBeUndefined();
+    await expect(handler.canHandle({ request: {} as HttpRequest, response: {} as HttpResponse })).resolves.toBeUndefined();
   });
 
   it('can handle input.', async(): Promise<void> => {
     const handler = new AuthenticatedLdpHandler(args);
 
-    await expect(handler.handle({ request: 'request' as any, response: 'response' as any })).resolves.toEqual(undefined);
+    await expect(handler.handle({ request: 'request' as any, response: 'response' as any })).resolves.toBeUndefined();
     expect(responseFn).toHaveBeenCalledTimes(1);
-    expect(responseFn).toHaveBeenLastCalledWith({ response: 'response', description: 'operation' as any });
+    expect(responseFn).toHaveBeenLastCalledWith({ response: 'response', result: 'operation' as any });
   });
 
   it('sends an error to the output if a handler does not support the input.', async(): Promise<void> => {
-    args.requestParser = new StaticAsyncHandler(false, null);
+    args.requestParser = new StaticAsyncHandler(false, {} as Operation);
     const handler = new AuthenticatedLdpHandler(args);
 
-    await expect(handler.handle({ request: 'request' as any, response: null })).resolves.toEqual(undefined);
+    await expect(handler.handle({ request: 'request' as any, response: {} as HttpResponse })).resolves.toBeUndefined();
     expect(responseFn).toHaveBeenCalledTimes(1);
-    expect(responseFn.mock.calls[0][0].error).toBeInstanceOf(Error);
+    expect(responseFn.mock.calls[0][0].result).toBeInstanceOf(Error);
   });
 
   it('errors if the response writer does not support the result.', async(): Promise< void> => {
-    args.responseWriter = new StaticAsyncHandler(false, null);
+    args.responseWriter = new StaticAsyncHandler(false, undefined);
     const handler = new AuthenticatedLdpHandler(args);
 
-    await expect(handler.handle({ request: 'request' as any, response: null })).rejects.toThrow(Error);
+    await expect(handler.handle({ request: 'request' as any, response: {} as HttpResponse })).rejects.toThrow(Error);
   });
 });
diff --git a/test/unit/ldp/http/SimpleBodyParser.test.ts b/test/unit/ldp/http/SimpleBodyParser.test.ts
index e5a0d4300..2bd078356 100644
--- a/test/unit/ldp/http/SimpleBodyParser.test.ts
+++ b/test/unit/ldp/http/SimpleBodyParser.test.ts
@@ -41,7 +41,7 @@ describe('A SimpleBodyparser', (): void => {
   it('returns a stream of quads if there was data.', async(): Promise<void> => {
     const input = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
     input.headers = { 'content-type': 'text/turtle' };
-    const result = await bodyParser.handle(input);
+    const result = (await bodyParser.handle(input))!;
     expect(result).toEqual({
       data: expect.any(Readable),
       dataType: 'quad',
@@ -61,7 +61,7 @@ describe('A SimpleBodyparser', (): void => {
   it('throws an UnsupportedHttpError on invalid triple data when reading the stream.', async(): Promise<void> => {
     const input = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>' ]) as HttpRequest;
     input.headers = { 'content-type': 'text/turtle' };
-    const result = await bodyParser.handle(input);
+    const result = (await bodyParser.handle(input))!;
     await expect(arrayifyStream(result.data)).rejects.toThrow(UnsupportedHttpError);
   });
 });
diff --git a/test/unit/ldp/http/SimpleRequestParser.test.ts b/test/unit/ldp/http/SimpleRequestParser.test.ts
index 0a74f2567..3104c919f 100644
--- a/test/unit/ldp/http/SimpleRequestParser.test.ts
+++ b/test/unit/ldp/http/SimpleRequestParser.test.ts
@@ -29,6 +29,10 @@ describe('A SimpleRequestParser', (): void => {
     await expect(requestParser.canHandle({ url: 'url' } as any)).rejects.toThrow('Missing method.');
   });
 
+  it('errors if called without method.', async(): Promise<void> => {
+    await expect(requestParser.handle({ url: 'url' } as any)).rejects.toThrow('Missing method.');
+  });
+
   it('returns the output of all input parsers after calling handle.', async(): Promise<void> => {
     await expect(requestParser.handle({ url: 'url', method: 'GET' } as any)).resolves.toEqual({
       method: 'GET',
diff --git a/test/unit/ldp/http/SimpleResponseWriter.test.ts b/test/unit/ldp/http/SimpleResponseWriter.test.ts
index 13bc02d9e..75d4de496 100644
--- a/test/unit/ldp/http/SimpleResponseWriter.test.ts
+++ b/test/unit/ldp/http/SimpleResponseWriter.test.ts
@@ -14,20 +14,14 @@ describe('A SimpleResponseWriter', (): void => {
     response = createResponse({ eventEmitter: EventEmitter });
   });
 
-  it('can handle input that has at least a description or an error.', async(): Promise<void> => {
-    await expect(writer.canHandle({ response, description: {} as ResponseDescription })).resolves.toBeUndefined();
-    await expect(writer.canHandle({ response, error: {} as Error })).resolves.toBeUndefined();
-    await expect(writer.canHandle({ response })).rejects.toThrow(UnsupportedHttpError);
-  });
-
   it('requires the description body to be a string or binary stream if present.', async(): Promise<void> => {
-    await expect(writer.canHandle({ response, description: { body: { dataType: 'quad' }} as ResponseDescription })).rejects.toThrow(UnsupportedHttpError);
-    await expect(writer.canHandle({ response, description: { body: { dataType: 'string' }} as ResponseDescription })).resolves.toBeUndefined();
-    await expect(writer.canHandle({ response, description: { body: { dataType: 'binary' }} as ResponseDescription })).resolves.toBeUndefined();
+    await expect(writer.canHandle({ response, result: { body: { dataType: 'quad' }} as ResponseDescription })).rejects.toThrow(UnsupportedHttpError);
+    await expect(writer.canHandle({ response, result: { body: { dataType: 'string' }} as ResponseDescription })).resolves.toBeUndefined();
+    await expect(writer.canHandle({ response, result: { body: { dataType: 'binary' }} as ResponseDescription })).resolves.toBeUndefined();
   });
 
   it('responds with status code 200 and a location header if there is a description.', async(): Promise<void> => {
-    await writer.handle({ response, description: { identifier: { path: 'path' }}});
+    await writer.handle({ response, result: { identifier: { path: 'path' }}});
     expect(response._isEndCalled()).toBeTruthy();
     expect(response._getStatusCode()).toBe(200);
     expect(response._getHeaders()).toMatchObject({ location: 'path' });
@@ -51,7 +45,7 @@ describe('A SimpleResponseWriter', (): void => {
       done();
     });
 
-    await writer.handle({ response, description: { identifier: { path: 'path' }, body }});
+    await writer.handle({ response, result: { identifier: { path: 'path' }, body }});
   });
 
   it('responds with a content-type if the metadata has it.', async(done): Promise<void> => {
@@ -73,11 +67,11 @@ describe('A SimpleResponseWriter', (): void => {
       done();
     });
 
-    await writer.handle({ response, description: { identifier: { path: 'path' }, body }});
+    await writer.handle({ response, result: { identifier: { path: 'path' }, body }});
   });
 
   it('responds with 500 if an error if there is an error.', async(): Promise<void> => {
-    await writer.handle({ response, error: new Error('error') });
+    await writer.handle({ response, result: new Error('error') });
     expect(response._isEndCalled()).toBeTruthy();
     expect(response._getStatusCode()).toBe(500);
     expect(response._getData()).toMatch('Error: error');
@@ -85,7 +79,7 @@ describe('A SimpleResponseWriter', (): void => {
 
   it('responds with the given statuscode if there is an HttpError.', async(): Promise<void> => {
     const error = new UnsupportedHttpError('error');
-    await writer.handle({ response, error });
+    await writer.handle({ response, result: error });
     expect(response._isEndCalled()).toBeTruthy();
     expect(response._getStatusCode()).toBe(error.statusCode);
     expect(response._getData()).toMatch('UnsupportedHttpError: error');
diff --git a/test/unit/ldp/operations/SimplePostOperationHandler.test.ts b/test/unit/ldp/operations/SimplePostOperationHandler.test.ts
index 733bd024f..b6a6ececb 100644
--- a/test/unit/ldp/operations/SimplePostOperationHandler.test.ts
+++ b/test/unit/ldp/operations/SimplePostOperationHandler.test.ts
@@ -16,6 +16,10 @@ describe('A SimplePostOperationHandler', (): void => {
     await expect(handler.canHandle({ method: 'POST' } as Operation)).rejects.toThrow(UnsupportedHttpError);
   });
 
+  it('errors if no body is present.', async(): Promise<void> => {
+    await expect(handler.handle({ method: 'POST' } as Operation)).rejects.toThrow(UnsupportedHttpError);
+  });
+
   it('adds the given representation to the store and returns the new identifier.', async(): Promise<void> => {
     await expect(handler.handle({ method: 'POST', body: { dataType: 'test' }} as Operation)).resolves.toEqual({ identifier: { path: 'newPath' }});
   });
diff --git a/test/unit/storage/LockingResourceStore.test.ts b/test/unit/storage/LockingResourceStore.test.ts
index 906f6daaa..f3567bdb2 100644
--- a/test/unit/storage/LockingResourceStore.test.ts
+++ b/test/unit/storage/LockingResourceStore.test.ts
@@ -1,5 +1,7 @@
 import { Lock } from '../../../src/storage/Lock';
 import { LockingResourceStore } from '../../../src/storage/LockingResourceStore';
+import { Patch } from '../../../src/ldp/http/Patch';
+import { Representation } from '../../../src/ldp/representation/Representation';
 import { ResourceLocker } from '../../../src/storage/ResourceLocker';
 import { ResourceStore } from '../../../src/storage/ResourceStore';
 
@@ -40,7 +42,7 @@ describe('A LockingResourceStore', (): void => {
   });
 
   it('acquires a lock on the resource when getting it.', async(): Promise<void> => {
-    await store.getRepresentation({ path: 'path' }, null);
+    await store.getRepresentation({ path: 'path' }, {});
     expect(locker.acquire).toHaveBeenCalledTimes(1);
     expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
     expect(source.getRepresentation).toHaveBeenCalledTimes(1);
@@ -49,7 +51,7 @@ describe('A LockingResourceStore', (): void => {
   });
 
   it('acquires a lock on the container when adding a representation.', async(): Promise<void> => {
-    await store.addResource({ path: 'path' }, null);
+    await store.addResource({ path: 'path' }, {} as Representation);
     expect(locker.acquire).toHaveBeenCalledTimes(1);
     expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
     expect(source.addResource).toHaveBeenCalledTimes(1);
@@ -58,7 +60,7 @@ describe('A LockingResourceStore', (): void => {
   });
 
   it('acquires a lock on the resource when setting its representation.', async(): Promise<void> => {
-    await store.setRepresentation({ path: 'path' }, null);
+    await store.setRepresentation({ path: 'path' }, {} as Representation);
     expect(locker.acquire).toHaveBeenCalledTimes(1);
     expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
     expect(source.setRepresentation).toHaveBeenCalledTimes(1);
@@ -76,7 +78,7 @@ describe('A LockingResourceStore', (): void => {
   });
 
   it('acquires a lock on the resource when modifying its representation.', async(): Promise<void> => {
-    await store.modifyResource({ path: 'path' }, null);
+    await store.modifyResource({ path: 'path' }, {} as Patch);
     expect(locker.acquire).toHaveBeenCalledTimes(1);
     expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
     expect(source.modifyResource).toHaveBeenCalledTimes(1);
@@ -88,7 +90,7 @@ describe('A LockingResourceStore', (): void => {
     source.getRepresentation = async(): Promise<any> => {
       throw new Error('dummy');
     };
-    await expect(store.getRepresentation({ path: 'path' }, null)).rejects.toThrow('dummy');
+    await expect(store.getRepresentation({ path: 'path' }, {})).rejects.toThrow('dummy');
     expect(locker.acquire).toHaveBeenCalledTimes(1);
     expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
     expect(lock.release).toHaveBeenCalledTimes(1);
diff --git a/test/unit/storage/PatchingStore.test.ts b/test/unit/storage/PatchingStore.test.ts
index 594476f5b..e68961f51 100644
--- a/test/unit/storage/PatchingStore.test.ts
+++ b/test/unit/storage/PatchingStore.test.ts
@@ -1,5 +1,7 @@
+import { Patch } from '../../../src/ldp/http/Patch';
 import { PatchHandler } from '../../../src/storage/patch/PatchHandler';
 import { PatchingStore } from '../../../src/storage/PatchingStore';
+import { Representation } from '../../../src/ldp/representation/Representation';
 import { ResourceStore } from '../../../src/storage/ResourceStore';
 
 describe('A PatchingStore', (): void => {
@@ -24,44 +26,44 @@ describe('A PatchingStore', (): void => {
   });
 
   it('calls getRepresentation directly from the source.', async(): Promise<void> => {
-    await expect(store.getRepresentation({ path: 'getPath' }, null)).resolves.toBe('get');
+    await expect(store.getRepresentation({ path: 'getPath' }, {})).resolves.toBe('get');
     expect(source.getRepresentation).toHaveBeenCalledTimes(1);
-    expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: 'getPath' }, null, undefined);
+    expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: 'getPath' }, {}, undefined);
   });
 
   it('calls addResource directly from the source.', async(): Promise<void> => {
-    await expect(store.addResource({ path: 'addPath' }, null)).resolves.toBe('add');
+    await expect(store.addResource({ path: 'addPath' }, {} as Representation)).resolves.toBe('add');
     expect(source.addResource).toHaveBeenCalledTimes(1);
-    expect(source.addResource).toHaveBeenLastCalledWith({ path: 'addPath' }, null, undefined);
+    expect(source.addResource).toHaveBeenLastCalledWith({ path: 'addPath' }, {}, undefined);
   });
 
   it('calls setRepresentation directly from the source.', async(): Promise<void> => {
-    await expect(store.setRepresentation({ path: 'setPath' }, null)).resolves.toBe('set');
+    await expect(store.setRepresentation({ path: 'setPath' }, {} as Representation)).resolves.toBe('set');
     expect(source.setRepresentation).toHaveBeenCalledTimes(1);
-    expect(source.setRepresentation).toHaveBeenLastCalledWith({ path: 'setPath' }, null, undefined);
+    expect(source.setRepresentation).toHaveBeenLastCalledWith({ path: 'setPath' }, {}, undefined);
   });
 
   it('calls deleteResource directly from the source.', async(): Promise<void> => {
-    await expect(store.deleteResource({ path: 'deletePath' }, null)).resolves.toBe('delete');
+    await expect(store.deleteResource({ path: 'deletePath' })).resolves.toBe('delete');
     expect(source.deleteResource).toHaveBeenCalledTimes(1);
-    expect(source.deleteResource).toHaveBeenLastCalledWith({ path: 'deletePath' }, null);
+    expect(source.deleteResource).toHaveBeenLastCalledWith({ path: 'deletePath' }, undefined);
   });
 
   it('calls modifyResource directly from the source if available.', async(): Promise<void> => {
-    await expect(store.modifyResource({ path: 'modifyPath' }, null)).resolves.toBe('modify');
+    await expect(store.modifyResource({ path: 'modifyPath' }, {} as Patch)).resolves.toBe('modify');
     expect(source.modifyResource).toHaveBeenCalledTimes(1);
-    expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, null, undefined);
+    expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, {}, undefined);
   });
 
   it('calls its patcher if modifyResource failed.', async(): Promise<void> => {
     source.modifyResource = jest.fn(async(): Promise<any> => {
       throw new Error('dummy');
     });
-    await expect(store.modifyResource({ path: 'modifyPath' }, null)).resolves.toBe('patcher');
+    await expect(store.modifyResource({ path: 'modifyPath' }, {} as Patch)).resolves.toBe('patcher');
     expect(source.modifyResource).toHaveBeenCalledTimes(1);
-    expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, null, undefined);
+    expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, {}, undefined);
     await expect((source.modifyResource as jest.Mock).mock.results[0].value).rejects.toThrow('dummy');
     expect(handleSafeFn).toHaveBeenCalledTimes(1);
-    expect(handleSafeFn).toHaveBeenLastCalledWith({ identifier: { path: 'modifyPath' }, patch: null });
+    expect(handleSafeFn).toHaveBeenLastCalledWith({ identifier: { path: 'modifyPath' }, patch: {}});
   });
 });
diff --git a/test/unit/storage/SimpleResourceStore.test.ts b/test/unit/storage/SimpleResourceStore.test.ts
index 53f1c5943..feaeb0645 100644
--- a/test/unit/storage/SimpleResourceStore.test.ts
+++ b/test/unit/storage/SimpleResourceStore.test.ts
@@ -2,6 +2,7 @@ import arrayifyStream from 'arrayify-stream';
 import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { QuadRepresentation } from '../../../src/ldp/representation/QuadRepresentation';
 import { Readable } from 'stream';
+import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
 import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore';
 import streamifyArray from 'streamify-array';
 import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError';
@@ -24,15 +25,15 @@ describe('A SimpleResourceStore', (): void => {
     representation = {
       data: streamifyArray([ quad ]),
       dataType: 'quad',
-      metadata: null,
+      metadata: {} as RepresentationMetadata,
     };
   });
 
   it('errors if a resource was not found.', async(): Promise<void> => {
     await expect(store.getRepresentation({ path: `${base}wrong` }, {})).rejects.toThrow(NotFoundHttpError);
-    await expect(store.addResource({ path: 'http://wrong.com/wrong' }, null)).rejects.toThrow(NotFoundHttpError);
+    await expect(store.addResource({ path: 'http://wrong.com/wrong' }, representation)).rejects.toThrow(NotFoundHttpError);
     await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError);
-    await expect(store.setRepresentation({ path: 'http://wrong.com/' }, null)).rejects.toThrow(NotFoundHttpError);
+    await expect(store.setRepresentation({ path: 'http://wrong.com/' }, representation)).rejects.toThrow(NotFoundHttpError);
   });
 
   it('errors when modifying resources.', async(): Promise<void> => {
diff --git a/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts
index 4a83bc6f3..fc0ddd042 100644
--- a/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts
+++ b/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts
@@ -41,15 +41,13 @@ describe('A SimpleSparqlUpdatePatchHandler', (): void => {
           metadata: null,
         };
       }),
-      addResource: null,
       setRepresentation: jest.fn(async(): Promise<any> => {
         order.push('setRepresentation');
       }),
-      deleteResource: null,
       modifyResource: jest.fn(async(): Promise<any> => {
         throw new Error('noModify');
       }),
-    };
+    } as unknown as ResourceStore;
 
     release = jest.fn(async(): Promise<any> => order.push('release'));
     locker = {