diff --git a/src/init/Setup.ts b/src/init/Setup.ts index a8437a096..3d7a8554a 100644 --- a/src/init/Setup.ts +++ b/src/init/Setup.ts @@ -1,10 +1,12 @@ import type { AclManager } from '../authorization/AclManager'; import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; +import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import type { LoggerFactory } from '../logging/LoggerFactory'; import { getLoggerFor, setGlobalLoggerFactory } from '../logging/LogUtil'; -import type { ExpressHttpServerFactory } from '../server/ExpressHttpServerFactory'; +import type { HttpServerFactory } from '../server/HttpServerFactory'; import type { ResourceStore } from '../storage/ResourceStore'; import { TEXT_TURTLE } from '../util/ContentTypes'; +import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { guardedStreamFrom } from '../util/StreamUtil'; import { CONTENT_TYPE } from '../util/UriConstants'; @@ -13,7 +15,7 @@ import { CONTENT_TYPE } from '../util/UriConstants'; */ export class Setup { protected readonly logger = getLoggerFor(this); - private readonly serverFactory: ExpressHttpServerFactory; + private readonly serverFactory: HttpServerFactory; private readonly store: ResourceStore; private readonly aclManager: AclManager; private readonly loggerFactory: LoggerFactory; @@ -21,7 +23,7 @@ export class Setup { private readonly port: number; public constructor( - serverFactory: ExpressHttpServerFactory, + serverFactory: HttpServerFactory, store: ResourceStore, aclManager: AclManager, loggerFactory: LoggerFactory, @@ -40,13 +42,35 @@ export class Setup { * Set up a server. */ public async setup(): Promise { - // Configure the logger factory so that others can statically call it. setGlobalLoggerFactory(this.loggerFactory); - // Set up acl so everything can still be done by default - // Note that this will need to be adapted to go through all the correct channels later on - const aclSetup = async(): Promise => { - const acl = `@prefix acl: . + const rootAcl = await this.aclManager.getAclDocument({ path: this.base }); + if (!await this.hasRootAclDocument(rootAcl)) { + await this.setRootAclDocument(rootAcl); + } + + this.serverFactory.startServer(this.port); + return this.base; + } + + protected async hasRootAclDocument(rootAcl: ResourceIdentifier): Promise { + try { + const result = await this.store.getRepresentation(rootAcl, {}); + this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`); + result.data.destroy(); + return true; + } catch (error: unknown) { + if (error instanceof NotFoundHttpError) { + return false; + } + throw error; + } + } + + // Set up ACL so everything can still be done by default + // Note that this will need to be adapted to go through all the correct channels later on + protected async setRootAclDocument(rootAcl: ResourceIdentifier): Promise { + const acl = `@prefix acl: . @prefix foaf: . <#authorization> @@ -59,22 +83,15 @@ export class Setup { acl:mode acl:Control; acl:accessTo <${this.base}>; acl:default <${this.base}>.`; - const baseAclId = await this.aclManager.getAclDocument({ path: this.base }); - const metadata = new RepresentationMetadata(baseAclId.path, { [CONTENT_TYPE]: TEXT_TURTLE }); - await this.store.setRepresentation( - baseAclId, - { - binary: true, - data: guardedStreamFrom([ acl ]), - metadata, - }, - ); - }; - this.logger.debug('Setup default ACL settings'); - await aclSetup(); - - this.serverFactory.startServer(this.port); - - return this.base; + const metadata = new RepresentationMetadata(rootAcl.path, { [CONTENT_TYPE]: TEXT_TURTLE }); + this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); + await this.store.setRepresentation( + rootAcl, + { + binary: true, + data: guardedStreamFrom([ acl ]), + metadata, + }, + ); } } diff --git a/test/unit/init/Setup.test.ts b/test/unit/init/Setup.test.ts index 822cfa15c..c6e2034c9 100644 --- a/test/unit/init/Setup.test.ts +++ b/test/unit/init/Setup.test.ts @@ -2,33 +2,61 @@ import type { AclManager } from '../../../src/authorization/AclManager'; import { Setup } from '../../../src/init/Setup'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import { VoidLoggerFactory } from '../../../src/logging/VoidLoggerFactory'; +import type { HttpServerFactory } from '../../../src/server/HttpServerFactory'; +import type { ResourceStore } from '../../../src/storage/ResourceStore'; +import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; describe('Setup', (): void => { - let serverFactory: any; - let store: any; - let aclManager: AclManager; + const serverFactory: jest.Mocked = { + startServer: jest.fn(), + }; + const store: jest.Mocked = { + getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()), + setRepresentation: jest.fn(), + } as any; + const aclManager: jest.Mocked = { + getAclDocument: jest.fn(async(): Promise => ({ path: 'http://test.com/.acl' })), + } as any; + let setup: Setup; beforeEach(async(): Promise => { - store = { - setRepresentation: jest.fn(async(): Promise => undefined), - }; - aclManager = { - getAclDocument: jest.fn(async(): Promise => ({ path: 'http://test.com/.acl' })), - } as any; - serverFactory = { - startServer: jest.fn(), - }; setup = new Setup(serverFactory, store, aclManager, new VoidLoggerFactory(), 'http://localhost:3000/', 3000); }); + afterEach((): void => { + jest.clearAllMocks(); + }); + it('starts an HTTP server.', async(): Promise => { await setup.setup(); + expect(serverFactory.startServer).toHaveBeenCalledWith(3000); }); it('invokes ACL initialization.', async(): Promise => { await setup.setup(); + expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); + expect(store.getRepresentation).toHaveBeenCalledTimes(1); + expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {}); expect(store.setRepresentation).toHaveBeenCalledTimes(1); }); + + it('does not invoke ACL initialization when a root ACL already exists.', async(): Promise => { + store.getRepresentation.mockReturnValueOnce(Promise.resolve({ + data: { destroy: jest.fn() }, + } as any)); + + await setup.setup(); + + expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); + expect(store.getRepresentation).toHaveBeenCalledTimes(1); + expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {}); + expect(store.setRepresentation).toHaveBeenCalledTimes(0); + }); + + it('errors when the root ACL check errors.', async(): Promise => { + store.getRepresentation.mockRejectedValueOnce(new Error('Fatal')); + await expect(setup.setup()).rejects.toThrow('Fatal'); + }); });