From 038d5728e306248057c3a8d3782050328de618e8 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Mon, 7 Dec 2020 17:25:55 +0100 Subject: [PATCH] feat: Add read-only store. Closes https://github.com/solid/community-server/issues/193 Closes https://github.com/solid/community-server/issues/323 --- index.ts | 3 +- src/storage/ReadOnlyStore.ts | 35 +++++++++++++++++ test/unit/storage/ReadOnlyStore.test.ts | 50 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/storage/ReadOnlyStore.ts create mode 100644 test/unit/storage/ReadOnlyStore.test.ts diff --git a/index.ts b/index.ts index 9e219723b..5c42ec6f0 100644 --- a/index.ts +++ b/index.ts @@ -148,14 +148,15 @@ export * from './src/storage/routing/RouterRule'; export * from './src/storage/AtomicResourceStore'; export * from './src/storage/Conditions'; export * from './src/storage/DataAccessorBasedStore'; -export * from './src/storage/mapping/FileIdentifierMapper'; export * from './src/storage/LockingResourceStore'; export * from './src/storage/MonitoringStore'; export * from './src/storage/PassthroughStore'; export * from './src/storage/PatchingStore'; +export * from './src/storage/ReadOnlyStore'; export * from './src/storage/RepresentationConvertingStore'; export * from './src/storage/ResourceStore'; export * from './src/storage/RoutingResourceStore'; +export * from './src/storage/mapping/FileIdentifierMapper'; // Util/Errors export * from './src/util/errors/BadRequestHttpError'; diff --git a/src/storage/ReadOnlyStore.ts b/src/storage/ReadOnlyStore.ts new file mode 100644 index 000000000..86de0dce8 --- /dev/null +++ b/src/storage/ReadOnlyStore.ts @@ -0,0 +1,35 @@ +import type { Patch } from '../ldp/http/Patch'; +import type { Representation } from '../ldp/representation/Representation'; +import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; +import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError'; +import type { Conditions } from './Conditions'; +import { PassthroughStore } from './PassthroughStore'; +import type { ResourceStore } from './ResourceStore'; + +/** + * Store that only allow read operations on the underlying source. + */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +export class ReadOnlyStore extends PassthroughStore { + public constructor(source: T) { + super(source); + } + + public async addResource(container: ResourceIdentifier, representation: Representation, + conditions?: Conditions): Promise { + throw new ForbiddenHttpError(); + } + + public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise { + throw new ForbiddenHttpError(); + } + + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise { + throw new ForbiddenHttpError(); + } + + public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, + conditions?: Conditions): Promise { + throw new ForbiddenHttpError(); + } +} diff --git a/test/unit/storage/ReadOnlyStore.test.ts b/test/unit/storage/ReadOnlyStore.test.ts new file mode 100644 index 000000000..03fc794af --- /dev/null +++ b/test/unit/storage/ReadOnlyStore.test.ts @@ -0,0 +1,50 @@ +import type { Patch } from '../../../src/ldp/http/Patch'; +import type { Representation } from '../../../src/ldp/representation/Representation'; +import { ReadOnlyStore } from '../../../src/storage/ReadOnlyStore'; +import type { ResourceStore } from '../../../src/storage/ResourceStore'; +import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError'; + +describe('A ReadOnlyStore', (): void => { + const source: jest.Mocked = { + getRepresentation: jest.fn(async(): Promise => 'get'), + addResource: jest.fn(), + setRepresentation: jest.fn(), + deleteResource: jest.fn(), + modifyResource: jest.fn(), + } as any; + let store: ReadOnlyStore; + + beforeAll((): void => { + store = new ReadOnlyStore(source); + }); + + it('calls getRepresentation directly from the source.', async(): Promise => { + await expect(store.getRepresentation({ path: 'getPath' }, {})).resolves.toBe('get'); + expect(source.getRepresentation).toHaveBeenCalledTimes(1); + expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: 'getPath' }, {}, undefined); + }); + + it('throws an error when calling addResource.', async(): Promise => { + await expect(store.addResource({ path: 'addPath' }, {} as Representation)) + .rejects.toThrow(ForbiddenHttpError); + expect(source.addResource).toHaveBeenCalledTimes(0); + }); + + it('throws an error when calling setRepresentation.', async(): Promise => { + await expect(store.setRepresentation({ path: 'setPath' }, {} as Representation)) + .rejects.toThrow(ForbiddenHttpError); + expect(source.setRepresentation).toHaveBeenCalledTimes(0); + }); + + it('throws an error when calling deleteResource.', async(): Promise => { + await expect(store.deleteResource({ path: 'deletePath' })) + .rejects.toThrow(ForbiddenHttpError); + expect(source.deleteResource).toHaveBeenCalledTimes(0); + }); + + it('throws an error when calling modifyResource.', async(): Promise => { + await expect(store.modifyResource({ path: 'modifyPath' }, {} as Patch)) + .rejects.toThrow(ForbiddenHttpError); + expect(source.modifyResource).toHaveBeenCalledTimes(0); + }); +});