Repository: solid/community-server
Pull request #429 (2020-12-16 14:10:02)

fix: Make default HttpHandler a WaterfallHandler
Accidentally stumbled upon this when making #428 . Currently all requests to `/pods` will also be sent to the LDP handler.

As it currently is, the config for inserting the pod http handler in the config has always been wrong. It used to work because the `AllVoidCompositeHandler` used to ignore errors, but since the change to `SequenceHandler` it probably never worked anymore. Perhaps this also caused the confusion in #421 .

This does mean that the middleware will only run for requests that reach the LDP handler. If we want the middleware to always run more changes will be required.

Should also investigate how the pod creation integration test didn't catch this. Perhaps a timing issue because the pod handler managed to respond first.

joachimvh: After some more testing it actually only starts failing after #428 is merged, so there might be an issue in that branch (even then, it still makes sense that this should be a WaterfallHandler).
joachimvh: Final update: the error was actually thrown by an unrelated problem. But I still suggest to make this change since this is the behaviour we actually want. If the middleware should run for all http handlers the order can be reversed (a Sequence containing a Waterfall).
RubenVerborgh: > If the middleware should run for all http handlers

Yeah!

> the order can be reversed (a Sequence containing a Waterfall).

Done in 7599e978acef3d98ad7d8db24f58a417716a7aeb

Patch:
diff --git a/config/presets/http.json b/config/presets/http.json
index 40f02dc1e..a67292862 100644
--- a/config/presets/http.json
+++ b/config/presets/http.json
@@ -26,10 +26,15 @@
           "@id": "urn:solid-server:default:Middleware"
         },
         {
-          "@id": "urn:solid-server:default:PodManagerHandler"
-        },
-        {
-          "@id": "urn:solid-server:default:LdpHandler"
+          "@type": "WaterfallHandler",
+          "WaterfallHandler:_handlers": [
+            {
+              "@id": "urn:solid-server:default:PodManagerHandler"
+            },
+            {
+              "@id": "urn:solid-server:default:LdpHandler"
+            }
+          ]
         }
       ]
     }
diff --git a/config/presets/init.json b/config/presets/init.json
index a7aff9a01..901aaa480 100644
--- a/config/presets/init.json
+++ b/config/presets/init.json
@@ -11,6 +11,15 @@
             "@id": "urn:solid-server:default:LoggerFactory"
           }
         },
+        {
+          "@type": "RootContainerInitializer",
+          "RootContainerInitializer:_baseUrl": {
+            "@id": "urn:solid-server:default:variable:baseUrl"
+          },
+          "RootContainerInitializer:_store": {
+            "@id": "urn:solid-server:default:ResourceStore"
+          }
+        },
         {
           "@type": "AclInitializer",
           "AclInitializer:_baseUrl": {
diff --git a/src/authentication/UnsecureConstantCredentialsExtractor.ts b/src/authentication/UnsecureConstantCredentialsExtractor.ts
new file mode 100644
index 000000000..0911a7386
--- /dev/null
+++ b/src/authentication/UnsecureConstantCredentialsExtractor.ts
@@ -0,0 +1,22 @@
+import { getLoggerFor } from '../logging/LogUtil';
+import type { Credentials } from './Credentials';
+import { CredentialsExtractor } from './CredentialsExtractor';
+
+/**
+ * Credentials extractor that authenticates a constant agent
+ * (useful for development or debugging purposes).
+ */
+export class UnsecureConstantCredentialsExtractor extends CredentialsExtractor {
+  private readonly agent: Credentials;
+  private readonly logger = getLoggerFor(this);
+
+  public constructor(agent: string | Credentials) {
+    super();
+    this.agent = typeof agent === 'string' ? { webId: agent } : agent;
+  }
+
+  public async handle(): Promise<Credentials> {
+    this.logger.info(`Agent unsecurely claims to be ${this.agent.webId}`);
+    return this.agent;
+  }
+}
diff --git a/src/index.ts b/src/index.ts
index 38d8bfd36..a81b04b3d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -17,6 +17,7 @@ export * from './init/AclInitializer';
 export * from './init/CliRunner';
 export * from './init/Initializer';
 export * from './init/LoggerInitializer';
+export * from './init/RootContainerInitializer';
 export * from './init/ServerInitializer';
 
 // LDP/HTTP/Metadata
diff --git a/src/init/RootContainerInitializer.ts b/src/init/RootContainerInitializer.ts
new file mode 100644
index 000000000..b9ba9a8f5
--- /dev/null
+++ b/src/init/RootContainerInitializer.ts
@@ -0,0 +1,72 @@
+import { DataFactory } from 'n3';
+import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
+import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
+import { getLoggerFor } from '../logging/LogUtil';
+import type { ResourceStore } from '../storage/ResourceStore';
+import { TEXT_TURTLE } from '../util/ContentTypes';
+import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
+import { ensureTrailingSlash } from '../util/PathUtil';
+import { generateResourceQuads } from '../util/ResourceUtil';
+import { guardedStreamFrom } from '../util/StreamUtil';
+import { PIM, RDF } from '../util/UriConstants';
+import { toNamedNode } from '../util/UriUtil';
+import { Initializer } from './Initializer';
+import namedNode = DataFactory.namedNode;
+
+/**
+ * Initializes ResourceStores by creating a root container if it didn't exist yet.
+ */
+export class RootContainerInitializer extends Initializer {
+  protected readonly logger = getLoggerFor(this);
+  private readonly baseId: ResourceIdentifier;
+  private readonly store: ResourceStore;
+
+  public constructor(baseUrl: string, store: ResourceStore) {
+    super();
+    this.baseId = { path: ensureTrailingSlash(baseUrl) };
+    this.store = store;
+  }
+
+  public async handle(): Promise<void> {
+    if (!await this.hasRootContainer()) {
+      await this.createRootContainer();
+    }
+  }
+
+  /**
+   * Verify if a root container already exists in a ResourceStore.
+   */
+  protected async hasRootContainer(): Promise<boolean> {
+    try {
+      const result = await this.store.getRepresentation(this.baseId, {});
+      this.logger.debug(`Existing root container found at ${this.baseId.path}`);
+      result.data.destroy();
+      return true;
+    } catch (error: unknown) {
+      if (!(error instanceof NotFoundHttpError)) {
+        throw error;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Create a root container in a ResourceStore.
+   */
+  protected async createRootContainer(): Promise<void> {
+    const metadata = new RepresentationMetadata(this.baseId);
+    metadata.addQuads(generateResourceQuads(namedNode(this.baseId.path), true));
+
+    // Make sure the root container is a pim:Storage
+    // This prevents deletion of the root container as storage root containers can not be deleted
+    metadata.add(RDF.type, toNamedNode(PIM.Storage));
+
+    metadata.contentType = TEXT_TURTLE;
+
+    await this.store.setRepresentation(this.baseId, {
+      binary: true,
+      data: guardedStreamFrom([]),
+      metadata,
+    });
+  }
+}
diff --git a/src/pods/generate/TemplatedResourcesGenerator.ts b/src/pods/generate/TemplatedResourcesGenerator.ts
index ae16fd218..a35cfefc6 100644
--- a/src/pods/generate/TemplatedResourcesGenerator.ts
+++ b/src/pods/generate/TemplatedResourcesGenerator.ts
@@ -1,8 +1,14 @@
 import { promises as fsPromises } from 'fs';
 import { posix } from 'path';
+import { Parser } from 'n3';
 import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
-import type { FileIdentifierMapper, FileIdentifierMapperFactory } from '../../storage/mapping/FileIdentifierMapper';
+import type {
+  FileIdentifierMapper,
+  FileIdentifierMapperFactory,
+  ResourceLink,
+} from '../../storage/mapping/FileIdentifierMapper';
+import { isContainerIdentifier } from '../../util/PathUtil';
 import { guardedStreamFrom } from '../../util/StreamUtil';
 import type { Resource, ResourcesGenerator } from './ResourcesGenerator';
 import type { TemplateEngine } from './TemplateEngine';
@@ -20,6 +26,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
   private readonly templateFolder: string;
   private readonly factory: FileIdentifierMapperFactory;
   private readonly engine: TemplateEngine;
+  private readonly metaExtension = '.meta';
 
   /**
    * A mapper is needed to convert the template file paths to identifiers relative to the given base identifier.
@@ -36,57 +43,132 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
 
   public async* generate(location: ResourceIdentifier, options: Dict<string>): AsyncIterable<Resource> {
     const mapper = await this.factory.create(location.path, this.templateFolder);
-    yield* this.parseFolder(this.templateFolder, mapper, options);
+    const folderLink = await mapper.mapFilePathToUrl(this.templateFolder, true);
+    yield* this.parseFolder(folderLink, mapper, options);
   }
 
   /**
    * Generates results for all entries in the given folder, including the folder itself.
    */
-  private async* parseFolder(filePath: string, mapper: FileIdentifierMapper, options: Dict<string>):
+  private async* parseFolder(folderLink: ResourceLink, mapper: FileIdentifierMapper, options: Dict<string>):
   AsyncIterable<Resource> {
-    // Generate representation for the container
-    const link = await mapper.mapFilePathToUrl(filePath, true);
-    yield {
-      identifier: link.identifier,
-      representation: {
-        binary: true,
-        data: guardedStreamFrom([]),
-        metadata: new RepresentationMetadata(link.identifier),
-      },
-    };
+    // Group resource links with their corresponding metadata links
+    const links = await this.groupLinks(this.generateLinks(folderLink.filePath, mapper));
+
+    // Remove root metadata if it exists
+    const metaLink = links[folderLink.identifier.path]?.meta;
+    // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+    delete links[folderLink.identifier.path];
 
-    // Generate representations for all resources in this container
-    const files = await fsPromises.readdir(filePath);
-    for (const childName of files) {
-      const childPath = joinPath(filePath, childName);
-      const childStats = await fsPromises.lstat(childPath);
-      if (childStats.isDirectory()) {
-        yield* this.parseFolder(childPath, mapper, options);
-      } else if (childStats.isFile()) {
-        yield this.generateDocument(childPath, mapper, options);
+    yield this.generateResource(folderLink, options, metaLink);
+
+    for (const { link, meta } of Object.values(links)) {
+      if (isContainerIdentifier(link.identifier)) {
+        yield* this.parseFolder(link, mapper, options);
+      } else {
+        yield this.generateResource(link, options, meta);
       }
     }
   }
 
   /**
-   * Generates a new Representation corresponding to the template file at the given location.
+   * Generates ResourceLinks for each entry in the given folder.
    */
-  private async generateDocument(filePath: string, mapper: FileIdentifierMapper, options: Dict<string>):
+  private async* generateLinks(folderPath: string, mapper: FileIdentifierMapper): AsyncIterable<ResourceLink> {
+    const files = await fsPromises.readdir(folderPath);
+    for (const name of files) {
+      const filePath = joinPath(folderPath, name);
+      const stats = await fsPromises.lstat(filePath);
+      yield mapper.mapFilePathToUrl(filePath, stats.isDirectory());
+    }
+  }
+
+  /**
+   * Parses a group of ResourceLinks so resources and their metadata are grouped together.
+   */
+  private async groupLinks(linkGen: AsyncIterable<ResourceLink>):
+  Promise<Record<string, { link: ResourceLink; meta?: ResourceLink }>> {
+    const links: Record<string, { link: ResourceLink; meta?: ResourceLink }> = { };
+    for await (const link of linkGen) {
+      const { path } = link.identifier;
+      if (this.isMeta(path)) {
+        const resourcePath = this.metaToResource(link.identifier).path;
+        links[resourcePath] = Object.assign(links[resourcePath] || {}, { meta: link });
+      } else {
+        links[path] = Object.assign(links[path] || {}, { link });
+      }
+    }
+    return links;
+  }
+
+  /**
+   * Generates a Resource object for the given ResourceLink.
+   * In the case of documents the corresponding template will be used.
+   * If a ResourceLink of metadata is provided the corresponding data will be added as metadata.
+   */
+  private async generateResource(link: ResourceLink, options: Dict<string>, metaLink?: ResourceLink):
   Promise<Resource> {
-    const link = await mapper.mapFilePathToUrl(filePath, false);
+    const data: string[] = [];
     const metadata = new RepresentationMetadata(link.identifier);
-    metadata.contentType = link.contentType;
 
-    const raw = await fsPromises.readFile(filePath, 'utf8');
-    const compiled = this.engine.apply(raw, options);
+    // Read file if it is not a container
+    if (!isContainerIdentifier(link.identifier)) {
+      const compiled = await this.parseTemplate(link.filePath, options);
+      data.push(compiled);
+      metadata.contentType = link.contentType;
+    }
+
+    // Add metadata from meta file if there is one
+    if (metaLink) {
+      const rawMetadata = await this.generateMetadata(metaLink, options);
+      metadata.addQuads(rawMetadata.quads());
+    }
 
     return {
       identifier: link.identifier,
       representation: {
         binary: true,
-        data: guardedStreamFrom([ compiled ]),
+        data: guardedStreamFrom(data),
         metadata,
       },
     };
   }
+
+  /**
+   * Generates a RepresentationMetadata using the given template.
+   */
+  private async generateMetadata(metaLink: ResourceLink, options: Dict<string>):
+  Promise<RepresentationMetadata> {
+    const identifier = this.metaToResource(metaLink.identifier);
+    const metadata = new RepresentationMetadata(identifier);
+
+    const data = await this.parseTemplate(metaLink.filePath, options);
+    const parser = new Parser({ format: metaLink.contentType, baseIRI: identifier.path });
+    const quads = parser.parse(data);
+    metadata.addQuads(quads);
+
+    return metadata;
+  }
+
+  /**
+   * Applies the given options to the template found at the given path.
+   */
+  private async parseTemplate(filePath: string, options: Dict<string>): Promise<string> {
+    const raw = await fsPromises.readFile(filePath, 'utf8');
+    return this.engine.apply(raw, options);
+  }
+
+  /**
+   * Verifies if the given path corresponds to a metadata file.
+   */
+  private isMeta(path: string): boolean {
+    return path.endsWith(this.metaExtension);
+  }
+
+  /**
+   * Converts a generated metadata identifier to the identifier of its corresponding resource.
+   */
+  private metaToResource(metaIdentifier: ResourceIdentifier): ResourceIdentifier {
+    return { path: metaIdentifier.path.slice(0, -this.metaExtension.length) };
+  }
 }
diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts
index e71b7544a..cad36e803 100644
--- a/src/storage/DataAccessorBasedStore.ts
+++ b/src/storage/DataAccessorBasedStore.ts
@@ -1,6 +1,6 @@
 import arrayifyStream from 'arrayify-stream';
 import { DataFactory } from 'n3';
-import type { Quad } from 'rdf-js';
+import type { Quad, Term } from 'rdf-js';
 import { v4 as uuid } from 'uuid';
 import type { Representation } from '../ldp/representation/Representation';
 import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
@@ -8,6 +8,7 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifie
 import { INTERNAL_QUADS } from '../util/ContentTypes';
 import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
 import { ConflictHttpError } from '../util/errors/ConflictHttpError';
+import { InternalServerError } from '../util/errors/InternalServerError';
 import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpError';
 import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
@@ -218,7 +219,8 @@ export class DataAccessorBasedStore implements ResourceStore {
       await this.handleContainerData(representation);
     }
 
-    if (createContainers) {
+    // Root container should not have a parent container
+    if (createContainers && !this.identifierStrategy.isRootContainer(identifier)) {
       await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(identifier));
     }
 
@@ -245,7 +247,7 @@ export class DataAccessorBasedStore implements ResourceStore {
       if (representation.metadata.contentType === INTERNAL_QUADS) {
         quads = await arrayifyStream(representation.data);
       } else {
-        quads = await parseQuads(representation.data);
+        quads = await parseQuads(representation.data, representation.metadata.contentType);
       }
     } catch (error: unknown) {
       if (error instanceof Error) {
@@ -317,14 +319,13 @@ export class DataAccessorBasedStore implements ResourceStore {
    * @param suffix - Suffix of the URI. Can be the full URI, but only the last part is required.
    */
   protected isNewContainer(metadata: RepresentationMetadata, suffix?: string): boolean {
-    let isContainer: boolean;
-    try {
-      isContainer = this.isExistingContainer(metadata);
-    } catch {
-      const slug = suffix ?? metadata.get(HTTP.slug)?.value;
-      isContainer = Boolean(slug && isContainerPath(slug));
+    // Should not use `isExistingContainer` since the metadata might contain unrelated type triples
+    // It's not because there is no container type triple that the new resource is not a container
+    if (this.hasContainerType(metadata.getAll(RDF.type))) {
+      return true;
     }
-    return isContainer;
+    const slug = suffix ?? metadata.get(HTTP.slug)?.value;
+    return Boolean(slug && isContainerPath(slug));
   }
 
   /**
@@ -334,9 +335,16 @@ export class DataAccessorBasedStore implements ResourceStore {
    */
   protected isExistingContainer(metadata: RepresentationMetadata): boolean {
     const types = metadata.getAll(RDF.type);
-    if (types.length === 0) {
-      throw new Error('Unknown resource type.');
+    if (!types.some((type): boolean => type.value === LDP.Resource)) {
+      throw new InternalServerError('Unknown resource type.');
     }
+    return this.hasContainerType(types);
+  }
+
+  /**
+   * Checks in a list of types if any of them match a Container type.
+   */
+  protected hasContainerType(types: Term[]): boolean {
     return types.some((type): boolean => type.value === LDP.Container || type.value === LDP.BasicContainer);
   }
 
diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts
index 9bd71978d..6fef0fdb3 100644
--- a/src/storage/accessors/FileDataAccessor.ts
+++ b/src/storage/accessors/FileDataAccessor.ts
@@ -7,7 +7,6 @@ import type { NamedNode, Quad } from 'rdf-js';
 import type { Representation } from '../../ldp/representation/Representation';
 import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
-import { TEXT_TURTLE } from '../../util/ContentTypes';
 import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
 import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
 import { isSystemError } from '../../util/errors/SystemError';
@@ -17,7 +16,7 @@ import type { Guarded } from '../../util/GuardedStream';
 import { isContainerIdentifier } from '../../util/PathUtil';
 import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
 import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
-import { CONTENT_TYPE, DCTERMS, POSIX, RDF, XSD } from '../../util/UriConstants';
+import { CONTENT_TYPE, DCTERMS, LDP, POSIX, RDF, XSD } from '../../util/UriConstants';
 import { toNamedNode, toTypedLiteral } from '../../util/UriUtil';
 import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper';
 import type { DataAccessor } from './DataAccessor';
@@ -95,7 +94,7 @@ export class FileDataAccessor implements DataAccessor {
     } catch (error: unknown) {
       // Delete the metadata if there was an error writing the file
       if (wroteMetadata) {
-        await fsPromises.unlink(await this.getMetadataPath(link.identifier));
+        await fsPromises.unlink((await this.getMetadataLink(link.identifier)).filePath);
       }
       throw error;
     }
@@ -126,7 +125,7 @@ export class FileDataAccessor implements DataAccessor {
     const stats = await this.getStats(link.filePath);
 
     try {
-      await fsPromises.unlink(await this.getMetadataPath(link.identifier));
+      await fsPromises.unlink((await this.getMetadataLink(link.identifier)).filePath);
     } catch (error: unknown) {
       // Ignore if it doesn't exist
       if (!isSystemError(error) || error.code !== 'ENOENT') {
@@ -162,11 +161,11 @@ export class FileDataAccessor implements DataAccessor {
   }
 
   /**
-   * Generates file path that corresponds to the metadata file of the given identifier.
-   * Starts from the identifier to make sure any potentially added extension has no impact on the path.
+   * Generates ResourceLink that corresponds to the metadata resource of the given identifier.
    */
-  private async getMetadataPath(identifier: ResourceIdentifier): Promise<string> {
-    return (await this.resourceMapper.mapUrlToFilePath({ path: `${identifier.path}.meta` }, TEXT_TURTLE)).filePath;
+  private async getMetadataLink(identifier: ResourceIdentifier): Promise<ResourceLink> {
+    const metaIdentifier = { path: `${identifier.path}.meta` };
+    return this.resourceMapper.mapUrlToFilePath(metaIdentifier);
   }
 
   /**
@@ -211,22 +210,25 @@ export class FileDataAccessor implements DataAccessor {
    */
   private async writeMetadata(link: ResourceLink, metadata: RepresentationMetadata): Promise<boolean> {
     // These are stored by file system conventions
-    metadata.removeAll(RDF.type);
+    metadata.remove(RDF.type, toNamedNode(LDP.Resource));
+    metadata.remove(RDF.type, toNamedNode(LDP.Container));
+    metadata.remove(RDF.type, toNamedNode(LDP.BasicContainer));
     metadata.removeAll(CONTENT_TYPE);
     const quads = metadata.quads();
-    const metadataPath = await this.getMetadataPath(link.identifier);
+    const metadataLink = await this.getMetadataLink(link.identifier);
     let wroteMetadata: boolean;
 
     // Write metadata to file if there are quads remaining
     if (quads.length > 0) {
-      const serializedMetadata = serializeQuads(quads);
-      await this.writeDataFile(metadataPath, serializedMetadata);
+      // Determine required content-type based on mapper
+      const serializedMetadata = serializeQuads(quads, metadataLink.contentType);
+      await this.writeDataFile(metadataLink.filePath, serializedMetadata);
       wroteMetadata = true;
 
     // Delete (potentially) existing metadata file if no metadata needs to be stored
     } else {
       try {
-        await fsPromises.unlink(metadataPath);
+        await fsPromises.unlink(metadataLink.filePath);
       } catch (error: unknown) {
         // Metadata file doesn't exist so nothing needs to be removed
         if (!isSystemError(error) || error.code !== 'ENOENT') {
@@ -261,13 +263,13 @@ export class FileDataAccessor implements DataAccessor {
    */
   private async getRawMetadata(identifier: ResourceIdentifier): Promise<Quad[]> {
     try {
-      const metadataPath = await this.getMetadataPath(identifier);
+      const metadataLink = await this.getMetadataLink(identifier);
 
       // Check if the metadata file exists first
-      await fsPromises.lstat(metadataPath);
+      await fsPromises.lstat(metadataLink.filePath);
 
-      const readMetadataStream = guardStream(createReadStream(metadataPath));
-      return await parseQuads(readMetadataStream);
+      const readMetadataStream = guardStream(createReadStream(metadataLink.filePath));
+      return await parseQuads(readMetadataStream, metadataLink.contentType);
     } catch (error: unknown) {
       // Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
       if (!isSystemError(error) || error.code !== 'ENOENT') {
@@ -350,8 +352,8 @@ export class FileDataAccessor implements DataAccessor {
         await fsPromises.unlink(oldLink.filePath);
       }
     } catch (error: unknown) {
-      // Ignore it if the file didn't exist yet
-      if (!(error instanceof NotFoundHttpError)) {
+      // Ignore it if the file didn't exist yet and couldn't be unlinked
+      if (!isSystemError(error) || error.code !== 'ENOENT') {
         throw error;
       }
     }
diff --git a/src/storage/accessors/InMemoryDataAccessor.ts b/src/storage/accessors/InMemoryDataAccessor.ts
index 33c38aba6..99ee86a35 100644
--- a/src/storage/accessors/InMemoryDataAccessor.ts
+++ b/src/storage/accessors/InMemoryDataAccessor.ts
@@ -23,14 +23,16 @@ type CacheEntry = DataEntry | ContainerEntry;
 
 export class InMemoryDataAccessor implements DataAccessor {
   private readonly base: string;
-  private readonly store: ContainerEntry;
+  // A dummy container with one entry which corresponds to the base
+  private readonly store: { entries: { ''?: ContainerEntry } };
 
   public constructor(base: string) {
     this.base = ensureTrailingSlash(base);
 
     const metadata = new RepresentationMetadata({ path: this.base });
     metadata.addQuads(generateResourceQuads(DataFactory.namedNode(this.base), true));
-    this.store = { entries: {}, metadata };
+    const rootContainer = { entries: {}, metadata };
+    this.store = { entries: { '': rootContainer }};
   }
 
   public async canHandle(): Promise<void> {
@@ -96,12 +98,16 @@ export class InMemoryDataAccessor implements DataAccessor {
   }
 
   private getParentEntry(identifier: ResourceIdentifier): { parent: ContainerEntry; name: string } {
-    const parts = identifier.path.slice(this.base.length).split('/').filter((part): boolean => part.length > 0);
-
-    if (parts.length === 0) {
-      throw new Error('Root container has no parent.');
+    if (identifier.path === this.base) {
+      // Casting is fine here as the parent should never be used as a real container
+      return { parent: this.store as any, name: '' };
+    }
+    if (!this.store.entries['']) {
+      throw new NotFoundHttpError();
     }
 
+    const parts = identifier.path.slice(this.base.length).split('/').filter((part): boolean => part.length > 0);
+
     // Name of the resource will be the last entry in the path
     const name = parts[parts.length - 1];
 
@@ -109,7 +115,8 @@ export class InMemoryDataAccessor implements DataAccessor {
     const containers = parts.slice(0, -1);
 
     // Step through the parts of the path up to the end
-    let parent = this.store;
+    // First entry is guaranteed to be a ContainerEntry
+    let parent = this.store.entries[''];
     for (const container of containers) {
       const child = parent.entries[container];
       if (!child) {
@@ -124,9 +131,6 @@ export class InMemoryDataAccessor implements DataAccessor {
   }
 
   private getEntry(identifier: ResourceIdentifier): CacheEntry {
-    if (identifier.path === this.base) {
-      return this.store;
-    }
     const { parent, name } = this.getParentEntry(identifier);
     const entry = parent.entries[name];
     if (!entry) {
diff --git a/src/storage/accessors/SparqlDataAccessor.ts b/src/storage/accessors/SparqlDataAccessor.ts
index 785d14dae..0b3b60f12 100644
--- a/src/storage/accessors/SparqlDataAccessor.ts
+++ b/src/storage/accessors/SparqlDataAccessor.ts
@@ -27,7 +27,6 @@ import { guardStream } from '../../util/GuardedStream';
 import type { Guarded } from '../../util/GuardedStream';
 import type { IdentifierStrategy } from '../../util/identifiers/IdentifierStrategy';
 import { isContainerIdentifier } from '../../util/PathUtil';
-import { generateResourceQuads } from '../../util/ResourceUtil';
 import { CONTENT_TYPE, LDP } from '../../util/UriConstants';
 import { toNamedNode } from '../../util/UriUtil';
 import type { DataAccessor } from './DataAccessor';
@@ -90,8 +89,7 @@ export class SparqlDataAccessor implements DataAccessor {
     const stream = await this.sendSparqlConstruct(query);
     const quads = await arrayifyStream(stream);
 
-    // Root container will not have metadata if there are no containment triples
-    if (quads.length === 0 && !this.identifierStrategy.isRootContainer(identifier)) {
+    if (quads.length === 0) {
       throw new NotFoundHttpError();
     }
 
@@ -100,11 +98,6 @@ export class SparqlDataAccessor implements DataAccessor {
       metadata.contentType = INTERNAL_QUADS;
     }
 
-    // Need to generate type metadata for the root container since it's not stored
-    if (this.identifierStrategy.isRootContainer(identifier)) {
-      metadata.addQuads(generateResourceQuads(name, true));
-    }
-
     return metadata;
   }
 
@@ -113,7 +106,7 @@ export class SparqlDataAccessor implements DataAccessor {
    */
   public async writeContainer(identifier: ResourceIdentifier, metadata: RepresentationMetadata): Promise<void> {
     const { name, parent } = this.getRelatedNames(identifier);
-    return this.sendSparqlUpdate(this.sparqlInsert(name, parent, metadata));
+    return this.sendSparqlUpdate(this.sparqlInsert(name, metadata, parent));
   }
 
   /**
@@ -135,7 +128,7 @@ export class SparqlDataAccessor implements DataAccessor {
     // Not relevant since all content is triples
     metadata.removeAll(CONTENT_TYPE);
 
-    return this.sendSparqlUpdate(this.sparqlInsert(name, parent, metadata, triples));
+    return this.sendSparqlUpdate(this.sparqlInsert(name, metadata, parent, triples));
   }
 
   /**
@@ -148,10 +141,17 @@ export class SparqlDataAccessor implements DataAccessor {
 
   /**
    * Helper function to get named nodes corresponding to the identifier and its parent container.
+   * In case of a root container only the name will be returned.
    */
-  private getRelatedNames(identifier: ResourceIdentifier): { name: NamedNode; parent: NamedNode } {
-    const parentIdentifier = this.identifierStrategy.getParentContainer(identifier);
+  private getRelatedNames(identifier: ResourceIdentifier): { name: NamedNode; parent?: NamedNode } {
     const name = namedNode(identifier.path);
+
+    // Root containers don't have a parent
+    if (this.identifierStrategy.isRootContainer(identifier)) {
+      return { name };
+    }
+
+    const parentIdentifier = this.identifierStrategy.getParentContainer(identifier);
     const parent = namedNode(parentIdentifier.path);
     return { name, parent };
   }
@@ -219,14 +219,15 @@ export class SparqlDataAccessor implements DataAccessor {
    * @param metadata - New metadata of the resource.
    * @param triples - New data of the resource.
    */
-  private sparqlInsert(name: NamedNode, parent: NamedNode, metadata: RepresentationMetadata, triples?: Quad[]): Update {
+  private sparqlInsert(name: NamedNode, metadata: RepresentationMetadata, parent?: NamedNode, triples?: Quad[]):
+  Update {
     const metaName = this.getMetadataNode(name);
 
     // Insert new metadata and containment triple
-    const insert: GraphQuads[] = [
-      this.sparqlUpdateGraph(metaName, metadata.quads()),
-      this.sparqlUpdateGraph(parent, [ quad(parent, toNamedNode(LDP.contains), name) ]),
-    ];
+    const insert: GraphQuads[] = [ this.sparqlUpdateGraph(metaName, metadata.quads()) ];
+    if (parent) {
+      insert.push(this.sparqlUpdateGraph(parent, [ quad(parent, toNamedNode(LDP.contains), name) ]));
+    }
 
     // Necessary updates: delete metadata and insert new data
     const updates: UpdateOperation[] = [
@@ -256,21 +257,26 @@ export class SparqlDataAccessor implements DataAccessor {
   /**
    * Creates a query that deletes everything related to the given name.
    * @param name - Name of resource to delete.
-   * @param parent - Parent of the resource to delete so containment triple can be removed.
+   * @param parent - Parent of the resource to delete so the containment triple can be removed (unless root).
    */
-  private sparqlDelete(name: NamedNode, parent: NamedNode): Update {
-    return {
+  private sparqlDelete(name: NamedNode, parent?: NamedNode): Update {
+    const update: Update = {
       updates: [
         this.sparqlUpdateDeleteAll(name),
         this.sparqlUpdateDeleteAll(this.getMetadataNode(name)),
-        {
-          updateType: 'delete',
-          delete: [ this.sparqlUpdateGraph(parent, [ quad(parent, toNamedNode(LDP.contains), name) ]) ],
-        },
       ],
       type: 'update',
       prefixes: {},
     };
+
+    if (parent) {
+      update.updates.push({
+        updateType: 'delete',
+        delete: [ this.sparqlUpdateGraph(parent, [ quad(parent, toNamedNode(LDP.contains), name) ]) ],
+      });
+    }
+
+    return update;
   }
 
   /**
diff --git a/src/storage/mapping/ExtensionBasedMapper.ts b/src/storage/mapping/ExtensionBasedMapper.ts
index 9e3a10185..ef214a92f 100644
--- a/src/storage/mapping/ExtensionBasedMapper.ts
+++ b/src/storage/mapping/ExtensionBasedMapper.ts
@@ -4,7 +4,6 @@ import * as mime from 'mime-types';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../../logging/LogUtil';
 import { APPLICATION_OCTET_STREAM, TEXT_TURTLE } from '../../util/ContentTypes';
-import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
 import {
   encodeUriPathComponents,
@@ -98,22 +97,18 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
         );
       } catch {
         // Parent folder does not exist (or is not a folder)
-        this.logger.warn(`No parent folder for ${identifier.path} found at ${folder}`);
-        throw new NotFoundHttpError();
       }
 
-      // File doesn't exist
-      if (!fileName) {
-        this.logger.warn(`File for URL ${identifier.path} does not exist in ${folder}`);
-        throw new NotFoundHttpError();
+      // Matching file found
+      if (fileName) {
+        filePath = joinPath(folder, fileName);
       }
 
-      filePath = joinPath(folder, fileName);
       this.logger.info(`The path for ${identifier.path} is ${filePath}`);
       return {
         identifier,
         filePath,
-        contentType: this.getContentTypeFromExtension(fileName),
+        contentType: this.getContentTypeFromExtension(filePath),
       };
     }
 
diff --git a/src/storage/mapping/FileIdentifierMapper.ts b/src/storage/mapping/FileIdentifierMapper.ts
index e55aa07a7..e87c85951 100644
--- a/src/storage/mapping/FileIdentifierMapper.ts
+++ b/src/storage/mapping/FileIdentifierMapper.ts
@@ -29,7 +29,8 @@ export interface FileIdentifierMapper {
   mapFilePathToUrl: (filePath: string, isContainer: boolean) => Promise<ResourceLink>;
   /**
    * Maps the given resource identifier / URL to a file path.
-   * Determines the content-type if no content-type was provided.
+   * Determines the content-type if no content-type was provided by finding the corresponding file.
+   * If there is no corresponding file a file path will be generated.
    * For containers the content-type input gets ignored.
    * @param identifier - The input identifier.
    * @param contentType - The (optional) content-type of the resource.
diff --git a/src/util/QuadUtil.ts b/src/util/QuadUtil.ts
index 6f2521bfd..8d3e1328f 100644
--- a/src/util/QuadUtil.ts
+++ b/src/util/QuadUtil.ts
@@ -3,7 +3,6 @@ import arrayifyStream from 'arrayify-stream';
 import { DataFactory, StreamParser, StreamWriter } from 'n3';
 import type { Literal, NamedNode, Quad } from 'rdf-js';
 import streamifyArray from 'streamify-array';
-import { TEXT_TURTLE } from './ContentTypes';
 import type { Guarded } from './GuardedStream';
 import { pipeSafely } from './StreamUtil';
 
@@ -17,17 +16,19 @@ export const pushQuad =
 /**
  * Helper function for serializing an array of quads, with as result a Readable object.
  * @param quads - The array of quads.
+ * @param contentType - The content-type to serialize to.
  *
  * @returns The Readable object.
  */
-export const serializeQuads = (quads: Quad[]): Guarded<Readable> =>
-  pipeSafely(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
+export const serializeQuads = (quads: Quad[], contentType?: string): Guarded<Readable> =>
+  pipeSafely(streamifyArray(quads), new StreamWriter({ format: contentType }));
 
 /**
  * Helper function to convert a Readable into an array of quads.
  * @param readable - The readable object.
+ * @param contentType - The content-type of the stream.
  *
  * @returns A promise containing the array of quads.
  */
-export const parseQuads = async(readable: Guarded<Readable>): Promise<Quad[]> =>
-  arrayifyStream(pipeSafely(readable, new StreamParser({ format: TEXT_TURTLE })));
+export const parseQuads = async(readable: Guarded<Readable>, contentType?: string): Promise<Quad[]> =>
+  arrayifyStream(pipeSafely(readable, new StreamParser({ format: contentType })));
diff --git a/src/util/UriConstants.ts b/src/util/UriConstants.ts
index 861ba1e91..58058f887 100644
--- a/src/util/UriConstants.ts
+++ b/src/util/UriConstants.ts
@@ -46,6 +46,11 @@ export const MA = {
   format: MA_PREFIX('format'),
 };
 
+const PIM_PREFIX = createNamespace('http://www.w3.org/ns/pim/space#');
+export const PIM = {
+  Storage: PIM_PREFIX('Storage'),
+};
+
 const POSIX_PREFIX = createNamespace('http://www.w3.org/ns/posix/stat#');
 export const POSIX = {
   mtime: POSIX_PREFIX('mtime'),
diff --git a/templates/.meta b/templates/.meta
new file mode 100644
index 000000000..5ecce5efa
--- /dev/null
+++ b/templates/.meta
@@ -0,0 +1,3 @@
+@prefix pim: <http://www.w3.org/ns/pim/space#>.
+
+<> a pim:Storage.


Test patch:
diff --git a/test/configs/Util.ts b/test/configs/Util.ts
index a39f87fe1..13a7b2589 100644
--- a/test/configs/Util.ts
+++ b/test/configs/Util.ts
@@ -1,6 +1,8 @@
+import type { Server } from 'http';
 import { join } from 'path';
 import * as Path from 'path';
 import { Loader } from 'componentsjs';
+import fetch from 'cross-fetch';
 import type {
   BodyParser,
   DataAccessor,
@@ -24,6 +26,7 @@ import {
   ErrorResponseWriter,
   GetOperationHandler,
   HeadOperationHandler,
+  HttpError,
   InMemoryDataAccessor,
   LinkRelMetadataWriter,
   LinkTypeParser,
@@ -192,3 +195,21 @@ export const instantiateFromConfig = async(componentUrl: string, configFile: str
   const configPath = Path.join(__dirname, configFile);
   return loader.instantiateFromUrl(componentUrl, configPath, undefined, { variables });
 };
+
+/**
+ * Initializes the root container of the server.
+ * Useful for when the RootContainerInitializer was not instantiated.
+ */
+export const initServerStore = async(server: Server, baseUrl: string, headers: HeadersInit = {}): Promise<void> => {
+  const res = await fetch(baseUrl, {
+    method: 'PUT',
+    headers: {
+      ...headers,
+      'content-type': 'text/turtle',
+    },
+    body: '',
+  });
+  if (res.status >= 400) {
+    throw new HttpError(res.status, 'Error', res.statusText);
+  }
+};
diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts
index 2723dcddb..f80f8c07e 100644
--- a/test/integration/AuthenticatedLdpHandler.test.ts
+++ b/test/integration/AuthenticatedLdpHandler.test.ts
@@ -3,14 +3,23 @@ import * as url from 'url';
 import { namedNode, quad } from '@rdfjs/data-model';
 import { Parser } from 'n3';
 import type { MockResponse } from 'node-mocks-http';
+import { RootContainerInitializer } from '../../src/init/RootContainerInitializer';
 import { LDP } from '../../src/util/UriConstants';
 import { BasicConfig } from '../configs/BasicConfig';
 import { BasicHandlersConfig } from '../configs/BasicHandlersConfig';
+import { BASE } from '../configs/Util';
 import { call } from '../util/Util';
 
 describe('An integrated AuthenticatedLdpHandler', (): void => {
   describe('with simple handlers', (): void => {
-    const handler = new BasicConfig().getHttpHandler();
+    const config = new BasicConfig();
+    const handler = config.getHttpHandler();
+
+    beforeAll(async(): Promise<void> => {
+      // Initialize store
+      const initializer = new RootContainerInitializer(BASE, config.store);
+      await initializer.handleSafe();
+    });
 
     it('can add, read and delete data based on incoming requests.', async(): Promise<void> => {
       // POST
@@ -61,7 +70,14 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
   });
 
   describe('with simple PATCH handlers', (): void => {
-    const handler = new BasicHandlersConfig().getHttpHandler();
+    const config = new BasicHandlersConfig();
+    const handler = config.getHttpHandler();
+
+    beforeAll(async(): Promise<void> => {
+      // Initialize store
+      const initializer = new RootContainerInitializer(BASE, config.store);
+      await initializer.handleSafe();
+    });
 
     it('can handle simple SPARQL updates.', async(): Promise<void> => {
       // POST
@@ -126,7 +142,14 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
   });
 
   describe('with simple PUT handlers', (): void => {
-    const handler = new BasicHandlersConfig().getHttpHandler();
+    const config = new BasicHandlersConfig();
+    const handler = config.getHttpHandler();
+
+    beforeAll(async(): Promise<void> => {
+      // Initialize store
+      const initializer = new RootContainerInitializer(BASE, config.store);
+      await initializer.handleSafe();
+    });
 
     it('should overwrite the content on PUT request.', async(): Promise<void> => {
       // POST
diff --git a/test/integration/Authorization.test.ts b/test/integration/Authorization.test.ts
index 480179a12..65019d85f 100644
--- a/test/integration/Authorization.test.ts
+++ b/test/integration/Authorization.test.ts
@@ -1,5 +1,7 @@
 import type { MockResponse } from 'node-mocks-http';
+import { RootContainerInitializer } from '../../src/init/RootContainerInitializer';
 import { BasicHandlersWithAclConfig } from '../configs/BasicHandlersWithAclConfig';
+import { BASE } from '../configs/Util';
 import { AclTestHelper } from '../util/TestHelpers';
 import { call } from '../util/Util';
 
@@ -9,6 +11,12 @@ describe('A server with authorization', (): void => {
   const { store } = config;
   const aclHelper = new AclTestHelper(store, 'http://test.com/');
 
+  beforeAll(async(): Promise<void> => {
+    // Initialize store
+    const initializer = new RootContainerInitializer(BASE, config.store);
+    await initializer.handleSafe();
+  });
+
   it('can create new entries.', async(): Promise<void> => {
     await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'agent');
 
diff --git a/test/integration/FullConfig.acl.test.ts b/test/integration/FullConfig.acl.test.ts
index 9a747a568..89e73846b 100644
--- a/test/integration/FullConfig.acl.test.ts
+++ b/test/integration/FullConfig.acl.test.ts
@@ -1,6 +1,7 @@
 import { createReadStream, mkdirSync } from 'fs';
 import { join } from 'path';
 import * as rimraf from 'rimraf';
+import { RootContainerInitializer } from '../../src/init/RootContainerInitializer';
 import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
 import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor';
 import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
@@ -40,6 +41,10 @@ describe.each([ dataAccessorStore, inMemoryDataAccessorStore ])('A server using
       // Make sure the root directory exists
       mkdirSync(rootFilePath, { recursive: true });
 
+      // Initialize store
+      const initializer = new RootContainerInitializer(BASE, config.store);
+      await initializer.handleSafe();
+
       // Use store instead of file access so tests also work for non-file backends
       await config.store.setRepresentation({ path: `${BASE}/permanent.txt` }, {
         binary: true,
diff --git a/test/integration/FullConfig.noAuth.test.ts b/test/integration/FullConfig.noAuth.test.ts
index e0a912368..4b83a4ba1 100644
--- a/test/integration/FullConfig.noAuth.test.ts
+++ b/test/integration/FullConfig.noAuth.test.ts
@@ -1,5 +1,6 @@
 import { mkdirSync } from 'fs';
 import * as rimraf from 'rimraf';
+import { RootContainerInitializer } from '../../src/init/RootContainerInitializer';
 import type { HttpHandler } from '../../src/server/HttpHandler';
 import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor';
 import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
@@ -35,6 +36,10 @@ describe.each(configs)('A server using a %s', (name, configFn): void => {
       config = configFn(rootFilePath);
       handler = config.getHttpHandler();
       fileHelper = new FileTestHelper(handler, new URL(BASE));
+
+      // Initialize store
+      const initializer = new RootContainerInitializer(BASE, config.store);
+      await initializer.handleSafe();
     });
 
     afterAll(async(): Promise<void> => {
diff --git a/test/integration/LockingResourceStore.test.ts b/test/integration/LockingResourceStore.test.ts
index 3c04fd9a3..f60ae002e 100644
--- a/test/integration/LockingResourceStore.test.ts
+++ b/test/integration/LockingResourceStore.test.ts
@@ -1,4 +1,5 @@
 import streamifyArray from 'streamify-array';
+import { RootContainerInitializer } from '../../src/init/RootContainerInitializer';
 import type { Representation } from '../../src/ldp/representation/Representation';
 import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
 import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
@@ -13,6 +14,7 @@ import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThrea
 import { WrappedExpiringResourceLocker } from '../../src/util/locking/WrappedExpiringResourceLocker';
 import { guardedStreamFrom } from '../../src/util/StreamUtil';
 import { CONTENT_TYPE } from '../../src/util/UriConstants';
+import { BASE } from '../configs/Util';
 
 describe('A LockingResourceStore', (): void => {
   let path: string;
@@ -28,6 +30,10 @@ describe('A LockingResourceStore', (): void => {
     path = `${base}path`;
     source = new DataAccessorBasedStore(new InMemoryDataAccessor(base), new SingleRootIdentifierStrategy(base));
 
+    // Initialize store
+    const initializer = new RootContainerInitializer(BASE, source);
+    await initializer.handleSafe();
+
     locker = new SingleThreadedResourceLocker();
     expiringLocker = new WrappedExpiringResourceLocker(locker, 1000);
 
diff --git a/test/integration/Middleware.test.ts b/test/integration/Middleware.test.ts
index a4a854bfb..c60085552 100644
--- a/test/integration/Middleware.test.ts
+++ b/test/integration/Middleware.test.ts
@@ -5,6 +5,7 @@ import { HttpHandler } from '../../src/server/HttpHandler';
 import type { HttpRequest } from '../../src/server/HttpRequest';
 import type { HttpResponse } from '../../src/server/HttpResponse';
 import { instantiateFromConfig } from '../configs/Util';
+import { StaticAsyncHandler } from '../util/StaticAsyncHandler';
 
 const port = 6002;
 
@@ -21,7 +22,7 @@ describe('An Express server with middleware', (): void => {
   beforeAll(async(): Promise<void> => {
     const factory = await instantiateFromConfig(
       'urn:solid-server:default:ExpressHttpServerFactory', 'middleware.json', {
-        'urn:solid-server:default:PodManagerHandler': new SimpleHttpHandler(),
+        'urn:solid-server:default:PodManagerHandler': new StaticAsyncHandler(false, null),
         'urn:solid-server:default:LdpHandler': new SimpleHttpHandler(),
         'urn:solid-server:default:variable:port': port,
         'urn:solid-server:default:variable:baseUrl': 'https://example.pod/',
diff --git a/test/integration/PodCreation.test.ts b/test/integration/PodCreation.test.ts
index f6737f384..9250d1008 100644
--- a/test/integration/PodCreation.test.ts
+++ b/test/integration/PodCreation.test.ts
@@ -3,7 +3,7 @@ import { join } from 'path';
 import fetch from 'cross-fetch';
 import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
 import { readableToString } from '../../src/util/StreamUtil';
-import { instantiateFromConfig } from '../configs/Util';
+import { initServerStore, instantiateFromConfig } from '../configs/Util';
 
 const port = 6003;
 const baseUrl = `http://localhost:${port}/`;
@@ -21,6 +21,7 @@ describe('A server with a pod handler', (): void => {
       },
     ) as HttpServerFactory;
     server = factory.startServer(port);
+    await initServerStore(server, baseUrl);
   });
 
   afterAll(async(): Promise<void> => {
diff --git a/test/integration/SparqlStorage.test.ts b/test/integration/SparqlStorage.test.ts
index e161cfad7..c3dc69b46 100644
--- a/test/integration/SparqlStorage.test.ts
+++ b/test/integration/SparqlStorage.test.ts
@@ -1,3 +1,4 @@
+import { RootContainerInitializer } from '../../src/init/RootContainerInitializer';
 import { SparqlDataAccessor } from '../../src/storage/accessors/SparqlDataAccessor';
 import { INTERNAL_QUADS } from '../../src/util/ContentTypes';
 import { SingleRootIdentifierStrategy } from '../../src/util/identifiers/SingleRootIdentifierStrategy';
@@ -13,6 +14,12 @@ describeIf('docker', 'a server with a SPARQL endpoint as storage', (): void => {
     const handler = config.getHttpHandler();
     const fileHelper = new FileTestHelper(handler, new URL(BASE));
 
+    beforeAll(async(): Promise<void> => {
+      // Initialize store
+      const initializer = new RootContainerInitializer(BASE, config.store);
+      await initializer.handleSafe();
+    });
+
     it('can add a Turtle file to the store.', async():
     Promise<void> => {
       // POST
diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts
index a118c9ce1..83132bc6e 100644
--- a/test/integration/WebSocketsProtocol.test.ts
+++ b/test/integration/WebSocketsProtocol.test.ts
@@ -2,7 +2,7 @@ import type { Server } from 'http';
 import fetch from 'cross-fetch';
 import WebSocket from 'ws';
 import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
-import { instantiateFromConfig } from '../configs/Util';
+import { initServerStore, instantiateFromConfig } from '../configs/Util';
 
 const port = 6001;
 const serverUrl = `http://localhost:${port}/`;
@@ -20,6 +20,7 @@ describe('A server with the Solid WebSockets API behind a proxy', (): void => {
       },
     ) as HttpServerFactory;
     server = factory.startServer(port);
+    await initServerStore(server, serverUrl, headers);
   });
 
   afterAll(async(): Promise<void> => {
diff --git a/test/unit/authentication/UnsecureConstantCredentialsExtractor.test.ts b/test/unit/authentication/UnsecureConstantCredentialsExtractor.test.ts
new file mode 100644
index 000000000..67e20e08a
--- /dev/null
+++ b/test/unit/authentication/UnsecureConstantCredentialsExtractor.test.ts
@@ -0,0 +1,15 @@
+import { UnsecureConstantCredentialsExtractor } from '../../../src/authentication/UnsecureConstantCredentialsExtractor';
+
+describe('An UnsecureConstantCredentialsExtractor', (): void => {
+  it('extracts a constant WebID.', async(): Promise<void> => {
+    const agent = 'http://alice.example/card#me';
+    const extractor = new UnsecureConstantCredentialsExtractor(agent);
+    await expect(extractor.handle()).resolves.toEqual({ webId: agent });
+  });
+
+  it('extracts constant credentials.', async(): Promise<void> => {
+    const agent = {};
+    const extractor = new UnsecureConstantCredentialsExtractor(agent);
+    await expect(extractor.handle()).resolves.toBe(agent);
+  });
+});
diff --git a/test/unit/init/RootContainerInitializer.test.ts b/test/unit/init/RootContainerInitializer.test.ts
new file mode 100644
index 000000000..ba7942f82
--- /dev/null
+++ b/test/unit/init/RootContainerInitializer.test.ts
@@ -0,0 +1,41 @@
+import { RootContainerInitializer } from '../../../src/init/RootContainerInitializer';
+import type { ResourceStore } from '../../../src/storage/ResourceStore';
+import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
+
+describe('A RootContainerInitializer', (): void => {
+  const baseUrl = 'http://test.com/';
+  const store: jest.Mocked<ResourceStore> = {
+    getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()),
+    setRepresentation: jest.fn(),
+  } as any;
+  const initializer = new RootContainerInitializer(baseUrl, store);
+
+  afterEach((): void => {
+    jest.clearAllMocks();
+  });
+
+  it('invokes ResourceStore initialization.', async(): Promise<void> => {
+    await initializer.handle();
+
+    expect(store.getRepresentation).toHaveBeenCalledTimes(1);
+    expect(store.getRepresentation).toHaveBeenCalledWith({ path: baseUrl }, {});
+    expect(store.setRepresentation).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not invoke ResourceStore initialization when a root container already exists.', async(): Promise<void> => {
+    store.getRepresentation.mockReturnValueOnce(Promise.resolve({
+      data: { destroy: jest.fn() },
+    } as any));
+
+    await initializer.handle();
+
+    expect(store.getRepresentation).toHaveBeenCalledTimes(1);
+    expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/' }, {});
+    expect(store.setRepresentation).toHaveBeenCalledTimes(0);
+  });
+
+  it('errors when the store errors writing the root container.', async(): Promise<void> => {
+    store.getRepresentation.mockRejectedValueOnce(new Error('Fatal'));
+    await expect(initializer.handle()).rejects.toThrow('Fatal');
+  });
+});
diff --git a/test/unit/pods/generate/TemplatedResourcesGenerator.test.ts b/test/unit/pods/generate/TemplatedResourcesGenerator.test.ts
index 3f48d1645..531bd5279 100644
--- a/test/unit/pods/generate/TemplatedResourcesGenerator.test.ts
+++ b/test/unit/pods/generate/TemplatedResourcesGenerator.test.ts
@@ -1,6 +1,6 @@
 import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
+import { HandlebarsTemplateEngine } from '../../../../src/pods/generate/HandlebarsTemplateEngine';
 import { TemplatedResourcesGenerator } from '../../../../src/pods/generate/TemplatedResourcesGenerator';
-import type { TemplateEngine } from '../../../../src/pods/generate/TemplateEngine';
 import type {
   FileIdentifierMapper,
   FileIdentifierMapperFactory,
@@ -9,7 +9,6 @@ import type {
 import { ensureTrailingSlash, trimTrailingSlashes } from '../../../../src/util/PathUtil';
 import { readableToString } from '../../../../src/util/StreamUtil';
 import { mockFs } from '../../../util/Util';
-import Dict = NodeJS.Dict;
 
 jest.mock('fs');
 
@@ -30,13 +29,6 @@ class DummyFactory implements FileIdentifierMapperFactory {
   }
 }
 
-class DummyEngine implements TemplateEngine {
-  public apply(template: string, options: Dict<string>): string {
-    const keys = Object.keys(options);
-    return `${template}${keys.map((key): string => `{${key}:${options[key]}}`).join('')}`;
-  }
-}
-
 const genToArray = async<T>(iterable: AsyncIterable<T>): Promise<T[]> => {
   const arr: T[] = [];
   for await (const result of iterable) {
@@ -47,7 +39,8 @@ const genToArray = async<T>(iterable: AsyncIterable<T>): Promise<T[]> => {
 
 describe('A TemplatedResourcesGenerator', (): void => {
   const rootFilePath = 'templates';
-  const generator = new TemplatedResourcesGenerator(rootFilePath, new DummyFactory(), new DummyEngine());
+  // Using handlebars engine since it's smaller than any possible dummy
+  const generator = new TemplatedResourcesGenerator(rootFilePath, new DummyFactory(), new HandlebarsTemplateEngine());
   let cache: { data: any };
   const template = '<{{webId}}> a <http://xmlns.com/foaf/0.1/Person>.';
   const location = { path: 'http://test.com/alice/' };
@@ -68,11 +61,11 @@ describe('A TemplatedResourcesGenerator', (): void => {
     expect(representation.binary).toBe(true);
     expect(representation.metadata.contentType).toBe('text/turtle');
     await expect(readableToString(representation.data)).resolves
-      .toEqual(`<{{webId}}> a <http://xmlns.com/foaf/0.1/Person>.{webId:${webId}}`);
+      .toEqual(`<${webId}> a <http://xmlns.com/foaf/0.1/Person>.`);
   });
 
-  it('creates the necessary containers and ignores non-files.', async(): Promise<void> => {
-    cache.data = { container: { container: { template }}, 2: 5 };
+  it('creates the necessary containers.', async(): Promise<void> => {
+    cache.data = { container: { container: { template }}};
     const result = await genToArray(generator.generate(location, { webId }));
     const identifiers = result.map((res): ResourceIdentifier => res.identifier);
     const id = { path: `${location.path}container/container/template` };
@@ -85,6 +78,37 @@ describe('A TemplatedResourcesGenerator', (): void => {
 
     const { representation } = result[3];
     await expect(readableToString(representation.data)).resolves
-      .toEqual(`<{{webId}}> a <http://xmlns.com/foaf/0.1/Person>.{webId:${webId}}`);
+      .toEqual(`<${webId}> a <http://xmlns.com/foaf/0.1/Person>.`);
+  });
+
+  it('adds metadata from .meta files.', async(): Promise<void> => {
+    const meta = '<> <pre:has> "metadata".';
+    cache.data = { '.meta': meta, container: { 'template.meta': meta, template }};
+
+    // Not using options since our dummy template generator generates invalid turtle
+    const result = await genToArray(generator.generate(location, { webId }));
+    const identifiers = result.map((res): ResourceIdentifier => res.identifier);
+    expect(identifiers).toEqual([
+      location,
+      { path: `${location.path}container/` },
+      { path: `${location.path}container/template` },
+    ]);
+    // Root has the 1 raw metadata triple (with <> changed to its identifier)
+    const rootMetadata = result[0].representation.metadata;
+    expect(rootMetadata.identifier.value).toBe(location.path);
+    expect(rootMetadata.quads()).toHaveLength(1);
+    expect(rootMetadata.get('pre:has')?.value).toBe('metadata');
+
+    // Container has no metadata triples
+    const contMetadata = result[1].representation.metadata;
+    expect(contMetadata.identifier.value).toBe(`${location.path}container/`);
+    expect(contMetadata.quads()).toHaveLength(0);
+
+    // Document has the 1 raw metadata triple (with <> changed to its identifier) and content-type
+    const docMetadata = result[2].representation.metadata;
+    expect(docMetadata.identifier.value).toBe(`${location.path}container/template`);
+    expect(docMetadata.quads()).toHaveLength(2);
+    expect(docMetadata.get('pre:has')?.value).toBe('metadata');
+    expect(docMetadata.contentType).toBe('text/turtle');
   });
 });
diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts
index 29a14c642..560f092d8 100644
--- a/test/unit/storage/DataAccessorBasedStore.test.ts
+++ b/test/unit/storage/DataAccessorBasedStore.test.ts
@@ -10,6 +10,7 @@ import { DataAccessorBasedStore } from '../../../src/storage/DataAccessorBasedSt
 import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
 import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
 import { ConflictHttpError } from '../../../src/util/errors/ConflictHttpError';
+import { InternalServerError } from '../../../src/util/errors/InternalServerError';
 import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAllowedHttpError';
 import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
@@ -83,7 +84,11 @@ describe('A DataAccessorBasedStore', (): void => {
     store = new DataAccessorBasedStore(accessor, identifierStrategy);
 
     containerMetadata = new RepresentationMetadata(
-      { [RDF.type]: [ DataFactory.namedNode(LDP.Container), DataFactory.namedNode(LDP.BasicContainer) ]},
+      { [RDF.type]: [
+        DataFactory.namedNode(LDP.Resource),
+        DataFactory.namedNode(LDP.Container),
+        DataFactory.namedNode(LDP.BasicContainer),
+      ]},
     );
     accessor.data[root] = { metadata: containerMetadata } as Representation;
 
@@ -270,12 +275,23 @@ describe('A DataAccessorBasedStore', (): void => {
     });
 
     it('will error if the ending slash does not match its resource type.', async(): Promise<void> => {
-      const resourceID = { path: `${root}resource/` };
+      const resourceID = { path: `${root}resource` };
+      representation.metadata.add(RDF.type, toNamedNode(LDP.Container));
       await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(
         new BadRequestHttpError('Containers should have a `/` at the end of their path, resources should not.'),
       );
     });
 
+    it('will error if the DataAccessor did not store the required type triples.', async(): Promise<void> => {
+      const resourceID = { path: `${root}resource` };
+      accessor.data[resourceID.path] = representation;
+      representation.metadata.identifier = namedNode(resourceID.path);
+      representation.metadata.removeAll(RDF.type);
+      await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(
+        new InternalServerError('Unknown resource type.'),
+      );
+    });
+
     it('errors when trying to create a container with non-RDF data.', async(): Promise<void> => {
       const resourceID = { path: `${root}container/` };
       representation.metadata.add(RDF.type, toNamedNode(LDP.Container));
@@ -343,6 +359,21 @@ describe('A DataAccessorBasedStore', (): void => {
         new ConflictHttpError(`Creating container ${root}a/ conflicts with an existing resource.`),
       );
     });
+
+    it('can write to root if it does not exist.', async(): Promise<void> => {
+      // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+      delete accessor.data[root];
+      const resourceID = { path: `${root}` };
+
+      // Generate based on URI
+      representation.metadata.removeAll(RDF.type);
+      representation.metadata.contentType = 'text/turtle';
+      representation.data = guardedStreamFrom([]);
+      await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined();
+      expect(accessor.data[resourceID.path]).toBeTruthy();
+      expect(Object.keys(accessor.data)).toHaveLength(1);
+      expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
+    });
   });
 
   describe('modifying a Representation', (): void => {
diff --git a/test/unit/storage/accessors/FileDataAccessor.test.ts b/test/unit/storage/accessors/FileDataAccessor.test.ts
index 4e87a6aaa..e6b99a20d 100644
--- a/test/unit/storage/accessors/FileDataAccessor.test.ts
+++ b/test/unit/storage/accessors/FileDataAccessor.test.ts
@@ -223,20 +223,27 @@ describe('A FileDataAccessor', (): void => {
       });
     });
 
+    it('does not try to update the content-type if there is no original file.', async(): Promise<void> => {
+      metadata.identifier = DataFactory.namedNode(`${base}resource.txt`);
+      metadata.contentType = 'text/turtle';
+      metadata.add('new', 'metadata');
+      await expect(accessor.writeDocument({ path: `${base}resource.txt` }, data, metadata))
+        .resolves.toBeUndefined();
+      expect(cache.data).toEqual({
+        'resource.txt$.ttl': 'data',
+        'resource.txt.meta': expect.stringMatching(`<${base}resource.txt> <new> "metadata".`),
+      });
+    });
+
     it('throws an error if there is an issue deleting the original file.', async(): Promise<void> => {
       cache.data = { 'resource$.ttl': '<this> <is> <data>.' };
       jest.requireMock('fs').promises.unlink = (): any => {
         const error = new Error('error') as SystemError;
-        error.code = 'ENOENT';
+        error.code = 'EISDIR';
         error.syscall = 'unlink';
         throw error;
       };
 
-      // `unlink` throwing ENOENT should not be an issue if the content-type does not change
-      metadata.contentType = 'text/turtle';
-      await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
-        .resolves.toBeUndefined();
-
       metadata.contentType = 'text/plain';
       await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
         .rejects.toThrow(new Error('error'));
@@ -287,6 +294,12 @@ describe('A FileDataAccessor', (): void => {
       await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
       expect(cache.data.container).toEqual({});
     });
+
+    it('can write to the root container.', async(): Promise<void> => {
+      metadata = new RepresentationMetadata({ path: `${base}` }, { likes: 'apples' });
+      await expect(accessor.writeContainer({ path: `${base}` }, metadata)).resolves.toBeUndefined();
+      expect(cache.data).toEqual({ '.meta': expect.stringMatching(`<${base}> <likes> "apples".`) });
+    });
   });
 
   describe('deleting a resource', (): void => {
@@ -334,5 +347,11 @@ describe('A FileDataAccessor', (): void => {
       await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined();
       expect(cache.data.container).toBeUndefined();
     });
+
+    it('can delete the root container.', async(): Promise<void> => {
+      cache.data = { };
+      await expect(accessor.deleteResource({ path: `${base}` })).resolves.toBeUndefined();
+      expect(cache.data).toBeUndefined();
+    });
   });
 });
diff --git a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
index cfecdf0e3..a83bafec1 100644
--- a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
+++ b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
@@ -18,6 +18,9 @@ describe('An InMemoryDataAccessor', (): void => {
   beforeEach(async(): Promise<void> => {
     accessor = new InMemoryDataAccessor(base);
 
+    // Create default root container
+    await accessor.writeContainer({ path: `${base}` }, new RepresentationMetadata());
+
     metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM });
 
     data = guardedStreamFrom([ 'data' ]);
@@ -38,12 +41,12 @@ describe('An InMemoryDataAccessor', (): void => {
     });
 
     it('throws an error if part of the path matches a document.', async(): Promise<void> => {
-      await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
+      await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
       await expect(accessor.getData({ path: `${base}resource/resource2` })).rejects.toThrow(new Error('Invalid path.'));
     });
 
     it('returns the corresponding data every time.', async(): Promise<void> => {
-      await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
+      await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
 
       // Run twice to make sure the data is stored correctly
       await expect(readableToString(await accessor.getData({ path: `${base}resource` }))).resolves.toBe('data');
@@ -56,29 +59,25 @@ describe('An InMemoryDataAccessor', (): void => {
       await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError);
     });
 
-    it('errors when trying to access the parent of root.', async(): Promise<void> => {
-      await expect(accessor.writeDocument({ path: `${base}` }, data, metadata))
-        .rejects.toThrow(new Error('Root container has no parent.'));
-    });
-
     it('throws a 404 if the trailing slash does not match its type.', async(): Promise<void> => {
-      await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
+      await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
       await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError);
-      await accessor.writeContainer({ path: `${base}container/` }, metadata);
+      await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
       await expect(accessor.getMetadata({ path: `${base}container` })).rejects.toThrow(NotFoundHttpError);
     });
 
     it('returns empty metadata if there was none stored.', async(): Promise<void> => {
       metadata = new RepresentationMetadata();
-      await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
+      await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
       metadata = await accessor.getMetadata({ path: `${base}resource` });
       expect(metadata.quads()).toHaveLength(0);
     });
 
     it('generates the containment metadata for a container.', async(): Promise<void> => {
-      await accessor.writeContainer({ path: `${base}container/` }, metadata);
-      await accessor.writeDocument({ path: `${base}container/resource` }, data, metadata);
-      await accessor.writeContainer({ path: `${base}container/container2` }, metadata);
+      await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
+      await expect(accessor.writeDocument({ path: `${base}container/resource` }, data, metadata))
+        .resolves.toBeUndefined();
+      await expect(accessor.writeContainer({ path: `${base}container/container2` }, metadata)).resolves.toBeUndefined();
       metadata = await accessor.getMetadata({ path: `${base}container/` });
       expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray(
         [ toNamedNode(`${base}container/resource`), toNamedNode(`${base}container/container2/`) ],
@@ -88,7 +87,7 @@ describe('An InMemoryDataAccessor', (): void => {
     it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
       const identifier = { path: `${base}resource` };
       const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Resource) });
-      await accessor.writeDocument(identifier, data, inputMetadata);
+      await expect(accessor.writeDocument(identifier, data, inputMetadata)).resolves.toBeUndefined();
       metadata = await accessor.getMetadata(identifier);
       expect(metadata.identifier.value).toBe(`${base}resource`);
       const quads = metadata.quads();
@@ -99,7 +98,7 @@ describe('An InMemoryDataAccessor', (): void => {
     it('adds stored metadata when requesting container metadata.', async(): Promise<void> => {
       const identifier = { path: `${base}container/` };
       const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) });
-      await accessor.writeContainer(identifier, inputMetadata);
+      await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined();
 
       metadata = await accessor.getMetadata(identifier);
       expect(metadata.identifier.value).toBe(`${base}container/`);
@@ -111,15 +110,15 @@ describe('An InMemoryDataAccessor', (): void => {
     it('can overwrite the metadata of an existing container without overwriting children.', async(): Promise<void> => {
       const identifier = { path: `${base}container/` };
       const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) });
-      await accessor.writeContainer(identifier, inputMetadata);
+      await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined();
       const resourceMetadata = new RepresentationMetadata();
-      await accessor.writeDocument(
+      await expect(accessor.writeDocument(
         { path: `${base}container/resource` }, data, resourceMetadata,
-      );
+      )).resolves.toBeUndefined();
 
       const newMetadata = new RepresentationMetadata(inputMetadata);
       newMetadata.add(RDF.type, toNamedNode(LDP.BasicContainer));
-      await accessor.writeContainer(identifier, newMetadata);
+      await expect(accessor.writeContainer(identifier, newMetadata)).resolves.toBeUndefined();
 
       metadata = await accessor.getMetadata(identifier);
       expect(metadata.identifier.value).toBe(`${base}container/`);
@@ -134,8 +133,29 @@ describe('An InMemoryDataAccessor', (): void => {
       expect(await readableToString(await accessor.getData({ path: `${base}container/resource` }))).toBe('data');
     });
 
+    it('can write to the root container without overriding its children.', async(): Promise<void> => {
+      const identifier = { path: `${base}` };
+      const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) });
+      await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined();
+      const resourceMetadata = new RepresentationMetadata();
+      await expect(accessor.writeDocument(
+        { path: `${base}resource` }, data, resourceMetadata,
+      )).resolves.toBeUndefined();
+
+      metadata = await accessor.getMetadata(identifier);
+      expect(metadata.identifier.value).toBe(`${base}`);
+      const quads = metadata.quads();
+      expect(quads).toHaveLength(2);
+      expect(metadata.getAll(RDF.type)).toHaveLength(1);
+      expect(metadata.getAll(LDP.contains)).toHaveLength(1);
+
+      await expect(accessor.getMetadata({ path: `${base}resource` }))
+        .resolves.toBeInstanceOf(RepresentationMetadata);
+      expect(await readableToString(await accessor.getData({ path: `${base}resource` }))).toBe('data');
+    });
+
     it('errors when writing to an invalid container path..', async(): Promise<void> => {
-      await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
+      await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
 
       await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata))
         .rejects.toThrow(new Error('Invalid path.'));
@@ -148,12 +168,21 @@ describe('An InMemoryDataAccessor', (): void => {
     });
 
     it('removes the corresponding resource.', async(): Promise<void> => {
-      await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
-      await accessor.writeContainer({ path: `${base}container/` }, metadata);
+      await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
+      await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
       await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined();
       await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined();
       await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError);
       await expect(accessor.getMetadata({ path: `${base}container/` })).rejects.toThrow(NotFoundHttpError);
     });
+
+    it('can delete the root container and write to it again.', async(): Promise<void> => {
+      await expect(accessor.deleteResource({ path: `${base}` })).resolves.toBeUndefined();
+      await expect(accessor.getMetadata({ path: `${base}` })).rejects.toThrow(NotFoundHttpError);
+      await expect(accessor.getMetadata({ path: `${base}test/` })).rejects.toThrow(NotFoundHttpError);
+      await expect(accessor.writeContainer({ path: `${base}` }, metadata)).resolves.toBeUndefined();
+      const resultMetadata = await accessor.getMetadata({ path: `${base}` });
+      expect(resultMetadata.quads()).toBeRdfIsomorphic(metadata.quads());
+    });
   });
 });
diff --git a/test/unit/storage/accessors/SparqlDataAccessor.test.ts b/test/unit/storage/accessors/SparqlDataAccessor.test.ts
index 935c68780..0d838dbc1 100644
--- a/test/unit/storage/accessors/SparqlDataAccessor.test.ts
+++ b/test/unit/storage/accessors/SparqlDataAccessor.test.ts
@@ -121,26 +121,6 @@ describe('A SparqlDataAccessor', (): void => {
     ]));
   });
 
-  it('generates resource metadata for the root container.', async(): Promise<void> => {
-    metadata = await accessor.getMetadata({ path: base });
-    expect(metadata.quads()).toBeRdfIsomorphic([
-      quad(namedNode('this'), namedNode('a'), namedNode('triple')),
-      quad(namedNode(base), toNamedNode(RDF.type), toNamedNode(LDP.Container)),
-      quad(namedNode(base), toNamedNode(RDF.type), toNamedNode(LDP.BasicContainer)),
-      quad(namedNode(base), toNamedNode(RDF.type), toNamedNode(LDP.Resource)),
-    ]);
-
-    expect(fetchTriples).toHaveBeenCalledTimes(1);
-    expect(fetchTriples.mock.calls[0][0]).toBe(endpoint);
-    expect(simplifyQuery(fetchTriples.mock.calls[0][1])).toBe(simplifyQuery([
-      'CONSTRUCT { ?s ?p ?o. } WHERE {',
-      `  { GRAPH <${base}> { ?s ?p ?o. } }`,
-      '  UNION',
-      `  { GRAPH <meta:${base}> { ?s ?p ?o. } }`,
-      '}',
-    ]));
-  });
-
   it('throws 404 if no metadata was found.', async(): Promise<void> => {
     // Clear triples array
     triples = [];
@@ -172,6 +152,24 @@ describe('A SparqlDataAccessor', (): void => {
     ]));
   });
 
+  it('does not write containment triples when writing to a root container.', async(): Promise<void> => {
+    metadata = new RepresentationMetadata({ path: 'http://test.com/' },
+      { [RDF.type]: [ toNamedNode(LDP.Resource), toNamedNode(LDP.Container) ]});
+    await expect(accessor.writeContainer({ path: 'http://test.com/' }, metadata)).resolves.toBeUndefined();
+
+    expect(fetchUpdate).toHaveBeenCalledTimes(1);
+    expect(fetchUpdate.mock.calls[0][0]).toBe(endpoint);
+    expect(simplifyQuery(fetchUpdate.mock.calls[0][1])).toBe(simplifyQuery([
+      'DELETE WHERE { GRAPH <meta:http://test.com/> { ?s ?p ?o. } };',
+      'INSERT DATA {',
+      '  GRAPH <meta:http://test.com/> {',
+      '    <http://test.com/> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/ldp#Resource>.',
+      '    <http://test.com/> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/ldp#Container>.',
+      '  }',
+      '}',
+    ]));
+  });
+
   it('overwrites the data and metadata when writing a resource and updates parent.', async(): Promise<void> => {
     metadata = new RepresentationMetadata({ path: 'http://test.com/container/resource' },
       { [RDF.type]: [ toNamedNode(LDP.Resource) ]});
@@ -224,6 +222,19 @@ describe('A SparqlDataAccessor', (): void => {
     ]));
   });
 
+  it('does not try to remove containment triples when deleting a root container.', async(): Promise<void> => {
+    metadata = new RepresentationMetadata({ path: 'http://test.com/' },
+      { [RDF.type]: [ toNamedNode(LDP.Resource), toNamedNode(LDP.Container) ]});
+    await expect(accessor.deleteResource({ path: 'http://test.com/' })).resolves.toBeUndefined();
+
+    expect(fetchUpdate).toHaveBeenCalledTimes(1);
+    expect(fetchUpdate.mock.calls[0][0]).toBe(endpoint);
+    expect(simplifyQuery(fetchUpdate.mock.calls[0][1])).toBe(simplifyQuery([
+      'DELETE WHERE { GRAPH <http://test.com/> { ?s ?p ?o. } };',
+      'DELETE WHERE { GRAPH <meta:http://test.com/> { ?s ?p ?o. } }',
+    ]));
+  });
+
   it('errors when trying to write to a metadata document.', async(): Promise<void> => {
     await expect(accessor.writeDocument({ path: 'meta:http://test.com/container/resource' }, data, metadata))
       .rejects.toThrow(new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.'));
diff --git a/test/unit/storage/mapping/ExtensionBasedMapper.test.ts b/test/unit/storage/mapping/ExtensionBasedMapper.test.ts
index 2a0296285..75c357f8d 100644
--- a/test/unit/storage/mapping/ExtensionBasedMapper.test.ts
+++ b/test/unit/storage/mapping/ExtensionBasedMapper.test.ts
@@ -50,16 +50,24 @@ describe('An ExtensionBasedMapper', (): void => {
         .rejects.toThrow(new BadRequestHttpError('Identifiers cannot contain a dollar sign before their extension'));
     });
 
-    it('throws 404 when looking in a folder that does not exist.', async(): Promise<void> => {
+    it('determines content-type by extension when looking in a folder that does not exist.', async(): Promise<void> => {
       fsPromises.readdir.mockImplementation((): void => {
         throw new Error('does not exist');
       });
-      await expect(mapper.mapUrlToFilePath({ path: `${base}no/test.txt` })).rejects.toThrow(NotFoundHttpError);
+      await expect(mapper.mapUrlToFilePath({ path: `${base}no/test.txt` })).resolves.toEqual({
+        identifier: { path: `${base}no/test.txt` },
+        filePath: `${rootFilepath}no/test.txt`,
+        contentType: 'text/plain',
+      });
     });
 
-    it('throws 404 when looking for a file that does not exist.', async(): Promise<void> => {
+    it('determines content-type by extension when looking for a file that does not exist.', async(): Promise<void> => {
       fsPromises.readdir.mockReturnValue([ 'test.ttl' ]);
-      await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` })).rejects.toThrow(NotFoundHttpError);
+      await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` })).resolves.toEqual({
+        identifier: { path: `${base}test.txt` },
+        filePath: `${rootFilepath}test.txt`,
+        contentType: 'text/plain',
+      });
     });
 
     it('determines the content-type based on the extension.', async(): Promise<void> => {
diff --git a/test/unit/util/QuadUtil.test.ts b/test/unit/util/QuadUtil.test.ts
index 39b9a793b..e52f25ef7 100644
--- a/test/unit/util/QuadUtil.test.ts
+++ b/test/unit/util/QuadUtil.test.ts
@@ -1,7 +1,8 @@
 import 'jest-rdf';
 import { DataFactory } from 'n3';
 import type { Quad } from 'rdf-js';
-import { pushQuad } from '../../../src/util/QuadUtil';
+import { parseQuads, pushQuad, serializeQuads } from '../../../src/util/QuadUtil';
+import { guardedStreamFrom, readableToString } from '../../../src/util/StreamUtil';
 
 describe('QuadUtil', (): void => {
   describe('#pushQuad', (): void => {
@@ -13,4 +14,27 @@ describe('QuadUtil', (): void => {
       ]);
     });
   });
+
+  describe('#serializeQuads', (): void => {
+    it('converts quads to the requested format.', async(): Promise<void> => {
+      const quads = [ DataFactory.quad(
+        DataFactory.namedNode('pre:sub'),
+        DataFactory.namedNode('pre:pred'),
+        DataFactory.literal('obj'),
+      ) ];
+      const stream = serializeQuads(quads, 'application/n-triples');
+      await expect(readableToString(stream)).resolves.toMatch('<pre:sub> <pre:pred> "obj" .');
+    });
+  });
+
+  describe('#parseQuads', (): void => {
+    it('parses quads from the requested format.', async(): Promise<void> => {
+      const stream = guardedStreamFrom([ '<pre:sub> <pre:pred> "obj".' ]);
+      await expect(parseQuads(stream, 'application/n-triples')).resolves.toEqualRdfQuadArray([ DataFactory.quad(
+        DataFactory.namedNode('pre:sub'),
+        DataFactory.namedNode('pre:pred'),
+        DataFactory.literal('obj'),
+      ) ]);
+    });
+  });
 });