diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts index 28fb4a3f3..f7e55fdf1 100644 --- a/src/init/AclInitializer.ts +++ b/src/init/AclInitializer.ts @@ -1,4 +1,4 @@ -import { promises as fsPromises } from 'fs'; +import { createReadStream } from 'fs'; import type { AclManager } from '../authorization/AclManager'; import { BasicRepresentation } from '../ldp/representation/BasicRepresentation'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; @@ -10,6 +10,7 @@ import { ensureTrailingSlash, joinFilePath } from '../util/PathUtil'; import { Initializer } from './Initializer'; const DEFAULT_ACL_PATH = joinFilePath(__dirname, '../../templates/root/.acl'); + /** * Ensures that a root ACL is present. */ @@ -33,21 +34,17 @@ export class AclInitializer extends Initializer { this.aclPath = settings.aclPath ?? DEFAULT_ACL_PATH; } - public async handle(): Promise { - const rootAcl = await this.aclManager.getAclDocument(this.root); - if (!await containsResource(this.store, rootAcl)) { - await this.setRootAclDocument(rootAcl); - } else { - this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`); - } - } - // Solid, ยง4.1: "The root container (pim:Storage) MUST have an ACL auxiliary resource directly associated to it. // The associated ACL document MUST include an authorization policy with acl:Control access privilege." // https://solid.github.io/specification/protocol#storage - protected async setRootAclDocument(rootAcl: ResourceIdentifier): Promise { - const acl = await fsPromises.readFile(this.aclPath, 'utf8'); - this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); - await this.store.setRepresentation(rootAcl, new BasicRepresentation(acl, rootAcl, TEXT_TURTLE)); + public async handle(): Promise { + const rootAcl = await this.aclManager.getAclDocument(this.root); + if (await containsResource(this.store, rootAcl)) { + this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`); + } else { + this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); + const aclDocument = createReadStream(this.aclPath, 'utf8'); + await this.store.setRepresentation(rootAcl, new BasicRepresentation(aclDocument, rootAcl, TEXT_TURTLE)); + } } } diff --git a/src/storage/conversion/ConstantConverter.ts b/src/storage/conversion/ConstantConverter.ts index 7ee4aecea..29010a284 100644 --- a/src/storage/conversion/ConstantConverter.ts +++ b/src/storage/conversion/ConstantConverter.ts @@ -1,4 +1,4 @@ -import { promises as fs } from 'fs'; +import { createReadStream } from 'fs'; import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation'; import type { Representation } from '../../ldp/representation/Representation'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; @@ -52,7 +52,7 @@ export class ConstantConverter extends RepresentationConverter { representation.data.destroy(); // Create a new representation from the constant file - const data = await fs.readFile(this.filePath, 'utf8'); + const data = createReadStream(this.filePath, 'utf8'); return new BasicRepresentation(data, representation.metadata, this.contentType); } } diff --git a/test/unit/init/AclInitializer.test.ts b/test/unit/init/AclInitializer.test.ts index 5b9d4d670..6554201b3 100644 --- a/test/unit/init/AclInitializer.test.ts +++ b/test/unit/init/AclInitializer.test.ts @@ -1,9 +1,12 @@ +import fs from 'fs'; import type { AclManager } from '../../../src/authorization/AclManager'; import { AclInitializer } from '../../../src/init/AclInitializer'; import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation'; -import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceStore } from '../../../src/storage/ResourceStore'; import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; +import { joinFilePath } from '../../../src/util/PathUtil'; + +const createReadStream = jest.spyOn(fs, 'createReadStream').mockReturnValue('file contents' as any); jest.mock('../../../src/ldp/representation/BasicRepresentation'); @@ -15,8 +18,9 @@ describe('AclInitializer', (): void => { getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()), setRepresentation: jest.fn(), } as any; + const aclIdentifier = { path: 'http://test.com/.acl' }; const aclManager: jest.Mocked = { - getAclDocument: jest.fn(async(): Promise => ({ path: 'http://test.com/.acl' })), + getAclDocument: jest.fn().mockResolvedValue(aclIdentifier), } as any; const baseUrl = 'http://localhost:3000/'; @@ -28,32 +32,32 @@ describe('AclInitializer', (): void => { const initializer = new AclInitializer({ baseUrl, store, aclManager }); await initializer.handle(); - expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); + expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl }); expect(store.getRepresentation).toHaveBeenCalledTimes(1); - expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {}); + expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {}); expect(store.setRepresentation).toHaveBeenCalledTimes(1); expect(store.setRepresentation).toHaveBeenCalledWith( { path: 'http://test.com/.acl' }, RepresentationMock.mock.instances[0], ); - expect(RepresentationMock).toHaveBeenCalledWith( - expect.stringMatching('<#authorization>'), { path: 'http://test.com/.acl' }, 'text/turtle', - ); + expect(createReadStream).toHaveBeenCalledTimes(1); + expect(createReadStream).toHaveBeenCalledWith(joinFilePath(__dirname, '../../../templates/root/.acl'), 'utf8'); + expect(RepresentationMock).toHaveBeenCalledWith('file contents', aclIdentifier, 'text/turtle'); }); it('sets the specific ACL when one was specified.', async(): Promise => { - const initializer = new AclInitializer({ baseUrl, store, aclManager, aclPath: __filename }); + const initializer = new AclInitializer({ baseUrl, store, aclManager, aclPath: '/path/doc.acl' }); await initializer.handle(); - expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); + expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl }); expect(store.getRepresentation).toHaveBeenCalledTimes(1); - expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {}); + expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {}); expect(store.setRepresentation).toHaveBeenCalledTimes(1); expect(store.setRepresentation).toHaveBeenCalledWith( { path: 'http://test.com/.acl' }, RepresentationMock.mock.instances[0], ); - expect(RepresentationMock).toHaveBeenCalledWith( - expect.stringMatching('Joachim'), { path: 'http://test.com/.acl' }, 'text/turtle', - ); + expect(createReadStream).toHaveBeenCalledTimes(1); + expect(createReadStream).toHaveBeenCalledWith('/path/doc.acl', 'utf8'); + expect(RepresentationMock).toHaveBeenCalledWith('file contents', aclIdentifier, 'text/turtle'); }); it('does not invoke ACL initialization when a root ACL already exists.', async(): Promise => { @@ -64,9 +68,9 @@ describe('AclInitializer', (): void => { const initializer = new AclInitializer({ baseUrl, store, aclManager }); await initializer.handle(); - expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); + expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl }); expect(store.getRepresentation).toHaveBeenCalledTimes(1); - expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {}); + expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {}); expect(store.setRepresentation).toHaveBeenCalledTimes(0); }); diff --git a/test/unit/storage/conversion/ConstantConverter.test.ts b/test/unit/storage/conversion/ConstantConverter.test.ts index f49f05fe6..f71585358 100644 --- a/test/unit/storage/conversion/ConstantConverter.test.ts +++ b/test/unit/storage/conversion/ConstantConverter.test.ts @@ -1,9 +1,9 @@ -import { promises as fs } from 'fs'; +import fs from 'fs'; import arrayifyStream from 'arrayify-stream'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { ConstantConverter } from '../../../../src/storage/conversion/ConstantConverter'; -const readFile = jest.spyOn(fs, 'readFile').mockResolvedValue('file contents'); +const createReadStream = jest.spyOn(fs, 'createReadStream').mockReturnValue('file contents' as any); describe('A ConstantConverter', (): void => { const identifier = { path: 'identifier' }; @@ -58,8 +58,8 @@ describe('A ConstantConverter', (): void => { expect(representation.data.destroy).toHaveBeenCalledTimes(1); - expect(readFile).toHaveBeenCalledTimes(1); - expect(readFile).toHaveBeenCalledWith('abc/def/index.html', 'utf8'); + expect(createReadStream).toHaveBeenCalledTimes(1); + expect(createReadStream).toHaveBeenCalledWith('abc/def/index.html', 'utf8'); expect(converted.metadata.contentType).toBe('text/html'); expect(await arrayifyStream(converted.data)).toEqual([ 'file contents' ]);