Repository: solid/community-server (ts)

Issue:
Investigate why root storage metadata is not added in memory stores
When starting the server now with the default memory config the `RootContainerInitializer` does not generate its metadata:
```
[RootContainerInitializer] debug: Existing root container found at http://localhost:3000/
```
This used to not be the case (I'm pretty sure?) so I don't know yet what caused this to start happening and how long it has been there. It still works correctly for a file backend so it is related to this specific memory backend somehow.

First I thought it might be related to #603 but resetting the branch before that the issue is still there.

Perhaps we should just always write the metadata though, even if the root container is already present. There's no reason to not have it.

Pull request description:
Remove default root container in memory DataAccessor + minor fixes
Closes #608

I was wrong in the issue, this probably never worked as I forgot to remove the default container from the memory store when doing the other root-related changes.

Also required changes in some tests which depended on that container being there.

Accidentally discovered a minor bug in the `DataAccessorBasedStore` when PUTting a resource if there is no root container (which in practice we would never see because of the initializer).

Patch:
diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts
index 5cfcf7bc3..c03dd75ca 100644
--- a/src/storage/DataAccessorBasedStore.ts
+++ b/src/storage/DataAccessorBasedStore.ts
@@ -385,7 +385,9 @@ export class DataAccessorBasedStore implements ResourceStore {
     } catch (error: unknown) {
       if (NotFoundHttpError.isInstance(error)) {
         // Make sure the parent exists first
-        await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container));
+        if (!this.identifierStrategy.isRootContainer(container)) {
+          await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container));
+        }
         await this.writeData(container, new BasicRepresentation([], container), true);
       } else {
         throw error;
diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts
index 0c38069c5..418461b5e 100644
--- a/src/storage/accessors/FileDataAccessor.ts
+++ b/src/storage/accessors/FileDataAccessor.ts
@@ -103,7 +103,7 @@ export class FileDataAccessor implements DataAccessor {
   public async writeContainer(identifier: ResourceIdentifier, metadata: RepresentationMetadata): Promise<void> {
     const link = await this.resourceMapper.mapUrlToFilePath(identifier);
     try {
-      await fsPromises.mkdir(link.filePath);
+      await fsPromises.mkdir(link.filePath, { recursive: true });
     } catch (error: unknown) {
       // Don't throw if directory already exists
       if (!isSystemError(error) || error.code !== 'EEXIST') {
diff --git a/src/storage/accessors/InMemoryDataAccessor.ts b/src/storage/accessors/InMemoryDataAccessor.ts
index cb5c479f4..1938cb430 100644
--- a/src/storage/accessors/InMemoryDataAccessor.ts
+++ b/src/storage/accessors/InMemoryDataAccessor.ts
@@ -31,8 +31,7 @@ export class InMemoryDataAccessor implements DataAccessor {
 
     const metadata = new RepresentationMetadata({ path: this.base });
     metadata.addQuads(generateResourceQuads(DataFactory.namedNode(this.base), true));
-    const rootContainer = { entries: {}, metadata };
-    this.store = { entries: { '': rootContainer }};
+    this.store = { entries: { }};
   }
 
   public async canHandle(): Promise<void> {


Test patch:
diff --git a/test/integration/Config.ts b/test/integration/Config.ts
index 017b12294..a19d55ca2 100644
--- a/test/integration/Config.ts
+++ b/test/integration/Config.ts
@@ -1,4 +1,3 @@
-import { mkdirSync } from 'fs';
 import type { IModuleState } from 'componentsjs';
 import { ComponentsManager } from 'componentsjs';
 import * as rimraf from 'rimraf';
@@ -27,10 +26,6 @@ export function getTestFolder(name: string): string {
   return joinFilePath(__dirname, '../tmp', name);
 }
 
-export function createFolder(folder: string): void {
-  mkdirSync(folder, { recursive: true });
-}
-
 export function removeFolder(folder: string): void {
   rimraf.sync(folder, { glob: false });
 }
diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts
index 661325bb7..7ece95aa9 100644
--- a/test/integration/LdpHandlerWithAuth.test.ts
+++ b/test/integration/LdpHandlerWithAuth.test.ts
@@ -2,30 +2,26 @@ import { createReadStream } from 'fs';
 import type { HttpHandler, Initializer, ResourceStore } from '../../src/';
 import { LDP, BasicRepresentation, joinFilePath } from '../../src/';
 import { AclHelper, ResourceHelper } from '../util/TestHelpers';
-import { BASE, getTestFolder, createFolder, removeFolder, instantiateFromConfig } from './Config';
+import { BASE, getTestFolder, removeFolder, instantiateFromConfig } from './Config';
 
 const rootFilePath = getTestFolder('full-config-acl');
 const stores: [string, any][] = [
   [ 'in-memory storage', {
     storeUrn: 'urn:solid-server:default:MemoryResourceStore',
-    setup: jest.fn(),
     teardown: jest.fn(),
   }],
   [ 'on-disk storage', {
     storeUrn: 'urn:solid-server:default:FileResourceStore',
-    setup: (): void => createFolder(rootFilePath),
     teardown: (): void => removeFolder(rootFilePath),
   }],
 ];
 
-describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, setup, teardown }): void => {
+describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, teardown }): void => {
   let handler: HttpHandler;
   let aclHelper: AclHelper;
   let resourceHelper: ResourceHelper;
 
   beforeAll(async(): Promise<void> => {
-    // Set up the internal store
-    await setup();
     const variables: Record<string, any> = {
       'urn:solid-server:default:variable:baseUrl': BASE,
       'urn:solid-server:default:variable:rootFilePath': rootFilePath,
@@ -46,6 +42,8 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, se
       variables,
     ) as Record<string, any>;
     ({ handler, store, initializer } = instances);
+
+    // Set up the internal store
     await initializer.handleSafe();
 
     // Create test helpers for manipulating the components
diff --git a/test/integration/LdpHandlerWithoutAuth.test.ts b/test/integration/LdpHandlerWithoutAuth.test.ts
index 3c2abba65..5f52a0e6e 100644
--- a/test/integration/LdpHandlerWithoutAuth.test.ts
+++ b/test/integration/LdpHandlerWithoutAuth.test.ts
@@ -1,33 +1,29 @@
 import { DataFactory, Parser } from 'n3';
 import type { MockResponse } from 'node-mocks-http';
-import { ensureTrailingSlash } from '../../src/';
+import { ensureTrailingSlash, PIM, RDF } from '../../src/';
 import type { HttpHandler, Initializer, ResourceStore } from '../../src/';
 import { LDP } from '../../src/util/Vocabularies';
 import { ResourceHelper } from '../util/TestHelpers';
-import { BASE, getTestFolder, createFolder, removeFolder, instantiateFromConfig } from './Config';
+import { BASE, getTestFolder, removeFolder, instantiateFromConfig } from './Config';
 const { literal, namedNode, quad } = DataFactory;
 
 const rootFilePath = getTestFolder('full-config-no-auth');
 const stores: [string, any][] = [
   [ 'in-memory storage', {
     storeUrn: 'urn:solid-server:default:MemoryResourceStore',
-    setup: jest.fn(),
     teardown: jest.fn(),
   }],
   [ 'on-disk storage', {
     storeUrn: 'urn:solid-server:default:FileResourceStore',
-    setup: (): void => createFolder(rootFilePath),
     teardown: (): void => removeFolder(rootFilePath),
   }],
 ];
 
-describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, setup, teardown }): void => {
+describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, teardown }): void => {
   let handler: HttpHandler;
   let resourceHelper: ResourceHelper;
 
   beforeAll(async(): Promise<void> => {
-    // Set up the internal store
-    await setup();
     const variables: Record<string, any> = {
       'urn:solid-server:default:variable:baseUrl': BASE,
       'urn:solid-server:default:variable:rootFilePath': rootFilePath,
@@ -47,6 +43,8 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn,
       variables,
     ) as Record<string, any>;
     ({ handler, initializer } = instances);
+
+    // Set up the internal store
     await initializer.handleSafe();
 
     // Create test helpers for manipulating the components
@@ -63,10 +61,15 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn,
     expect(response.statusCode).toBe(200);
     expect(response.getHeaders()).toHaveProperty('content-type', 'text/turtle');
 
-    const data = response._getData().toString();
-    expect(data).toContain(`<> a ldp:Container`);
+    const parser = new Parser({ baseIRI: `${BASE}/` });
+    const quads = parser.parse(response._getData().toString());
+    expect(quads.some((entry): boolean => entry.equals(
+      quad(namedNode(`${BASE}/`), RDF.terms.type, LDP.terms.Container),
+    ))).toBeTruthy();
     expect(response.getHeaders().link).toContain(`<${LDP.Container}>; rel="type"`);
     expect(response.getHeaders().link).toContain(`<${BASE}/.acl>; rel="acl"`);
+    // This is only here because we're accessing the root container
+    expect(response.getHeaders().link).toContain(`<${PIM.Storage}>; rel="type"`);
   });
 
   it('can read a folder listing with a query string.', async():
@@ -75,10 +78,14 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn,
     expect(response.statusCode).toBe(200);
     expect(response.getHeaders()).toHaveProperty('content-type', 'text/turtle');
 
-    const data = response._getData().toString();
-    expect(data).toContain(`<> a ldp:Container`);
+    const parser = new Parser({ baseIRI: `${BASE}/` });
+    const quads = parser.parse(response._getData().toString());
+    expect(quads.some((entry): boolean => entry.equals(
+      quad(namedNode(`${BASE}/`), RDF.terms.type, LDP.terms.Container),
+    ))).toBeTruthy();
     expect(response.getHeaders().link).toContain(`<${LDP.Container}>; rel="type"`);
     expect(response.getHeaders().link).toContain(`<${BASE}/.acl>; rel="acl"`);
+    expect(response.getHeaders().link).toContain(`<${PIM.Storage}>; rel="type"`);
   });
 
   it('can add a file to the store, read it and delete it.', async():
diff --git a/test/integration/LpdHandlerOperations.test.ts b/test/integration/LpdHandlerOperations.test.ts
index c4254d952..185d1ccd2 100644
--- a/test/integration/LpdHandlerOperations.test.ts
+++ b/test/integration/LpdHandlerOperations.test.ts
@@ -17,6 +17,15 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
         'urn:solid-server:default:variable:baseUrl': BASE,
       },
     ) as HttpHandler;
+
+    // The tests depend on there being a root container here
+    await performRequest(
+      handler,
+      new URL('http://test.com/'),
+      'PUT',
+      { 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' },
+      [ ],
+    );
   });
 
   it('can add, read and delete data based on incoming requests.', async(): Promise<void> => {
diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts
index afb97c7fb..f437a8047 100644
--- a/test/integration/WebSocketsProtocol.test.ts
+++ b/test/integration/WebSocketsProtocol.test.ts
@@ -28,9 +28,9 @@ describe('A server with the Solid WebSockets API behind a proxy', (): void => {
     });
   });
 
-  it('returns a 200.', async(): Promise<void> => {
+  it('returns a 404 if no data was initialized.', async(): Promise<void> => {
     const response = await fetch(serverUrl, { headers });
-    expect(response.status).toBe(200);
+    expect(response.status).toBe(404);
   });
 
   it('sets the Updates-Via header.', async(): Promise<void> => {
diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts
index 8cc5af2f6..4fcb51412 100644
--- a/test/unit/storage/DataAccessorBasedStore.test.ts
+++ b/test/unit/storage/DataAccessorBasedStore.test.ts
@@ -305,6 +305,14 @@ describe('A DataAccessorBasedStore', (): void => {
       expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
     });
 
+    it('can write resources even if root does not exist.', async(): Promise<void> => {
+      // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+      delete accessor.data[root];
+      const resourceID = { path: `${root}resource` };
+      await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined();
+      await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
+    });
+
     it('can write containers with quad data.', async(): Promise<void> => {
       const resourceID = { path: `${root}container/` };