mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create InMemoryDataAccessor
This commit is contained in:
1
index.ts
1
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';
|
||||
|
||||
170
src/storage/accessors/InMemoryDataAccessor.ts
Normal file
170
src/storage/accessors/InMemoryDataAccessor.ts
Normal file
@@ -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<void> {
|
||||
// All data is supported since streams never get read, only copied
|
||||
}
|
||||
|
||||
public async getData(identifier: ResourceIdentifier): Promise<Readable> {
|
||||
const entry = this.getEntry(identifier);
|
||||
if (!this.isDataEntry(entry)) {
|
||||
throw new NotFoundHttpError();
|
||||
}
|
||||
return new ArrayReadable(entry.data);
|
||||
}
|
||||
|
||||
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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() ],
|
||||
);
|
||||
@@ -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() ],
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
156
test/unit/storage/accessors/InMemoryDataAccessor.test.ts
Normal file
156
test/unit/storage/accessors/InMemoryDataAccessor.test.ts
Normal file
@@ -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<void> => {
|
||||
accessor = new InMemoryDataAccessor(
|
||||
base,
|
||||
new MetadataController(),
|
||||
);
|
||||
|
||||
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM });
|
||||
});
|
||||
|
||||
it('can only handle all data.', async(): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
await expect(accessor.getData({ path: base })).rejects.toThrow(NotFoundHttpError);
|
||||
});
|
||||
|
||||
it('throws an error if part of the path matches a data resource.', async(): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError);
|
||||
});
|
||||
|
||||
it('errors when trying to access the parent of root.', async(): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
await expect(accessor.deleteResource({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError);
|
||||
});
|
||||
|
||||
it('removes the corresponding resource.', async(): Promise<void> => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -153,7 +153,9 @@ export class FileTestHelper {
|
||||
public async getFile(requestUrl: string): Promise<MockResponse<any>> {
|
||||
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<MockResponse<any>> {
|
||||
|
||||
Reference in New Issue
Block a user