feat: Verify conditions in DataAccessorBasedStore

This commit is contained in:
Joachim Van Herwegen
2021-08-16 17:35:23 +02:00
parent 20f783a581
commit 0d42987bbd
7 changed files with 133 additions and 17 deletions

View File

@@ -9,6 +9,7 @@ import type { Representation } from '../../../src/ldp/representation/Representat
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
import type { DataAccessor } from '../../../src/storage/accessors/DataAccessor';
import { BasicConditions } from '../../../src/storage/BasicConditions';
import { DataAccessorBasedStore } from '../../../src/storage/DataAccessorBasedStore';
import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
@@ -17,6 +18,7 @@ import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError'
import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAllowedHttpError';
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
import { PreconditionFailedHttpError } from '../../../src/util/errors/PreconditionFailedHttpError';
import type { Guarded } from '../../../src/util/GuardedStream';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
@@ -117,8 +119,8 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
}
describe('A DataAccessorBasedStore', (): void => {
const now = new Date(1234567489);
const later = new Date(987654321);
const now = new Date(2020, 5, 12);
const later = new Date(2021, 6, 13);
let mockDate: jest.SpyInstance;
let store: DataAccessorBasedStore;
let accessor: SimpleDataAccessor;
@@ -233,6 +235,13 @@ describe('A DataAccessorBasedStore', (): void => {
await expect(result).rejects.toThrow('The given path is not a container.');
});
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
const resourceID = { path: root };
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
await expect(store.addResource(resourceID, representation, conditions))
.rejects.toThrow(PreconditionFailedHttpError);
});
it('errors when trying to create a container with non-RDF data.', async(): Promise<void> => {
const resourceID = { path: root };
representation.metadata.add(RDF.type, LDP.terms.Container);
@@ -341,6 +350,13 @@ describe('A DataAccessorBasedStore', (): void => {
await expect(prom).rejects.toThrow(ForbiddenHttpError);
});
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
const resourceID = { path: root };
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
await expect(store.setRepresentation(resourceID, representation, conditions))
.rejects.toThrow(PreconditionFailedHttpError);
});
// As discussed in #475, trimming the trailing slash of a root container in getNormalizedMetadata
// can result in undefined behaviour since there is no parent container.
it('will not trim the slash of root containers since there is no parent.', async(): Promise<void> => {
@@ -527,8 +543,33 @@ describe('A DataAccessorBasedStore', (): void => {
});
describe('modifying a Representation', (): void => {
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
const resourceID = { path: root };
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
await expect(store.modifyResource(resourceID, representation, conditions))
.rejects.toThrow(PreconditionFailedHttpError);
});
it('throws a 412 if the conditions are not matched on resources that do not exist.', async(): Promise<void> => {
const resourceID = { path: `${root}notHere` };
const conditions = new BasicConditions({ matchesETag: [ '*' ]});
await expect(store.modifyResource(resourceID, representation, conditions))
.rejects.toThrow(PreconditionFailedHttpError);
});
it('re-throws the error if something goes wrong accessing the metadata.', async(): Promise<void> => {
accessor.getMetadata = jest.fn(async(): Promise<any> => {
throw new Error('error');
});
const resourceID = { path: root };
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
await expect(store.modifyResource(resourceID, representation, conditions))
.rejects.toThrow('error');
});
it('is not supported.', async(): Promise<void> => {
const result = store.modifyResource();
const result = store.modifyResource({ path: root }, representation);
await expect(result).rejects.toThrow(NotImplementedHttpError);
await expect(result).rejects.toThrow('Patches are not supported by the default store.');
});
@@ -569,6 +610,13 @@ describe('A DataAccessorBasedStore', (): void => {
await expect(result).rejects.toThrow('Can only delete empty containers.');
});
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
const resourceID = { path: root };
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
await expect(store.deleteResource(resourceID, conditions))
.rejects.toThrow(PreconditionFailedHttpError);
});
it('will delete resources.', async(): Promise<void> => {
accessor.data[`${root}resource`] = representation;
await expect(store.deleteResource({ path: `${root}resource` })).resolves.toEqual([

View File

@@ -2,6 +2,7 @@ import type { Patch } from '../../../src/ldp/http/Patch';
import type { PatchHandler } from '../../../src/storage/patch/PatchHandler';
import { PatchingStore } from '../../../src/storage/PatchingStore';
import type { ResourceStore } from '../../../src/storage/ResourceStore';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
describe('A PatchingStore', (): void => {
let store: PatchingStore;
@@ -26,15 +27,25 @@ describe('A PatchingStore', (): void => {
expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, {}, undefined);
});
it('calls its patcher if modifyResource failed.', async(): Promise<void> => {
it('calls its patcher if modifyResource is not implemented.', async(): Promise<void> => {
source.modifyResource = jest.fn(async(): Promise<any> => {
throw new Error('dummy');
throw new NotImplementedHttpError();
});
await expect(store.modifyResource({ path: 'modifyPath' }, {} as Patch)).resolves.toBe('patcher');
expect(source.modifyResource).toHaveBeenCalledTimes(1);
expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, {}, undefined);
await expect((source.modifyResource as jest.Mock).mock.results[0].value).rejects.toThrow('dummy');
await expect((source.modifyResource as jest.Mock).mock.results[0].value).rejects.toThrow(NotImplementedHttpError);
expect(handleSafeFn).toHaveBeenCalledTimes(1);
expect(handleSafeFn).toHaveBeenLastCalledWith({ source, identifier: { path: 'modifyPath' }, patch: {}});
});
it('rethrows source modifyResource errors.', async(): Promise<void> => {
source.modifyResource = jest.fn(async(): Promise<any> => {
throw new Error('dummy');
});
await expect(store.modifyResource({ path: 'modifyPath' }, {} as Patch)).rejects.toThrow('dummy');
expect(source.modifyResource).toHaveBeenCalledTimes(1);
expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, {}, undefined);
expect(handleSafeFn).toHaveBeenCalledTimes(0);
});
});

View File

@@ -7,6 +7,7 @@ import { InternalServerError } from '../../../../src/util/errors/InternalServerE
import { MethodNotAllowedHttpError } from '../../../../src/util/errors/MethodNotAllowedHttpError';
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { PreconditionFailedHttpError } from '../../../../src/util/errors/PreconditionFailedHttpError';
import { UnauthorizedHttpError } from '../../../../src/util/errors/UnauthorizedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
@@ -25,7 +26,7 @@ describe('HttpError', (): void => {
[ 'NotFoundHttpError', 404, NotFoundHttpError ],
[ 'MethodNotAllowedHttpError', 405, MethodNotAllowedHttpError ],
[ 'ConflictHttpError', 409, ConflictHttpError ],
[ 'MethodNotAllowedHttpError', 405, MethodNotAllowedHttpError ],
[ 'PreconditionFailedHttpError', 412, PreconditionFailedHttpError ],
[ 'UnsupportedMediaTypeHttpError', 415, UnsupportedMediaTypeHttpError ],
[ 'InternalServerError', 500, InternalServerError ],
[ 'NotImplementedHttpError', 501, NotImplementedHttpError ],