Repository: solid/community-server (ts)

Issue:
Can't delete root container in memory data accessor
Not that deleting the root is that required, but it means it also can't handle having an empty root which would make #412 easier to generalize.

Pull request:
fix: Allow deletion of root in InMemoryDataAccessor
Closes #417 and brings the memory data accessor in line with the changes from #418 

Patch:
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) {


Test patch:
diff --git a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
index cfecdf0e3..8c61102d9 100644
--- a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
+++ b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
@@ -38,12 +38,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 +56,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 +84,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 +95,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 +107,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 +130,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 +165,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());
+    });
   });
 });