From b896004bac421a3999eeb2db529025333ec03002 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 1 Oct 2020 11:38:29 +0200 Subject: [PATCH] feat: Create InMemoryDataAccessor --- index.ts | 1 + src/storage/accessors/InMemoryDataAccessor.ts | 170 ++++++++++++++++++ ...> AuthenticatedDataAccessorBasedConfig.ts} | 15 +- ...orConfig.ts => DataAccessorBasedConfig.ts} | 17 +- test/configs/Util.ts | 12 +- .../AuthenticatedFileBasedStore.test.ts | 12 +- ...tore.test.ts => FullConfig.noAuth.test.ts} | 20 ++- .../accessors/InMemoryDataAccessor.test.ts | 156 ++++++++++++++++ test/util/TestHelpers.ts | 4 +- 9 files changed, 380 insertions(+), 27 deletions(-) create mode 100644 src/storage/accessors/InMemoryDataAccessor.ts rename test/configs/{AuthenticatedFileBasedDataAccessorConfig.ts => AuthenticatedDataAccessorBasedConfig.ts} (83%) rename test/configs/{FileBasedDataAccessorConfig.ts => DataAccessorBasedConfig.ts} (82%) rename test/integration/{FileBasedStore.test.ts => FullConfig.noAuth.test.ts} (87%) create mode 100644 test/unit/storage/accessors/InMemoryDataAccessor.test.ts diff --git a/index.ts b/index.ts index e7482de31..c75d35f92 100644 --- a/index.ts +++ b/index.ts @@ -83,6 +83,7 @@ export * from './src/server/HttpResponse'; // Storage/Accessors export * from './src/storage/accessors/DataAccessor'; export * from './src/storage/accessors/FileDataAccessor'; +export * from './src/storage/accessors/InMemoryDataAccessor'; // Storage/Conversion export * from './src/storage/conversion/ChainedConverter'; diff --git a/src/storage/accessors/InMemoryDataAccessor.ts b/src/storage/accessors/InMemoryDataAccessor.ts new file mode 100644 index 000000000..495303e27 --- /dev/null +++ b/src/storage/accessors/InMemoryDataAccessor.ts @@ -0,0 +1,170 @@ +import { Readable } from 'stream'; +import arrayifyStream from 'arrayify-stream'; +import { DataFactory } from 'n3'; +import type { NamedNode } from 'rdf-js'; +import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; +import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; +import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; +import type { MetadataController } from '../../util/MetadataController'; +import { ensureTrailingSlash } from '../../util/Util'; +import type { DataAccessor } from './DataAccessor'; + +interface DataEntry { + data: any[]; + metadata: RepresentationMetadata; +} +interface ContainerEntry { + entries: { [name: string]: CacheEntry }; + metadata: RepresentationMetadata; +} +type CacheEntry = DataEntry | ContainerEntry; + +class ArrayReadable extends Readable { + private readonly data: any[]; + private idx: number; + + public constructor(data: any[]) { + super({ objectMode: true }); + this.data = data; + this.idx = 0; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + public _read(): void { + if (this.idx < this.data.length) { + this.push(this.data[this.idx]); + this.idx += 1; + } else { + this.push(null); + } + } +} + +export class InMemoryDataAccessor implements DataAccessor { + private readonly base: string; + private readonly store: ContainerEntry; + private readonly metadataController: MetadataController; + + public constructor(base: string, metadataController: MetadataController) { + this.base = ensureTrailingSlash(base); + this.metadataController = metadataController; + + const metadata = new RepresentationMetadata(this.base); + metadata.addQuads(this.metadataController.generateResourceQuads(DataFactory.namedNode(this.base), true)); + this.store = { entries: {}, metadata }; + } + + public async canHandle(): Promise { + // All data is supported since streams never get read, only copied + } + + public async getData(identifier: ResourceIdentifier): Promise { + const entry = this.getEntry(identifier); + if (!this.isDataEntry(entry)) { + throw new NotFoundHttpError(); + } + return new ArrayReadable(entry.data); + } + + public async getMetadata(identifier: ResourceIdentifier): Promise { + const entry = this.getEntry(identifier); + if (this.isDataEntry(entry) === identifier.path.endsWith('/')) { + throw new NotFoundHttpError(); + } + return this.generateMetadata(identifier, entry); + } + + public async writeDocument(identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata): + Promise { + const { parent, name } = this.getParentEntry(identifier); + parent.entries[name] = { + // Drain original stream and create copy + data: await arrayifyStream(data), + metadata, + }; + } + + public async writeContainer(identifier: ResourceIdentifier, metadata: RepresentationMetadata): Promise { + try { + // Overwrite existing metadata but keep children if container already exists + const entry = this.getEntry(identifier); + entry.metadata = metadata; + } catch (error: unknown) { + // Create new entry if it didn't exist yet + if (error instanceof NotFoundHttpError) { + const { parent, name } = this.getParentEntry(identifier); + parent.entries[name] = { + entries: {}, + metadata, + }; + } else { + throw error; + } + } + } + + public async deleteResource(identifier: ResourceIdentifier): Promise { + const { parent, name } = this.getParentEntry(identifier); + if (!parent.entries[name]) { + throw new NotFoundHttpError(); + } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete parent.entries[name]; + } + + private isDataEntry(entry: CacheEntry): entry is DataEntry { + return Boolean((entry as DataEntry).data); + } + + private getParentEntry(identifier: ResourceIdentifier): { parent: ContainerEntry; name: string } { + const parts = identifier.path.slice(this.base.length).split('/').filter((part): boolean => part.length > 0); + + if (parts.length === 0) { + throw new Error('Root container has no parent.'); + } + + // Name of the resource will be the last entry in the path + const name = parts[parts.length - 1]; + + // All names preceding the last should be nested containers + const containers = parts.slice(0, -1); + + // Step through the parts of the path up to the end + let parent = this.store; + for (const container of containers) { + const child = parent.entries[container]; + if (!child) { + throw new NotFoundHttpError(); + } else if (this.isDataEntry(child)) { + throw new Error('Invalid path.'); + } + parent = child; + } + + return { parent, name }; + } + + private getEntry(identifier: ResourceIdentifier): CacheEntry { + if (identifier.path === this.base) { + return this.store; + } + const { parent, name } = this.getParentEntry(identifier); + const entry = parent.entries[name]; + if (!entry) { + throw new NotFoundHttpError(); + } + return entry; + } + + private generateMetadata(identifier: ResourceIdentifier, entry: CacheEntry): RepresentationMetadata { + const metadata = new RepresentationMetadata(entry.metadata); + if (!this.isDataEntry(entry)) { + const childNames = Object.keys(entry.entries).map((name): string => + `${identifier.path}${name}${this.isDataEntry(entry.entries[name]) ? '' : '/'}`); + const quads = this.metadataController + .generateContainerContainsResourceQuads(metadata.identifier as NamedNode, childNames); + metadata.addQuads(quads); + } + return metadata; + } +} diff --git a/test/configs/AuthenticatedFileBasedDataAccessorConfig.ts b/test/configs/AuthenticatedDataAccessorBasedConfig.ts similarity index 83% rename from test/configs/AuthenticatedFileBasedDataAccessorConfig.ts rename to test/configs/AuthenticatedDataAccessorBasedConfig.ts index c3ea60929..a39c77887 100644 --- a/test/configs/AuthenticatedFileBasedDataAccessorConfig.ts +++ b/test/configs/AuthenticatedDataAccessorBasedConfig.ts @@ -1,5 +1,8 @@ -import type { HttpHandler, - ResourceStore } from '../../index'; +import type { + DataAccessor, + HttpHandler, + ResourceStore, +} from '../../index'; import { AuthenticatedLdpHandler, BasicResponseWriter, @@ -15,7 +18,7 @@ import { getBasicRequestParser, getOperationHandler, getWebAclAuthorizer, - getFileDataAccessorStore, + getDataAccessorStore, } from './Util'; /** @@ -24,14 +27,14 @@ import { * - a FileResourceStore wrapped in a converting store (rdf to quad & quad to rdf) * - GET, POST, PUT & DELETE operation handlers */ -export class AuthenticatedFileBasedDataAccessorConfig implements ServerConfig { +export class AuthenticatedDataAccessorBasedConfig implements ServerConfig { public base: string; public store: ResourceStore; - public constructor(base: string, rootFilepath: string) { + public constructor(base: string, dataAccessor: DataAccessor) { this.base = base; this.store = getConvertingStore( - getFileDataAccessorStore(base, rootFilepath), + getDataAccessorStore(base, dataAccessor), [ new QuadToRdfConverter(), new RdfToQuadConverter() ], ); diff --git a/test/configs/FileBasedDataAccessorConfig.ts b/test/configs/DataAccessorBasedConfig.ts similarity index 82% rename from test/configs/FileBasedDataAccessorConfig.ts rename to test/configs/DataAccessorBasedConfig.ts index ff0a61bfa..3de1f0182 100644 --- a/test/configs/FileBasedDataAccessorConfig.ts +++ b/test/configs/DataAccessorBasedConfig.ts @@ -1,5 +1,8 @@ -import type { HttpHandler, - ResourceStore } from '../../index'; +import type { + DataAccessor, + HttpHandler, + ResourceStore, +} from '../../index'; import { AllowEverythingAuthorizer, AuthenticatedLdpHandler, @@ -16,21 +19,21 @@ import { getOperationHandler, getConvertingStore, getBasicRequestParser, - getFileDataAccessorStore, + getDataAccessorStore, } from './Util'; /** - * FileBasedDataAccessorConfig works with + * DataAccessorBasedConfig 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 { +export class DataAccessorBasedConfig implements ServerConfig { public store: ResourceStore; - public constructor(base: string, rootFilepath: string) { + public constructor(base: string, dataAccessor: DataAccessor) { this.store = getConvertingStore( - getFileDataAccessorStore(base, rootFilepath), + getDataAccessorStore(base, dataAccessor), [ new QuadToRdfConverter(), new RdfToQuadConverter() ], ); } diff --git a/test/configs/Util.ts b/test/configs/Util.ts index 5c92073cc..335d1c62a 100644 --- a/test/configs/Util.ts +++ b/test/configs/Util.ts @@ -1,9 +1,12 @@ import { join } from 'path'; -import type { BodyParser, +import type { + BodyParser, + DataAccessor, Operation, RepresentationConverter, ResourceStore, - ResponseDescription } from '../../index'; + ResponseDescription, +} from '../../index'; import { AcceptPreferenceParser, BasicMetadataExtractor, @@ -14,7 +17,6 @@ import { DataAccessorBasedStore, DeleteOperationHandler, ExtensionBasedMapper, - FileDataAccessor, FileResourceStore, GetOperationHandler, HeadOperationHandler, @@ -65,9 +67,9 @@ export const getFileResourceStore = (base: string, rootFilepath: string): FileRe * * @returns The data accessor based store. */ -export const getFileDataAccessorStore = (base: string, rootFilepath: string): DataAccessorBasedStore => +export const getDataAccessorStore = (base: string, dataAccessor: DataAccessor): DataAccessorBasedStore => new DataAccessorBasedStore( - new FileDataAccessor(new ExtensionBasedMapper(base, rootFilepath), new MetadataController()), + dataAccessor, base, new MetadataController(), new UrlContainerManager(base), diff --git a/test/integration/AuthenticatedFileBasedStore.test.ts b/test/integration/AuthenticatedFileBasedStore.test.ts index b16567ebf..2204c7cfb 100644 --- a/test/integration/AuthenticatedFileBasedStore.test.ts +++ b/test/integration/AuthenticatedFileBasedStore.test.ts @@ -1,20 +1,24 @@ import { copyFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import * as rimraf from 'rimraf'; +import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor'; +import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper'; +import { MetadataController } from '../../src/util/MetadataController'; import { ensureTrailingSlash } from '../../src/util/Util'; -import { AuthenticatedFileBasedDataAccessorConfig } from '../configs/AuthenticatedFileBasedDataAccessorConfig'; +import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig'; import { AuthenticatedFileResourceStoreConfig } from '../configs/AuthenticatedFileResourceStoreConfig'; import type { ServerConfig } from '../configs/ServerConfig'; import { BASE, getRootFilePath } from '../configs/Util'; import { AclTestHelper, FileTestHelper } from '../util/TestHelpers'; const fileResourceStore: [string, (rootFilePath: string) => ServerConfig] = [ - 'FileResourceStore', + 'AuthenticatedFileResourceStore', (rootFilePath: string): ServerConfig => new AuthenticatedFileResourceStoreConfig(BASE, rootFilePath), ]; const dataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [ - 'FileDataAccessorBasedStore', - (rootFilePath: string): ServerConfig => new AuthenticatedFileBasedDataAccessorConfig(BASE, rootFilePath), + 'AuthenticatedFileDataAccessorBasedStore', + (rootFilePath: string): ServerConfig => new AuthenticatedDataAccessorBasedConfig(BASE, + new FileDataAccessor(new ExtensionBasedMapper(BASE, rootFilePath), new MetadataController())), ]; describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (name, configFn): void => { diff --git a/test/integration/FileBasedStore.test.ts b/test/integration/FullConfig.noAuth.test.ts similarity index 87% rename from test/integration/FileBasedStore.test.ts rename to test/integration/FullConfig.noAuth.test.ts index 908d90827..1582a4a4a 100644 --- a/test/integration/FileBasedStore.test.ts +++ b/test/integration/FullConfig.noAuth.test.ts @@ -1,7 +1,11 @@ import { mkdirSync } from 'fs'; import * as rimraf from 'rimraf'; import type { HttpHandler } from '../../src/server/HttpHandler'; -import { FileBasedDataAccessorConfig } from '../configs/FileBasedDataAccessorConfig'; +import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor'; +import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor'; +import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper'; +import { MetadataController } from '../../src/util/MetadataController'; +import { DataAccessorBasedConfig } from '../configs/DataAccessorBasedConfig'; import { FileResourceStoreConfig } from '../configs/FileResourceStoreConfig'; import type { ServerConfig } from '../configs/ServerConfig'; import { BASE, getRootFilePath } from '../configs/Util'; @@ -11,12 +15,20 @@ const fileResourceStore: [string, (rootFilePath: string) => ServerConfig] = [ 'FileResourceStore', (rootFilePath: string): ServerConfig => new FileResourceStoreConfig(BASE, rootFilePath), ]; -const dataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [ +const fileDataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [ 'FileDataAccessorBasedStore', - (rootFilePath: string): ServerConfig => new FileBasedDataAccessorConfig(BASE, rootFilePath), + (rootFilePath: string): ServerConfig => new DataAccessorBasedConfig(BASE, + new FileDataAccessor(new ExtensionBasedMapper(BASE, rootFilePath), new MetadataController())), +]; +const inMemoryDataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [ + 'InMemoryDataAccessorBasedStore', + (): ServerConfig => new DataAccessorBasedConfig(BASE, + new InMemoryDataAccessor(BASE, new MetadataController())), ]; -describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (name, configFn): void => { +const configs = [ fileResourceStore, fileDataAccessorStore, inMemoryDataAccessorStore ]; + +describe.each(configs)('A server using a %s', (name, configFn): void => { describe('without acl', (): void => { let rootFilePath: string; let config: ServerConfig; diff --git a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts new file mode 100644 index 000000000..01fb8b76f --- /dev/null +++ b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts @@ -0,0 +1,156 @@ +import streamifyArray from 'streamify-array'; +import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; +import { InMemoryDataAccessor } from '../../../../src/storage/accessors/InMemoryDataAccessor'; +import { APPLICATION_OCTET_STREAM } from '../../../../src/util/ContentTypes'; +import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; +import { MetadataController } from '../../../../src/util/MetadataController'; +import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants'; +import { toNamedNode } from '../../../../src/util/UriUtil'; +import { readableToString } from '../../../../src/util/Util'; + +describe('An InMemoryDataAccessor', (): void => { + const base = 'http://test.com/'; + let accessor: InMemoryDataAccessor; + let metadata: RepresentationMetadata; + + beforeEach(async(): Promise => { + accessor = new InMemoryDataAccessor( + base, + new MetadataController(), + ); + + metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM }); + }); + + it('can only handle all data.', async(): Promise => { + await expect(accessor.canHandle()).resolves.toBeUndefined(); + }); + + describe('reading and writing data', (): void => { + it('throws a 404 if the identifier does not match an existing data resource.', async(): Promise => { + await expect(accessor.getData({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError); + await expect(accessor.getData({ path: `${base}container/resource` })).rejects.toThrow(NotFoundHttpError); + }); + + it('throws a 404 if the identifier matches a container.', async(): Promise => { + await expect(accessor.getData({ path: base })).rejects.toThrow(NotFoundHttpError); + }); + + it('throws an error if part of the path matches a data resource.', async(): Promise => { + await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); + await expect(accessor.getData({ path: `${base}resource/resource2` })).rejects.toThrow(new Error('Invalid path.')); + }); + + it('returns the corresponding data every time.', async(): Promise => { + const data = streamifyArray([ 'data' ]); + await accessor.writeDocument({ path: `${base}resource` }, data, metadata); + + // Run twice to make sure the data is stored correctly + await expect(readableToString(await accessor.getData({ path: `${base}resource` }))).resolves.toBe('data'); + await expect(readableToString(await accessor.getData({ path: `${base}resource` }))).resolves.toBe('data'); + }); + }); + + describe('reading and writing metadata', (): void => { + it('throws a 404 if the identifier does not match an existing data resource.', async(): Promise => { + await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError); + }); + + it('errors when trying to access the parent of root.', async(): Promise => { + await expect(accessor.writeDocument({ path: `${base}` }, streamifyArray([ 'data' ]), metadata)) + .rejects.toThrow(new Error('Root container has no parent.')); + }); + + it('throws a 404 if the trailing slash does not match its type.', async(): Promise => { + await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); + await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError); + await accessor.writeContainer({ path: `${base}container/` }, metadata); + await expect(accessor.getMetadata({ path: `${base}container` })).rejects.toThrow(NotFoundHttpError); + }); + + it('returns empty metadata if there was none stored.', async(): Promise => { + metadata = new RepresentationMetadata(); + await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); + metadata = await accessor.getMetadata({ path: `${base}resource` }); + expect(metadata.quads()).toHaveLength(0); + }); + + it('generates the containment metadata for a container.', async(): Promise => { + await accessor.writeContainer({ path: `${base}container/` }, metadata); + await accessor.writeDocument({ path: `${base}container/resource` }, streamifyArray([ 'data' ]), metadata); + await accessor.writeContainer({ path: `${base}container/container2` }, metadata); + metadata = await accessor.getMetadata({ path: `${base}container/` }); + expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray( + [ toNamedNode(`${base}container/resource`), toNamedNode(`${base}container/container2/`) ], + ); + }); + + it('adds stored metadata when requesting data resource metadata.', async(): Promise => { + const inputMetadata = new RepresentationMetadata(`${base}resource`, { [RDF.type]: toNamedNode(LDP.Resource) }); + await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), inputMetadata); + metadata = await accessor.getMetadata({ path: `${base}resource` }); + expect(metadata.identifier.value).toBe(`${base}resource`); + const quads = metadata.quads(); + expect(quads).toHaveLength(1); + expect(quads[0].object.value).toBe(LDP.Resource); + }); + + it('adds stored metadata when requesting container metadata.', async(): Promise => { + const inputMetadata = new RepresentationMetadata(`${base}container/`, { [RDF.type]: toNamedNode(LDP.Container) }); + await accessor.writeContainer({ path: `${base}container/` }, inputMetadata); + + metadata = await accessor.getMetadata({ path: `${base}container/` }); + expect(metadata.identifier.value).toBe(`${base}container/`); + const quads = metadata.quads(); + expect(quads).toHaveLength(1); + expect(quads[0].object.value).toBe(LDP.Container); + }); + + it('can overwrite the metadata of an existing container without overwriting children.', async(): Promise => { + const inputMetadata = new RepresentationMetadata(`${base}container/`, { [RDF.type]: toNamedNode(LDP.Container) }); + await accessor.writeContainer({ path: `${base}container/` }, inputMetadata); + const resourceMetadata = new RepresentationMetadata(); + await accessor.writeDocument( + { path: `${base}container/resource` }, streamifyArray([ 'data' ]), resourceMetadata, + ); + + const newMetadata = new RepresentationMetadata(inputMetadata); + newMetadata.add(RDF.type, toNamedNode(LDP.BasicContainer)); + await accessor.writeContainer({ path: `${base}container/` }, newMetadata); + + metadata = await accessor.getMetadata({ path: `${base}container/` }); + expect(metadata.identifier.value).toBe(`${base}container/`); + const quads = metadata.quads(); + expect(quads).toHaveLength(3); + expect(metadata.getAll(RDF.type).map((term): string => term.value)) + .toEqual([ LDP.Container, LDP.BasicContainer ]); + expect(metadata.get(LDP.contains)?.value).toEqual(`${base}container/resource`); + + await expect(accessor.getMetadata({ path: `${base}container/resource` })) + .resolves.toBeInstanceOf(RepresentationMetadata); + expect(await readableToString(await accessor.getData({ path: `${base}container/resource` }))).toBe('data'); + }); + + it('errors when writing to an invalid container path..', async(): Promise => { + await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); + + await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata)) + .rejects.toThrow(new Error('Invalid path.')); + }); + }); + + describe('deleting a resource', (): void => { + it('throws a 404 if the identifier does not match an existing entry.', async(): Promise => { + await expect(accessor.deleteResource({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError); + }); + + it('removes the corresponding resource.', async(): Promise => { + await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); + await accessor.writeContainer({ path: `${base}container/` }, metadata); + await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined(); + await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError); + await expect(accessor.getMetadata({ path: `${base}container/` })).rejects.toThrow(NotFoundHttpError); + }); + }); +}); diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts index d330f26d0..6595d49ad 100644 --- a/test/util/TestHelpers.ts +++ b/test/util/TestHelpers.ts @@ -153,7 +153,9 @@ export class FileTestHelper { public async getFile(requestUrl: string): Promise> { const getUrl = new URL(requestUrl); - return this.simpleCall(getUrl, 'GET', { accept: '*/*' }); + const response = await this.simpleCall(getUrl, 'GET', { accept: '*/*' }); + expect(response.statusCode).toBe(200); + return response; } public async deleteFile(requestUrl: string, mayFail = false): Promise> {