diff --git a/.eslintrc.js b/.eslintrc.js index 425193af0..0fe626a44 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,6 +46,7 @@ module.exports = { } }], 'unused-imports/no-unused-imports-ts': 'error', - 'import/no-extraneous-dependencies': 'error' + 'import/no-extraneous-dependencies': 'error', + 'unicorn/import-index': 'off' }, }; diff --git a/index.ts b/index.ts index 5b5b6c763..d0ceae8a8 100644 --- a/index.ts +++ b/index.ts @@ -79,6 +79,7 @@ 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/FileResourceStore'; export * from './src/storage/InMemoryResourceStore'; export * from './src/storage/Lock'; export * from './src/storage/LockingResourceStore'; @@ -103,4 +104,6 @@ export * from './src/util/errors/UnsupportedMediaTypeHttpError'; export * from './src/util/AcceptParser'; export * from './src/util/AsyncHandler'; export * from './src/util/CompositeAsyncHandler'; +export * from './src/util/InteractionController'; +export * from './src/util/MetadataController'; export * from './src/util/Util'; diff --git a/jest.config.js b/jest.config.js index 7e3071cc2..5c175413d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,7 +16,8 @@ module.exports = { "setupFilesAfterEnv": ["jest-rdf"], "collectCoverage": true, "coveragePathIgnorePatterns": [ - "/node_modules/" + "/node_modules/", + "/src/init/CliRunner.ts" ], "coverageThreshold": { "./src": { diff --git a/package-lock.json b/package-lock.json index d15c1db05..12c461288 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1324,6 +1324,16 @@ "@types/range-parser": "*" } }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "@types/graceful-fs": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", @@ -1411,6 +1421,12 @@ "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz", "integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=" }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, "@types/minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", @@ -1471,6 +1487,16 @@ "@types/node": "*" } }, + "@types/rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-7WhJ0MdpFgYQPXlF4Dx+DhgvlPCfz/x5mHaeDQAKhcenvQP1KCpLQ18JklAqeGMYSAT2PxLpzd0g2/HE7fj7hQ==", + "dev": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, "@types/serve-static": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz", @@ -4349,6 +4375,17 @@ "flatted": "^2.0.0", "rimraf": "2.6.3", "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { @@ -8780,9 +8817,9 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" diff --git a/package.json b/package.json index 9a70bc0e9..f476c1b7c 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ }, "devDependencies": { "@types/jest": "^26.0.0", + "@types/rimraf": "^3.0.0", "@types/supertest": "^2.0.10", "@typescript-eslint/eslint-plugin": "^2.33.0", "@typescript-eslint/parser": "^2.33.0", @@ -108,6 +109,7 @@ "manual-git-changelog": "^1.0.1", "node-mocks-http": "^1.8.1", "nodemon": "^2.0.4", + "rimraf": "^3.0.2", "stream-to-string": "^1.1.0", "supertest": "^4.0.2", "ts-jest": "^26.0.0", diff --git a/test/assets/permanent.txt b/test/assets/permanent.txt new file mode 100644 index 000000000..2a02d41ce --- /dev/null +++ b/test/assets/permanent.txt @@ -0,0 +1 @@ +TEST diff --git a/test/assets/testfile0.txt b/test/assets/testfile0.txt new file mode 100644 index 000000000..368534e7c --- /dev/null +++ b/test/assets/testfile0.txt @@ -0,0 +1 @@ +TESTFILE0 diff --git a/test/assets/testfile1.txt b/test/assets/testfile1.txt new file mode 100644 index 000000000..e1e3b3d69 --- /dev/null +++ b/test/assets/testfile1.txt @@ -0,0 +1 @@ +TESTFILE1 diff --git a/test/assets/testfile2.txt b/test/assets/testfile2.txt new file mode 100644 index 000000000..8dd26c0d5 --- /dev/null +++ b/test/assets/testfile2.txt @@ -0,0 +1 @@ +TESTFILE2 diff --git a/test/assets/testimage.png b/test/assets/testimage.png new file mode 100644 index 000000000..95938d0ba Binary files /dev/null and b/test/assets/testimage.png differ diff --git a/test/configs/AuthenticatedFileResourceStoreConfig.ts b/test/configs/AuthenticatedFileResourceStoreConfig.ts new file mode 100644 index 000000000..84bdf8f20 --- /dev/null +++ b/test/configs/AuthenticatedFileResourceStoreConfig.ts @@ -0,0 +1,66 @@ +import { + AuthenticatedLdpHandler, + BasicResponseWriter, + CompositeAsyncHandler, + HttpHandler, + MethodPermissionsExtractor, + RdfToQuadConverter, + ResourceStore, + UnsecureWebIdExtractor, + QuadToRdfConverter, + RuntimeConfig, +} from '../../index'; +import { ServerConfig } from './ServerConfig'; +import { + getFileResourceStore, + getConvertingStore, + getBasicRequestParser, + getOperationHandler, + getWebAclAuthorizer, +} 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 AuthenticatedFileResourceStoreConfig implements ServerConfig { + private readonly runtimeConfig: RuntimeConfig; + public store: ResourceStore; + + public constructor(runtimeConfig: RuntimeConfig) { + this.runtimeConfig = runtimeConfig; + this.store = getConvertingStore( + getFileResourceStore(runtimeConfig), + [ 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.runtimeConfig.base); + + const handler = new AuthenticatedLdpHandler({ + requestParser, + credentialsExtractor, + permissionsExtractor, + authorizer, + operationHandler, + responseWriter, + }); + + return handler; + } +} diff --git a/test/configs/BasicConfig.ts b/test/configs/BasicConfig.ts new file mode 100644 index 000000000..4140efe01 --- /dev/null +++ b/test/configs/BasicConfig.ts @@ -0,0 +1,49 @@ +import { + AllowEverythingAuthorizer, + AuthenticatedLdpHandler, + BasicResponseWriter, + HttpHandler, + MethodPermissionsExtractor, + ResourceStore, + UnsecureWebIdExtractor, +} from '../../index'; +import { ServerConfig } from './ServerConfig'; +import { getOperationHandler, getInMemoryResourceStore, getBasicRequestParser } from './Util'; + +/** + * BasicConfig works with + * - an AllowEverythingAuthorizer (no acl) + * - an InMemoryResourceStore + * - GET, POST & DELETE operation handlers + */ + +export class BasicConfig implements ServerConfig { + public store: ResourceStore; + + public constructor() { + this.store = getInMemoryResourceStore(); + } + + public getHttpHandler(): HttpHandler { + const requestParser = getBasicRequestParser(); + + const credentialsExtractor = new UnsecureWebIdExtractor(); + const permissionsExtractor = 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/BasicHandlersConfig.ts b/test/configs/BasicHandlersConfig.ts new file mode 100644 index 000000000..3f7894ed1 --- /dev/null +++ b/test/configs/BasicHandlersConfig.ts @@ -0,0 +1,69 @@ +import { + AllowEverythingAuthorizer, + AuthenticatedLdpHandler, + BasicResponseWriter, + CompositeAsyncHandler, + HttpHandler, + MethodPermissionsExtractor, + QuadToRdfConverter, + RawBodyParser, + RdfToQuadConverter, + ResourceStore, + SparqlUpdateBodyParser, + SparqlPatchPermissionsExtractor, + UnsecureWebIdExtractor, +} from '../../index'; + +import { ServerConfig } from './ServerConfig'; +import { getInMemoryResourceStore, + getOperationHandler, + getConvertingStore, + getPatchingStore, getBasicRequestParser } from './Util'; + +/** + * BasicHandlersConfig works with + * - an AllowEverythingAuthorizer (no acl) + * - an InMemoryResourceStore wrapped in a converting store & wrapped in a patching store + * - GET, POST, PUT, PATCH & DELETE operation handlers + */ + +export class BasicHandlersConfig implements ServerConfig { + public store: ResourceStore; + + public constructor() { + const convertingStore = getConvertingStore( + getInMemoryResourceStore(), + [ new QuadToRdfConverter(), new RdfToQuadConverter() ], + ); + this.store = getPatchingStore(convertingStore); + } + + public getHttpHandler(): HttpHandler { + const requestParser = getBasicRequestParser([ + new SparqlUpdateBodyParser(), + new RawBodyParser(), + ]); + + const credentialsExtractor = new UnsecureWebIdExtractor(); + const permissionsExtractor = new CompositeAsyncHandler([ + new MethodPermissionsExtractor(), + new SparqlPatchPermissionsExtractor(), + ]); + 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/BasicHandlersWithAclConfig.ts b/test/configs/BasicHandlersWithAclConfig.ts new file mode 100644 index 000000000..a291898dd --- /dev/null +++ b/test/configs/BasicHandlersWithAclConfig.ts @@ -0,0 +1,63 @@ +import { + AuthenticatedLdpHandler, + BasicResponseWriter, + CompositeAsyncHandler, + HttpHandler, + MethodPermissionsExtractor, + RdfToQuadConverter, + ResourceStore, + UnsecureWebIdExtractor, + QuadToRdfConverter, +} from '../../index'; +import { ServerConfig } from './ServerConfig'; +import { + getInMemoryResourceStore, + getConvertingStore, + getBasicRequestParser, + getOperationHandler, + getWebAclAuthorizer, +} from './Util'; + +/** + * BasicHandlersWithAclConfig works with + * - an WebAclAuthorizer + * - an InMemoryResourceStore wrapped in a converting store & wrapped in a patching store + * - GET, POST, PUT, PATCH & DELETE operation handlers + */ + +export class BasicHandlersWithAclConfig implements ServerConfig { + public store: ResourceStore; + + public constructor() { + this.store = getConvertingStore( + getInMemoryResourceStore(), + [ 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); + + const handler = new AuthenticatedLdpHandler({ + requestParser, + credentialsExtractor, + permissionsExtractor, + authorizer, + operationHandler, + responseWriter, + }); + + return handler; + } +} diff --git a/test/configs/FileResourceStoreConfig.ts b/test/configs/FileResourceStoreConfig.ts new file mode 100644 index 000000000..6b6eeb0af --- /dev/null +++ b/test/configs/FileResourceStoreConfig.ts @@ -0,0 +1,59 @@ +import { + AllowEverythingAuthorizer, + AuthenticatedLdpHandler, + BasicResponseWriter, + CompositeAsyncHandler, + HttpHandler, + MethodPermissionsExtractor, + QuadToRdfConverter, + RawBodyParser, + RdfToQuadConverter, + ResourceStore, + RuntimeConfig, + UnsecureWebIdExtractor, +} from '../../index'; +import { ServerConfig } from './ServerConfig'; +import { getFileResourceStore, getOperationHandler, getConvertingStore, getBasicRequestParser } from './Util'; + +/** + * FileResourceStoreConfig works with + * - an AllowEverythingAuthorizer (no acl) + * - a FileResourceStore wrapped in a converting store (rdf to quad & quad to rdf) + * - GET, POST, PUT & DELETE operation handlers + */ + +export class FileResourceStoreConfig implements ServerConfig { + public store: ResourceStore; + + public constructor(runtimeConfig: RuntimeConfig) { + this.store = getConvertingStore( + getFileResourceStore(runtimeConfig), + [ 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 new file mode 100644 index 000000000..b29bdce18 --- /dev/null +++ b/test/configs/ServerConfig.ts @@ -0,0 +1,5 @@ +import { HttpHandler } from '../../src/server/HttpHandler'; + +export interface ServerConfig { + getHttpHandler(): HttpHandler; +} diff --git a/test/configs/Util.ts b/test/configs/Util.ts new file mode 100644 index 000000000..428fd74bf --- /dev/null +++ b/test/configs/Util.ts @@ -0,0 +1,143 @@ +import { join } from 'path'; +import { + AcceptPreferenceParser, + BasicRequestParser, + BasicTargetExtractor, + BodyParser, + CompositeAsyncHandler, + DeleteOperationHandler, + FileResourceStore, + GetOperationHandler, + HttpRequest, + InMemoryResourceStore, + InteractionController, + MetadataController, + Operation, + PatchingStore, + PatchOperationHandler, + PostOperationHandler, + PutOperationHandler, + RawBodyParser, + Representation, + RepresentationConverter, + RepresentationConvertingStore, + ResourceStore, + ResponseDescription, + RuntimeConfig, + SingleThreadedResourceLocker, + SparqlUpdatePatchHandler, + UrlBasedAclManager, + UrlContainerManager, + WebAclAuthorizer, +} from '../../index'; +import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper'; + +const BASE = 'http://test.com'; + +/** + * Creates a RuntimeConfig with its rootFilePath set based on the given subfolder. + * @param subfolder - Folder to use in the global testData folder. + */ +export const getRuntimeConfig = (subfolder: string): RuntimeConfig => new RuntimeConfig({ + base: BASE, + rootFilepath: join(__dirname, '../testData', subfolder), +}); + +/** + * Gives a file resource store based on (default) runtime config. + * @param runtimeConfig - Optional runtime config. + * + * @returns The file resource store. + */ +export const getFileResourceStore = (runtimeConfig: RuntimeConfig): FileResourceStore => + new FileResourceStore( + new ExtensionBasedMapper(runtimeConfig), + new InteractionController(), + new MetadataController(), + ); + +/** + * Gives an in memory resource store based on (default) base url. + * @param base - Optional base parameter for the run time config. + * + * @returns The in memory resource store. + */ +export const getInMemoryResourceStore = (base = BASE): InMemoryResourceStore => + new InMemoryResourceStore(new RuntimeConfig({ base })); + +/** + * Gives a converting store given some converters. + * @param store - Initial store. + * @param converters - Converters to be used. + * + * @returns The converting store. + */ +export const getConvertingStore = +(store: ResourceStore, converters: RepresentationConverter[]): RepresentationConvertingStore => + new RepresentationConvertingStore(store, new CompositeAsyncHandler(converters)); + +/** + * Gives a patching store based on initial store. + * @param store - Inital resource store. + * + * @returns The patching store. + */ +export const getPatchingStore = (store: ResourceStore): PatchingStore => { + const locker = new SingleThreadedResourceLocker(); + const patcher = new SparqlUpdatePatchHandler(store, locker); + return new PatchingStore(store, patcher); +}; + +/** + * Gives an operation handler given a store with all the common operation handlers. + * @param store - Initial resource store. + * + * @returns The operation handler. + */ +export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler => { + const handlers = [ + new GetOperationHandler(store), + new PostOperationHandler(store), + new PutOperationHandler(store), + new PatchOperationHandler(store), + new DeleteOperationHandler(store), + ]; + return new CompositeAsyncHandler(handlers); +}; + +/** + * Gives a basic request parser based on some body parses. + * @param bodyParsers - Optional list of body parsers, default is RawBodyParser. + * + * @returns The request parser. + */ +export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequestParser => { + let bodyParser: BodyParser; + if (bodyParsers.length === 1) { + bodyParser = bodyParsers[0]; + } else if (bodyParsers.length === 0) { + // If no body parser is given (array is empty), default to RawBodyParser + bodyParser = new RawBodyParser(); + } else { + bodyParser = new CompositeAsyncHandler(bodyParsers); + } + return new BasicRequestParser({ + targetExtractor: new BasicTargetExtractor(), + preferenceParser: new AcceptPreferenceParser(), + bodyParser, + }); +}; + +/** + * Gives a web acl authorizer, using a UrlContainerManager & based on a (default) runtimeConfig. + * @param store - Initial resource store. + * @param base - Base URI of the pod. + * @param aclManager - Optional acl manager, default is UrlBasedAclManager. + * + * @returns The acl authorizer. + */ +export const getWebAclAuthorizer = +(store: ResourceStore, base = BASE, aclManager = new UrlBasedAclManager()): WebAclAuthorizer => { + const containerManager = new UrlContainerManager(new RuntimeConfig({ base })); + return new WebAclAuthorizer(aclManager, containerManager, store); +}; diff --git a/test/integration/AuthenticatedFileResourceStore.test.ts b/test/integration/AuthenticatedFileResourceStore.test.ts new file mode 100644 index 000000000..647eb5095 --- /dev/null +++ b/test/integration/AuthenticatedFileResourceStore.test.ts @@ -0,0 +1,86 @@ +import { copyFileSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import * as rimraf from 'rimraf'; +import { HttpHandler, ResourceStore, RuntimeConfig } from '../../index'; +import { AuthenticatedFileResourceStoreConfig } from '../configs/AuthenticatedFileResourceStoreConfig'; +import { getRuntimeConfig } 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 runtimeConfig: RuntimeConfig; + + beforeAll(async(): Promise => { + runtimeConfig = getRuntimeConfig('AuthenticatedFileResourceStore'); + config = new AuthenticatedFileResourceStoreConfig(runtimeConfig); + const { base, rootFilepath } = runtimeConfig; + handler = config.getHttpHandler(); + ({ store } = config); + aclHelper = new AclTestHelper(store, base); + fileHelper = new FileTestHelper(handler, new URL('http://test.com/')); + + // Make sure the root directory exists + mkdirSync(rootFilepath, { recursive: true }); + copyFileSync(join(__dirname, '../assets/permanent.txt'), `${rootFilepath}/permanent.txt`); + }); + + afterAll(async(): Promise => { + rimraf.sync(runtimeConfig.rootFilepath, { glob: false }); + }); + + describe('with acl', (): void => { + it('can add a file to the store, read it and delete it if allowed.', async(): Promise< + void + > => { + // Set acl + await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'agent'); + + // Create file + let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt'); + const id = response._getHeaders().location; + + // Get file + response = await fileHelper.getFile(id); + expect(response.statusCode).toBe(200); + expect(response._getHeaders().location).toBe(id); + expect(response._getBuffer().toString()).toContain('TESTFILE2'); + + // DELETE file + await fileHelper.deleteFile(id); + await fileHelper.shouldNotExist(id); + }); + + it('can not add a file to the store if not allowed.', async(): + Promise => { + // Set acl + 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); + expect(response.statusCode).toBe(401); + }); + + it('can not add/delete, but only read files if allowed.', async(): + Promise => { + // Set acl + 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); + expect(response.statusCode).toBe(401); + + // GET permanent file + response = await fileHelper.getFile('http://test.com/permanent.txt'); + expect(response._getHeaders().location).toBe('http://test.com/permanent.txt'); + expect(response._getBuffer().toString()).toContain('TEST'); + + // Try to delete permanent file + response = await fileHelper.deleteFile('http://test.com/permanent.txt', true); + expect(response.statusCode).toBe(401); + }); + }); +}); diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts index 993779c89..41c200c04 100644 --- a/test/integration/AuthenticatedLdpHandler.test.ts +++ b/test/integration/AuthenticatedLdpHandler.test.ts @@ -2,66 +2,13 @@ import * as url from 'url'; import { namedNode, quad } from '@rdfjs/data-model'; import { Parser } from 'n3'; import { MockResponse } from 'node-mocks-http'; -import { UnsecureWebIdExtractor } from '../../src/authentication/UnsecureWebIdExtractor'; -import { AllowEverythingAuthorizer } from '../../src/authorization/AllowEverythingAuthorizer'; -import { RuntimeConfig } from '../../src/init/RuntimeConfig'; -import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler'; -import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser'; -import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser'; -import { BasicResponseWriter } from '../../src/ldp/http/BasicResponseWriter'; -import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor'; -import { BodyParser } from '../../src/ldp/http/BodyParser'; -import { RawBodyParser } from '../../src/ldp/http/RawBodyParser'; -import { SparqlUpdateBodyParser } from '../../src/ldp/http/SparqlUpdateBodyParser'; -import { DeleteOperationHandler } from '../../src/ldp/operations/DeleteOperationHandler'; -import { GetOperationHandler } from '../../src/ldp/operations/GetOperationHandler'; -import { Operation } from '../../src/ldp/operations/Operation'; -import { PatchOperationHandler } from '../../src/ldp/operations/PatchOperationHandler'; -import { PostOperationHandler } from '../../src/ldp/operations/PostOperationHandler'; -import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription'; -import { MethodPermissionsExtractor } from '../../src/ldp/permissions/MethodPermissionsExtractor'; -import { SparqlPatchPermissionsExtractor } from '../../src/ldp/permissions/SparqlPatchPermissionsExtractor'; -import { Representation } from '../../src/ldp/representation/Representation'; -import { HttpRequest } from '../../src/server/HttpRequest'; -import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtleConverter'; -import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter'; -import { InMemoryResourceStore } from '../../src/storage/InMemoryResourceStore'; -import { SparqlUpdatePatchHandler } from '../../src/storage/patch/SparqlUpdatePatchHandler'; -import { PatchingStore } from '../../src/storage/PatchingStore'; -import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore'; -import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker'; -import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler'; +import { BasicConfig } from '../configs/BasicConfig'; +import { BasicHandlersConfig } from '../configs/BasicHandlersConfig'; import { call } from '../util/Util'; describe('An integrated AuthenticatedLdpHandler', (): void => { describe('with simple handlers', (): void => { - const requestParser = new BasicRequestParser({ - targetExtractor: new BasicTargetExtractor(), - preferenceParser: new AcceptPreferenceParser(), - bodyParser: new RawBodyParser(), - }); - - const credentialsExtractor = new UnsecureWebIdExtractor(); - const permissionsExtractor = new MethodPermissionsExtractor(); - const authorizer = new AllowEverythingAuthorizer(); - - const store = new InMemoryResourceStore(new RuntimeConfig({ base: 'http://test.com/' })); - const operationHandler = new CompositeAsyncHandler([ - new GetOperationHandler(store), - new PostOperationHandler(store), - new DeleteOperationHandler(store), - ]); - - const responseWriter = new BasicResponseWriter(); - - const handler = new AuthenticatedLdpHandler({ - requestParser, - credentialsExtractor, - permissionsExtractor, - authorizer, - operationHandler, - responseWriter, - }); + const handler = new BasicConfig().getHttpHandler(); it('can add, read and delete data based on incoming requests.', async(): Promise => { // POST @@ -80,9 +27,17 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { // GET requestUrl = new URL(id); - response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []); + response = await call( + handler, + requestUrl, + 'GET', + { accept: 'text/turtle' }, + [], + ); expect(response.statusCode).toBe(200); - expect(response._getData()).toContain(' .'); + expect(response._getData()).toContain( + ' .', + ); expect(response._getHeaders().location).toBe(id); // DELETE @@ -92,57 +47,20 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { expect(response._getHeaders().location).toBe(url.format(requestUrl)); // GET - response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []); + response = await call( + handler, + requestUrl, + 'GET', + { accept: 'text/turtle' }, + [], + ); expect(response.statusCode).toBe(404); expect(response._getData()).toContain('NotFoundHttpError'); }); }); describe('with simple PATCH handlers', (): void => { - const bodyParser: BodyParser = new CompositeAsyncHandler([ - new SparqlUpdateBodyParser(), - new RawBodyParser(), - ]); - const requestParser = new BasicRequestParser({ - targetExtractor: new BasicTargetExtractor(), - preferenceParser: new AcceptPreferenceParser(), - bodyParser, - }); - - const credentialsExtractor = new UnsecureWebIdExtractor(); - const permissionsExtractor = new CompositeAsyncHandler([ - new MethodPermissionsExtractor(), - new SparqlPatchPermissionsExtractor(), - ]); - const authorizer = new AllowEverythingAuthorizer(); - - const store = new InMemoryResourceStore(new RuntimeConfig({ base: 'http://test.com/' })); - const converter = new CompositeAsyncHandler([ - new QuadToTurtleConverter(), - new TurtleToQuadConverter(), - ]); - const convertingStore = new RepresentationConvertingStore(store, converter); - const locker = new SingleThreadedResourceLocker(); - const patcher = new SparqlUpdatePatchHandler(convertingStore, locker); - const patchingStore = new PatchingStore(convertingStore, patcher); - - const operationHandler = new CompositeAsyncHandler([ - new GetOperationHandler(patchingStore), - new PostOperationHandler(patchingStore), - new DeleteOperationHandler(patchingStore), - new PatchOperationHandler(patchingStore), - ]); - - const responseWriter = new BasicResponseWriter(); - - const handler = new AuthenticatedLdpHandler({ - requestParser, - credentialsExtractor, - permissionsExtractor, - authorizer, - operationHandler, - responseWriter, - }); + const handler = new BasicHandlersConfig().getHttpHandler(); it('can handle simple SPARQL updates.', async(): Promise => { // POST @@ -169,7 +87,8 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { { 'content-type': 'application/sparql-update', 'transfer-encoding': 'chunked' }, [ 'DELETE { }', 'INSERT { }', - 'WHERE {}' ], + 'WHERE {}', + ], ); expect(response.statusCode).toBe(200); expect(response._getData()).toHaveLength(0); @@ -177,18 +96,89 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { // GET requestUrl = new URL(id); - response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []); + response = await call( + handler, + requestUrl, + 'GET', + { accept: 'text/turtle' }, + [], + ); + expect(response.statusCode).toBe(200); + expect(response._getBuffer().toString()).toContain( + ' .', + ); + expect(response._getHeaders().location).toBe(id); + const parser = new Parser(); + const triples = parser.parse(response._getBuffer().toString()); + expect(triples).toBeRdfIsomorphic([ + quad( + namedNode('http://test.com/s2'), + namedNode('http://test.com/p2'), + namedNode('http://test.com/o2'), + ), + quad( + namedNode('http://test.com/s3'), + namedNode('http://test.com/p3'), + namedNode('http://test.com/o3'), + ), + ]); + }); + }); + + describe('with simple PUT handlers', (): void => { + const handler = new BasicHandlersConfig().getHttpHandler(); + + it('should overwrite the content on PUT request.', async(): Promise => { + // POST + let requestUrl = new URL('http://test.com/'); + let response: MockResponse = await call( + handler, + requestUrl, + 'POST', + { 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' }, + [ + ' .', + ' .', + ], + ); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + const id = response._getHeaders().location; + expect(id).toContain(url.format(requestUrl)); + + // PUT + requestUrl = new URL(id); + response = await call( + handler, + requestUrl, + 'PUT', + { 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' }, + [ ' .' ], + ); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toBe(id); + + // GET + requestUrl = new URL(id); + response = await call( + handler, + requestUrl, + 'GET', + { accept: 'text/turtle' }, + [], + ); expect(response.statusCode).toBe(200); - expect(response._getData()).toContain(' .'); expect(response._getHeaders().location).toBe(id); const parser = new Parser(); const triples = parser.parse(response._getData()); - expect(triples).toBeRdfIsomorphic( - [ - quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')), - quad(namedNode('http://test.com/s3'), namedNode('http://test.com/p3'), namedNode('http://test.com/o3')), - ], - ); + expect(triples).toBeRdfIsomorphic([ + quad( + namedNode('http://test.com/s3'), + namedNode('http://test.com/p3'), + namedNode('http://test.com/o3'), + ), + ]); }); }); }); diff --git a/test/integration/Authorization.test.ts b/test/integration/Authorization.test.ts index 02cdcfc30..31a43d24e 100644 --- a/test/integration/Authorization.test.ts +++ b/test/integration/Authorization.test.ts @@ -1,127 +1,16 @@ import { MockResponse } from 'node-mocks-http'; -import streamifyArray from 'streamify-array'; -import { UnsecureWebIdExtractor } from '../../src/authentication/UnsecureWebIdExtractor'; -import { UrlBasedAclManager } from '../../src/authorization/UrlBasedAclManager'; -import { WebAclAuthorizer } from '../../src/authorization/WebAclAuthorizer'; -import { RuntimeConfig } from '../../src/init/RuntimeConfig'; -import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler'; -import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser'; -import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser'; -import { BasicResponseWriter } from '../../src/ldp/http/BasicResponseWriter'; -import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor'; -import { BodyParser } from '../../src/ldp/http/BodyParser'; -import { RawBodyParser } from '../../src/ldp/http/RawBodyParser'; -import { DeleteOperationHandler } from '../../src/ldp/operations/DeleteOperationHandler'; -import { GetOperationHandler } from '../../src/ldp/operations/GetOperationHandler'; -import { Operation } from '../../src/ldp/operations/Operation'; -import { PostOperationHandler } from '../../src/ldp/operations/PostOperationHandler'; -import { PutOperationHandler } from '../../src/ldp/operations/PutOperationHandler'; -import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription'; -import { MethodPermissionsExtractor } from '../../src/ldp/permissions/MethodPermissionsExtractor'; -import { PermissionSet } from '../../src/ldp/permissions/PermissionSet'; -import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtleConverter'; -import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter'; -import { InMemoryResourceStore } from '../../src/storage/InMemoryResourceStore'; -import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore'; -import { ResourceStore } from '../../src/storage/ResourceStore'; -import { UrlContainerManager } from '../../src/storage/UrlContainerManager'; -import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler'; +import { BasicHandlersWithAclConfig } from '../configs/BasicHandlersWithAclConfig'; +import { AclTestHelper } from '../util/TestHelpers'; import { call } from '../util/Util'; -const setAcl = async(store: ResourceStore, id: string, permissions: PermissionSet, control: boolean, - access: boolean, def: boolean, agent?: string, agentClass?: 'agent' | 'authenticated'): Promise => { - const acl: string[] = [ - '@prefix acl: .\n', - '@prefix foaf: .\n', - ' a acl:Authorization', - ]; - - for (const perm of [ 'Read', 'Append', 'Write', 'Delete' ]) { - if (permissions[perm.toLowerCase() as keyof PermissionSet]) { - acl.push(`;\n acl:mode acl:${perm}`); - } - } - if (control) { - acl.push(';\n acl:mode acl:Control'); - } - if (access) { - acl.push(`;\n acl:accessTo <${id}>`); - } - if (def) { - acl.push(`;\n acl:default <${id}>`); - } - if (agent) { - acl.push(`;\n acl:agent <${agent}>`); - } - if (agentClass) { - acl.push(`;\n acl:agentClass ${agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent'}`); - } - - acl.push('.'); - - const representation = { - binary: true, - data: streamifyArray(acl), - metadata: { - raw: [], - profiles: [], - contentType: 'text/turtle', - }, - }; - - return store.setRepresentation({ path: `${id}.acl` }, representation); -}; - describe('A server with authorization', (): void => { - const bodyParser: BodyParser = new RawBodyParser(); - const requestParser = new BasicRequestParser({ - targetExtractor: new BasicTargetExtractor(), - preferenceParser: new AcceptPreferenceParser(), - bodyParser, - }); - - const store = new InMemoryResourceStore(new RuntimeConfig({ base: 'http://test.com/' })); - const converter = new CompositeAsyncHandler([ - new QuadToTurtleConverter(), - new TurtleToQuadConverter(), - ]); - const convertingStore = new RepresentationConvertingStore(store, converter); - - const credentialsExtractor = new UnsecureWebIdExtractor(); - const permissionsExtractor = new MethodPermissionsExtractor(); - const authorizer = new WebAclAuthorizer( - new UrlBasedAclManager(), - new UrlContainerManager(new RuntimeConfig({ base: 'http://test.com/' })), - convertingStore, - ); - - const operationHandler = new CompositeAsyncHandler([ - new GetOperationHandler(convertingStore), - new PostOperationHandler(convertingStore), - new DeleteOperationHandler(convertingStore), - new PutOperationHandler(convertingStore), - ]); - - const responseWriter = new BasicResponseWriter(); - - const handler = new AuthenticatedLdpHandler({ - requestParser, - credentialsExtractor, - permissionsExtractor, - authorizer, - operationHandler, - responseWriter, - }); + const config = new BasicHandlersWithAclConfig(); + const handler = config.getHttpHandler(); + const { store } = config; + const aclHelper = new AclTestHelper(store, 'http://test.com/'); it('can create new entries.', async(): Promise => { - await setAcl(convertingStore, - 'http://test.com/', - { read: true, write: true, append: true }, - true, - true, - true, - undefined, - 'agent'); + await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'agent'); // POST let requestUrl = new URL('http://test.com/'); @@ -147,14 +36,7 @@ describe('A server with authorization', (): void => { }); it('can not create new entries if not allowed.', async(): Promise => { - await setAcl(convertingStore, - 'http://test.com/', - { read: true, write: true, append: true }, - true, - true, - true, - undefined, - 'authenticated'); + await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'authenticated'); // POST let requestUrl = new URL('http://test.com/'); diff --git a/test/integration/FileResourceStore.test.ts b/test/integration/FileResourceStore.test.ts new file mode 100644 index 000000000..594a4b1bc --- /dev/null +++ b/test/integration/FileResourceStore.test.ts @@ -0,0 +1,186 @@ +import * as rimraf from 'rimraf'; +import { RuntimeConfig } from '../../src/init/RuntimeConfig'; +import { HttpHandler } from '../../src/server/HttpHandler'; +import { FileResourceStoreConfig } from '../configs/FileResourceStoreConfig'; +import { getRuntimeConfig } from '../configs/Util'; +import { FileTestHelper } from '../util/TestHelpers'; + +describe('A server using a FileResourceStore', (): void => { + describe('without acl', (): void => { + let config: FileResourceStoreConfig; + let handler: HttpHandler; + let fileHelper: FileTestHelper; + let runtimeConfig: RuntimeConfig; + + beforeAll(async(): Promise => { + runtimeConfig = getRuntimeConfig('FileResourceStore'); + config = new FileResourceStoreConfig(runtimeConfig); + handler = config.getHttpHandler(); + fileHelper = new FileTestHelper(handler, new URL(runtimeConfig.base)); + }); + + afterAll(async(): Promise => { + rimraf.sync(runtimeConfig.rootFilepath, { glob: false }); + }); + + 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'); + const id = response._getHeaders().location; + + // GET + response = await fileHelper.getFile(id); + expect(response.statusCode).toBe(200); + expect(response._getHeaders().location).toBe(id); + expect(response._getBuffer().toString()).toContain('TESTFILE0'); + + // DELETE + await fileHelper.deleteFile(id); + await fileHelper.shouldNotExist(id); + }); + + it('can add and overwrite a file.', async(): Promise => { + let response = await fileHelper.createFile('../assets/testfile0.txt', 'file.txt'); + const id = response._getHeaders().location; + + // GET + response = await fileHelper.getFile(id); + expect(response.statusCode).toBe(200); + expect(response._getHeaders().location).toBe(id); + expect(response._getBuffer().toString()).toContain('TESTFILE0'); + + // PUT + response = await fileHelper.overwriteFile('../assets/testfile1.txt', id); + + // GET + response = await fileHelper.getFile(id); + expect(response.statusCode).toBe(200); + expect(response._getHeaders().location).toBe(id); + expect(response._getBuffer().toString()).toContain('TESTFILE1'); + + // DELETE + await fileHelper.deleteFile(id); + await fileHelper.shouldNotExist(id); + }); + + it('can create a folder and delete it.', async(): Promise => { + // POST + let response = await fileHelper.createFolder('secondfolder/'); + const id = response._getHeaders().location; + + // GET + response = await fileHelper.getFolder(id); + expect(response.statusCode).toBe(200); + expect(response._getHeaders().location).toBe(id); + + // DELETE + await fileHelper.deleteFolder(id); + await fileHelper.shouldNotExist(id); + }); + + it('can make a folder and put a file in it.', async(): Promise => { + // Create folder + await fileHelper.createFolder('testfolder0/'); + + // Create file + let response = await fileHelper.createFile('../assets/testfile0.txt', 'testfolder0/testfile0.txt'); + const id = response._getHeaders().location; + + // GET File + response = await fileHelper.getFile(id); + expect(response.statusCode).toBe(200); + expect(response._getHeaders().location).toBe(id); + + // DELETE + await fileHelper.deleteFile(id); + await fileHelper.shouldNotExist(id); + await fileHelper.deleteFolder('http://test.com/testfolder0/'); + await fileHelper.shouldNotExist('http://test.com/testfolder0/'); + }); + + it('cannot remove a folder when the folder contains a file.', async(): Promise => { + // Create folder + let response = await fileHelper.createFolder('testfolder1/'); + const folderId = response._getHeaders().location; + + // Create file + await fileHelper.createFile('../assets/testfile0.txt', 'testfolder1/testfile0.txt'); + + // Try DELETE folder + response = await fileHelper.simpleCall(new URL(folderId), 'DELETE', {}); + expect(response.statusCode).toBe(409); + expect(response._getData()).toContain('ConflictHttpError: Container is not empty.'); + + // DELETE + await fileHelper.deleteFile('http://test.com/testfolder1/testfile0.txt'); + await fileHelper.shouldNotExist('http://test.com/testfolder1/testfile0.txt'); + await fileHelper.deleteFolder(folderId); + await fileHelper.shouldNotExist(folderId); + }); + + it('cannot remove a folder when the folder contains a subfolder.', async(): Promise => { + // Create folder + let response = await fileHelper.createFolder('testfolder2/'); + const folderId = response._getHeaders().location; + + // Create subfolder + response = await fileHelper.createFolder('testfolder2/subfolder0'); + const subFolderId = response._getHeaders().location; + + // Try DELETE folder + response = await fileHelper.simpleCall(new URL(folderId), 'DELETE', {}); + expect(response.statusCode).toBe(409); + expect(response._getData()).toContain('ConflictHttpError: Container is not empty.'); + + // DELETE + await fileHelper.deleteFolder(subFolderId); + await fileHelper.shouldNotExist(subFolderId); + await fileHelper.deleteFolder(folderId); + await fileHelper.shouldNotExist(folderId); + }); + + it('can read the contents of a folder.', async(): Promise => { + // Create folder + let response = await fileHelper.createFolder('testfolder3/'); + const folderId = response._getHeaders().location; + + // Create subfolder + response = await fileHelper.createFolder('testfolder3/subfolder0'); + const subFolderId = response._getHeaders().location; + + // Create file + response = await fileHelper.createFile('../assets/testfile0.txt', 'testfolder3/testfile0.txt'); + const fileId = response._getHeaders().location; + + response = await fileHelper.getFolder(folderId); + expect(response.statusCode).toBe(200); + expect(response._getHeaders().location).toBe(folderId); + expect(response._getBuffer().toString()).toContain(' .'); + expect(response._getBuffer().toString()).toContain(' .'); + + // DELETE + await fileHelper.deleteFile(fileId); + await fileHelper.shouldNotExist(fileId); + await fileHelper.deleteFolder(subFolderId); + await fileHelper.shouldNotExist(subFolderId); + await fileHelper.deleteFolder(folderId); + await fileHelper.shouldNotExist(folderId); + }); + + it('can upload and delete a image.', async(): Promise => { + let response = await fileHelper.createFile('../assets/testimage.png', 'image.png'); + const fileId = response._getHeaders().location; + + // GET + response = await fileHelper.getFile(fileId); + expect(response.statusCode).toBe(200); + expect(response._getHeaders().location).toBe(fileId); + expect(response._getHeaders()['content-type']).toBe('image/png'); + + // DELETE + await fileHelper.deleteFile(fileId); + await fileHelper.shouldNotExist(fileId); + }); + }); +}); diff --git a/test/testData/.gitkeep b/test/testData/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/storage/FileResourceStore.test.ts b/test/unit/storage/FileResourceStore.test.ts index 2924b3b84..0463cf61f 100644 --- a/test/unit/storage/FileResourceStore.test.ts +++ b/test/unit/storage/FileResourceStore.test.ts @@ -25,12 +25,18 @@ const { join: joinPath } = posix; const base = 'http://test.com/'; const rootFilepath = '/Users/default/home/public/'; -fsPromises.rmdir = jest.fn(); -fsPromises.lstat = jest.fn(); -fsPromises.readdir = jest.fn(); -fsPromises.mkdir = jest.fn(); -fsPromises.unlink = jest.fn(); -fsPromises.access = jest.fn(); +jest.mock('fs', (): any => ({ + createReadStream: jest.fn(), + createWriteStream: jest.fn(), + promises: { + rmdir: jest.fn(), + lstat: jest.fn(), + readdir: jest.fn(), + mkdir: jest.fn(), + unlink: jest.fn(), + access: jest.fn(), + }, +})); describe('A FileResourceStore', (): void => { let store: FileResourceStore; @@ -45,8 +51,6 @@ describe('A FileResourceStore', (): void => { namedNode('http://test.com/o'), ); - fs.createReadStream = jest.fn(); - beforeEach(async(): Promise => { jest.clearAllMocks(); @@ -68,8 +72,6 @@ describe('A FileResourceStore', (): void => { mtime: new Date(), } as jest.Mocked; - // Mock the fs functions for the createDataFile function. - fs.createWriteStream = jest.fn(); writeStream = { on: jest.fn((name: string, func: () => void): any => { if (name === 'finish') { diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts new file mode 100644 index 000000000..ad86eb690 --- /dev/null +++ b/test/util/TestHelpers.ts @@ -0,0 +1,211 @@ +import { EventEmitter } from 'events'; +import { promises as fs } from 'fs'; +import { IncomingHttpHeaders } from 'http'; +import { join } from 'path'; +import * as url from 'url'; +import { createResponse, MockResponse } from 'node-mocks-http'; +import streamifyArray from 'streamify-array'; +import { ResourceStore } from '../../index'; +import { PermissionSet } from '../../src/ldp/permissions/PermissionSet'; +import { HttpHandler } from '../../src/server/HttpHandler'; +import { HttpRequest } from '../../src/server/HttpRequest'; +import { call } from './Util'; + +export class AclTestHelper { + public readonly store: ResourceStore; + public id: string; + + public constructor(store: ResourceStore, id: string) { + this.store = store; + this.id = id; + } + + public async setSimpleAcl( + permissions: PermissionSet, + agentClass: 'agent' | 'authenticated', + ): Promise { + const acl: string[] = [ + '@prefix acl: .\n', + '@prefix foaf: .\n', + ' a acl:Authorization', + ]; + + for (const perm of [ 'Read', 'Append', 'Write', 'Delete' ]) { + if (permissions[perm.toLowerCase() as keyof PermissionSet]) { + acl.push(`;\n acl:mode acl:${perm}`); + } + } + acl.push(';\n acl:mode acl:Control'); + acl.push(`;\n acl:accessTo <${this.id}>`); + acl.push(`;\n acl:default <${this.id}>`); + acl.push( + `;\n acl:agentClass ${ + agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent' + }`, + ); + + acl.push('.'); + + const representation = { + binary: true, + data: streamifyArray(acl), + metadata: { + raw: [], + profiles: [], + contentType: 'text/turtle', + }, + }; + + return this.store.setRepresentation( + { path: `${this.id}.acl` }, + representation, + ); + } +} + +export class FileTestHelper { + public readonly handler: HttpHandler; + public readonly baseUrl: URL; + + public constructor(handler: HttpHandler, baseUrl: URL) { + this.handler = handler; + this.baseUrl = baseUrl; + } + + public async simpleCall( + requestUrl: URL, + method: string, + headers: IncomingHttpHeaders, + ): Promise> { + return call(this.handler, requestUrl, method, headers, []); + } + + public async callWithFile( + requestUrl: URL, + method: string, + headers: IncomingHttpHeaders, + data: Buffer, + ): Promise> { + const request = streamifyArray([ data ]) as HttpRequest; + + request.url = requestUrl.pathname; + request.method = method; + request.headers = headers; + request.headers.host = requestUrl.host; + const response: MockResponse = createResponse({ + eventEmitter: EventEmitter, + }); + + const endPromise = new Promise((resolve): void => { + response.on('end', (): void => { + expect(response._isEndCalled()).toBeTruthy(); + resolve(); + }); + }); + + await this.handler.handleSafe({ request, response }); + await endPromise; + + return response; + } + + public async createFile(fileLocation: string, slug: string, mayFail = false): Promise> { + const fileData = await fs.readFile( + join(__dirname, fileLocation), + ); + + const response: MockResponse = await this.callWithFile( + this.baseUrl, + 'POST', + { 'content-type': 'application/octet-stream', + slug, + 'transfer-encoding': 'chunked' }, + fileData, + ); + if (!mayFail) { + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toContain(url.format(this.baseUrl)); + } + return response; + } + + public async overwriteFile(fileLocation: string, requestUrl: string): Promise> { + const fileData = await fs.readFile( + join(__dirname, fileLocation), + ); + + const putUrl = new URL(requestUrl); + + const response: MockResponse = await this.callWithFile( + putUrl, + 'PUT', + { 'content-type': 'application/octet-stream', 'transfer-encoding': 'chunked' }, + fileData, + ); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toContain(url.format(putUrl)); + return response; + } + + public async getFile(requestUrl: string): Promise> { + const getUrl = new URL(requestUrl); + + return this.simpleCall(getUrl, 'GET', { accept: '*/*' }); + } + + public async deleteFile(requestUrl: string, mayFail = false): Promise> { + const deleteUrl = new URL(requestUrl); + + const response = await this.simpleCall(deleteUrl, 'DELETE', {}); + if (!mayFail) { + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toBe(url.format(requestUrl)); + } + return response; + } + + public async createFolder(slug: string): Promise> { + const response: MockResponse = await this.simpleCall( + this.baseUrl, + 'POST', + { + slug, + link: '; rel="type"', + 'content-type': 'text/plain', + 'transfer-encoding': 'chunked', + }, + ); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toContain(url.format(this.baseUrl)); + return response; + } + + public async getFolder(requestUrl: string): Promise> { + const getUrl = new URL(requestUrl); + + return await this.simpleCall(getUrl, 'GET', { accept: 'text/turtle' }); + } + + public async deleteFolder(requestUrl: string): Promise> { + const deleteUrl = new URL(requestUrl); + + const response = await this.simpleCall(deleteUrl, 'DELETE', {}); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toBe(url.format(requestUrl)); + return response; + } + + public async shouldNotExist(requestUrl: string): Promise> { + const getUrl = new URL(requestUrl); + + const response = await this.simpleCall(getUrl, 'GET', { accept: '*/*' }); + expect(response.statusCode).toBe(404); + expect(response._getData()).toContain('NotFoundHttpError'); + return response; + } +} diff --git a/test/util/Util.ts b/test/util/Util.ts index bf3726aba..d6ed16355 100644 --- a/test/util/Util.ts +++ b/test/util/Util.ts @@ -5,14 +5,21 @@ import streamifyArray from 'streamify-array'; import { HttpHandler } from '../../src/server/HttpHandler'; import { HttpRequest } from '../../src/server/HttpRequest'; -export const call = async(handler: HttpHandler, requestUrl: URL, method: string, - headers: IncomingHttpHeaders, data: string[]): Promise> => { +export const call = async( + handler: HttpHandler, + requestUrl: URL, + method: string, + headers: IncomingHttpHeaders, + data: string[], +): Promise> => { const request = streamifyArray(data) as HttpRequest; request.url = requestUrl.pathname; request.method = method; request.headers = headers; request.headers.host = requestUrl.host; - const response: MockResponse = createResponse({ eventEmitter: EventEmitter }); + const response: MockResponse = createResponse({ + eventEmitter: EventEmitter, + }); const endPromise = new Promise((resolve): void => { response.on('end', (): void => {