From 9b26bbef2d2c26402bf01fbe04f85b08a8ec8be9 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Wed, 30 Sep 2020 12:07:31 +0200 Subject: [PATCH] feat: Add DataAccessorBasedStore integration --- index.ts | 9 +++ ...uthenticatedFileBasedDataAccessorConfig.ts | 64 +++++++++++++++++++ test/configs/FileBasedDataAccessorConfig.ts | 62 ++++++++++++++++++ test/configs/ServerConfig.ts | 2 + test/configs/Util.ts | 19 +++++- ...ts => AuthenticatedFileBasedStore.test.ts} | 64 ++++++++++--------- ...ceStore.test.ts => FileBasedStore.test.ts} | 33 ++++++---- test/util/TestHelpers.ts | 10 +-- 8 files changed, 218 insertions(+), 45 deletions(-) create mode 100644 test/configs/AuthenticatedFileBasedDataAccessorConfig.ts create mode 100644 test/configs/FileBasedDataAccessorConfig.ts rename test/integration/{AuthenticatedFileResourceStore.test.ts => AuthenticatedFileBasedStore.test.ts} (58%) rename test/integration/{FileResourceStore.test.ts => FileBasedStore.test.ts} (86%) diff --git a/index.ts b/index.ts index beca627f1..e7482de31 100644 --- a/index.ts +++ b/index.ts @@ -80,6 +80,10 @@ export * from './src/server/HttpHandler'; export * from './src/server/HttpRequest'; export * from './src/server/HttpResponse'; +// Storage/Accessors +export * from './src/storage/accessors/DataAccessor'; +export * from './src/storage/accessors/FileDataAccessor'; + // Storage/Conversion export * from './src/storage/conversion/ChainedConverter'; export * from './src/storage/conversion/QuadToRdfConverter'; @@ -95,6 +99,8 @@ export * from './src/storage/patch/SparqlUpdatePatchHandler'; export * from './src/storage/AtomicResourceStore'; export * from './src/storage/Conditions'; export * from './src/storage/ContainerManager'; +export * from './src/storage/DataAccessorBasedStore'; +export * from './src/storage/ExtensionBasedMapper'; export * from './src/storage/FileResourceStore'; export * from './src/storage/InMemoryResourceStore'; export * from './src/storage/Lock'; @@ -109,9 +115,12 @@ export * from './src/storage/SingleThreadedResourceLocker'; export * from './src/storage/UrlContainerManager'; // Util/Errors +export * from './src/util/errors/ConflictHttpError'; export * from './src/util/errors/ForbiddenHttpError'; export * from './src/util/errors/HttpError'; +export * from './src/util/errors/MethodNotAllowedHttpError'; export * from './src/util/errors/NotFoundHttpError'; +export * from './src/util/errors/SystemError'; export * from './src/util/errors/UnauthorizedHttpError'; export * from './src/util/errors/UnsupportedHttpError'; export * from './src/util/errors/UnsupportedMediaTypeHttpError'; diff --git a/test/configs/AuthenticatedFileBasedDataAccessorConfig.ts b/test/configs/AuthenticatedFileBasedDataAccessorConfig.ts new file mode 100644 index 000000000..c3ea60929 --- /dev/null +++ b/test/configs/AuthenticatedFileBasedDataAccessorConfig.ts @@ -0,0 +1,64 @@ +import type { HttpHandler, + ResourceStore } from '../../index'; +import { + AuthenticatedLdpHandler, + BasicResponseWriter, + CompositeAsyncHandler, + MethodPermissionsExtractor, + RdfToQuadConverter, + UnsecureWebIdExtractor, + QuadToRdfConverter, +} from '../../index'; +import type { ServerConfig } from './ServerConfig'; +import { + getConvertingStore, + getBasicRequestParser, + getOperationHandler, + getWebAclAuthorizer, + getFileDataAccessorStore, +} from './Util'; + +/** + * AuthenticatedFileResourceStoreConfig works with + * - a WebAclAuthorizer + * - a FileResourceStore wrapped in a converting store (rdf to quad & quad to rdf) + * - GET, POST, PUT & DELETE operation handlers + */ +export class AuthenticatedFileBasedDataAccessorConfig implements ServerConfig { + public base: string; + public store: ResourceStore; + + public constructor(base: string, rootFilepath: string) { + this.base = base; + this.store = getConvertingStore( + getFileDataAccessorStore(base, rootFilepath), + [ new QuadToRdfConverter(), + new RdfToQuadConverter() ], + ); + } + + public getHttpHandler(): HttpHandler { + const requestParser = getBasicRequestParser(); + + const credentialsExtractor = new UnsecureWebIdExtractor(); + const permissionsExtractor = new CompositeAsyncHandler([ + new MethodPermissionsExtractor(), + ]); + + const operationHandler = getOperationHandler(this.store); + + const responseWriter = new BasicResponseWriter(); + const authorizer = getWebAclAuthorizer(this.store, this.base); + + const handler = new AuthenticatedLdpHandler({ + requestParser, + credentialsExtractor, + permissionsExtractor, + authorizer, + operationHandler, + responseWriter, + }); + + return handler; + } +} diff --git a/test/configs/FileBasedDataAccessorConfig.ts b/test/configs/FileBasedDataAccessorConfig.ts new file mode 100644 index 000000000..ff0a61bfa --- /dev/null +++ b/test/configs/FileBasedDataAccessorConfig.ts @@ -0,0 +1,62 @@ +import type { HttpHandler, + ResourceStore } from '../../index'; +import { + AllowEverythingAuthorizer, + AuthenticatedLdpHandler, + BasicResponseWriter, + CompositeAsyncHandler, + MethodPermissionsExtractor, + QuadToRdfConverter, + RawBodyParser, + RdfToQuadConverter, + UnsecureWebIdExtractor, +} from '../../index'; +import type { ServerConfig } from './ServerConfig'; +import { + getOperationHandler, + getConvertingStore, + getBasicRequestParser, + getFileDataAccessorStore, +} from './Util'; + +/** + * FileBasedDataAccessorConfig works with + * - an AllowEverythingAuthorizer (no acl) + * - a DataAccessorBasedStore with a FileDataAccessor wrapped in a converting store (rdf to quad & quad to rdf) + * - GET, POST, PUT & DELETE operation handlers + */ +export class FileBasedDataAccessorConfig implements ServerConfig { + public store: ResourceStore; + + public constructor(base: string, rootFilepath: string) { + this.store = getConvertingStore( + getFileDataAccessorStore(base, rootFilepath), + [ new QuadToRdfConverter(), new RdfToQuadConverter() ], + ); + } + + public getHttpHandler(): HttpHandler { + // This is for the sake of test coverage, as it could also be just getBasicRequestParser() + const requestParser = getBasicRequestParser([ new RawBodyParser() ]); + + const credentialsExtractor = new UnsecureWebIdExtractor(); + const permissionsExtractor = new CompositeAsyncHandler([ + new MethodPermissionsExtractor(), + ]); + const authorizer = new AllowEverythingAuthorizer(); + + const operationHandler = getOperationHandler(this.store); + const responseWriter = new BasicResponseWriter(); + + const handler = new AuthenticatedLdpHandler({ + requestParser, + credentialsExtractor, + permissionsExtractor, + authorizer, + operationHandler, + responseWriter, + }); + + return handler; + } +} diff --git a/test/configs/ServerConfig.ts b/test/configs/ServerConfig.ts index f9dc5c3b1..13081c77c 100644 --- a/test/configs/ServerConfig.ts +++ b/test/configs/ServerConfig.ts @@ -1,5 +1,7 @@ import type { HttpHandler } from '../../src/server/HttpHandler'; +import type { ResourceStore } from '../../src/storage/ResourceStore'; export interface ServerConfig { + store: ResourceStore; getHttpHandler: () => HttpHandler; } diff --git a/test/configs/Util.ts b/test/configs/Util.ts index bee534881..5c92073cc 100644 --- a/test/configs/Util.ts +++ b/test/configs/Util.ts @@ -11,7 +11,10 @@ import { BasicTargetExtractor, CompositeAsyncHandler, ContentTypeParser, + DataAccessorBasedStore, DeleteOperationHandler, + ExtensionBasedMapper, + FileDataAccessor, FileResourceStore, GetOperationHandler, HeadOperationHandler, @@ -32,7 +35,6 @@ import { UrlContainerManager, WebAclAuthorizer, } from '../../index'; -import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper'; export const BASE = 'http://test.com'; @@ -56,6 +58,21 @@ export const getFileResourceStore = (base: string, rootFilepath: string): FileRe new MetadataController(), ); +/** + * Gives a file data accessor store based on (default) runtime config. + * @param base - Base URL. + * @param rootFilepath - The root file path. + * + * @returns The data accessor based store. + */ +export const getFileDataAccessorStore = (base: string, rootFilepath: string): DataAccessorBasedStore => + new DataAccessorBasedStore( + new FileDataAccessor(new ExtensionBasedMapper(base, rootFilepath), new MetadataController()), + base, + new MetadataController(), + new UrlContainerManager(base), + ); + /** * Gives an in memory resource store based on (default) base url. * @param base - Optional base parameter for the run time config. diff --git a/test/integration/AuthenticatedFileResourceStore.test.ts b/test/integration/AuthenticatedFileBasedStore.test.ts similarity index 58% rename from test/integration/AuthenticatedFileResourceStore.test.ts rename to test/integration/AuthenticatedFileBasedStore.test.ts index ae008acd8..b16567ebf 100644 --- a/test/integration/AuthenticatedFileResourceStore.test.ts +++ b/test/integration/AuthenticatedFileBasedStore.test.ts @@ -1,39 +1,45 @@ import { copyFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import * as rimraf from 'rimraf'; -import type { HttpHandler, ResourceStore } from '../../index'; import { ensureTrailingSlash } from '../../src/util/Util'; +import { AuthenticatedFileBasedDataAccessorConfig } from '../configs/AuthenticatedFileBasedDataAccessorConfig'; import { AuthenticatedFileResourceStoreConfig } from '../configs/AuthenticatedFileResourceStoreConfig'; +import type { ServerConfig } from '../configs/ServerConfig'; import { BASE, getRootFilePath } from '../configs/Util'; import { AclTestHelper, FileTestHelper } from '../util/TestHelpers'; -describe('A server using a AuthenticatedFileResourceStore', (): void => { - let config: AuthenticatedFileResourceStoreConfig; - let handler: HttpHandler; - let store: ResourceStore; - let aclHelper: AclTestHelper; - let fileHelper: FileTestHelper; - let rootFilePath: string; - - beforeAll(async(): Promise => { - rootFilePath = getRootFilePath('AuthenticatedFileResourceStore'); - mkdirSync(rootFilePath, { recursive: true }); - config = new AuthenticatedFileResourceStoreConfig(BASE, rootFilePath); - handler = config.getHttpHandler(); - ({ store } = config); - aclHelper = new AclTestHelper(store, ensureTrailingSlash(BASE)); - fileHelper = new FileTestHelper(handler, new URL(ensureTrailingSlash(BASE))); - - // Make sure the root directory exists - mkdirSync(rootFilePath, { recursive: true }); - copyFileSync(join(__dirname, '../assets/permanent.txt'), `${rootFilePath}/permanent.txt`); - }); - - afterAll(async(): Promise => { - rimraf.sync(rootFilePath, { glob: false }); - }); +const fileResourceStore: [string, (rootFilePath: string) => ServerConfig] = [ + 'FileResourceStore', + (rootFilePath: string): ServerConfig => new AuthenticatedFileResourceStoreConfig(BASE, rootFilePath), +]; +const dataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [ + 'FileDataAccessorBasedStore', + (rootFilePath: string): ServerConfig => new AuthenticatedFileBasedDataAccessorConfig(BASE, rootFilePath), +]; +describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (name, configFn): void => { describe('with acl', (): void => { + let config: ServerConfig; + let aclHelper: AclTestHelper; + let fileHelper: FileTestHelper; + let rootFilePath: string; + + beforeAll(async(): Promise => { + rootFilePath = getRootFilePath(name); + mkdirSync(rootFilePath, { recursive: true }); + config = configFn(rootFilePath); + aclHelper = new AclTestHelper(config.store, ensureTrailingSlash(BASE)); + fileHelper = new FileTestHelper(config.getHttpHandler(), new URL(ensureTrailingSlash(BASE))); + + // Make sure the root directory exists + mkdirSync(rootFilePath, { recursive: true }); + copyFileSync(join(__dirname, '../assets/permanent.txt'), `${rootFilePath}/permanent.txt`); + }); + + afterAll(async(): Promise => { + rimraf.sync(rootFilePath, { glob: false }); + }); + it('can add a file to the store, read it and delete it if allowed.', async(): Promise< void > => { @@ -41,7 +47,7 @@ describe('A server using a AuthenticatedFileResourceStore', (): void => { await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'agent'); // Create file - let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt'); + let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', 'text/plain'); const id = response._getHeaders().location; // Get file @@ -61,7 +67,7 @@ describe('A server using a AuthenticatedFileResourceStore', (): void => { await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'authenticated'); // Try to create file - const response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', true); + const response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', 'text/plain', true); expect(response.statusCode).toBe(401); }); @@ -71,7 +77,7 @@ describe('A server using a AuthenticatedFileResourceStore', (): void => { await aclHelper.setSimpleAcl({ read: true, write: false, append: false }, 'agent'); // Try to create file - let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', true); + let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', 'text/plain', true); expect(response.statusCode).toBe(401); // GET permanent file diff --git a/test/integration/FileResourceStore.test.ts b/test/integration/FileBasedStore.test.ts similarity index 86% rename from test/integration/FileResourceStore.test.ts rename to test/integration/FileBasedStore.test.ts index 2c75e7691..908d90827 100644 --- a/test/integration/FileResourceStore.test.ts +++ b/test/integration/FileBasedStore.test.ts @@ -1,21 +1,32 @@ import { mkdirSync } from 'fs'; import * as rimraf from 'rimraf'; import type { HttpHandler } from '../../src/server/HttpHandler'; +import { FileBasedDataAccessorConfig } from '../configs/FileBasedDataAccessorConfig'; import { FileResourceStoreConfig } from '../configs/FileResourceStoreConfig'; +import type { ServerConfig } from '../configs/ServerConfig'; import { BASE, getRootFilePath } from '../configs/Util'; import { FileTestHelper } from '../util/TestHelpers'; -describe('A server using a FileResourceStore', (): void => { +const fileResourceStore: [string, (rootFilePath: string) => ServerConfig] = [ + 'FileResourceStore', + (rootFilePath: string): ServerConfig => new FileResourceStoreConfig(BASE, rootFilePath), +]; +const dataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [ + 'FileDataAccessorBasedStore', + (rootFilePath: string): ServerConfig => new FileBasedDataAccessorConfig(BASE, rootFilePath), +]; + +describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (name, configFn): void => { describe('without acl', (): void => { let rootFilePath: string; - let config: FileResourceStoreConfig; + let config: ServerConfig; let handler: HttpHandler; let fileHelper: FileTestHelper; beforeAll(async(): Promise => { - rootFilePath = getRootFilePath('FileResourceStore'); + rootFilePath = getRootFilePath(name); mkdirSync(rootFilePath, { recursive: true }); - config = new FileResourceStoreConfig(BASE, rootFilePath); + config = configFn(rootFilePath); handler = config.getHttpHandler(); fileHelper = new FileTestHelper(handler, new URL(BASE)); }); @@ -27,7 +38,7 @@ describe('A server using a FileResourceStore', (): void => { it('can add a file to the store, read it and delete it.', async(): Promise => { // POST - let response = await fileHelper.createFile('../assets/testfile0.txt', 'testfile0.txt'); + let response = await fileHelper.createFile('../assets/testfile0.txt', 'testfile0.txt', 'text/plain'); const id = response._getHeaders().location; // GET @@ -42,7 +53,7 @@ describe('A server using a FileResourceStore', (): void => { }); it('can add and overwrite a file.', async(): Promise => { - let response = await fileHelper.createFile('../assets/testfile0.txt', 'file.txt'); + let response = await fileHelper.createFile('../assets/testfile0.txt', 'file.txt', 'text/plain'); const id = response._getHeaders().location; // GET @@ -52,7 +63,7 @@ describe('A server using a FileResourceStore', (): void => { expect(response._getBuffer().toString()).toContain('TESTFILE0'); // PUT - response = await fileHelper.overwriteFile('../assets/testfile1.txt', id); + response = await fileHelper.overwriteFile('../assets/testfile1.txt', id, 'text/plain'); // GET response = await fileHelper.getFile(id); @@ -85,7 +96,7 @@ describe('A server using a FileResourceStore', (): void => { await fileHelper.createFolder('testfolder0/'); // Create file - let response = await fileHelper.createFile('../assets/testfile0.txt', 'testfolder0/testfile0.txt'); + let response = await fileHelper.createFile('../assets/testfile0.txt', 'testfolder0/testfile0.txt', 'text/plain'); const id = response._getHeaders().location; // GET File @@ -106,7 +117,7 @@ describe('A server using a FileResourceStore', (): void => { const folderId = response._getHeaders().location; // Create file - await fileHelper.createFile('../assets/testfile0.txt', 'testfolder1/testfile0.txt'); + await fileHelper.createFile('../assets/testfile0.txt', 'testfolder1/testfile0.txt', 'text/plain'); // Try DELETE folder response = await fileHelper.simpleCall(new URL(folderId), 'DELETE', {}); @@ -151,7 +162,7 @@ describe('A server using a FileResourceStore', (): void => { const subFolderId = response._getHeaders().location; // Create file - response = await fileHelper.createFile('../assets/testfile0.txt', 'testfolder3/testfile0.txt'); + response = await fileHelper.createFile('../assets/testfile0.txt', 'testfolder3/testfile0.txt', 'text/plain'); const fileId = response._getHeaders().location; response = await fileHelper.getFolder(folderId); @@ -170,7 +181,7 @@ describe('A server using a FileResourceStore', (): void => { }); it('can upload and delete a image.', async(): Promise => { - let response = await fileHelper.createFile('../assets/testimage.png', 'image.png'); + let response = await fileHelper.createFile('../assets/testimage.png', 'image.png', 'image/png'); const fileId = response._getHeaders().location; // GET diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts index c2d913b6e..d330f26d0 100644 --- a/test/util/TestHelpers.ts +++ b/test/util/TestHelpers.ts @@ -108,7 +108,8 @@ export class FileTestHelper { return response; } - public async createFile(fileLocation: string, slug: string, mayFail = false): Promise> { + public async createFile(fileLocation: string, slug: string, contentType: string, mayFail = false): + Promise> { const fileData = await fs.readFile( join(__dirname, fileLocation), ); @@ -116,7 +117,7 @@ export class FileTestHelper { const response: MockResponse = await this.callWithFile( this.baseUrl, 'POST', - { 'content-type': 'application/octet-stream', + { 'content-type': contentType, slug, 'transfer-encoding': 'chunked' }, fileData, @@ -129,7 +130,8 @@ export class FileTestHelper { return response; } - public async overwriteFile(fileLocation: string, requestUrl: string): Promise> { + public async overwriteFile(fileLocation: string, requestUrl: string, contentType: string): + Promise> { const fileData = await fs.readFile( join(__dirname, fileLocation), ); @@ -139,7 +141,7 @@ export class FileTestHelper { const response: MockResponse = await this.callWithFile( putUrl, 'PUT', - { 'content-type': 'application/octet-stream', 'transfer-encoding': 'chunked' }, + { 'content-type': contentType, 'transfer-encoding': 'chunked' }, fileData, ); expect(response.statusCode).toBe(200);