Repository: solid/community-server (ts)

Issue:
Initialization versus runtime config
While setting up our dependency injection, I ran into some problems in the way some config options are being passed around.

Concretely, starting the server from the CLI allows you to pass a `port`. This `port` determines the `baseUrl`. This `baseUrl` is passed into the constructors of several components.
The problem here is that this makes it impossible to have a clean standalone dependency injection config to instantiate components, since we depend on values from the CLI, which are only available _after_ component instantiation.

If we would make a **cleaner separation** of **initialization config** (what goes in the config file), and **runtime config** (what is passed from the CLI), we should be able to solve this.

Concretely, what I would propose is to create a new **`RuntimeConfig`** component that can be passed to other components during dependency injection. While DI is going on, this component will not contain any data yet. When DI is done, and the server is started, it can be populated with data derived from the CLI, such as `port`, `baseUrl`, ... Other components can then read this data from `RuntimeConfig` at runtime.

The important thing to take into account here is that `RuntimeConfig` is passed around at initialization time, but can only be read at runtime.

Of course, we could still define default values in `RuntimeConfig` via our config file. This would mean that we could define a port in our config file, but if we pass one via the CLI, it would be overridden.

_For reference, LDF server does something similar with [`UrlData`](https://github.com/LinkedDataFragments/Server.js/blob/6cd5b59c6ba8a7dc1e7ecc7072d90bdfab492849/packages/core/lib/UrlData.js)_

Any thoughts before I implement this?

Pull request description:
Move runtime config into dedicated component, Closes #67

Patch:
diff --git a/bin/server.ts b/bin/server.ts
index acfaeaeb1..042b71779 100644
--- a/bin/server.ts
+++ b/bin/server.ts
@@ -11,6 +11,7 @@ import {
   QuadToTurtleConverter,
   Representation,
   RepresentationConvertingStore,
+  RuntimeConfig,
   Setup,
   SimpleAclAuthorizer,
   SimpleBodyParser,
@@ -36,15 +37,13 @@ import {
 const { argv } = yargs
   .usage('node ./bin/server.js [args]')
   .options({
-    port: { type: 'number', alias: 'p', default: 3000 },
+    port: { type: 'number', alias: 'p' },
   })
   .help();
 
-const { port } = argv;
-
-const base = `http://localhost:${port}/`;
-
 // This is instead of the dependency injection that still needs to be added
+const runtimeConfig = new RuntimeConfig();
+
 const bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
   new SimpleSparqlUpdateBodyParser(),
   new SimpleBodyParser(),
@@ -62,7 +61,7 @@ const permissionsExtractor = new CompositeAsyncHandler([
 ]);
 
 // Will have to see how to best handle this
-const store = new SimpleResourceStore(base);
+const store = new SimpleResourceStore(runtimeConfig);
 const converter = new CompositeAsyncHandler([
   new TurtleToQuadConverter(),
   new QuadToTurtleConverter(),
@@ -73,7 +72,7 @@ const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
 const patchingStore = new PatchingStore(convertingStore, patcher);
 
 const aclManager = new SimpleExtensionAclManager();
-const containerManager = new UrlContainerManager(base);
+const containerManager = new UrlContainerManager(runtimeConfig);
 const authorizer = new SimpleAclAuthorizer(aclManager, containerManager, patchingStore);
 
 const operationHandler = new CompositeAsyncHandler([
@@ -97,9 +96,11 @@ const httpHandler = new AuthenticatedLdpHandler({
 
 const httpServer = new ExpressHttpServer(httpHandler);
 
-const setup = new Setup(httpServer, store, aclManager);
-setup.setup(port, base).then((): void => {
-  process.stdout.write(`Running at ${base}\n`);
+const setup = new Setup(httpServer, store, aclManager, runtimeConfig);
+
+runtimeConfig.reset({ port: argv.port });
+setup.setup().then((): void => {
+  process.stdout.write(`Running at ${runtimeConfig.base}\n`);
 }).catch((error): void => {
   process.stderr.write(`${error}\n`);
   process.exit(1);
diff --git a/index.ts b/index.ts
index b1cdcb5f4..5425865dd 100644
--- a/index.ts
+++ b/index.ts
@@ -11,6 +11,7 @@ export * from './src/authorization/SimpleAuthorizer';
 export * from './src/authorization/SimpleExtensionAclManager';
 
 // Init
+export * from './src/init/RuntimeConfig';
 export * from './src/init/Setup';
 
 // LDP/HTTP
diff --git a/src/init/RuntimeConfig.ts b/src/init/RuntimeConfig.ts
new file mode 100644
index 000000000..d0d4d572c
--- /dev/null
+++ b/src/init/RuntimeConfig.ts
@@ -0,0 +1,38 @@
+/**
+ * This class holds all configuration options that can be defined by the user via the command line.
+ *
+ * Concretely, this contains data that is only relevant *after* dependency injection.
+ */
+export class RuntimeConfig implements RuntimeConfigData {
+  private pport!: number;
+  private pbase!: string;
+  private prootFilepath!: string;
+
+  public constructor(data: RuntimeConfigData = {}) {
+    this.reset(data);
+  }
+
+  public reset(data: RuntimeConfigData): void {
+    this.pport = data.port ?? 3000;
+    this.pbase = data.base ?? `http://localhost:${this.port}/`;
+    this.prootFilepath = data.rootFilepath ?? process.cwd();
+  }
+
+  public get base(): string {
+    return this.pbase;
+  }
+
+  public get port(): number {
+    return this.pport;
+  }
+
+  public get rootFilepath(): string {
+    return this.prootFilepath;
+  }
+}
+
+export interface RuntimeConfigData {
+  port?: number;
+  base?: string;
+  rootFilepath?: string;
+}
diff --git a/src/init/Setup.ts b/src/init/Setup.ts
index 9aa3d9a18..8a45bbfb0 100644
--- a/src/init/Setup.ts
+++ b/src/init/Setup.ts
@@ -2,6 +2,7 @@ import { AclManager } from '../authorization/AclManager';
 import { DATA_TYPE_BINARY } from '../util/ContentTypes';
 import { ExpressHttpServer } from '../server/ExpressHttpServer';
 import { ResourceStore } from '../storage/ResourceStore';
+import { RuntimeConfig } from './RuntimeConfig';
 import streamifyArray from 'streamify-array';
 
 /**
@@ -11,11 +12,18 @@ export class Setup {
   private readonly httpServer: ExpressHttpServer;
   private readonly store: ResourceStore;
   private readonly aclManager: AclManager;
+  private readonly runtimeConfig: RuntimeConfig;
 
-  public constructor(httpServer: ExpressHttpServer, store: ResourceStore, aclManager: AclManager) {
+  public constructor(
+    httpServer: ExpressHttpServer,
+    store: ResourceStore,
+    aclManager: AclManager,
+    runtimeConfig: RuntimeConfig,
+  ) {
     this.httpServer = httpServer;
     this.store = store;
     this.aclManager = aclManager;
+    this.runtimeConfig = runtimeConfig;
   }
 
   /**
@@ -23,7 +31,7 @@ export class Setup {
    * @param port - A port number.
    * @param base - A base URL.
    */
-  public async setup(port: number, base: string): Promise<void> {
+  public async setup(): Promise<void> {
     // Set up acl so everything can still be done by default
     // Note that this will need to be adapted to go through all the correct channels later on
     const aclSetup = async(): Promise<void> => {
@@ -38,10 +46,10 @@ export class Setup {
     acl:mode        acl:Append;
     acl:mode        acl:Delete;
     acl:mode        acl:Control;
-    acl:accessTo    <${base}>;
-    acl:default     <${base}>.`;
+    acl:accessTo    <${this.runtimeConfig.base}>;
+    acl:default     <${this.runtimeConfig.base}>.`;
       await this.store.setRepresentation(
-        await this.aclManager.getAcl({ path: base }),
+        await this.aclManager.getAcl({ path: this.runtimeConfig.base }),
         {
           dataType: DATA_TYPE_BINARY,
           data: streamifyArray([ acl ]),
@@ -56,6 +64,6 @@ export class Setup {
 
     await aclSetup();
 
-    this.httpServer.listen(port);
+    this.httpServer.listen(this.runtimeConfig.port);
   }
 }
diff --git a/src/storage/FileResourceStore.ts b/src/storage/FileResourceStore.ts
index 4df3b8543..d04cef932 100644
--- a/src/storage/FileResourceStore.ts
+++ b/src/storage/FileResourceStore.ts
@@ -12,6 +12,7 @@ import { Representation } from '../ldp/representation/Representation';
 import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
 import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 import { ResourceStore } from './ResourceStore';
+import { RuntimeConfig } from '../init/RuntimeConfig';
 import streamifyArray from 'streamify-array';
 import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError';
 import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY, DATA_TYPE_QUAD } from '../util/ContentTypes';
@@ -25,26 +26,30 @@ const { extname, join: joinPath, normalize: normalizePath } = posix;
  * All requests will throw an {@link NotFoundHttpError} if unknown identifiers get passed.
  */
 export class FileResourceStore implements ResourceStore {
-  private readonly baseRequestURI: string;
-  private readonly rootFilepath: string;
+  private readonly runtimeConfig: RuntimeConfig;
   private readonly interactionController: InteractionController;
   private readonly metadataController: MetadataController;
 
   /**
-   * @param baseRequestURI - Will be stripped of all incoming URIs and added to all outgoing ones to find the relative
-   * path.
-   * @param rootFilepath - Root filepath in which the resources and containers will be saved as files and directories.
+   * @param runtimeConfig - The runtime config.
    * @param interactionController - Instance of InteractionController to use.
    * @param metadataController - Instance of MetadataController to use.
    */
-  public constructor(baseRequestURI: string, rootFilepath: string, interactionController: InteractionController,
+  public constructor(runtimeConfig: RuntimeConfig, interactionController: InteractionController,
     metadataController: MetadataController) {
-    this.baseRequestURI = trimTrailingSlashes(baseRequestURI);
-    this.rootFilepath = trimTrailingSlashes(rootFilepath);
+    this.runtimeConfig = runtimeConfig;
     this.interactionController = interactionController;
     this.metadataController = metadataController;
   }
 
+  public get baseRequestURI(): string {
+    return trimTrailingSlashes(this.runtimeConfig.base);
+  }
+
+  public get rootFilepath(): string {
+    return trimTrailingSlashes(this.runtimeConfig.rootFilepath);
+  }
+
   /**
    * Store the incoming data as a file under a file path corresponding to `container.path`,
    * where slashes correspond to subdirectories.
diff --git a/src/storage/SimpleResourceStore.ts b/src/storage/SimpleResourceStore.ts
index b7fa1d0e4..2fae99a55 100644
--- a/src/storage/SimpleResourceStore.ts
+++ b/src/storage/SimpleResourceStore.ts
@@ -5,6 +5,7 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { Representation } from '../ldp/representation/Representation';
 import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 import { ResourceStore } from './ResourceStore';
+import { RuntimeConfig } from '../init/RuntimeConfig';
 import streamifyArray from 'streamify-array';
 
 /**
@@ -13,14 +14,15 @@ import streamifyArray from 'streamify-array';
  */
 export class SimpleResourceStore implements ResourceStore {
   private readonly store: { [id: string]: Representation };
-  private readonly base: string;
+  private readonly runtimeConfig: RuntimeConfig;
   private index = 0;
 
   /**
-   * @param base - Will be stripped of all incoming URIs and added to all outgoing ones to find the relative path.
+   * @param runtimeConfig - Config containing base that will be stripped of all incoming URIs
+   *                        and added to all outgoing ones to find the relative path.
    */
-  public constructor(base: string) {
-    this.base = base;
+  public constructor(runtimeConfig: RuntimeConfig) {
+    this.runtimeConfig = runtimeConfig;
 
     this.store = {
       // Default root entry (what you get when the identifier is equal to the base)
@@ -102,8 +104,8 @@ export class SimpleResourceStore implements ResourceStore {
    * @returns A string representing the relative path.
    */
   private parseIdentifier(identifier: ResourceIdentifier): string {
-    const path = identifier.path.slice(this.base.length);
-    if (!identifier.path.startsWith(this.base)) {
+    const path = identifier.path.slice(this.runtimeConfig.base.length);
+    if (!identifier.path.startsWith(this.runtimeConfig.base)) {
       throw new NotFoundHttpError();
     }
     return path;
diff --git a/src/storage/UrlContainerManager.ts b/src/storage/UrlContainerManager.ts
index e6ce845ea..07a1435f1 100644
--- a/src/storage/UrlContainerManager.ts
+++ b/src/storage/UrlContainerManager.ts
@@ -1,20 +1,21 @@
 import { ContainerManager } from './ContainerManager';
 import { ensureTrailingSlash } from '../util/Util';
 import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
+import { RuntimeConfig } from '../init/RuntimeConfig';
 
 /**
  * Determines containers based on URL decomposition.
  */
 export class UrlContainerManager implements ContainerManager {
-  private readonly root: string;
+  private readonly runtimeConfig: RuntimeConfig;
 
-  public constructor(root: string) {
-    this.root = this.canonicalUrl(root);
+  public constructor(runtimeConfig: RuntimeConfig) {
+    this.runtimeConfig = runtimeConfig;
   }
 
   public async getContainer(id: ResourceIdentifier): Promise<ResourceIdentifier> {
     const path = this.canonicalUrl(id.path);
-    if (this.root === path) {
+    if (this.canonicalUrl(this.runtimeConfig.base) === path) {
       throw new Error('Root does not have a container.');
     }
 


Test patch:
diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts
index d5434ad9e..41238e1fc 100644
--- a/test/integration/AuthenticatedLdpHandler.test.ts
+++ b/test/integration/AuthenticatedLdpHandler.test.ts
@@ -13,6 +13,7 @@ import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtle
 import { Representation } from '../../src/ldp/representation/Representation';
 import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore';
 import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription';
+import { RuntimeConfig } from '../../src/init/RuntimeConfig';
 import { SimpleAuthorizer } from '../../src/authorization/SimpleAuthorizer';
 import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
 import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor';
@@ -44,7 +45,7 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
     const permissionsExtractor = new BasePermissionsExtractor();
     const authorizer = new SimpleAuthorizer();
 
-    const store = new SimpleResourceStore('http://test.com/');
+    const store = new SimpleResourceStore(new RuntimeConfig({ base: 'http://test.com/' }));
     const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
       new SimpleGetOperationHandler(store),
       new SimplePostOperationHandler(store),
@@ -115,7 +116,7 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
     ]);
     const authorizer = new SimpleAuthorizer();
 
-    const store = new SimpleResourceStore('http://test.com/');
+    const store = new SimpleResourceStore(new RuntimeConfig({ base: 'http://test.com/' }));
     const converter = new CompositeAsyncHandler([
       new QuadToTurtleConverter(),
       new TurtleToQuadConverter(),
diff --git a/test/integration/Authorization.test.ts b/test/integration/Authorization.test.ts
index d47df48a5..aa3dd809b 100644
--- a/test/integration/Authorization.test.ts
+++ b/test/integration/Authorization.test.ts
@@ -12,6 +12,7 @@ import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtle
 import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore';
 import { ResourceStore } from '../../src/storage/ResourceStore';
 import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription';
+import { RuntimeConfig } from '../../src/init/RuntimeConfig';
 import { SimpleAclAuthorizer } from '../../src/authorization/SimpleAclAuthorizer';
 import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
 import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor';
@@ -80,7 +81,7 @@ describe('A server with authorization', (): void => {
     bodyParser,
   });
 
-  const store = new SimpleResourceStore('http://test.com/');
+  const store = new SimpleResourceStore(new RuntimeConfig({ base: 'http://test.com/' }));
   const converter = new CompositeAsyncHandler([
     new QuadToTurtleConverter(),
     new TurtleToQuadConverter(),
@@ -91,7 +92,7 @@ describe('A server with authorization', (): void => {
   const permissionsExtractor = new BasePermissionsExtractor();
   const authorizer = new SimpleAclAuthorizer(
     new SimpleExtensionAclManager(),
-    new UrlContainerManager('http://test.com/'),
+    new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/' })),
     convertingStore,
   );
 
diff --git a/test/unit/init/RuntimeConfig.test.ts b/test/unit/init/RuntimeConfig.test.ts
new file mode 100644
index 000000000..36a609674
--- /dev/null
+++ b/test/unit/init/RuntimeConfig.test.ts
@@ -0,0 +1,43 @@
+import { RuntimeConfig } from '../../../src/init/RuntimeConfig';
+
+describe('RuntimeConfig', (): void => {
+  it('handles undefined args.', async(): Promise<void> => {
+    const config = new RuntimeConfig();
+    expect(config.port).toEqual(3000);
+    expect(config.base).toEqual('http://localhost:3000/');
+  });
+
+  it('handles empty args.', async(): Promise<void> => {
+    const config = new RuntimeConfig({});
+    expect(config.port).toEqual(3000);
+    expect(config.base).toEqual('http://localhost:3000/');
+  });
+
+  it('handles args with port.', async(): Promise<void> => {
+    const config = new RuntimeConfig({ port: 1234 });
+    expect(config.port).toEqual(1234);
+    expect(config.base).toEqual('http://localhost:1234/');
+  });
+
+  it('handles args with base.', async(): Promise<void> => {
+    const config = new RuntimeConfig({ base: 'http://example.org/' });
+    expect(config.port).toEqual(3000);
+    expect(config.base).toEqual('http://example.org/');
+  });
+
+  it('handles args with port and base.', async(): Promise<void> => {
+    const config = new RuntimeConfig({ port: 1234, base: 'http://example.org/' });
+    expect(config.port).toEqual(1234);
+    expect(config.base).toEqual('http://example.org/');
+  });
+
+  it('handles resetting data.', async(): Promise<void> => {
+    const config = new RuntimeConfig({});
+    expect(config.port).toEqual(3000);
+    expect(config.base).toEqual('http://localhost:3000/');
+
+    config.reset({ port: 1234, base: 'http://example.org/' });
+    expect(config.port).toEqual(1234);
+    expect(config.base).toEqual('http://example.org/');
+  });
+});
diff --git a/test/unit/init/Setup.test.ts b/test/unit/init/Setup.test.ts
index 230c70901..1a9311c8e 100644
--- a/test/unit/init/Setup.test.ts
+++ b/test/unit/init/Setup.test.ts
@@ -1,3 +1,4 @@
+import { RuntimeConfig } from '../../../src/init/RuntimeConfig';
 import { Setup } from '../../../src/init/Setup';
 
 describe('Setup', (): void => {
@@ -15,16 +16,16 @@ describe('Setup', (): void => {
     httpServer = {
       listen: jest.fn(),
     };
-    setup = new Setup(httpServer, store, aclManager);
+    setup = new Setup(httpServer, store, aclManager, new RuntimeConfig());
   });
 
   it('starts an HTTP server.', async(): Promise<void> => {
-    await setup.setup(3000, 'http://localhost:3000/');
+    await setup.setup();
     expect(httpServer.listen).toHaveBeenCalledWith(3000);
   });
 
   it('invokes ACL initialization.', async(): Promise<void> => {
-    await setup.setup(3000, 'http://localhost:3000/');
+    await setup.setup();
     expect(aclManager.getAcl).toHaveBeenCalledWith({ path: 'http://localhost:3000/' });
     expect(store.setRepresentation).toHaveBeenCalledTimes(1);
   });
diff --git a/test/unit/storage/FileResourceStore.test.ts b/test/unit/storage/FileResourceStore.test.ts
index d88128cbe..db1b380ed 100644
--- a/test/unit/storage/FileResourceStore.test.ts
+++ b/test/unit/storage/FileResourceStore.test.ts
@@ -10,6 +10,7 @@ import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { posix } from 'path';
 import { Readable } from 'stream';
 import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
+import { RuntimeConfig } from '../../../src/init/RuntimeConfig';
 import streamifyArray from 'streamify-array';
 import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError';
 import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY, DATA_TYPE_QUAD } from '../../../src/util/ContentTypes';
@@ -21,7 +22,7 @@ import { literal, namedNode, quad as quadRDF, triple } from '@rdfjs/data-model';
 const { join: joinPath } = posix;
 
 const base = 'http://test.com/';
-const root = '/Users/default/home/public/';
+const rootFilepath = '/Users/default/home/public/';
 
 fsPromises.rmdir = jest.fn();
 fsPromises.lstat = jest.fn();
@@ -48,7 +49,11 @@ describe('A FileResourceStore', (): void => {
   beforeEach(async(): Promise<void> => {
     jest.clearAllMocks();
 
-    store = new FileResourceStore(base, root, new InteractionController(), new MetadataController());
+    store = new FileResourceStore(
+      new RuntimeConfig({ base, rootFilepath }),
+      new InteractionController(),
+      new MetadataController(),
+    );
 
     representation = {
       data: streamifyArray([ rawData ]),
@@ -131,7 +136,7 @@ describe('A FileResourceStore', (): void => {
     // Write container (POST)
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []};
     const identifier = await store.addResource({ path: base }, representation);
-    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'myContainer/'), { recursive: true });
+    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'myContainer/'), { recursive: true });
     expect(identifier.path).toBe(`${base}myContainer/`);
 
     // Read container
@@ -155,7 +160,7 @@ describe('A FileResourceStore', (): void => {
     // Tests
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []};
     await expect(store.addResource({ path: `${base}foo` }, representation)).rejects.toThrow(MethodNotAllowedHttpError);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo'));
   });
 
   it('errors 405 for POST invalid path ending without slash.', async(): Promise<void> => {
@@ -172,17 +177,17 @@ describe('A FileResourceStore', (): void => {
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []};
     await expect(store.addResource({ path: `${base}doesnotexist` }, representation))
       .rejects.toThrow(MethodNotAllowedHttpError);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'doesnotexist'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexist'));
 
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: []};
     await expect(store.addResource({ path: `${base}doesnotexist` }, representation))
       .rejects.toThrow(MethodNotAllowedHttpError);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'doesnotexist'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexist'));
 
     representation.metadata = { linkRel: { type: new Set() }, slug: 'file.txt', raw: []};
     await expect(store.addResource({ path: `${base}existingresource` }, representation))
       .rejects.toThrow(MethodNotAllowedHttpError);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'existingresource'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'existingresource'));
   });
 
   it('can set data.', async(): Promise<void> => {
@@ -204,7 +209,7 @@ describe('A FileResourceStore', (): void => {
 
     // Tests
     await store.setRepresentation({ path: `${base}file.txt` }, representation);
-    expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt'));
+    expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
     const result = await store.getRepresentation({ path: `${base}file.txt` });
     expect(result).toEqual({
       dataType: DATA_TYPE_BINARY,
@@ -217,9 +222,9 @@ describe('A FileResourceStore', (): void => {
       },
     });
     await expect(arrayifyStream(result.data)).resolves.toEqual([ rawData ]);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt'));
-    expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt'));
-    expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt.metadata'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
+    expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
+    expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata'));
   });
 
   it('can delete data.', async(): Promise<void> => {
@@ -239,7 +244,7 @@ describe('A FileResourceStore', (): void => {
 
     // Tests
     await store.deleteResource({ path: `${base}file.txt` });
-    expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt'));
+    expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
     await expect(store.getRepresentation({ path: `${base}file.txt` })).rejects.toThrow(NotFoundHttpError);
   });
 
@@ -253,8 +258,9 @@ describe('A FileResourceStore', (): void => {
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: []};
     const identifier = await store.addResource({ path: `${base}doesnotexistyet/` }, representation);
     expect(identifier.path).toBe(`${base}doesnotexistyet/file.txt`);
-    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'doesnotexistyet/'), { recursive: true });
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'doesnotexistyet/'));
+    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexistyet/'),
+      { recursive: true });
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexistyet/'));
   });
 
   it('creates metadata file when metadata triples are passed.', async(): Promise<void> => {
@@ -277,8 +283,8 @@ describe('A FileResourceStore', (): void => {
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: [ quad ]};
     representation.data = readableMock;
     await store.addResource({ path: `${base}foo/` }, representation);
-    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'), { recursive: true });
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'));
+    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'), { recursive: true });
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
 
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: [ quad ]};
     await store.setRepresentation({ path: `${base}foo/file.txt` }, representation);
@@ -298,8 +304,8 @@ describe('A FileResourceStore', (): void => {
 
     // Tests
     await expect(store.deleteResource({ path: `${base}notempty/` })).rejects.toThrow(ConflictHttpError);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'notempty/'));
-    expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(root, 'notempty/'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'notempty/'));
+    expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'notempty/'));
   });
 
   it('deletes metadata file when deleting container.', async(): Promise<void> => {
@@ -312,10 +318,10 @@ describe('A FileResourceStore', (): void => {
 
     // Tests
     await store.deleteResource({ path: `${base}foo/` });
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'));
-    expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'));
-    expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(root, 'foo', '.metadata'));
-    expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
+    expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
+    expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', '.metadata'));
+    expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
   });
 
   it('errors 404 when accessing non resource (file/directory), e.g. special files.', async(): Promise<void> => {
@@ -368,10 +374,10 @@ describe('A FileResourceStore', (): void => {
       },
     });
     await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray(quads);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'));
-    expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'));
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo', 'file.txt'));
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo', '.nonresource'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
+    expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', 'file.txt'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', '.nonresource'));
   });
 
   it('can overwrite representation with PUT.', async(): Promise<void> => {
@@ -387,8 +393,8 @@ describe('A FileResourceStore', (): void => {
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: []};
     await store.setRepresentation({ path: `${base}alreadyexists.txt` }, representation);
     expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(1);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'alreadyexists.txt'));
-    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(root, { recursive: true });
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists.txt'));
+    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(rootFilepath, { recursive: true });
   });
 
   it('errors when overwriting container with PUT.', async(): Promise<void> => {
@@ -399,7 +405,7 @@ describe('A FileResourceStore', (): void => {
     // Tests
     await expect(store.setRepresentation({ path: `${base}alreadyexists` }, representation)).rejects
       .toThrow(ConflictHttpError);
-    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'alreadyexists'));
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists'));
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, raw: []};
     await expect(store.setRepresentation({ path: `${base}alreadyexists/` }, representation)).rejects
       .toThrow(ConflictHttpError);
@@ -445,9 +451,9 @@ describe('A FileResourceStore', (): void => {
     // Tests
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: [ quad ]};
     await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error);
-    expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt.metadata'));
-    expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt'));
-    expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt.metadata'));
+    expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata'));
+    expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
+    expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata'));
   });
 
   it('undoes container creation when metadata file creation fails.', async(): Promise<void> => {
@@ -461,7 +467,7 @@ describe('A FileResourceStore', (): void => {
     // Tests
     representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'foo/', raw: [ quad ]};
     await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error);
-    expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'));
+    expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
   });
 
   it('creates container when POSTing without linkRel and with slug ending with slash.', async(): Promise<void> => {
@@ -474,7 +480,7 @@ describe('A FileResourceStore', (): void => {
     const identifier = await store.addResource({ path: base }, representation);
     expect(identifier.path).toBe(`${base}myContainer/`);
     expect(fsPromises.mkdir as jest.Mock).toBeCalledTimes(1);
-    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'myContainer/'), { recursive: true });
+    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'myContainer/'), { recursive: true });
   });
 
   it('returns no contentType when unknown for representation.', async(): Promise<void> => {
@@ -514,9 +520,9 @@ describe('A FileResourceStore', (): void => {
     // Tests
     representation.metadata = { raw: []};
     await store.setRepresentation({ path: `${base}file.txt` }, representation);
-    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(root, { recursive: true });
+    expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(rootFilepath, { recursive: true });
     expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(1);
-    expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt'));
+    expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
   });
 
   it('creates container when POST to existing container path ending without slash and slug without slash.',
@@ -530,7 +536,7 @@ describe('A FileResourceStore', (): void => {
       representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'bar', raw: []};
       const identifier = await store.addResource({ path: `${base}foo` }, representation);
       expect(identifier.path).toBe(`${base}foo/bar/`);
-      expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo'));
-      expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo', 'bar/'), { recursive: false });
+      expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo'));
+      expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', 'bar/'), { recursive: false });
     });
 });
diff --git a/test/unit/storage/SimpleResourceStore.test.ts b/test/unit/storage/SimpleResourceStore.test.ts
index c63040c42..546337d7b 100644
--- a/test/unit/storage/SimpleResourceStore.test.ts
+++ b/test/unit/storage/SimpleResourceStore.test.ts
@@ -4,6 +4,7 @@ import { DATA_TYPE_BINARY } from '../../../src/util/ContentTypes';
 import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { Readable } from 'stream';
 import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
+import { RuntimeConfig } from '../../../src/init/RuntimeConfig';
 import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore';
 import streamifyArray from 'streamify-array';
 
@@ -15,7 +16,7 @@ describe('A SimpleResourceStore', (): void => {
   const dataString = '<http://test.com/s> <http://test.com/p> <http://test.com/o>.';
 
   beforeEach(async(): Promise<void> => {
-    store = new SimpleResourceStore(base);
+    store = new SimpleResourceStore(new RuntimeConfig({ base }));
 
     representation = {
       data: streamifyArray([ dataString ]),
diff --git a/test/unit/storage/UrlContainerManager.test.ts b/test/unit/storage/UrlContainerManager.test.ts
index a6c786071..4df65bf8f 100644
--- a/test/unit/storage/UrlContainerManager.test.ts
+++ b/test/unit/storage/UrlContainerManager.test.ts
@@ -1,8 +1,9 @@
+import { RuntimeConfig } from '../../../src/init/RuntimeConfig';
 import { UrlContainerManager } from '../../../src/storage/UrlContainerManager';
 
 describe('An UrlContainerManager', (): void => {
   it('returns the parent URl for a single call.', async(): Promise<void> => {
-    const manager = new UrlContainerManager('http://test.com/foo/');
+    const manager = new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/foo/' }));
     await expect(manager.getContainer({ path: 'http://test.com/foo/bar' }))
       .resolves.toEqual({ path: 'http://test.com/foo/' });
     await expect(manager.getContainer({ path: 'http://test.com/foo/bar/' }))
@@ -10,13 +11,13 @@ describe('An UrlContainerManager', (): void => {
   });
 
   it('errors when getting the container of root.', async(): Promise<void> => {
-    let manager = new UrlContainerManager('http://test.com/foo/');
+    let manager = new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/foo/' }));
     await expect(manager.getContainer({ path: 'http://test.com/foo/' }))
       .rejects.toThrow('Root does not have a container.');
     await expect(manager.getContainer({ path: 'http://test.com/foo' }))
       .rejects.toThrow('Root does not have a container.');
 
-    manager = new UrlContainerManager('http://test.com/foo');
+    manager = new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/foo' }));
     await expect(manager.getContainer({ path: 'http://test.com/foo/' }))
       .rejects.toThrow('Root does not have a container.');
     await expect(manager.getContainer({ path: 'http://test.com/foo' }))
@@ -24,7 +25,7 @@ describe('An UrlContainerManager', (): void => {
   });
 
   it('errors when the root of an URl is reached that does not match the input root.', async(): Promise<void> => {
-    const manager = new UrlContainerManager('http://test.com/foo/');
+    const manager = new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/foo/' }));
     await expect(manager.getContainer({ path: 'http://test.com/' }))
       .rejects.toThrow('URL root reached.');
   });