feat: Emit container pub event on PUT.

Closes https://github.com/solid/community-server/issues/612
This commit is contained in:
Ruben Verborgh
2021-02-23 15:41:23 +01:00
committed by Joachim Van Herwegen
parent 6edc255707
commit c3cff553e3
15 changed files with 91 additions and 129 deletions

View File

@@ -68,14 +68,20 @@ describe('A server with the Solid WebSockets API behind a proxy', (): void => {
]);
});
describe('when the client subscribes to a resource', (): void => {
describe('when the client subscribes to resources', (): void => {
beforeAll(async(): Promise<void> => {
client.send(`sub https://example.pod/my-resource`);
client.send('sub https://example.pod/my-resource');
client.send('sub https://example.pod/other-resource');
client.send('sub https://example.pod/');
await new Promise((resolve): any => client.once('message', resolve));
});
it('acknowledges the subscription.', async(): Promise<void> => {
expect(messages).toEqual([ `ack https://example.pod/my-resource` ]);
expect(messages).toEqual([
'ack https://example.pod/my-resource',
'ack https://example.pod/other-resource',
'ack https://example.pod/',
]);
});
it('notifies the client of resource updates.', async(): Promise<void> => {
@@ -87,7 +93,10 @@ describe('A server with the Solid WebSockets API behind a proxy', (): void => {
},
body: '{}',
});
expect(messages).toEqual([ `pub https://example.pod/my-resource` ]);
expect(messages).toEqual([
'pub https://example.pod/',
'pub https://example.pod/my-resource',
]);
});
});
});

View File

@@ -40,11 +40,10 @@ class SimpleDataAccessor implements DataAccessor {
}
}
public async deleteResource(identifier: ResourceIdentifier): Promise<ResourceIdentifier[]> {
public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
this.checkExists(identifier);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.data[identifier.path];
return [ identifier ];
}
public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
@@ -563,12 +562,11 @@ describe('A DataAccessorBasedStore', (): void => {
accessor.data[`${root}resource`] = representation;
accessor.data[`${root}resource.dummy`] = representation;
const deleteFn = accessor.deleteResource;
accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise<ResourceIdentifier[]> => {
accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise<void> => {
if (auxStrategy.isAuxiliaryIdentifier(identifier)) {
throw new Error('auxiliary error!');
}
await deleteFn.call(accessor, identifier);
return [ identifier ];
});
const { logger } = store as any;
logger.error = jest.fn();
@@ -587,12 +585,11 @@ describe('A DataAccessorBasedStore', (): void => {
accessor.data[`${root}resource`] = representation;
accessor.data[`${root}resource.dummy`] = representation;
const deleteFn = accessor.deleteResource;
accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise<ResourceIdentifier[]> => {
accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise<void> => {
if (auxStrategy.isAuxiliaryIdentifier(identifier)) {
throw 'auxiliary error!';
}
await deleteFn.call(accessor, identifier);
return [ identifier ];
});
const { logger } = store as any;
logger.error = jest.fn();

View File

@@ -2,23 +2,25 @@ import type { Patch } from '../../../src/ldp/http/Patch';
import type { Representation } from '../../../src/ldp/representation/Representation';
import { MonitoringStore } from '../../../src/storage/MonitoringStore';
import type { ResourceStore } from '../../../src/storage/ResourceStore';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
describe('A MonitoringStore', (): void => {
let store: MonitoringStore;
let source: ResourceStore;
const identifierStrategy = new SingleRootIdentifierStrategy('http://example.org/');
let changedCallback: () => void;
const modified = [
{ path: 'http://example.org/modified/1' },
{ path: 'http://example.org/modified/2' },
];
beforeEach(async(): Promise<void> => {
source = {
getRepresentation: jest.fn(async(): Promise<any> => ({ success: true })),
addResource: jest.fn(async(): Promise<any> => ({ path: 'http://example.org/foo/bar/new' })),
setRepresentation: jest.fn(async(): Promise<any> => undefined),
deleteResource: jest.fn(async(): Promise<any> => undefined),
modifyResource: jest.fn(async(): Promise<any> => undefined),
setRepresentation: jest.fn(async(): Promise<any> => modified),
deleteResource: jest.fn(async(): Promise<any> => modified),
modifyResource: jest.fn(async(): Promise<any> => modified),
};
store = new MonitoringStore(source, identifierStrategy);
store = new MonitoringStore(source);
changedCallback = jest.fn();
store.on('changed', changedCallback);
});
@@ -33,7 +35,7 @@ describe('A MonitoringStore', (): void => {
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: 'getPath' }, {}, undefined);
});
it('does not fire a change event after completing getRepresentation.', async(): Promise<void> => {
it('does not fire a change event after getRepresentation.', async(): Promise<void> => {
expect(changedCallback).toHaveBeenCalledTimes(0);
await store.getRepresentation({ path: 'http://example.org/foo/bar' }, {});
expect(changedCallback).toHaveBeenCalledTimes(0);
@@ -46,65 +48,60 @@ describe('A MonitoringStore', (): void => {
expect(source.addResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
});
it('fires resource and container change events after completing addResource.', async(): Promise<void> => {
const result = store.addResource({ path: 'http://example.org/foo/bar' }, {} as Representation);
it('fires container and resource change events after addResource.', async(): Promise<void> => {
const result = store.addResource({ path: 'http://example.org/foo/bar/' }, {} as Representation);
expect(changedCallback).toHaveBeenCalledTimes(0);
await result;
expect(changedCallback).toHaveBeenCalledTimes(2);
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar' });
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
});
it('calls setRepresentation directly from the source.', async(): Promise<void> => {
await expect(store.setRepresentation({ path: 'http://example.org/foo/bar' }, {} as Representation))
.resolves.toBeUndefined();
.resolves.toEqual(modified);
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
expect(source.setRepresentation).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
});
it('fires a resource change event after completing setRepresentation.', async(): Promise<void> => {
it('fires all modified change events after setRepresentation.', async(): Promise<void> => {
const result = store.setRepresentation({ path: 'http://example.org/foo/bar' }, {} as Representation);
expect(changedCallback).toHaveBeenCalledTimes(0);
await result;
expect(changedCallback).toHaveBeenCalledTimes(1);
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar' });
expect(changedCallback).toHaveBeenCalledTimes(2);
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/1' });
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/2' });
});
it('calls deleteResource directly from the source.', async(): Promise<void> => {
await expect(store.deleteResource({ path: 'http://example.org/foo/bar' })).resolves.toBeUndefined();
await expect(store.deleteResource({ path: 'http://example.org/foo/bar' }))
.resolves.toEqual(modified);
expect(source.deleteResource).toHaveBeenCalledTimes(1);
expect(source.deleteResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, undefined);
});
it('fires resource and container change events after completing deleteResource.', async(): Promise<void> => {
it('fires all modified change events after deleteResource.', async(): Promise<void> => {
const result = store.deleteResource({ path: 'http://example.org/foo/bar' });
expect(changedCallback).toHaveBeenCalledTimes(0);
await result;
expect(changedCallback).toHaveBeenCalledTimes(2);
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/' });
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar' });
});
it('fires a resource change event after completing deleteResource on the root.', async(): Promise<void> => {
const result = store.deleteResource({ path: 'http://example.org/' });
expect(changedCallback).toHaveBeenCalledTimes(0);
await result;
expect(changedCallback).toHaveBeenCalledTimes(1);
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/' });
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/1' });
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/2' });
});
it('calls modifyResource directly from the source.', async(): Promise<void> => {
await expect(store.modifyResource({ path: 'http://example.org/foo/bar' }, {} as Patch))
.resolves.toBeUndefined();
.resolves.toEqual(modified);
expect(source.modifyResource).toHaveBeenCalledTimes(1);
expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
});
it('fires a resource change event after completing modifyResource.', async(): Promise<void> => {
it('fires all modified change events after modifyResource.', async(): Promise<void> => {
const result = store.modifyResource({ path: 'http://example.org/foo/bar' }, {} as Patch);
expect(changedCallback).toHaveBeenCalledTimes(0);
await result;
expect(changedCallback).toHaveBeenCalledTimes(1);
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar' });
expect(changedCallback).toHaveBeenCalledTimes(2);
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/1' });
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/2' });
});
});

View File

@@ -333,8 +333,7 @@ describe('A FileDataAccessor', (): void => {
it('deletes the corresponding file for document.', async(): Promise<void> => {
cache.data = { resource: 'apple' };
await expect(accessor.deleteResource({ path: `${base}resource` })).resolves
.toEqual([{ path: `${base}resource` }]);
await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined();
expect(cache.data.resource).toBeUndefined();
});
@@ -345,31 +344,22 @@ describe('A FileDataAccessor', (): void => {
it('removes the corresponding folder for containers.', async(): Promise<void> => {
cache.data = { container: {}};
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves
.toEqual([{ path: `${base}container/` }]);
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined();
expect(cache.data.container).toBeUndefined();
});
it('removes the corresponding metadata.', async(): Promise<void> => {
cache.data = { container: { resource: 'apple', 'resource.meta': 'metaApple', '.meta': 'metadata' }};
await expect(accessor.deleteResource({ path: `${base}container/resource` })).resolves.toEqual([
{ path: `${base}container/resource` },
{ path: `${base}container/resource.meta` },
]);
await expect(accessor.deleteResource({ path: `${base}container/resource` })).resolves.toBeUndefined();
expect(cache.data.container.resource).toBeUndefined();
expect(cache.data.container['resource.meta']).toBeUndefined();
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toEqual([
{ path: `${base}container/` },
{ path: `${base}container/.meta` },
]);
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined();
expect(cache.data.container).toBeUndefined();
});
it('can delete the root container.', async(): Promise<void> => {
cache.data = { };
await expect(accessor.deleteResource({ path: `${base}` })).resolves.toEqual([
{ path: base },
]);
await expect(accessor.deleteResource({ path: `${base}` })).resolves.toBeUndefined();
expect(cache.data).toBeUndefined();
});
});

View File

@@ -170,20 +170,17 @@ describe('An InMemoryDataAccessor', (): void => {
it('removes the corresponding resource.', async(): Promise<void> => {
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
await expect(accessor.deleteResource({ path: `${base}resource` })).resolves
.toEqual([{ path: 'http://test.com/resource' }]);
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves
.toEqual([{ path: 'http://test.com/container/' }]);
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);
});
it('can delete the root container and write to it again.', async(): Promise<void> => {
await expect(accessor.deleteResource({ path: base })).resolves
.toEqual([{ path: base }]);
await expect(accessor.getMetadata({ path: base })).rejects.toThrow(NotFoundHttpError);
await expect(accessor.deleteResource({ path: `${base}` })).resolves.toBeUndefined();
await expect(accessor.getMetadata({ path: `${base}` })).rejects.toThrow(NotFoundHttpError);
await expect(accessor.getMetadata({ path: `${base}test/` })).rejects.toThrow(NotFoundHttpError);
await expect(accessor.writeContainer({ path: base }, metadata)).resolves.toBeUndefined();
await expect(accessor.writeContainer({ path: `${base}` }, metadata)).resolves.toBeUndefined();
const resultMetadata = await accessor.getMetadata({ path: `${base}` });
expect(resultMetadata.quads()).toBeRdfIsomorphic(metadata.quads());
});

View File

@@ -210,8 +210,7 @@ describe('A SparqlDataAccessor', (): void => {
it('removes all references when deleting a resource.', async(): Promise<void> => {
metadata = new RepresentationMetadata({ path: 'http://test.com/container/' },
{ [RDF.type]: [ LDP.terms.Resource, LDP.terms.Container ]});
await expect(accessor.deleteResource({ path: 'http://test.com/container/' })).resolves
.toEqual([{ path: 'http://test.com/container/' }]);
await expect(accessor.deleteResource({ path: 'http://test.com/container/' })).resolves.toBeUndefined();
expect(fetchUpdate).toHaveBeenCalledTimes(1);
expect(fetchUpdate.mock.calls[0][0]).toBe(endpoint);
@@ -225,8 +224,7 @@ describe('A SparqlDataAccessor', (): void => {
it('does not try to remove containment triples when deleting a root container.', async(): Promise<void> => {
metadata = new RepresentationMetadata({ path: 'http://test.com/' },
{ [RDF.type]: [ LDP.terms.Resource, LDP.terms.Container ]});
await expect(accessor.deleteResource({ path: 'http://test.com/' })).resolves
.toEqual([{ path: 'http://test.com/' }]);
await expect(accessor.deleteResource({ path: 'http://test.com/' })).resolves.toBeUndefined();
expect(fetchUpdate).toHaveBeenCalledTimes(1);
expect(fetchUpdate.mock.calls[0][0]).toBe(endpoint);

View File

@@ -59,7 +59,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
async function handle(query: string): Promise<void> {
const sparqlPrefix = 'prefix : <http://test.com/>\n';
return handler.handle({ identifier: { path: 'path' },
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(sparqlPrefix.concat(query), { quads: true }) } as SparqlUpdatePatch });
}