mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Support Add/Remove notifications on containers
This commit is contained in:
@@ -223,6 +223,66 @@ describe.each(stores)('A server supporting WebSocketChannel2023 using %s', (name
|
||||
expect(message).toBe('Notification channel has expired');
|
||||
});
|
||||
|
||||
it('emits container notifications if contents get added or removed.', async(): Promise<void> => {
|
||||
const resource = joinUrl(baseUrl, '/resource');
|
||||
// Subscribing to the base URL, which is the parent container
|
||||
const { receiveFrom } = await subscribe(notificationType, webId, subscriptionUrl, baseUrl) as any;
|
||||
|
||||
const socket = new WebSocket(receiveFrom);
|
||||
let notificationPromise = new Promise<Buffer>((resolve): any => socket.on('message', resolve));
|
||||
await new Promise<void>((resolve): any => socket.on('open', resolve));
|
||||
|
||||
let response = await fetch(resource, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
body: 'abc',
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
|
||||
// Will receive the Add notification
|
||||
let notification = JSON.parse((await notificationPromise).toString());
|
||||
|
||||
// Slightly differs from the other notifications due to the combination of object and target
|
||||
expect(notification).toEqual({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://www.w3.org/ns/solid/notification/v1',
|
||||
],
|
||||
id: expect.stringContaining(baseUrl),
|
||||
type: 'Add',
|
||||
object: resource,
|
||||
target: baseUrl,
|
||||
published: expect.anything(),
|
||||
state: expect.anything(),
|
||||
});
|
||||
|
||||
// Reset the notifications promise
|
||||
notificationPromise = new Promise<Buffer>((resolve): any => socket.on('message', resolve));
|
||||
|
||||
response = await fetch(resource, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
expect(response.status).toBe(205);
|
||||
|
||||
// Will receive the Remove notification
|
||||
notification = JSON.parse((await notificationPromise).toString());
|
||||
|
||||
expect(notification).toEqual({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://www.w3.org/ns/solid/notification/v1',
|
||||
],
|
||||
id: expect.stringContaining(baseUrl),
|
||||
type: 'Remove',
|
||||
object: resource,
|
||||
target: baseUrl,
|
||||
published: expect.anything(),
|
||||
state: expect.anything(),
|
||||
});
|
||||
|
||||
socket.close();
|
||||
});
|
||||
|
||||
it('can use other RDF formats and content negotiation when creating a channel.', async(): Promise<void> => {
|
||||
const turtleChannel = `
|
||||
_:id <${RDF.type}> <${notificationType}> ;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { Server } from 'http';
|
||||
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
||||
import { UnsecureWebSocketsProtocol } from '../../../src/http/UnsecureWebSocketsProtocol';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
import { BaseActivityEmitter } from '../../../src/server/notifications/ActivityEmitter';
|
||||
@@ -26,6 +27,7 @@ class DummySocket extends EventEmitter {
|
||||
describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
let server: Server;
|
||||
let webSocket: DummySocket;
|
||||
const metadata = new RepresentationMetadata();
|
||||
const source = new BaseActivityEmitter();
|
||||
let protocol: UnsecureWebSocketsProtocol;
|
||||
|
||||
@@ -67,7 +69,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
|
||||
describe('before subscribing to resources', (): void => {
|
||||
it('does not emit pub messages.', (): void => {
|
||||
source.emit('changed', { path: 'https://mypod.example/foo/bar' }, AS.terms.Update);
|
||||
source.emit('changed', { path: 'https://mypod.example/foo/bar' }, AS.terms.Update, metadata);
|
||||
expect(webSocket.messages).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -83,7 +85,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
});
|
||||
|
||||
it('emits pub messages for that resource.', (): void => {
|
||||
source.emit('changed', { path: 'https://mypod.example/foo/bar' }, AS.terms.Update);
|
||||
source.emit('changed', { path: 'https://mypod.example/foo/bar' }, AS.terms.Update, metadata);
|
||||
expect(webSocket.messages).toHaveLength(1);
|
||||
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/foo/bar');
|
||||
});
|
||||
@@ -100,7 +102,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
});
|
||||
|
||||
it('emits pub messages for that resource.', (): void => {
|
||||
source.emit('changed', { path: 'https://mypod.example/relative/foo' }, AS.terms.Update);
|
||||
source.emit('changed', { path: 'https://mypod.example/relative/foo' }, AS.terms.Update, metadata);
|
||||
expect(webSocket.messages).toHaveLength(1);
|
||||
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/relative/foo');
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
|
||||
import type { Logger } from '../../../../src/logging/Logger';
|
||||
import { getLoggerFor } from '../../../../src/logging/LogUtil';
|
||||
@@ -21,6 +22,7 @@ describe('A ListeningActivityHandler', (): void => {
|
||||
const logger: jest.Mocked<Logger> = getLoggerFor('mock') as any;
|
||||
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
|
||||
const activity = AS.terms.Update;
|
||||
const metadata = new RepresentationMetadata();
|
||||
let channel: NotificationChannel;
|
||||
let storage: jest.Mocked<NotificationChannelStorage>;
|
||||
let emitter: ActivityEmitter;
|
||||
@@ -52,12 +54,12 @@ describe('A ListeningActivityHandler', (): void => {
|
||||
});
|
||||
|
||||
it('calls the NotificationHandler if there is an event.', async(): Promise<void> => {
|
||||
emitter.emit('changed', topic, activity);
|
||||
emitter.emit('changed', topic, activity, metadata);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(notificationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(notificationHandler.handleSafe).toHaveBeenLastCalledWith({ channel, activity, topic });
|
||||
expect(notificationHandler.handleSafe).toHaveBeenLastCalledWith({ channel, activity, topic, metadata });
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
expect(storage.update).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -65,12 +67,12 @@ describe('A ListeningActivityHandler', (): void => {
|
||||
it('updates the lastEmit value of the channel if it has a rate limit.', async(): Promise<void> => {
|
||||
jest.useFakeTimers();
|
||||
channel.rate = 10 * 1000;
|
||||
emitter.emit('changed', topic, activity);
|
||||
emitter.emit('changed', topic, activity, metadata);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(notificationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(notificationHandler.handleSafe).toHaveBeenLastCalledWith({ channel, activity, topic });
|
||||
expect(notificationHandler.handleSafe).toHaveBeenLastCalledWith({ channel, activity, topic, metadata });
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
expect(storage.update).toHaveBeenCalledTimes(1);
|
||||
expect(storage.update).toHaveBeenLastCalledWith({
|
||||
@@ -84,7 +86,7 @@ describe('A ListeningActivityHandler', (): void => {
|
||||
channel.rate = 100000;
|
||||
channel.lastEmit = Date.now();
|
||||
|
||||
emitter.emit('changed', topic, activity);
|
||||
emitter.emit('changed', topic, activity, metadata);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
@@ -95,7 +97,7 @@ describe('A ListeningActivityHandler', (): void => {
|
||||
it('does not emit an event on channels if their start time has not been reached.', async(): Promise<void> => {
|
||||
channel.startAt = Date.now() + 100000;
|
||||
|
||||
emitter.emit('changed', topic, activity);
|
||||
emitter.emit('changed', topic, activity, metadata);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
@@ -107,7 +109,7 @@ describe('A ListeningActivityHandler', (): void => {
|
||||
storage.getAll.mockResolvedValue([ channel.id, channel.id ]);
|
||||
notificationHandler.handleSafe.mockRejectedValueOnce(new Error('bad input'));
|
||||
|
||||
emitter.emit('changed', topic, activity);
|
||||
emitter.emit('changed', topic, activity, metadata);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
@@ -119,7 +121,7 @@ describe('A ListeningActivityHandler', (): void => {
|
||||
it('logs an error if something goes wrong handling the event.', async(): Promise<void> => {
|
||||
storage.getAll.mockRejectedValue(new Error('bad event'));
|
||||
|
||||
emitter.emit('changed', topic, activity);
|
||||
emitter.emit('changed', topic, activity, metadata);
|
||||
|
||||
await flushPromises();
|
||||
expect(notificationHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||
@@ -130,7 +132,7 @@ describe('A ListeningActivityHandler', (): void => {
|
||||
it('ignores undefined channels.', async(): Promise<void> => {
|
||||
storage.get.mockResolvedValue(undefined);
|
||||
|
||||
emitter.emit('changed', topic, activity);
|
||||
emitter.emit('changed', topic, activity, metadata);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
|
||||
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../../../../src/http/representation/ResourceIdentifier';
|
||||
import {
|
||||
AddRemoveNotificationGenerator,
|
||||
} from '../../../../../src/server/notifications/generate/AddRemoveNotificationGenerator';
|
||||
import type { NotificationChannel } from '../../../../../src/server/notifications/NotificationChannel';
|
||||
import type { ResourceStore } from '../../../../../src/storage/ResourceStore';
|
||||
import { AS, DC, LDP, RDF } from '../../../../../src/util/Vocabularies';
|
||||
|
||||
describe('An AddRemoveNotificationGenerator', (): void => {
|
||||
const topic: ResourceIdentifier = { path: 'http://example.com/' };
|
||||
const object: ResourceIdentifier = { path: 'http://example.com/foo' };
|
||||
const channel: NotificationChannel = {
|
||||
id: 'id',
|
||||
topic: topic.path,
|
||||
type: 'type',
|
||||
};
|
||||
let metadata: RepresentationMetadata;
|
||||
let store: jest.Mocked<ResourceStore>;
|
||||
let generator: AddRemoveNotificationGenerator;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
metadata = new RepresentationMetadata(topic, { [AS.object]: object.path });
|
||||
|
||||
const responseMetadata = new RepresentationMetadata({
|
||||
[RDF.type]: LDP.terms.Resource,
|
||||
// Needed for ETag
|
||||
[DC.modified]: new Date().toISOString(),
|
||||
});
|
||||
store = {
|
||||
getRepresentation: jest.fn().mockResolvedValue(new BasicRepresentation('', responseMetadata)),
|
||||
} as any;
|
||||
|
||||
generator = new AddRemoveNotificationGenerator(store);
|
||||
});
|
||||
|
||||
it('only handles Add/Remove activities.', async(): Promise<void> => {
|
||||
await expect(generator.canHandle({ topic, channel, metadata }))
|
||||
.rejects.toThrow('Only Add/Remove activity updates are supported.');
|
||||
await expect(generator.canHandle({ topic, channel, metadata, activity: AS.terms.Add })).resolves.toBeUndefined();
|
||||
await expect(generator.canHandle({ topic, channel, metadata, activity: AS.terms.Remove })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('requires one object metadata to be present.', async(): Promise<void> => {
|
||||
metadata = new RepresentationMetadata();
|
||||
await expect(generator.handle({ topic, channel, activity: AS.terms.Add })).rejects.toThrow(
|
||||
'Missing as:object metadata for https://www.w3.org/ns/activitystreams#Add activity on http://example.com/',
|
||||
);
|
||||
await expect(generator.handle({ topic, channel, metadata, activity: AS.terms.Add })).rejects.toThrow(
|
||||
'Missing as:object metadata for https://www.w3.org/ns/activitystreams#Add activity on http://example.com/',
|
||||
);
|
||||
|
||||
metadata = new RepresentationMetadata(topic, { [AS.object]: [ object.path, 'http://example.com/otherObject' ]});
|
||||
await expect(generator.handle({ topic, channel, metadata, activity: AS.terms.Add })).rejects.toThrow(
|
||||
'Found more than one as:object for https://www.w3.org/ns/activitystreams#Add activity on http://example.com/',
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a notification.', async(): Promise<void> => {
|
||||
const date = '1988-03-09T14:48:00.000Z';
|
||||
const ms = Date.parse(date);
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(ms);
|
||||
|
||||
await expect(generator.handle({ topic, channel, metadata, activity: AS.terms.Add })).resolves.toEqual({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://www.w3.org/ns/solid/notification/v1',
|
||||
],
|
||||
id: `urn:${ms}:http://example.com/`,
|
||||
type: 'Add',
|
||||
object: 'http://example.com/foo',
|
||||
target: 'http://example.com/',
|
||||
state: expect.stringMatching(/"\d+"/u),
|
||||
published: date,
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,6 @@ import { RepresentationMetadata } from '../../../src/http/representation/Represe
|
||||
import type { ResourceIdentifier } from '../../../src/http/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';
|
||||
@@ -265,7 +264,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.metadata.add(RDF.terms.type, LDP.terms.Container);
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
|
||||
const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!;
|
||||
expect(generatedID).toBeDefined();
|
||||
@@ -278,7 +277,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
|
||||
const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!;
|
||||
expect(generatedID).toBeDefined();
|
||||
@@ -288,6 +287,8 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
await expect(arrayifyStream(accessor.data[generatedID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[generatedID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
expect(result.get(generatedID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
|
||||
expect(result.get(resourceID)?.get(AS.terms.object)?.value).toEqual(generatedID.path);
|
||||
});
|
||||
|
||||
it('can write containers.', async(): Promise<void> => {
|
||||
@@ -296,7 +297,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
|
||||
const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!;
|
||||
expect(generatedID).toBeDefined();
|
||||
@@ -317,7 +318,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
expect(result.get({ path: `${root}newName` })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
});
|
||||
|
||||
@@ -342,7 +343,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
expect(result.get({ path: `${root}newContainer/` })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
});
|
||||
|
||||
@@ -366,7 +367,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
expect(result.get({ path: `${root}%26%26` })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
});
|
||||
|
||||
@@ -459,7 +460,8 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const resourceID = { path: `${root}resource` };
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
expect(result.get({ path: root })?.get(AS.terms.object)?.value).toEqual(resourceID.path);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
@@ -476,7 +478,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.data = guardedStreamFrom([ `<${root}resource/> a <coolContainer>.` ]);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||
@@ -489,7 +491,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const resourceID = { path: `${root}resource` };
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
@@ -513,7 +515,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.metadata.add(namedNode('gen'), 'value', SOLID_META.terms.ResponseMetadata);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[resourceID.path].metadata.get(namedNode('notGen'))?.value).toBe('value');
|
||||
@@ -535,7 +537,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const resourceID = { path: `${root}a/b/resource` };
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(4);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Add);
|
||||
expect(result.get({ path: `${root}a/` })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
expect(result.get({ path: `${root}a/b/` })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
expect(result.get({ path: `${root}a/b/resource` })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||
@@ -770,7 +772,8 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
accessor.data[resourceID.path] = representation;
|
||||
const result = await store.deleteResource(resourceID);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Remove);
|
||||
expect(result.get({ path: root })?.get(AS.terms.object)?.value).toEqual(resourceID.path);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
@@ -794,7 +797,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
auxiliaryStrategy.isRequiredInRoot = jest.fn().mockReturnValue(true);
|
||||
const result = await store.deleteResource(auxResourceID);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Remove);
|
||||
expect(result.get(auxResourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[auxResourceID.path]).toBeUndefined();
|
||||
});
|
||||
@@ -807,7 +810,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
const result = await store.deleteResource(resourceID);
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Remove);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||
expect(result.get(auxResourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||
@@ -830,7 +833,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
logger.error = jest.fn();
|
||||
const result = await store.deleteResource(resourceID);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Remove);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||
expect(accessor.data[auxResourceID.path]).toBeDefined();
|
||||
|
||||
@@ -10,26 +10,30 @@ describe('A MonitoringStore', (): void => {
|
||||
let store: MonitoringStore;
|
||||
let source: ResourceStore;
|
||||
|
||||
const id = { path: 'http://example.org/foo/bar/' };
|
||||
const idNew = { path: 'http://example.org/foo/bar/new' };
|
||||
const idOld = { path: 'http://example.org/foo/bar/old' };
|
||||
|
||||
let changedCallback: () => void;
|
||||
let createdCallback: () => void;
|
||||
let updatedCallback: () => void;
|
||||
let deletedCallback: () => void;
|
||||
|
||||
const addResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Create }) ],
|
||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||
[ idNew, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Create }) ],
|
||||
[ id, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||
]);
|
||||
const setRepresentationReturnMock: ChangeMap = new IdentifierMap([
|
||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||
[ idNew, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||
]);
|
||||
const deleteResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Delete }) ],
|
||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||
[ idNew, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Delete }) ],
|
||||
[ id, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||
]);
|
||||
const modifyResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||
[{ path: 'http://example.org/foo/bar/old' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Delete }) ],
|
||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Create }) ],
|
||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||
[ idOld, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Delete }) ],
|
||||
[ idNew, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Create }) ],
|
||||
[ id, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||
]);
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
@@ -61,98 +65,98 @@ describe('A MonitoringStore', (): 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' }, {});
|
||||
await store.getRepresentation(id, {});
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('calls addResource directly from the source.', async(): Promise<void> => {
|
||||
await expect(store.addResource({ path: 'http://example.org/foo/bar' }, {} as Representation))
|
||||
await expect(store.addResource(id, {} as Representation))
|
||||
.resolves.toBe(addResourceReturnMock);
|
||||
expect(source.addResource).toHaveBeenCalledTimes(1);
|
||||
expect(source.addResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
||||
expect(source.addResource).toHaveBeenLastCalledWith(id, {}, undefined);
|
||||
});
|
||||
|
||||
it('fires appropriate events according to the return value of source.addResource.', async(): Promise<void> => {
|
||||
const result = store.addResource({ path: 'http://example.org/foo/bar/' }, {} as Representation);
|
||||
const result = store.addResource(id, {} as Representation);
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
await result;
|
||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Create);
|
||||
expect(changedCallback).toHaveBeenCalledWith(id, AS.terms.Update, addResourceReturnMock.get(id));
|
||||
expect(changedCallback).toHaveBeenCalledWith(idNew, AS.terms.Create, addResourceReturnMock.get(idNew));
|
||||
expect(createdCallback).toHaveBeenCalledTimes(1);
|
||||
expect(createdCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||
expect(createdCallback).toHaveBeenCalledWith(idNew, addResourceReturnMock.get(idNew));
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
||||
expect(updatedCallback).toHaveBeenCalledWith(id, addResourceReturnMock.get(id));
|
||||
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('calls setRepresentation directly from the source.', async(): Promise<void> => {
|
||||
await expect(store.setRepresentation({ path: 'http://example.org/foo/bar' }, {} as Representation))
|
||||
await expect(store.setRepresentation(id, {} as Representation))
|
||||
.resolves.toEqual(setRepresentationReturnMock);
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.setRepresentation).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
||||
expect(source.setRepresentation).toHaveBeenLastCalledWith(id, {}, undefined);
|
||||
});
|
||||
|
||||
it('fires appropriate events according to the return value of source.setRepresentation.', async(): Promise<void> => {
|
||||
const result = store.setRepresentation({ path: 'http://example.org/foo/bar' }, {} as Representation);
|
||||
const result = store.setRepresentation(id, {} as Representation);
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
await result;
|
||||
expect(changedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith(idNew, AS.terms.Update, setRepresentationReturnMock.get(idNew));
|
||||
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||
expect(updatedCallback).toHaveBeenCalledWith(idNew, setRepresentationReturnMock.get(idNew));
|
||||
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('calls deleteResource directly from the source.', async(): Promise<void> => {
|
||||
await expect(store.deleteResource({ path: 'http://example.org/foo/bar' }))
|
||||
await expect(store.deleteResource(id))
|
||||
.resolves.toEqual(deleteResourceReturnMock);
|
||||
expect(source.deleteResource).toHaveBeenCalledTimes(1);
|
||||
expect(source.deleteResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, undefined);
|
||||
expect(source.deleteResource).toHaveBeenLastCalledWith(id, undefined);
|
||||
});
|
||||
|
||||
it('fires appropriate events according to the return value of source.deleteResource.', async(): Promise<void> => {
|
||||
const result = store.deleteResource({ path: 'http://example.org/foo/bar' });
|
||||
const result = store.deleteResource(id);
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
await result;
|
||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Delete);
|
||||
expect(changedCallback).toHaveBeenCalledWith(id, AS.terms.Update, deleteResourceReturnMock.get(id));
|
||||
expect(changedCallback).toHaveBeenCalledWith(idNew, AS.terms.Delete, deleteResourceReturnMock.get(idNew));
|
||||
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
||||
expect(updatedCallback).toHaveBeenCalledWith(id, deleteResourceReturnMock.get(id));
|
||||
expect(deletedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(deletedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||
expect(deletedCallback).toHaveBeenCalledWith(idNew, deleteResourceReturnMock.get(idNew));
|
||||
});
|
||||
|
||||
it('calls modifyResource directly from the source.', async(): Promise<void> => {
|
||||
await expect(store.modifyResource({ path: 'http://example.org/foo/bar' }, {} as Patch))
|
||||
await expect(store.modifyResource(id, {} as Patch))
|
||||
.resolves.toEqual(modifyResourceReturnMock);
|
||||
expect(source.modifyResource).toHaveBeenCalledTimes(1);
|
||||
expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
||||
expect(source.modifyResource).toHaveBeenLastCalledWith(id, {}, undefined);
|
||||
});
|
||||
|
||||
it('fires appropriate events according to the return value of source.modifyResource.', async(): Promise<void> => {
|
||||
const result = store.modifyResource({ path: 'http://example.org/foo/bar' }, {} as Patch);
|
||||
const result = store.modifyResource(id, {} as Patch);
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
await result;
|
||||
expect(changedCallback).toHaveBeenCalledTimes(3);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Create);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' }, AS.terms.Delete);
|
||||
expect(changedCallback).toHaveBeenCalledWith(idNew, AS.terms.Create, modifyResourceReturnMock.get(idNew));
|
||||
expect(changedCallback).toHaveBeenCalledWith(id, AS.terms.Update, modifyResourceReturnMock.get(id));
|
||||
expect(changedCallback).toHaveBeenCalledWith(idOld, AS.terms.Delete, modifyResourceReturnMock.get(idOld));
|
||||
expect(createdCallback).toHaveBeenCalledTimes(1);
|
||||
expect(createdCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||
expect(createdCallback).toHaveBeenCalledWith(idNew, modifyResourceReturnMock.get(idNew));
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
||||
expect(updatedCallback).toHaveBeenCalledWith(id, modifyResourceReturnMock.get(id));
|
||||
expect(deletedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(deletedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' });
|
||||
expect(deletedCallback).toHaveBeenCalledWith(idOld, modifyResourceReturnMock.get(idOld));
|
||||
});
|
||||
|
||||
it('calls hasResource directly from the source.', async(): Promise<void> => {
|
||||
await expect(store.hasResource({ path: 'http://example.org/foo/bar' })).resolves.toBe(true);
|
||||
await expect(store.hasResource(id)).resolves.toBe(true);
|
||||
expect(source.hasResource).toHaveBeenCalledTimes(1);
|
||||
expect(source.hasResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' });
|
||||
expect(source.hasResource).toHaveBeenLastCalledWith(id);
|
||||
});
|
||||
|
||||
it('should not emit an event when the Activity is not a valid AS value.', async(): Promise<void> => {
|
||||
@@ -160,7 +164,7 @@ describe('A MonitoringStore', (): void => {
|
||||
[{ path: 'http://example.org/path' }, new RepresentationMetadata({ [SOLID_AS.activity]: 'SomethingRandom' }) ],
|
||||
]));
|
||||
|
||||
await store.addResource({ path: 'http://example.org/foo/bar' }, {} as Patch);
|
||||
await store.addResource(id, {} as Patch);
|
||||
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function subscribe(type: string, webId: string, subscriptionUrl: st
|
||||
* @param topic - The topic of the notification.
|
||||
* @param type - What type of notification is expected.
|
||||
*/
|
||||
export function expectNotification(notification: unknown, topic: string, type: 'Create' | 'Update' | 'Delete'): void {
|
||||
export function expectNotification(notification: unknown, topic: string, type: string): void {
|
||||
const expected: any = {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
|
||||
Reference in New Issue
Block a user