Repository: solid/community-server (ts)

Issue:
Default server leaves dangling locks on PATCH
Below are the logs of the default server receiving a `PATCH` on any path.

```
$ npm start

> @solid/community-server@0.2.0 start /Users/ruben/Documents/UGent/Solid/solid-community-server
> node ./bin/server.js -p 3000

2020-11-29T09:55:11.934Z [CliRunner] info: Running at http://localhost:3000/
2020-11-29T09:55:13.027Z [ExpressHttpServerFactory] info: Received request for /
2020-11-29T09:55:13.044Z [RepresentationConvertingStore] info: Convert http://localhost:3000/.acl from text/turtle to internal/quads;q=1
2020-11-29T09:55:13.047Z [WebAclAuthorizer] info: Reading ACL statements from http://localhost:3000/.acl
2020-11-29T09:55:13.053Z [RepresentationConvertingStore] info: Convert http://localhost:3000/ from internal/quads to text/turtle;q=1
2020-11-29T09:55:16.057Z [WrappedExpiringLock] warn: Lock expired after 3000ms
```

Since this went undetected, I have bumped the log level of lock expiry, which should be a rare event: 1d08f463f692ac4f44c781d101176aa4ec36ac2e

Pull request description:
fix: Always release lock when patching
Closes #367 

Patch:
diff --git a/componentsjs-generator-ignore-classes.json b/.componentsignore
similarity index 100%
rename from componentsjs-generator-ignore-classes.json
rename to .componentsignore
diff --git a/bin/server.ts b/bin/server.ts
index d06aab527..9a850be23 100644
--- a/bin/server.ts
+++ b/bin/server.ts
@@ -1,4 +1,3 @@
 #!/usr/bin/env node
-import * as Path from 'path';
 import { runCli } from '../src/init/CliRunner';
-runCli(Path.join(__dirname, '..'));
+runCli();
diff --git a/config/presets/cli-params.json b/config/presets/cli-params.json
index af95638df..260b76994 100644
--- a/config/presets/cli-params.json
+++ b/config/presets/cli-params.json
@@ -6,7 +6,7 @@
       "@type": "Variable"
     },
     {
-      "@id": "urn:solid-server:default:variable:base",
+      "@id": "urn:solid-server:default:variable:baseUrl",
       "@type": "Variable"
     },
     {
diff --git a/config/presets/middleware.json b/config/presets/middleware.json
index 32a989809..564319ad6 100644
--- a/config/presets/middleware.json
+++ b/config/presets/middleware.json
@@ -31,8 +31,8 @@
         },
         {
           "@type": "WebSocketAdvertiser",
-          "WebSocketAdvertiser:_settings_port": {
-            "@id": "urn:solid-server:default:variable:port"
+          "WebSocketAdvertiser:_baseUrl": {
+            "@id": "urn:solid-server:default:variable:baseUrl"
           }
         }
       ]
diff --git a/config/presets/setup.json b/config/presets/setup.json
index 5b849c784..a1d01bb12 100644
--- a/config/presets/setup.json
+++ b/config/presets/setup.json
@@ -17,7 +17,7 @@
         "@id": "urn:solid-server:default:LoggerFactory"
       },
       "Setup:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       },
       "Setup:_port": {
         "@id": "urn:solid-server:default:variable:port"
diff --git a/config/presets/storage/backend/storage-filesystem.json b/config/presets/storage/backend/storage-filesystem.json
index 971d404c3..9e8c7bbb7 100644
--- a/config/presets/storage/backend/storage-filesystem.json
+++ b/config/presets/storage/backend/storage-filesystem.json
@@ -5,7 +5,7 @@
       "@id": "urn:solid-server:default:FileIdentifierMapper",
       "@type": "ExtensionBasedMapper",
       "ExtensionBasedMapper:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       },
       "ExtensionBasedMapper:_rootFilepath": {
         "@id": "urn:solid-server:default:variable:rootFilePath"
@@ -27,7 +27,7 @@
         "@id": "urn:solid-server:default:FileDataAccessor"
       },
       "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     }
   ]
diff --git a/config/presets/storage/backend/storage-memory.json b/config/presets/storage/backend/storage-memory.json
index d5d44d200..2ae772557 100644
--- a/config/presets/storage/backend/storage-memory.json
+++ b/config/presets/storage/backend/storage-memory.json
@@ -5,7 +5,7 @@
       "@id": "urn:solid-server:default:MemoryDataAccessor",
       "@type": "InMemoryDataAccessor",
       "InMemoryDataAccessor:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
     {
@@ -15,7 +15,7 @@
         "@id": "urn:solid-server:default:MemoryDataAccessor"
       },
       "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     }
   ]
diff --git a/config/presets/storage/backend/storage-sparql-endpoint.json b/config/presets/storage/backend/storage-sparql-endpoint.json
index a2c9c0e68..0aa123bce 100644
--- a/config/presets/storage/backend/storage-sparql-endpoint.json
+++ b/config/presets/storage/backend/storage-sparql-endpoint.json
@@ -8,7 +8,7 @@
         "@id": "urn:solid-server:default:variable:sparqlEndpoint"
       },
       "SparqlDataAccessor:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
 
@@ -19,7 +19,7 @@
         "@id": "urn:solid-server:default:SparqlDataAccessor"
       },
       "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
 
diff --git a/config/presets/storage/routing/regex-routing.json b/config/presets/storage/routing/regex-routing.json
index e5f5b9728..32be5c179 100644
--- a/config/presets/storage/routing/regex-routing.json
+++ b/config/presets/storage/routing/regex-routing.json
@@ -5,7 +5,7 @@
       "@id": "urn:solid-server:default:RegexRouterRule",
       "@type": "RegexRouterRule",
       "RegexRouterRule:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
     {
diff --git a/package.json b/package.json
index dc491a116..d8dee9821 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
   "homepage": "https://github.com/solid/community-server#readme",
   "scripts": {
     "build": "npm run build:ts && npm run build:components",
-    "build:components": "componentsjs-generator -s src -i componentsjs-generator-ignore-classes.json",
+    "build:components": "componentsjs-generator -s src -i .componentsignore",
     "build:ts": "tsc",
     "docker": "npm run docker:setup && npm run docker:start",
     "docker:clean": "./test/docker/docker-clean.sh",
diff --git a/src/authentication/Credentials.ts b/src/authentication/Credentials.ts
index bae6d0ba9..af40670a7 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/DPoPWebIdExtractor.ts b/src/authentication/DPoPWebIdExtractor.ts
index 8b412e8a4..e495ee842 100644
--- a/src/authentication/DPoPWebIdExtractor.ts
+++ b/src/authentication/DPoPWebIdExtractor.ts
@@ -33,9 +33,9 @@ export class DPoPWebIdExtractor extends CredentialsExtractor {
     }
     const resource = await this.targetExtractor.handleSafe(request);
     try {
-      const webID = await verify(authorization as string, dpop as string, method as any, resource.path);
-      this.logger.info(`Verified WebID via DPoP token: ${webID}`);
-      return { webID };
+      const webId = await verify(authorization as string, dpop as string, method as any, resource.path);
+      this.logger.info(`Verified WebID via DPoP token: ${webId}`);
+      return { webId };
     } catch (error: unknown) {
       const message = `Error verifying WebID via DPoP token: ${(error as Error).message}`;
       this.logger.warn(message);
diff --git a/src/authentication/UnsecureWebIdExtractor.ts b/src/authentication/UnsecureWebIdExtractor.ts
index 6905128ec..a7e935d4e 100644
--- a/src/authentication/UnsecureWebIdExtractor.ts
+++ b/src/authentication/UnsecureWebIdExtractor.ts
@@ -5,7 +5,7 @@ import type { Credentials } from './Credentials';
 import { CredentialsExtractor } from './CredentialsExtractor';
 
 /**
- * Credentials extractor which simply interprets the contents of the Authorization header as a webID.
+ * Credentials extractor which simply interprets the contents of the Authorization header as a WebID.
  */
 export class UnsecureWebIdExtractor extends CredentialsExtractor {
   protected readonly logger = getLoggerFor(this);
@@ -18,8 +18,8 @@ export class UnsecureWebIdExtractor extends CredentialsExtractor {
   }
 
   public async handle({ headers }: HttpRequest): Promise<Credentials> {
-    const webID = /^WebID\s+(.*)/u.exec(headers.authorization as string)![1];
-    this.logger.info(`Agent unsecurely claims to be ${webID}`);
-    return { webID };
+    const webId = /^WebID\s+(.*)/u.exec(headers.authorization as string)![1];
+    this.logger.info(`Agent unsecurely claims to be ${webId}`);
+    return { webId };
   }
 }
diff --git a/src/authorization/WebAclAuthorizer.ts b/src/authorization/WebAclAuthorizer.ts
index c6efdddaa..62da50f13 100644
--- a/src/authorization/WebAclAuthorizer.ts
+++ b/src/authorization/WebAclAuthorizer.ts
@@ -63,9 +63,9 @@ export class WebAclAuthorizer extends Authorizer {
     const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control'];
     const auths = store.getQuads(null, ACL.mode, modeString, null).map((quad: Quad): Term => quad.subject);
     if (!auths.some((term): boolean => this.hasAccess(agent, term, store))) {
-      const isLoggedIn = typeof agent.webID === 'string';
+      const isLoggedIn = typeof agent.webId === 'string';
       if (isLoggedIn) {
-        this.logger.warn(`Agent ${agent.webID} has no ${mode} permissions`);
+        this.logger.warn(`Agent ${agent.webId} has no ${mode} permissions`);
         throw new ForbiddenHttpError();
       } else {
         this.logger.warn(`Unauthenticated agent has no ${mode} permissions`);
@@ -96,13 +96,13 @@ export class WebAclAuthorizer extends Authorizer {
     if (store.countQuads(auth, ACL.agentClass, FOAF.Agent, null) > 0) {
       return true;
     }
-    if (typeof agent.webID !== 'string') {
+    if (typeof agent.webId !== 'string') {
       return false;
     }
     if (store.countQuads(auth, ACL.agentClass, FOAF.AuthenticatedAgent, null) > 0) {
       return true;
     }
-    return store.countQuads(auth, ACL.agent, agent.webID, null) > 0;
+    return store.countQuads(auth, ACL.agent, agent.webId, null) > 0;
   }
 
   /**
diff --git a/src/init/CliRunner.ts b/src/init/CliRunner.ts
index d7222ba34..bd6fa4023 100644
--- a/src/init/CliRunner.ts
+++ b/src/init/CliRunner.ts
@@ -1,9 +1,10 @@
-import * as Path from 'path';
+import * as path from 'path';
 import type { ReadStream, WriteStream } from 'tty';
 import type { LoaderProperties } from 'componentsjs';
 import { Loader } from 'componentsjs';
 import yargs from 'yargs';
 import { getLoggerFor } from '../logging/LogUtil';
+import { ensureTrailingSlash } from '../util/PathUtil';
 import type { Setup } from './Setup';
 
 const logger = getLoggerFor('CliRunner');
@@ -11,34 +12,39 @@ const logger = getLoggerFor('CliRunner');
 /**
  * Generic run function for starting the server from a given config
  * @param args - Command line arguments.
- * @param stdin - Standard input stream.
- * @param stdout - Standard output stream.
  * @param stderr - Standard error stream.
  * @param properties - Components loader properties.
  */
-export const runCustom = function(
-  args: string[],
-  stdin: ReadStream,
-  stdout: WriteStream,
-  stderr: WriteStream,
-  properties: LoaderProperties,
-): void {
-  const { argv } = yargs
+export const runCli = function({
+  argv = process.argv,
+  stderr = process.stderr,
+  properties = {
+    mainModulePath: path.join(__dirname, '../../'),
+  },
+}: {
+  argv?: string[];
+  stdin?: ReadStream;
+  stdout?: WriteStream;
+  stderr?: WriteStream;
+  properties?: LoaderProperties;
+} = {}): void {
+  const { argv: params } = yargs(argv.slice(2))
     .usage('node ./bin/server.js [args]')
     .options({
-      port: { type: 'number', alias: 'p', default: 3000 },
+      baseUrl: { type: 'string', alias: 'b' },
       config: { type: 'string', alias: 'c' },
+      loggingLevel: { type: 'string', alias: 'l', default: 'info' },
+      port: { type: 'number', alias: 'p', default: 3000 },
       rootFilePath: { type: 'string', alias: 'f' },
       sparqlEndpoint: { type: 'string', alias: 's' },
-      level: { type: 'string', alias: 'l', default: 'info' },
     })
     .help();
 
   (async(): Promise<string> => {
     // Load provided or default config file
-    const configPath = argv.config ?
-      Path.join(process.cwd(), argv.config) :
-      `${__dirname}/../../config/config-default.json`;
+    const configPath = params.config ?
+      path.join(process.cwd(), params.config) :
+      path.join(__dirname, '/../../config/config-default.json');
 
     // Setup from config file
     const loader = new Loader(properties);
@@ -46,27 +52,19 @@ export const runCustom = function(
     const setup: Setup = await loader
       .instantiateFromUrl('urn:solid-server:default', configPath, undefined, {
         variables: {
-          'urn:solid-server:default:variable:port': argv.port,
-          'urn:solid-server:default:variable:base': `http://localhost:${argv.port}/`,
-          'urn:solid-server:default:variable:rootFilePath': argv.rootFilePath ?? process.cwd(),
-          'urn:solid-server:default:variable:sparqlEndpoint': argv.sparqlEndpoint,
-          'urn:solid-server:default:variable:loggingLevel': argv.level,
+          'urn:solid-server:default:variable:baseUrl':
+            params.baseUrl ? ensureTrailingSlash(params.baseUrl) : `http://localhost:${params.port}/`,
+          'urn:solid-server:default:variable:loggingLevel': params.loggingLevel,
+          'urn:solid-server:default:variable:port': params.port,
+          'urn:solid-server:default:variable:rootFilePath': params.rootFilePath ?? process.cwd(),
+          'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint,
         },
       }) as Setup;
     return await setup.setup();
-  })().then((base: string): void => {
-    logger.info(`Running at ${base}`);
+  })().then((baseUrl: string): void => {
+    logger.info(`Running at ${baseUrl}`);
   }).catch((error): void => {
     // This is the only time we can *not* use the logger to print error messages, as dependency injection has failed.
     stderr.write(`${error}\n`);
   });
 };
-
-/**
- * Run function for starting the server from the command line
- * @param moduleRootPath - Path to the module's root.
- */
-export const runCli = function(moduleRootPath: string): void {
-  const argv = process.argv.slice(2);
-  runCustom(argv, process.stdin, process.stdout, process.stderr, { mainModulePath: moduleRootPath });
-};
diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts
index cddbca4e7..a304fc1c9 100644
--- a/src/ldp/AuthenticatedLdpHandler.ts
+++ b/src/ldp/AuthenticatedLdpHandler.ts
@@ -1,6 +1,7 @@
 import type { Credentials } from '../authentication/Credentials';
 import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
 import type { Authorizer } from '../authorization/Authorizer';
+import { getLoggerFor } from '../logging/LogUtil';
 import { HttpHandler } from '../server/HttpHandler';
 import type { HttpRequest } from '../server/HttpRequest';
 import type { HttpResponse } from '../server/HttpResponse';
@@ -52,6 +53,7 @@ export class AuthenticatedLdpHandler extends HttpHandler {
   private readonly authorizer!: Authorizer;
   private readonly operationHandler!: OperationHandler;
   private readonly responseWriter!: ResponseWriter;
+  private readonly logger = getLoggerFor(this);
 
   /**
    * Creates the handler.
@@ -110,10 +112,26 @@ export class AuthenticatedLdpHandler extends HttpHandler {
    * @returns A promise resolving to the generated Operation.
    */
   private async runHandlers(request: HttpRequest): Promise<ResponseDescription> {
-    const op: Operation = await this.requestParser.handleSafe(request);
+    this.logger.verbose(`Handling LDP request for ${request.url}`);
+
+    const operation: Operation = await this.requestParser.handleSafe(request);
+    this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`);
+
     const credentials: Credentials = await this.credentialsExtractor.handleSafe(request);
-    const permissions: PermissionSet = await this.permissionsExtractor.handleSafe(op);
-    await this.authorizer.handleSafe({ credentials, identifier: op.target, permissions });
-    return this.operationHandler.handleSafe(op);
+    this.logger.verbose(`Extracted credentials: ${credentials.webId}`);
+
+    const permissions: PermissionSet = await this.permissionsExtractor.handleSafe(operation);
+    const { read, write, append } = permissions;
+    this.logger.verbose(`Required permissions are read: ${read}, write: ${write}, append: ${append}`);
+
+    try {
+      await this.authorizer.handleSafe({ credentials, identifier: operation.target, permissions });
+    } catch (error: unknown) {
+      this.logger.verbose(`Authorization failed: ${(error as any).message}`);
+      throw error;
+    }
+
+    this.logger.verbose(`Authorization succeeded, performing operation`);
+    return this.operationHandler.handleSafe(operation);
   }
 }
diff --git a/src/server/middleware/WebSocketAdvertiser.ts b/src/server/middleware/WebSocketAdvertiser.ts
index bb121175c..c1fa4e1fb 100644
--- a/src/server/middleware/WebSocketAdvertiser.ts
+++ b/src/server/middleware/WebSocketAdvertiser.ts
@@ -2,26 +2,16 @@ import { addHeader } from '../../util/HeaderUtil';
 import { HttpHandler } from '../HttpHandler';
 import type { HttpResponse } from '../HttpResponse';
 
-interface WebSocketSettings {
-  hostname?: string;
-  port?: number;
-  protocol?: string;
-}
-
 /**
  * Handler that advertises a WebSocket through the Updates-Via header.
  */
 export class WebSocketAdvertiser extends HttpHandler {
   private readonly socketUrl: string;
 
-  public constructor(settings: WebSocketSettings = {}) {
+  public constructor(baseUrl: string) {
     super();
-    const { hostname = 'localhost', port = 80, protocol = 'ws:' } = settings;
-    const secure = /^(?:https|wss)/u.test(protocol);
-    const socketUrl = new URL(`${secure ? 'wss' : 'ws'}://${hostname}:${port}/`);
-    if (socketUrl.hostname !== hostname) {
-      throw new Error(`Invalid hostname: ${hostname}`);
-    }
+    const socketUrl = new URL(baseUrl);
+    socketUrl.protocol = /^(?:http|ws):/u.test(baseUrl) ? 'ws:' : 'wss:';
     this.socketUrl = socketUrl.href;
   }
 
diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts
index bf85e4af9..470dfe131 100644
--- a/src/storage/patch/SparqlUpdatePatchHandler.ts
+++ b/src/storage/patch/SparqlUpdatePatchHandler.ts
@@ -65,10 +65,25 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
       throw new NotImplementedHttpError('WHERE statements are not supported');
     }
 
-    // Read the quads of the current representation
     const lock = await this.locker.acquire(identifier);
-    const quads = await this.source.getRepresentation(identifier,
-      { type: [{ value: INTERNAL_QUADS, weight: 1 }]});
+    try {
+      await this.applyPatch(identifier, deletes, inserts);
+    } finally {
+      await lock.release();
+    }
+  }
+
+  private isDeleteInsert(op: Algebra.Operation): op is Algebra.DeleteInsert {
+    return op.type === Algebra.types.DELETE_INSERT;
+  }
+
+  /**
+   * Applies the given deletes and inserts to the resource.
+   */
+  private async applyPatch(identifier: ResourceIdentifier, deletes: Algebra.Pattern[], inserts: Algebra.Pattern[]):
+  Promise<void> {
+    // Read the quads of the current representation
+    const quads = await this.source.getRepresentation(identifier, { type: [{ value: INTERNAL_QUADS, weight: 1 }]});
     const store = new Store<BaseQuad>();
     const importEmitter = store.import(quads.data);
     await new Promise((resolve, reject): void => {
@@ -91,11 +106,5 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
       metadata,
     };
     await this.source.setRepresentation(identifier, representation);
-
-    await lock.release();
-  }
-
-  private isDeleteInsert(op: Algebra.Operation): op is Algebra.DeleteInsert {
-    return op.type === Algebra.types.DELETE_INSERT;
   }
 }
diff --git a/src/util/PathUtil.ts b/src/util/PathUtil.ts
index fda439ae5..f0cbc2455 100644
--- a/src/util/PathUtil.ts
+++ b/src/util/PathUtil.ts
@@ -53,7 +53,7 @@ export const getParentContainer = (id: ResourceIdentifier): ResourceIdentifier =
 
   // This probably means there is an issue with the root
   if (parentPath === id.path) {
-    throw new InternalServerError('URL root reached');
+    throw new InternalServerError(`Resource ${id.path} does not have a parent container`);
   }
 
   return { path: parentPath };


Test patch:
diff --git a/test/integration/Middleware.test.ts b/test/integration/Middleware.test.ts
index 8d0710bb0..7e2b1888d 100644
--- a/test/integration/Middleware.test.ts
+++ b/test/integration/Middleware.test.ts
@@ -23,6 +23,7 @@ describe('An Express server with middleware', (): void => {
       'urn:solid-server:default:ExpressHttpServerFactory', 'middleware.json', {
         'urn:solid-server:default:LdpHandler': new SimpleHttpHandler(),
         'urn:solid-server:default:variable:port': port,
+        'urn:solid-server:default:variable:baseUrl': 'https://example.pod/',
       },
     ) as ExpressHttpServerFactory;
     server = factory.startServer(port);
diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts
index d3d64ef1b..c679df58c 100644
--- a/test/integration/WebSocketsProtocol.test.ts
+++ b/test/integration/WebSocketsProtocol.test.ts
@@ -5,7 +5,7 @@ import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
 import { instantiateFromConfig } from '../configs/Util';
 
 const port = 6001;
-const baseUrl = `http://localhost:${port}/`;
+const serverUrl = `http://localhost:${port}/`;
 
 describe('A server with the Solid WebSockets API', (): void => {
   let server: Server;
@@ -14,7 +14,7 @@ describe('A server with the Solid WebSockets API', (): void => {
     const factory = await instantiateFromConfig(
       'urn:solid-server:default:ServerFactory', 'websockets.json', {
         'urn:solid-server:default:variable:port': port,
-        'urn:solid-server:default:variable:base': baseUrl,
+        'urn:solid-server:default:variable:baseUrl': 'http://example.pod/',
       },
     ) as HttpServerFactory;
     server = factory.startServer(port);
@@ -27,17 +27,17 @@ describe('A server with the Solid WebSockets API', (): void => {
   });
 
   it('returns a 200.', async(): Promise<void> => {
-    const response = await fetch(baseUrl);
+    const response = await fetch(serverUrl, { headers: { host: 'example.pod' }});
     expect(response.status).toBe(200);
   });
 
   it('sets the Updates-Via header.', async(): Promise<void> => {
-    const response = await fetch(baseUrl);
-    expect(response.headers.get('Updates-Via')).toBe(`ws://localhost:${port}/`);
+    const response = await fetch(serverUrl, { headers: { host: 'example.pod' }});
+    expect(response.headers.get('Updates-Via')).toBe('ws://example.pod/');
   });
 
   it('exposes the Updates-Via header via CORS.', async(): Promise<void> => {
-    const response = await fetch(baseUrl);
+    const response = await fetch(serverUrl, { headers: { host: 'example.pod' }});
     expect(response.headers.get('Access-Control-Expose-Headers')!.split(','))
       .toContain('Updates-Via');
   });
@@ -47,7 +47,7 @@ describe('A server with the Solid WebSockets API', (): void => {
     const messages = new Array<string>();
 
     beforeAll(async(): Promise<void> => {
-      client = new WebSocket(`ws://localhost:${port}`, [ 'solid/0.1.0-alpha' ]);
+      client = new WebSocket(`ws://localhost:${port}`, [ 'solid/0.1.0-alpha' ], { headers: { host: 'example.pod' }});
       client.on('message', (message: string): any => messages.push(message));
       await new Promise((resolve): any => client.on('open', resolve));
     });
@@ -69,21 +69,24 @@ describe('A server with the Solid WebSockets API', (): void => {
 
     describe('when the client subscribes to a resource', (): void => {
       beforeAll(async(): Promise<void> => {
-        client.send(`sub ${baseUrl}my-resource`);
+        client.send(`sub http://example.pod/my-resource`);
         await new Promise((resolve): any => client.once('message', resolve));
       });
 
       it('acknowledges the subscription.', async(): Promise<void> => {
-        expect(messages).toEqual([ `ack ${baseUrl}my-resource` ]);
+        expect(messages).toEqual([ `ack http://example.pod/my-resource` ]);
       });
 
       it('notifies the client of resource updates.', async(): Promise<void> => {
-        await fetch(`${baseUrl}my-resource`, {
+        await fetch(`${serverUrl}my-resource`, {
           method: 'PUT',
-          headers: { 'content-type': 'application/json' },
+          headers: {
+            host: 'example.pod',
+            'content-type': 'application/json',
+          },
           body: '{}',
         });
-        expect(messages).toEqual([ `pub ${baseUrl}my-resource` ]);
+        expect(messages).toEqual([ `pub http://example.pod/my-resource` ]);
       });
     });
   });
diff --git a/test/unit/authentication/DPoPWebIdExtractor.test.ts b/test/unit/authentication/DPoPWebIdExtractor.test.ts
index b50f53c62..5dc21bb5a 100644
--- a/test/unit/authentication/DPoPWebIdExtractor.test.ts
+++ b/test/unit/authentication/DPoPWebIdExtractor.test.ts
@@ -83,7 +83,7 @@ describe('A DPoPWebIdExtractor', (): void => {
 
     it('returns the extracted WebID.', async(): Promise<void> => {
       const result = webIdExtractor.handleSafe(request);
-      await expect(result).resolves.toEqual({ webID: 'http://alice.example/card#me' });
+      await expect(result).resolves.toEqual({ webId: 'http://alice.example/card#me' });
     });
   });
 
diff --git a/test/unit/authentication/UnsecureWebIdExtractor.test.ts b/test/unit/authentication/UnsecureWebIdExtractor.test.ts
index 15505fc2d..8fd249b54 100644
--- a/test/unit/authentication/UnsecureWebIdExtractor.test.ts
+++ b/test/unit/authentication/UnsecureWebIdExtractor.test.ts
@@ -22,6 +22,6 @@ describe('An UnsecureWebIdExtractor', (): void => {
   it('returns the authorization header as WebID if there is one.', async(): Promise<void> => {
     const headers = { authorization: 'WebID http://alice.example/card#me' };
     const result = extractor.handleSafe({ headers } as HttpRequest);
-    await expect(result).resolves.toEqual({ webID: 'http://alice.example/card#me' });
+    await expect(result).resolves.toEqual({ webId: 'http://alice.example/card#me' });
   });
 });
diff --git a/test/unit/authorization/WebAclAuthorizer.test.ts b/test/unit/authorization/WebAclAuthorizer.test.ts
index fb9354cff..56b2ecd32 100644
--- a/test/unit/authorization/WebAclAuthorizer.test.ts
+++ b/test/unit/authorization/WebAclAuthorizer.test.ts
@@ -84,7 +84,7 @@ describe('A WebAclAuthorizer', (): void => {
       ]) } as Representation),
     } as unknown as ResourceStore;
     authorizer = new WebAclAuthorizer(aclManager, store);
-    credentials.webID = 'http://test.com/user';
+    credentials.webId = 'http://test.com/user';
     await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined();
   });
 
@@ -101,10 +101,10 @@ describe('A WebAclAuthorizer', (): void => {
   });
 
   it('allows access to specific agents if the acl files identifies them.', async(): Promise<void> => {
-    credentials.webID = 'http://test.com/user';
+    credentials.webId = 'http://test.com/user';
     const store = {
       getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
-        quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webID!)),
+        quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
         quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
         quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
       ]) } as Representation),
@@ -114,7 +114,7 @@ describe('A WebAclAuthorizer', (): void => {
   });
 
   it('errors if a specific agents wants to access files not assigned to them.', async(): Promise<void> => {
-    credentials.webID = 'http://test.com/user';
+    credentials.webId = 'http://test.com/user';
     const store = {
       getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
         quad(nn('auth'), nn(`${acl}agent`), nn('http://test.com/differentUser')),
@@ -127,11 +127,11 @@ describe('A WebAclAuthorizer', (): void => {
   });
 
   it('allows access to the acl file if control is allowed.', async(): Promise<void> => {
-    credentials.webID = 'http://test.com/user';
+    credentials.webId = 'http://test.com/user';
     identifier.path = 'http://test.com/foo';
     const store = {
       getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
-        quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webID!)),
+        quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
         quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
         quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)),
       ]) } as Representation),
@@ -142,11 +142,11 @@ describe('A WebAclAuthorizer', (): void => {
   });
 
   it('errors if an agent tries to edit the acl file without control permissions.', async(): Promise<void> => {
-    credentials.webID = 'http://test.com/user';
+    credentials.webId = 'http://test.com/user';
     identifier.path = 'http://test.com/foo';
     const store = {
       getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
-        quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webID!)),
+        quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
         quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
         quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
       ]) } as Representation),
diff --git a/test/unit/init/CliRunner.test.ts b/test/unit/init/CliRunner.test.ts
index a4cbb118e..9a8376d08 100644
--- a/test/unit/init/CliRunner.test.ts
+++ b/test/unit/init/CliRunner.test.ts
@@ -1,98 +1,126 @@
-import * as Path from 'path';
-import type { Loader } from 'componentsjs';
+import * as path from 'path';
+import { Loader } from 'componentsjs';
 import { runCli } from '../../../src/init/CliRunner';
 import type { Setup } from '../../../src/init/Setup';
 
-let calledInstantiateFromUrl: boolean;
-let calledRegisterAvailableModuleResources: boolean;
-let throwError: boolean;
-let outsideResolve: () => void;
-let functionToResolve: Promise<unknown>;
+const mainModulePath = path.join(__dirname, '../../../');
 
-const mockSetup = {
-  setup: jest.fn(),
-} as unknown as jest.Mocked<Setup>;
+const mockSetup: jest.Mocked<Setup> = {
+  setup: jest.fn(async(): Promise<any> => null),
+} as any;
+const loader: jest.Mocked<Loader> = {
+  instantiateFromUrl: jest.fn(async(): Promise<any> => mockSetup),
+  registerAvailableModuleResources: jest.fn(async(): Promise<any> => mockSetup),
+} as any;
 
 // Mock the Loader class.
-jest.mock('componentsjs', (): any => (
-  // eslint-disable-next-line @typescript-eslint/naming-convention, object-shorthand
-  { Loader: function(): Loader {
-    return {
-      instantiateFromUrl(): any {
-        calledInstantiateFromUrl = true;
-        if (throwError) {
-          throw new Error('Error! :o');
-        }
-        return mockSetup;
-      },
-      registerAvailableModuleResources(): any {
-        calledRegisterAvailableModuleResources = true;
-      },
-    } as unknown as Loader;
-  } }
-));
-
-jest.mock('yargs', (): any => ({
-  usage(): any {
-    return this;
-  },
-  options(): any {
-    return this;
-  },
-  help(): any {
-    // Return once with and once without values so that both branches are tested.
-    if (throwError) {
-      return {
-        argv: { config: 'value', rootFilePath: 'root' },
-      };
-    }
-    return {
-      argv: { },
-    };
-  },
+jest.mock('componentsjs', (): any => ({
+  // eslint-disable-next-line @typescript-eslint/naming-convention
+  Loader: jest.fn((): Loader => loader),
 }));
 
 describe('CliRunner', (): void => {
-  beforeAll(async(): Promise<void> => {
-    mockSetup.setup.mockImplementation(async(): Promise<any> => {
-      // The info method will be called when all other code has been executed, so end the waiting function.
-      outsideResolve();
-    });
+  afterEach((): void => {
+    jest.clearAllMocks();
   });
 
-  beforeEach(async(): Promise<void> => {
-    calledInstantiateFromUrl = false;
-    calledRegisterAvailableModuleResources = false;
-    throwError = false;
-
-    // Initialize a function that will be resolved as soon as all necessary but asynchronous calls are completed.
-    functionToResolve = new Promise((resolve): any => {
-      outsideResolve = resolve;
+  it('starts the server with default settings.', async(): Promise<void> => {
+    runCli({
+      argv: [ 'node', 'script' ],
     });
-  });
+    await mockSetup.setup();
 
-  it('Runs function for starting the server from the command line.', async(): Promise<void> => {
-    runCli(Path.join(__dirname, '..'));
+    expect(Loader).toHaveBeenCalledTimes(1);
+    expect(Loader).toHaveBeenCalledWith({ mainModulePath });
+    expect(loader.instantiateFromUrl).toHaveBeenCalledTimes(1);
+    expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
+      'urn:solid-server:default',
+      path.join(__dirname, '/../../../config/config-default.json'),
+      undefined,
+      {
+        variables: {
+          'urn:solid-server:default:variable:port': 3000,
+          'urn:solid-server:default:variable:baseUrl': 'http://localhost:3000/',
+          'urn:solid-server:default:variable:rootFilePath': process.cwd(),
+          'urn:solid-server:default:variable:sparqlEndpoint': undefined,
+          'urn:solid-server:default:variable:loggingLevel': 'info',
+        },
+      },
+    );
+    expect(loader.registerAvailableModuleResources).toHaveBeenCalledTimes(1);
+    expect(loader.registerAvailableModuleResources).toHaveBeenCalledWith();
+    expect(mockSetup.setup).toHaveBeenCalledTimes(1);
+    expect(mockSetup.setup).toHaveBeenCalledWith();
+  });
 
-    await functionToResolve;
+  it('accepts abbreviated flags.', async(): Promise<void> => {
+    runCli({
+      argv: [
+        'node', 'script',
+        '-p', '4000',
+        '-b', 'http://pod.example/',
+        '-c', 'myconfig.json',
+        '-f', '/root',
+        '-s', 'http://localhost:5000/sparql',
+        '-l', 'debug',
+      ],
+    });
+    await mockSetup.setup();
 
-    expect(calledInstantiateFromUrl).toBeTruthy();
-    expect(calledRegisterAvailableModuleResources).toBeTruthy();
-    expect(mockSetup.setup).toHaveBeenCalledTimes(1);
+    expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
+      'urn:solid-server:default',
+      path.join(process.cwd(), 'myconfig.json'),
+      undefined,
+      {
+        variables: {
+          'urn:solid-server:default:variable:port': 4000,
+          'urn:solid-server:default:variable:baseUrl': 'http://pod.example/',
+          'urn:solid-server:default:variable:rootFilePath': '/root',
+          'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
+          'urn:solid-server:default:variable:loggingLevel': 'debug',
+        },
+      },
+    );
   });
 
-  it('Writes to stderr when an exception occurs.', async(): Promise<void> => {
-    const mockStderr = jest.spyOn(process.stderr, 'write').mockImplementation((): any => {
-      // This method will be called when an error has occurred, so end the waiting function.
-      outsideResolve();
+  it('accepts full flags.', async(): Promise<void> => {
+    runCli({
+      argv: [
+        'node', 'script',
+        '--port', '4000',
+        '--baseUrl', 'http://pod.example/',
+        '--config', 'myconfig.json',
+        '--rootFilePath', '/root',
+        '--sparqlEndpoint', 'http://localhost:5000/sparql',
+        '--loggingLevel', 'debug',
+      ],
     });
-    throwError = true;
+    await mockSetup.setup();
+
+    expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
+      'urn:solid-server:default',
+      path.join(process.cwd(), 'myconfig.json'),
+      undefined,
+      {
+        variables: {
+          'urn:solid-server:default:variable:port': 4000,
+          'urn:solid-server:default:variable:baseUrl': 'http://pod.example/',
+          'urn:solid-server:default:variable:rootFilePath': '/root',
+          'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
+          'urn:solid-server:default:variable:loggingLevel': 'debug',
+        },
+      },
+    );
+  });
 
-    runCli(Path.join(__dirname, '..'));
+  it('writes to stderr when an error occurs.', async(): Promise<void> => {
+    jest.spyOn(process.stderr, 'write');
+    loader.instantiateFromUrl.mockRejectedValueOnce(new Error('Fatal'));
 
-    await functionToResolve;
+    runCli();
+    await new Promise((resolve): any => setImmediate(resolve));
 
-    expect(mockStderr).toHaveBeenCalledTimes(1);
-    mockStderr.mockRestore();
+    expect(process.stderr.write).toHaveBeenCalledTimes(1);
+    expect(process.stderr.write).toHaveBeenCalledWith('Error: Fatal\n');
   });
 });
diff --git a/test/unit/ldp/AuthenticatedLdpHandler.test.ts b/test/unit/ldp/AuthenticatedLdpHandler.test.ts
index 690499cda..313b34343 100644
--- a/test/unit/ldp/AuthenticatedLdpHandler.test.ts
+++ b/test/unit/ldp/AuthenticatedLdpHandler.test.ts
@@ -16,7 +16,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
   let responseFn: jest.Mock<Promise<void>, [any]>;
 
   beforeEach(async(): Promise<void> => {
-    const requestParser: RequestParser = new StaticAsyncHandler(true, 'parser' as any);
+    const requestParser: RequestParser = new StaticAsyncHandler(true, ({ target: 'target' }) as any);
     const credentialsExtractor: CredentialsExtractor = new StaticAsyncHandler(true, 'credentials' as any);
     const permissionsExtractor: PermissionsExtractor = new StaticAsyncHandler(true, 'permissions' as any);
     const authorizer: Authorizer = new StaticAsyncHandler(true, 'authorizer' as any);
diff --git a/test/unit/server/middleware/WebSocketAdvertiser.test.ts b/test/unit/server/middleware/WebSocketAdvertiser.test.ts
index 5421c5346..37695f968 100644
--- a/test/unit/server/middleware/WebSocketAdvertiser.test.ts
+++ b/test/unit/server/middleware/WebSocketAdvertiser.test.ts
@@ -2,36 +2,31 @@ import { createResponse } from 'node-mocks-http';
 import { WebSocketAdvertiser } from '../../../../src/server/middleware/WebSocketAdvertiser';
 
 describe('A WebSocketAdvertiser', (): void => {
-  it('writes a default HTTP WebSocket.', async(): Promise<void> => {
-    const writer = new WebSocketAdvertiser();
+  it('writes a ws: socket when given an http: URL.', async(): Promise<void> => {
+    const writer = new WebSocketAdvertiser('http://test.example/');
     const response = createResponse();
     await writer.handle({ response } as any);
-    expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://localhost/' });
+    expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example/' });
   });
 
-  it('writes an HTTP WebSocket with port 80.', async(): Promise<void> => {
-    const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 80, protocol: 'http' });
+  it('writes a ws: socket when given a ws: URL.', async(): Promise<void> => {
+    const writer = new WebSocketAdvertiser('ws://test.example/');
     const response = createResponse();
     await writer.handle({ response } as any);
     expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example/' });
   });
 
-  it('writes an HTTP WebSocket with port 3000.', async(): Promise<void> => {
-    const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 3000, protocol: 'http' });
+  it('writes a wss: socket when given an https: URL.', async(): Promise<void> => {
+    const writer = new WebSocketAdvertiser('https://test.example/');
     const response = createResponse();
     await writer.handle({ response } as any);
-    expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example:3000/' });
+    expect(response.getHeaders()).toEqual({ 'updates-via': 'wss://test.example/' });
   });
 
-  it('writes an HTTPS WebSocket with port 443.', async(): Promise<void> => {
-    const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 443, protocol: 'https' });
+  it('writes a wss: socket when given a wss: URL.', async(): Promise<void> => {
+    const writer = new WebSocketAdvertiser('wss://test.example/');
     const response = createResponse();
     await writer.handle({ response } as any);
     expect(response.getHeaders()).toEqual({ 'updates-via': 'wss://test.example/' });
   });
-
-  it('rejects an invalid hostname.', (): void => {
-    expect((): any => new WebSocketAdvertiser({ hostname: 'test.example/invalid' }))
-      .toThrow('Invalid hostname: test.example/invalid');
-  });
 });
diff --git a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
index 20bb3ecab..8180edf44 100644
--- a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
+++ b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
@@ -199,4 +199,20 @@ describe('A SparqlUpdatePatchHandler', (): void => {
     await expect(handle).rejects.toThrow('Only DELETE/INSERT SPARQL update operations are supported');
     expect(order).toEqual([]);
   });
+
+  it('releases the lock if an error occurs while patching.', async(): Promise<void> => {
+    source.getRepresentation = jest.fn(async(): Promise<any> => {
+      order.push('getRepresentation');
+      throw new Error('error');
+    });
+
+    const input = { identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. ' +
+        '<http://test.com/s2> <http://test.com/p2> <http://test.com/o2> }',
+        { quads: true },
+      ) } as SparqlUpdatePatch };
+    await expect(handler.handle(input)).rejects.toThrow('error');
+    expect(order).toEqual([ 'acquire', 'getRepresentation', 'release' ]);
+  });
 });
diff --git a/test/unit/util/PathUtil.test.ts b/test/unit/util/PathUtil.test.ts
index 30e090aad..e319b11d5 100644
--- a/test/unit/util/PathUtil.test.ts
+++ b/test/unit/util/PathUtil.test.ts
@@ -37,7 +37,8 @@ describe('PathUtil', (): void => {
     });
 
     it('errors when the root of an URl is reached that does not match the input root.', async(): Promise<void> => {
-      expect((): any => getParentContainer({ path: 'http://test.com/' })).toThrow('URL root reached');
+      expect((): any => getParentContainer({ path: 'http://test.com/' }))
+        .toThrow('Resource http://test.com/ does not have a parent container');
     });
   });
 });