From 5126356c940bb12d9765bbd3571b6f1f6fa65cd0 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 26 Aug 2020 10:25:47 +0200 Subject: [PATCH] feat: Move runtime config into dedicated component, Closes #67 * Move runtime config into dedicated component, Closes #67 * Migrate FileResourceStore to RuntimeConfig --- bin/server.ts | 21 ++--- index.ts | 1 + src/init/RuntimeConfig.ts | 38 +++++++++ src/init/Setup.ts | 20 +++-- src/storage/FileResourceStore.ts | 21 +++-- src/storage/SimpleResourceStore.ts | 14 ++-- src/storage/UrlContainerManager.ts | 9 +- .../AuthenticatedLdpHandler.test.ts | 5 +- test/integration/Authorization.test.ts | 5 +- test/unit/init/RuntimeConfig.test.ts | 43 ++++++++++ test/unit/init/Setup.test.ts | 7 +- test/unit/storage/FileResourceStore.test.ts | 82 ++++++++++--------- test/unit/storage/SimpleResourceStore.test.ts | 3 +- test/unit/storage/UrlContainerManager.test.ts | 9 +- 14 files changed, 194 insertions(+), 84 deletions(-) create mode 100644 src/init/RuntimeConfig.ts create mode 100644 test/unit/init/RuntimeConfig.test.ts diff --git a/bin/server.ts b/bin/server.ts index acfaeaeb1f..042b71779a 100644 --- a/bin/server.ts +++ b/bin/server.ts @@ -11,6 +11,7 @@ import { QuadToTurtleConverter, Representation, RepresentationConvertingStore, + RuntimeConfig, Setup, SimpleAclAuthorizer, SimpleBodyParser, @@ -36,15 +37,13 @@ import { const { argv } = yargs .usage('node ./bin/server.js [args]') .options({ - port: { type: 'number', alias: 'p', default: 3000 }, + port: { type: 'number', alias: 'p' }, }) .help(); -const { port } = argv; - -const base = `http://localhost:${port}/`; - // This is instead of the dependency injection that still needs to be added +const runtimeConfig = new RuntimeConfig(); + const bodyParser = new CompositeAsyncHandler([ new SimpleSparqlUpdateBodyParser(), new SimpleBodyParser(), @@ -62,7 +61,7 @@ const permissionsExtractor = new CompositeAsyncHandler([ ]); // Will have to see how to best handle this -const store = new SimpleResourceStore(base); +const store = new SimpleResourceStore(runtimeConfig); const converter = new CompositeAsyncHandler([ new TurtleToQuadConverter(), new QuadToTurtleConverter(), @@ -73,7 +72,7 @@ const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker); const patchingStore = new PatchingStore(convertingStore, patcher); const aclManager = new SimpleExtensionAclManager(); -const containerManager = new UrlContainerManager(base); +const containerManager = new UrlContainerManager(runtimeConfig); const authorizer = new SimpleAclAuthorizer(aclManager, containerManager, patchingStore); const operationHandler = new CompositeAsyncHandler([ @@ -97,9 +96,11 @@ const httpHandler = new AuthenticatedLdpHandler({ const httpServer = new ExpressHttpServer(httpHandler); -const setup = new Setup(httpServer, store, aclManager); -setup.setup(port, base).then((): void => { - process.stdout.write(`Running at ${base}\n`); +const setup = new Setup(httpServer, store, aclManager, runtimeConfig); + +runtimeConfig.reset({ port: argv.port }); +setup.setup().then((): void => { + process.stdout.write(`Running at ${runtimeConfig.base}\n`); }).catch((error): void => { process.stderr.write(`${error}\n`); process.exit(1); diff --git a/index.ts b/index.ts index b1cdcb5f4c..5425865dd6 100644 --- a/index.ts +++ b/index.ts @@ -11,6 +11,7 @@ export * from './src/authorization/SimpleAuthorizer'; export * from './src/authorization/SimpleExtensionAclManager'; // Init +export * from './src/init/RuntimeConfig'; export * from './src/init/Setup'; // LDP/HTTP diff --git a/src/init/RuntimeConfig.ts b/src/init/RuntimeConfig.ts new file mode 100644 index 0000000000..d0d4d572cf --- /dev/null +++ b/src/init/RuntimeConfig.ts @@ -0,0 +1,38 @@ +/** + * This class holds all configuration options that can be defined by the user via the command line. + * + * Concretely, this contains data that is only relevant *after* dependency injection. + */ +export class RuntimeConfig implements RuntimeConfigData { + private pport!: number; + private pbase!: string; + private prootFilepath!: string; + + public constructor(data: RuntimeConfigData = {}) { + this.reset(data); + } + + public reset(data: RuntimeConfigData): void { + this.pport = data.port ?? 3000; + this.pbase = data.base ?? `http://localhost:${this.port}/`; + this.prootFilepath = data.rootFilepath ?? process.cwd(); + } + + public get base(): string { + return this.pbase; + } + + public get port(): number { + return this.pport; + } + + public get rootFilepath(): string { + return this.prootFilepath; + } +} + +export interface RuntimeConfigData { + port?: number; + base?: string; + rootFilepath?: string; +} diff --git a/src/init/Setup.ts b/src/init/Setup.ts index 9aa3d9a18c..8a45bbfb0c 100644 --- a/src/init/Setup.ts +++ b/src/init/Setup.ts @@ -2,6 +2,7 @@ import { AclManager } from '../authorization/AclManager'; import { DATA_TYPE_BINARY } from '../util/ContentTypes'; import { ExpressHttpServer } from '../server/ExpressHttpServer'; import { ResourceStore } from '../storage/ResourceStore'; +import { RuntimeConfig } from './RuntimeConfig'; import streamifyArray from 'streamify-array'; /** @@ -11,11 +12,18 @@ export class Setup { private readonly httpServer: ExpressHttpServer; private readonly store: ResourceStore; private readonly aclManager: AclManager; + private readonly runtimeConfig: RuntimeConfig; - public constructor(httpServer: ExpressHttpServer, store: ResourceStore, aclManager: AclManager) { + public constructor( + httpServer: ExpressHttpServer, + store: ResourceStore, + aclManager: AclManager, + runtimeConfig: RuntimeConfig, + ) { this.httpServer = httpServer; this.store = store; this.aclManager = aclManager; + this.runtimeConfig = runtimeConfig; } /** @@ -23,7 +31,7 @@ export class Setup { * @param port - A port number. * @param base - A base URL. */ - public async setup(port: number, base: string): Promise { + public async setup(): Promise { // 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 => { @@ -38,10 +46,10 @@ export class Setup { acl:mode acl:Append; acl:mode acl:Delete; acl:mode acl:Control; - acl:accessTo <${base}>; - acl:default <${base}>.`; + acl:accessTo <${this.runtimeConfig.base}>; + acl:default <${this.runtimeConfig.base}>.`; await this.store.setRepresentation( - await this.aclManager.getAcl({ path: base }), + await this.aclManager.getAcl({ path: this.runtimeConfig.base }), { dataType: DATA_TYPE_BINARY, data: streamifyArray([ acl ]), @@ -56,6 +64,6 @@ export class Setup { await aclSetup(); - this.httpServer.listen(port); + this.httpServer.listen(this.runtimeConfig.port); } } diff --git a/src/storage/FileResourceStore.ts b/src/storage/FileResourceStore.ts index 4df3b8543d..d04cef932a 100644 --- a/src/storage/FileResourceStore.ts +++ b/src/storage/FileResourceStore.ts @@ -12,6 +12,7 @@ import { Representation } from '../ldp/representation/Representation'; import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import { ResourceStore } from './ResourceStore'; +import { RuntimeConfig } from '../init/RuntimeConfig'; import streamifyArray from 'streamify-array'; import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError'; import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY, DATA_TYPE_QUAD } from '../util/ContentTypes'; @@ -25,26 +26,30 @@ const { extname, join: joinPath, normalize: normalizePath } = posix; * All requests will throw an {@link NotFoundHttpError} if unknown identifiers get passed. */ export class FileResourceStore implements ResourceStore { - private readonly baseRequestURI: string; - private readonly rootFilepath: string; + private readonly runtimeConfig: RuntimeConfig; private readonly interactionController: InteractionController; private readonly metadataController: MetadataController; /** - * @param baseRequestURI - Will be stripped of all incoming URIs and added to all outgoing ones to find the relative - * path. - * @param rootFilepath - Root filepath in which the resources and containers will be saved as files and directories. + * @param runtimeConfig - The runtime config. * @param interactionController - Instance of InteractionController to use. * @param metadataController - Instance of MetadataController to use. */ - public constructor(baseRequestURI: string, rootFilepath: string, interactionController: InteractionController, + public constructor(runtimeConfig: RuntimeConfig, interactionController: InteractionController, metadataController: MetadataController) { - this.baseRequestURI = trimTrailingSlashes(baseRequestURI); - this.rootFilepath = trimTrailingSlashes(rootFilepath); + this.runtimeConfig = runtimeConfig; this.interactionController = interactionController; this.metadataController = metadataController; } + public get baseRequestURI(): string { + return trimTrailingSlashes(this.runtimeConfig.base); + } + + public get rootFilepath(): string { + return trimTrailingSlashes(this.runtimeConfig.rootFilepath); + } + /** * Store the incoming data as a file under a file path corresponding to `container.path`, * where slashes correspond to subdirectories. diff --git a/src/storage/SimpleResourceStore.ts b/src/storage/SimpleResourceStore.ts index b7fa1d0e45..2fae99a55e 100644 --- a/src/storage/SimpleResourceStore.ts +++ b/src/storage/SimpleResourceStore.ts @@ -5,6 +5,7 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { Representation } from '../ldp/representation/Representation'; import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import { ResourceStore } from './ResourceStore'; +import { RuntimeConfig } from '../init/RuntimeConfig'; import streamifyArray from 'streamify-array'; /** @@ -13,14 +14,15 @@ import streamifyArray from 'streamify-array'; */ export class SimpleResourceStore implements ResourceStore { private readonly store: { [id: string]: Representation }; - private readonly base: string; + private readonly runtimeConfig: RuntimeConfig; private index = 0; /** - * @param base - Will be stripped of all incoming URIs and added to all outgoing ones to find the relative path. + * @param runtimeConfig - Config containing base that will be stripped of all incoming URIs + * and added to all outgoing ones to find the relative path. */ - public constructor(base: string) { - this.base = base; + public constructor(runtimeConfig: RuntimeConfig) { + this.runtimeConfig = runtimeConfig; this.store = { // Default root entry (what you get when the identifier is equal to the base) @@ -102,8 +104,8 @@ export class SimpleResourceStore implements ResourceStore { * @returns A string representing the relative path. */ private parseIdentifier(identifier: ResourceIdentifier): string { - const path = identifier.path.slice(this.base.length); - if (!identifier.path.startsWith(this.base)) { + const path = identifier.path.slice(this.runtimeConfig.base.length); + if (!identifier.path.startsWith(this.runtimeConfig.base)) { throw new NotFoundHttpError(); } return path; diff --git a/src/storage/UrlContainerManager.ts b/src/storage/UrlContainerManager.ts index e6ce845ea2..07a1435f12 100644 --- a/src/storage/UrlContainerManager.ts +++ b/src/storage/UrlContainerManager.ts @@ -1,20 +1,21 @@ import { ContainerManager } from './ContainerManager'; import { ensureTrailingSlash } from '../util/Util'; import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; +import { RuntimeConfig } from '../init/RuntimeConfig'; /** * Determines containers based on URL decomposition. */ export class UrlContainerManager implements ContainerManager { - private readonly root: string; + private readonly runtimeConfig: RuntimeConfig; - public constructor(root: string) { - this.root = this.canonicalUrl(root); + public constructor(runtimeConfig: RuntimeConfig) { + this.runtimeConfig = runtimeConfig; } public async getContainer(id: ResourceIdentifier): Promise { const path = this.canonicalUrl(id.path); - if (this.root === path) { + if (this.canonicalUrl(this.runtimeConfig.base) === path) { throw new Error('Root does not have a container.'); } diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts index d5434ad9ed..41238e1fc6 100644 --- a/test/integration/AuthenticatedLdpHandler.test.ts +++ b/test/integration/AuthenticatedLdpHandler.test.ts @@ -13,6 +13,7 @@ import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtle import { Representation } from '../../src/ldp/representation/Representation'; import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore'; import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription'; +import { RuntimeConfig } from '../../src/init/RuntimeConfig'; import { SimpleAuthorizer } from '../../src/authorization/SimpleAuthorizer'; import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser'; import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor'; @@ -44,7 +45,7 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { const permissionsExtractor = new BasePermissionsExtractor(); const authorizer = new SimpleAuthorizer(); - const store = new SimpleResourceStore('http://test.com/'); + const store = new SimpleResourceStore(new RuntimeConfig({ base: 'http://test.com/' })); const operationHandler = new CompositeAsyncHandler([ new SimpleGetOperationHandler(store), new SimplePostOperationHandler(store), @@ -115,7 +116,7 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { ]); const authorizer = new SimpleAuthorizer(); - const store = new SimpleResourceStore('http://test.com/'); + const store = new SimpleResourceStore(new RuntimeConfig({ base: 'http://test.com/' })); const converter = new CompositeAsyncHandler([ new QuadToTurtleConverter(), new TurtleToQuadConverter(), diff --git a/test/integration/Authorization.test.ts b/test/integration/Authorization.test.ts index d47df48a52..aa3dd809b2 100644 --- a/test/integration/Authorization.test.ts +++ b/test/integration/Authorization.test.ts @@ -12,6 +12,7 @@ import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtle import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore'; import { ResourceStore } from '../../src/storage/ResourceStore'; import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription'; +import { RuntimeConfig } from '../../src/init/RuntimeConfig'; import { SimpleAclAuthorizer } from '../../src/authorization/SimpleAclAuthorizer'; import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser'; import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor'; @@ -80,7 +81,7 @@ describe('A server with authorization', (): void => { bodyParser, }); - const store = new SimpleResourceStore('http://test.com/'); + const store = new SimpleResourceStore(new RuntimeConfig({ base: 'http://test.com/' })); const converter = new CompositeAsyncHandler([ new QuadToTurtleConverter(), new TurtleToQuadConverter(), @@ -91,7 +92,7 @@ describe('A server with authorization', (): void => { const permissionsExtractor = new BasePermissionsExtractor(); const authorizer = new SimpleAclAuthorizer( new SimpleExtensionAclManager(), - new UrlContainerManager('http://test.com/'), + new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/' })), convertingStore, ); diff --git a/test/unit/init/RuntimeConfig.test.ts b/test/unit/init/RuntimeConfig.test.ts new file mode 100644 index 0000000000..36a6096746 --- /dev/null +++ b/test/unit/init/RuntimeConfig.test.ts @@ -0,0 +1,43 @@ +import { RuntimeConfig } from '../../../src/init/RuntimeConfig'; + +describe('RuntimeConfig', (): void => { + it('handles undefined args.', async(): Promise => { + const config = new RuntimeConfig(); + expect(config.port).toEqual(3000); + expect(config.base).toEqual('http://localhost:3000/'); + }); + + it('handles empty args.', async(): Promise => { + const config = new RuntimeConfig({}); + expect(config.port).toEqual(3000); + expect(config.base).toEqual('http://localhost:3000/'); + }); + + it('handles args with port.', async(): Promise => { + const config = new RuntimeConfig({ port: 1234 }); + expect(config.port).toEqual(1234); + expect(config.base).toEqual('http://localhost:1234/'); + }); + + it('handles args with base.', async(): Promise => { + const config = new RuntimeConfig({ base: 'http://example.org/' }); + expect(config.port).toEqual(3000); + expect(config.base).toEqual('http://example.org/'); + }); + + it('handles args with port and base.', async(): Promise => { + const config = new RuntimeConfig({ port: 1234, base: 'http://example.org/' }); + expect(config.port).toEqual(1234); + expect(config.base).toEqual('http://example.org/'); + }); + + it('handles resetting data.', async(): Promise => { + const config = new RuntimeConfig({}); + expect(config.port).toEqual(3000); + expect(config.base).toEqual('http://localhost:3000/'); + + config.reset({ port: 1234, base: 'http://example.org/' }); + expect(config.port).toEqual(1234); + expect(config.base).toEqual('http://example.org/'); + }); +}); diff --git a/test/unit/init/Setup.test.ts b/test/unit/init/Setup.test.ts index 230c70901b..1a9311c8e2 100644 --- a/test/unit/init/Setup.test.ts +++ b/test/unit/init/Setup.test.ts @@ -1,3 +1,4 @@ +import { RuntimeConfig } from '../../../src/init/RuntimeConfig'; import { Setup } from '../../../src/init/Setup'; describe('Setup', (): void => { @@ -15,16 +16,16 @@ describe('Setup', (): void => { httpServer = { listen: jest.fn(), }; - setup = new Setup(httpServer, store, aclManager); + setup = new Setup(httpServer, store, aclManager, new RuntimeConfig()); }); it('starts an HTTP server.', async(): Promise => { - await setup.setup(3000, 'http://localhost:3000/'); + await setup.setup(); expect(httpServer.listen).toHaveBeenCalledWith(3000); }); it('invokes ACL initialization.', async(): Promise => { - await setup.setup(3000, 'http://localhost:3000/'); + await setup.setup(); expect(aclManager.getAcl).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); expect(store.setRepresentation).toHaveBeenCalledTimes(1); }); diff --git a/test/unit/storage/FileResourceStore.test.ts b/test/unit/storage/FileResourceStore.test.ts index d88128cbe5..db1b380ed9 100644 --- a/test/unit/storage/FileResourceStore.test.ts +++ b/test/unit/storage/FileResourceStore.test.ts @@ -10,6 +10,7 @@ import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; import { posix } from 'path'; import { Readable } from 'stream'; import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata'; +import { RuntimeConfig } from '../../../src/init/RuntimeConfig'; import streamifyArray from 'streamify-array'; import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError'; import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY, DATA_TYPE_QUAD } from '../../../src/util/ContentTypes'; @@ -21,7 +22,7 @@ import { literal, namedNode, quad as quadRDF, triple } from '@rdfjs/data-model'; const { join: joinPath } = posix; const base = 'http://test.com/'; -const root = '/Users/default/home/public/'; +const rootFilepath = '/Users/default/home/public/'; fsPromises.rmdir = jest.fn(); fsPromises.lstat = jest.fn(); @@ -48,7 +49,11 @@ describe('A FileResourceStore', (): void => { beforeEach(async(): Promise => { jest.clearAllMocks(); - store = new FileResourceStore(base, root, new InteractionController(), new MetadataController()); + store = new FileResourceStore( + new RuntimeConfig({ base, rootFilepath }), + new InteractionController(), + new MetadataController(), + ); representation = { data: streamifyArray([ rawData ]), @@ -131,7 +136,7 @@ describe('A FileResourceStore', (): void => { // Write container (POST) representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []}; const identifier = await store.addResource({ path: base }, representation); - expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'myContainer/'), { recursive: true }); + expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'myContainer/'), { recursive: true }); expect(identifier.path).toBe(`${base}myContainer/`); // Read container @@ -155,7 +160,7 @@ describe('A FileResourceStore', (): void => { // Tests representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []}; await expect(store.addResource({ path: `${base}foo` }, representation)).rejects.toThrow(MethodNotAllowedHttpError); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo')); }); it('errors 405 for POST invalid path ending without slash.', async(): Promise => { @@ -172,17 +177,17 @@ describe('A FileResourceStore', (): void => { representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []}; await expect(store.addResource({ path: `${base}doesnotexist` }, representation)) .rejects.toThrow(MethodNotAllowedHttpError); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'doesnotexist')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexist')); representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: []}; await expect(store.addResource({ path: `${base}doesnotexist` }, representation)) .rejects.toThrow(MethodNotAllowedHttpError); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'doesnotexist')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexist')); representation.metadata = { linkRel: { type: new Set() }, slug: 'file.txt', raw: []}; await expect(store.addResource({ path: `${base}existingresource` }, representation)) .rejects.toThrow(MethodNotAllowedHttpError); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'existingresource')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'existingresource')); }); it('can set data.', async(): Promise => { @@ -204,7 +209,7 @@ describe('A FileResourceStore', (): void => { // Tests await store.setRepresentation({ path: `${base}file.txt` }, representation); - expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt')); + expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); const result = await store.getRepresentation({ path: `${base}file.txt` }); expect(result).toEqual({ dataType: DATA_TYPE_BINARY, @@ -217,9 +222,9 @@ describe('A FileResourceStore', (): void => { }, }); await expect(arrayifyStream(result.data)).resolves.toEqual([ rawData ]); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt')); - expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt')); - expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt.metadata')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); + expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); + expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata')); }); it('can delete data.', async(): Promise => { @@ -239,7 +244,7 @@ describe('A FileResourceStore', (): void => { // Tests await store.deleteResource({ path: `${base}file.txt` }); - expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt')); + expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); await expect(store.getRepresentation({ path: `${base}file.txt` })).rejects.toThrow(NotFoundHttpError); }); @@ -253,8 +258,9 @@ describe('A FileResourceStore', (): void => { representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: []}; const identifier = await store.addResource({ path: `${base}doesnotexistyet/` }, representation); expect(identifier.path).toBe(`${base}doesnotexistyet/file.txt`); - expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'doesnotexistyet/'), { recursive: true }); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'doesnotexistyet/')); + expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexistyet/'), + { recursive: true }); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexistyet/')); }); it('creates metadata file when metadata triples are passed.', async(): Promise => { @@ -277,8 +283,8 @@ describe('A FileResourceStore', (): void => { representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: [ quad ]}; representation.data = readableMock; await store.addResource({ path: `${base}foo/` }, representation); - expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/'), { recursive: true }); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo/')); + expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'), { recursive: true }); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: [ quad ]}; await store.setRepresentation({ path: `${base}foo/file.txt` }, representation); @@ -298,8 +304,8 @@ describe('A FileResourceStore', (): void => { // Tests await expect(store.deleteResource({ path: `${base}notempty/` })).rejects.toThrow(ConflictHttpError); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'notempty/')); - expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(root, 'notempty/')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'notempty/')); + expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'notempty/')); }); it('deletes metadata file when deleting container.', async(): Promise => { @@ -312,10 +318,10 @@ describe('A FileResourceStore', (): void => { // Tests await store.deleteResource({ path: `${base}foo/` }); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo/')); - expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/')); - expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(root, 'foo', '.metadata')); - expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); + expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); + expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', '.metadata')); + expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); }); it('errors 404 when accessing non resource (file/directory), e.g. special files.', async(): Promise => { @@ -368,10 +374,10 @@ describe('A FileResourceStore', (): void => { }, }); await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray(quads); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo/')); - expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/')); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo', 'file.txt')); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo', '.nonresource')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); + expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', 'file.txt')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', '.nonresource')); }); it('can overwrite representation with PUT.', async(): Promise => { @@ -387,8 +393,8 @@ describe('A FileResourceStore', (): void => { representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: []}; await store.setRepresentation({ path: `${base}alreadyexists.txt` }, representation); expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(1); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'alreadyexists.txt')); - expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(root, { recursive: true }); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists.txt')); + expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(rootFilepath, { recursive: true }); }); it('errors when overwriting container with PUT.', async(): Promise => { @@ -399,7 +405,7 @@ describe('A FileResourceStore', (): void => { // Tests await expect(store.setRepresentation({ path: `${base}alreadyexists` }, representation)).rejects .toThrow(ConflictHttpError); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'alreadyexists')); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists')); representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, raw: []}; await expect(store.setRepresentation({ path: `${base}alreadyexists/` }, representation)).rejects .toThrow(ConflictHttpError); @@ -445,9 +451,9 @@ describe('A FileResourceStore', (): void => { // Tests representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: [ quad ]}; await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error); - expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt.metadata')); - expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt')); - expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt.metadata')); + expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata')); + expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); + expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata')); }); it('undoes container creation when metadata file creation fails.', async(): Promise => { @@ -461,7 +467,7 @@ describe('A FileResourceStore', (): void => { // Tests representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'foo/', raw: [ quad ]}; await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error); - expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo/')); + expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); }); it('creates container when POSTing without linkRel and with slug ending with slash.', async(): Promise => { @@ -474,7 +480,7 @@ describe('A FileResourceStore', (): void => { const identifier = await store.addResource({ path: base }, representation); expect(identifier.path).toBe(`${base}myContainer/`); expect(fsPromises.mkdir as jest.Mock).toBeCalledTimes(1); - expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'myContainer/'), { recursive: true }); + expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'myContainer/'), { recursive: true }); }); it('returns no contentType when unknown for representation.', async(): Promise => { @@ -514,9 +520,9 @@ describe('A FileResourceStore', (): void => { // Tests representation.metadata = { raw: []}; await store.setRepresentation({ path: `${base}file.txt` }, representation); - expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(root, { recursive: true }); + expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(rootFilepath, { recursive: true }); expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(1); - expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(root, 'file.txt')); + expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); }); it('creates container when POST to existing container path ending without slash and slug without slash.', @@ -530,7 +536,7 @@ describe('A FileResourceStore', (): void => { representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'bar', raw: []}; const identifier = await store.addResource({ path: `${base}foo` }, representation); expect(identifier.path).toBe(`${base}foo/bar/`); - expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(root, 'foo')); - expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(root, 'foo', 'bar/'), { recursive: false }); + expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo')); + expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', 'bar/'), { recursive: false }); }); }); diff --git a/test/unit/storage/SimpleResourceStore.test.ts b/test/unit/storage/SimpleResourceStore.test.ts index c63040c427..546337d7b7 100644 --- a/test/unit/storage/SimpleResourceStore.test.ts +++ b/test/unit/storage/SimpleResourceStore.test.ts @@ -4,6 +4,7 @@ import { DATA_TYPE_BINARY } from '../../../src/util/ContentTypes'; import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; import { Readable } from 'stream'; import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata'; +import { RuntimeConfig } from '../../../src/init/RuntimeConfig'; import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore'; import streamifyArray from 'streamify-array'; @@ -15,7 +16,7 @@ describe('A SimpleResourceStore', (): void => { const dataString = ' .'; beforeEach(async(): Promise => { - store = new SimpleResourceStore(base); + store = new SimpleResourceStore(new RuntimeConfig({ base })); representation = { data: streamifyArray([ dataString ]), diff --git a/test/unit/storage/UrlContainerManager.test.ts b/test/unit/storage/UrlContainerManager.test.ts index a6c7860719..4df65bf8f5 100644 --- a/test/unit/storage/UrlContainerManager.test.ts +++ b/test/unit/storage/UrlContainerManager.test.ts @@ -1,8 +1,9 @@ +import { RuntimeConfig } from '../../../src/init/RuntimeConfig'; import { UrlContainerManager } from '../../../src/storage/UrlContainerManager'; describe('An UrlContainerManager', (): void => { it('returns the parent URl for a single call.', async(): Promise => { - const manager = new UrlContainerManager('http://test.com/foo/'); + const manager = new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/foo/' })); await expect(manager.getContainer({ path: 'http://test.com/foo/bar' })) .resolves.toEqual({ path: 'http://test.com/foo/' }); await expect(manager.getContainer({ path: 'http://test.com/foo/bar/' })) @@ -10,13 +11,13 @@ describe('An UrlContainerManager', (): void => { }); it('errors when getting the container of root.', async(): Promise => { - let manager = new UrlContainerManager('http://test.com/foo/'); + let manager = new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/foo/' })); await expect(manager.getContainer({ path: 'http://test.com/foo/' })) .rejects.toThrow('Root does not have a container.'); await expect(manager.getContainer({ path: 'http://test.com/foo' })) .rejects.toThrow('Root does not have a container.'); - manager = new UrlContainerManager('http://test.com/foo'); + manager = new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/foo' })); await expect(manager.getContainer({ path: 'http://test.com/foo/' })) .rejects.toThrow('Root does not have a container.'); await expect(manager.getContainer({ path: 'http://test.com/foo' })) @@ -24,7 +25,7 @@ describe('An UrlContainerManager', (): void => { }); it('errors when the root of an URl is reached that does not match the input root.', async(): Promise => { - const manager = new UrlContainerManager('http://test.com/foo/'); + const manager = new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/foo/' })); await expect(manager.getContainer({ path: 'http://test.com/' })) .rejects.toThrow('URL root reached.'); });