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:
parent
9e1e65cdb9
commit
134237a80f
@ -13,6 +13,12 @@
|
|||||||
"@type": "WaterfallHandler",
|
"@type": "WaterfallHandler",
|
||||||
"handlers": [
|
"handlers": [
|
||||||
{ "@type": "DeleteNotificationGenerator" },
|
{ "@type": "DeleteNotificationGenerator" },
|
||||||
|
{
|
||||||
|
"@type": "AddRemoveNotificationGenerator",
|
||||||
|
"store": {
|
||||||
|
"@id": "urn:solid-server:default:ResourceStore"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"@type": "ActivityNotificationGenerator",
|
"@type": "ActivityNotificationGenerator",
|
||||||
"store": {
|
"store": {
|
||||||
|
@ -9,7 +9,7 @@ import type { ResourceIdentifier } from './ResourceIdentifier';
|
|||||||
import { isResourceIdentifier } from './ResourceIdentifier';
|
import { isResourceIdentifier } from './ResourceIdentifier';
|
||||||
|
|
||||||
export type MetadataIdentifier = ResourceIdentifier | NamedNode | BlankNode;
|
export type MetadataIdentifier = ResourceIdentifier | NamedNode | BlankNode;
|
||||||
export type MetadataValue = NamedNode | Literal | string | (NamedNode | Literal | string)[];
|
export type MetadataValue = NamedNode | BlankNode | Literal | string | (NamedNode | Literal | BlankNode | string)[];
|
||||||
export type MetadataRecord = Record<string, MetadataValue>;
|
export type MetadataRecord = Record<string, MetadataValue>;
|
||||||
export type MetadataGraph = NamedNode | BlankNode | DefaultGraph | string;
|
export type MetadataGraph = NamedNode | BlankNode | DefaultGraph | string;
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ export class RepresentationMetadata {
|
|||||||
* Runs the given function on all predicate/object pairs, but only converts the predicate to a named node once.
|
* Runs the given function on all predicate/object pairs, but only converts the predicate to a named node once.
|
||||||
*/
|
*/
|
||||||
private forQuads(predicate: NamedNode, object: MetadataValue,
|
private forQuads(predicate: NamedNode, object: MetadataValue,
|
||||||
forFn: (pred: NamedNode, obj: NamedNode | Literal) => void): this {
|
forFn: (pred: NamedNode, obj: NamedNode | BlankNode | Literal) => void): this {
|
||||||
const objects = Array.isArray(object) ? object : [ object ];
|
const objects = Array.isArray(object) ? object : [ object ];
|
||||||
for (const obj of objects) {
|
for (const obj of objects) {
|
||||||
forFn(predicate, toObjectTerm(obj, true));
|
forFn(predicate, toObjectTerm(obj, true));
|
||||||
|
@ -311,6 +311,7 @@ export * from './server/middleware/WebSocketAdvertiser';
|
|||||||
|
|
||||||
// Server/Notifications/Generate
|
// Server/Notifications/Generate
|
||||||
export * from './server/notifications/generate/ActivityNotificationGenerator';
|
export * from './server/notifications/generate/ActivityNotificationGenerator';
|
||||||
|
export * from './server/notifications/generate/AddRemoveNotificationGenerator';
|
||||||
export * from './server/notifications/generate/DeleteNotificationGenerator';
|
export * from './server/notifications/generate/DeleteNotificationGenerator';
|
||||||
export * from './server/notifications/generate/NotificationGenerator';
|
export * from './server/notifications/generate/NotificationGenerator';
|
||||||
export * from './server/notifications/generate/StateNotificationGenerator';
|
export * from './server/notifications/generate/StateNotificationGenerator';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
import type { GenericEventEmitter } from '../../util/GenericEventEmitter';
|
import type { GenericEventEmitter } from '../../util/GenericEventEmitter';
|
||||||
import { createGenericEventEmitterClass } from '../../util/GenericEventEmitter';
|
import { createGenericEventEmitterClass } from '../../util/GenericEventEmitter';
|
||||||
@ -8,8 +9,11 @@ import type { AS, VocabularyTerm, VocabularyValue } from '../../util/Vocabularie
|
|||||||
* Both generic `change` events and ActivityStream-specific events are emitted.
|
* Both generic `change` events and ActivityStream-specific events are emitted.
|
||||||
*/
|
*/
|
||||||
export type ActivityEmitter =
|
export type ActivityEmitter =
|
||||||
GenericEventEmitter<'changed', (target: ResourceIdentifier, activity: VocabularyTerm<typeof AS>) => void> &
|
GenericEventEmitter<'changed',
|
||||||
GenericEventEmitter<VocabularyValue<typeof AS>, (target: ResourceIdentifier) => void>;
|
(target: ResourceIdentifier, activity: VocabularyTerm<typeof AS>, metadata: RepresentationMetadata) => void>
|
||||||
|
&
|
||||||
|
GenericEventEmitter<VocabularyValue<typeof AS>,
|
||||||
|
(target: ResourceIdentifier, metadata: RepresentationMetadata) => void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class implementation of {@link ActivityEmitter}.
|
* A class implementation of {@link ActivityEmitter}.
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||||
@ -28,14 +29,15 @@ export class ListeningActivityHandler extends StaticHandler {
|
|||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
|
|
||||||
emitter.on('changed', (topic, activity): void => {
|
emitter.on('changed', (topic, activity, metadata): void => {
|
||||||
this.emit(topic, activity).catch((error): void => {
|
this.emit(topic, activity, metadata).catch((error): void => {
|
||||||
this.logger.error(`Something went wrong emitting notifications: ${createErrorMessage(error)}`);
|
this.logger.error(`Something went wrong emitting notifications: ${createErrorMessage(error)}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async emit(topic: ResourceIdentifier, activity: VocabularyTerm<typeof AS>): Promise<void> {
|
private async emit(topic: ResourceIdentifier, activity: VocabularyTerm<typeof AS>,
|
||||||
|
metadata: RepresentationMetadata): Promise<void> {
|
||||||
const channelIds = await this.storage.getAll(topic);
|
const channelIds = await this.storage.getAll(topic);
|
||||||
|
|
||||||
for (const id of channelIds) {
|
for (const id of channelIds) {
|
||||||
@ -57,7 +59,7 @@ export class ListeningActivityHandler extends StaticHandler {
|
|||||||
|
|
||||||
// No need to wait on this to resolve before going to the next channel.
|
// No need to wait on this to resolve before going to the next channel.
|
||||||
// Prevent failed notification from blocking other notifications.
|
// Prevent failed notification from blocking other notifications.
|
||||||
this.handler.handleSafe({ channel, activity, topic })
|
this.handler.handleSafe({ channel, activity, topic, metadata })
|
||||||
.then((): Promise<void> => {
|
.then((): Promise<void> => {
|
||||||
// Update the `lastEmit` value if the channel has a rate limit
|
// Update the `lastEmit` value if the channel has a rate limit
|
||||||
if (channel.rate) {
|
if (channel.rate) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||||
import type { AS, VocabularyTerm } from '../../util/Vocabularies';
|
import type { AS, VocabularyTerm } from '../../util/Vocabularies';
|
||||||
@ -7,6 +8,7 @@ export interface NotificationHandlerInput {
|
|||||||
topic: ResourceIdentifier;
|
topic: ResourceIdentifier;
|
||||||
channel: NotificationChannel;
|
channel: NotificationChannel;
|
||||||
activity?: VocabularyTerm<typeof AS>;
|
activity?: VocabularyTerm<typeof AS>;
|
||||||
|
metadata?: RepresentationMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
import { getETag } from '../../../storage/Conditions';
|
||||||
|
import type { ResourceStore } from '../../../storage/ResourceStore';
|
||||||
|
import { InternalServerError } from '../../../util/errors/InternalServerError';
|
||||||
|
import { NotImplementedHttpError } from '../../../util/errors/NotImplementedHttpError';
|
||||||
|
import { AS } from '../../../util/Vocabularies';
|
||||||
|
import type { Notification } from '../Notification';
|
||||||
|
import { CONTEXT_ACTIVITYSTREAMS, CONTEXT_NOTIFICATION } from '../Notification';
|
||||||
|
import type { NotificationHandlerInput } from '../NotificationHandler';
|
||||||
|
import { NotificationGenerator } from './NotificationGenerator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link NotificationGenerator} specifically for Add/Remove notifications.
|
||||||
|
* Creates the notification so the `target` is set to input topic,
|
||||||
|
* and the `object` value is extracted from the provided metadata.
|
||||||
|
*/
|
||||||
|
export class AddRemoveNotificationGenerator extends NotificationGenerator {
|
||||||
|
private readonly store: ResourceStore;
|
||||||
|
|
||||||
|
public constructor(store: ResourceStore) {
|
||||||
|
super();
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async canHandle({ activity }: NotificationHandlerInput): Promise<void> {
|
||||||
|
if (!activity || (!activity.equals(AS.terms.Add) && !activity.equals(AS.terms.Remove))) {
|
||||||
|
throw new NotImplementedHttpError(`Only Add/Remove activity updates are supported.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle({ activity, topic, metadata }: NotificationHandlerInput): Promise<Notification> {
|
||||||
|
const representation = await this.store.getRepresentation(topic, {});
|
||||||
|
representation.data.destroy();
|
||||||
|
|
||||||
|
const state = getETag(representation.metadata);
|
||||||
|
const objects = metadata?.getAll(AS.terms.object);
|
||||||
|
if (!objects || objects.length === 0) {
|
||||||
|
throw new InternalServerError(`Missing as:object metadata for ${activity?.value} activity on ${topic.path}`);
|
||||||
|
}
|
||||||
|
if (objects.length > 1) {
|
||||||
|
throw new InternalServerError(`Found more than one as:object for ${activity?.value} activity on ${topic.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'@context': [
|
||||||
|
CONTEXT_ACTIVITYSTREAMS,
|
||||||
|
CONTEXT_NOTIFICATION,
|
||||||
|
],
|
||||||
|
id: `urn:${Date.now()}:${topic.path}`,
|
||||||
|
type: activity!.value.slice(AS.namespace.length),
|
||||||
|
object: objects[0].value,
|
||||||
|
target: topic.path,
|
||||||
|
state,
|
||||||
|
published: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -324,7 +324,8 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
|
|
||||||
if (!this.identifierStrategy.isRootContainer(identifier)) {
|
if (!this.identifierStrategy.isRootContainer(identifier)) {
|
||||||
const container = this.identifierStrategy.getParentContainer(identifier);
|
const container = this.identifierStrategy.getParentContainer(identifier);
|
||||||
this.addActivityMetadata(changes, container, AS.terms.Update);
|
|
||||||
|
this.addContainerActivity(changes, container, false, identifier);
|
||||||
|
|
||||||
// Update modified date of parent
|
// Update modified date of parent
|
||||||
await this.updateContainerModifiedDate(container);
|
await this.updateContainerModifiedDate(container);
|
||||||
@ -424,7 +425,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
|
|
||||||
const changes: ChangeMap = new IdentifierMap();
|
const changes: ChangeMap = new IdentifierMap();
|
||||||
|
|
||||||
// Tranform representation data to quads and add them to the metadata object
|
// Transform representation data to quads and add them to the metadata object
|
||||||
const metadata = new RepresentationMetadata(subjectIdentifier);
|
const metadata = new RepresentationMetadata(subjectIdentifier);
|
||||||
const quads = await arrayifyStream(representation.data);
|
const quads = await arrayifyStream(representation.data);
|
||||||
metadata.addQuads(quads);
|
metadata.addQuads(quads);
|
||||||
@ -482,7 +483,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
|
|
||||||
// No changes means the parent container exists and will be updated
|
// No changes means the parent container exists and will be updated
|
||||||
if (changes.size === 0) {
|
if (changes.size === 0) {
|
||||||
this.addActivityMetadata(changes, parent, AS.terms.Update);
|
this.addContainerActivity(changes, parent, true, identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent container is also modified
|
// Parent container is also modified
|
||||||
@ -710,4 +711,19 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
private addActivityMetadata(map: ChangeMap, id: ResourceIdentifier, activity: NamedNode): void {
|
private addActivityMetadata(map: ChangeMap, id: ResourceIdentifier, activity: NamedNode): void {
|
||||||
map.set(id, new RepresentationMetadata(id, { [SOLID_AS.activity]: activity }));
|
map.set(id, new RepresentationMetadata(id, { [SOLID_AS.activity]: activity }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates activity metadata specifically for Add/Remove events on a container.
|
||||||
|
* @param map - ChangeMap to update.
|
||||||
|
* @param id - Identifier of the container.
|
||||||
|
* @param add - If there is a resource being added (`true`) or removed (`false`).
|
||||||
|
* @param object - The object that is being added/removed.
|
||||||
|
*/
|
||||||
|
private addContainerActivity(map: ChangeMap, id: ResourceIdentifier, add: boolean, object: ResourceIdentifier): void {
|
||||||
|
const metadata = new RepresentationMetadata({
|
||||||
|
[SOLID_AS.activity]: add ? AS.terms.Add : AS.terms.Remove,
|
||||||
|
[AS.object]: namedNode(object.path),
|
||||||
|
});
|
||||||
|
map.set(id, metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import type { Conditions } from './Conditions';
|
|||||||
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
// The ActivityStream terms for which we emit an event
|
// The ActivityStream terms for which we emit an event
|
||||||
const knownActivities = [ AS.terms.Create, AS.terms.Delete, AS.terms.Update ];
|
const knownActivities = [ AS.terms.Add, AS.terms.Create, AS.terms.Delete, AS.terms.Remove, AS.terms.Update ];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store that notifies listeners of changes to its source
|
* Store that notifies listeners of changes to its source
|
||||||
@ -57,8 +57,8 @@ export class MonitoringStore<T extends ResourceStore = ResourceStore>
|
|||||||
for (const [ identifier, metadata ] of changes) {
|
for (const [ identifier, metadata ] of changes) {
|
||||||
const activity = metadata.get(SOLID_AS.terms.activity);
|
const activity = metadata.get(SOLID_AS.terms.activity);
|
||||||
if (this.isKnownActivity(activity)) {
|
if (this.isKnownActivity(activity)) {
|
||||||
this.emit('changed', identifier, activity);
|
this.emit('changed', identifier, activity, metadata);
|
||||||
this.emit(activity.value, identifier);
|
this.emit(activity.value, identifier, metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,8 +145,12 @@ export const ACP = createVocabulary('http://www.w3.org/ns/solid/acp#',
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const AS = createVocabulary('https://www.w3.org/ns/activitystreams#',
|
export const AS = createVocabulary('https://www.w3.org/ns/activitystreams#',
|
||||||
|
'object',
|
||||||
|
|
||||||
|
'Add',
|
||||||
'Create',
|
'Create',
|
||||||
'Delete',
|
'Delete',
|
||||||
|
'Remove',
|
||||||
'Update',
|
'Update',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -223,6 +223,66 @@ describe.each(stores)('A server supporting WebSocketChannel2023 using %s', (name
|
|||||||
expect(message).toBe('Notification channel has expired');
|
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> => {
|
it('can use other RDF formats and content negotiation when creating a channel.', async(): Promise<void> => {
|
||||||
const turtleChannel = `
|
const turtleChannel = `
|
||||||
_:id <${RDF.type}> <${notificationType}> ;
|
_:id <${RDF.type}> <${notificationType}> ;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import type { Server } from 'http';
|
import type { Server } from 'http';
|
||||||
|
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
||||||
import { UnsecureWebSocketsProtocol } from '../../../src/http/UnsecureWebSocketsProtocol';
|
import { UnsecureWebSocketsProtocol } from '../../../src/http/UnsecureWebSocketsProtocol';
|
||||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||||
import { BaseActivityEmitter } from '../../../src/server/notifications/ActivityEmitter';
|
import { BaseActivityEmitter } from '../../../src/server/notifications/ActivityEmitter';
|
||||||
@ -26,6 +27,7 @@ class DummySocket extends EventEmitter {
|
|||||||
describe('An UnsecureWebSocketsProtocol', (): void => {
|
describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||||
let server: Server;
|
let server: Server;
|
||||||
let webSocket: DummySocket;
|
let webSocket: DummySocket;
|
||||||
|
const metadata = new RepresentationMetadata();
|
||||||
const source = new BaseActivityEmitter();
|
const source = new BaseActivityEmitter();
|
||||||
let protocol: UnsecureWebSocketsProtocol;
|
let protocol: UnsecureWebSocketsProtocol;
|
||||||
|
|
||||||
@ -67,7 +69,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
|||||||
|
|
||||||
describe('before subscribing to resources', (): void => {
|
describe('before subscribing to resources', (): void => {
|
||||||
it('does not emit pub messages.', (): 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);
|
expect(webSocket.messages).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -83,7 +85,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('emits pub messages for that resource.', (): 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).toHaveLength(1);
|
||||||
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/foo/bar');
|
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 => {
|
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).toHaveLength(1);
|
||||||
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/relative/foo');
|
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/relative/foo');
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
|
||||||
import type { Logger } from '../../../../src/logging/Logger';
|
import type { Logger } from '../../../../src/logging/Logger';
|
||||||
import { getLoggerFor } from '../../../../src/logging/LogUtil';
|
import { getLoggerFor } from '../../../../src/logging/LogUtil';
|
||||||
@ -21,6 +22,7 @@ describe('A ListeningActivityHandler', (): void => {
|
|||||||
const logger: jest.Mocked<Logger> = getLoggerFor('mock') as any;
|
const logger: jest.Mocked<Logger> = getLoggerFor('mock') as any;
|
||||||
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
|
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
|
||||||
const activity = AS.terms.Update;
|
const activity = AS.terms.Update;
|
||||||
|
const metadata = new RepresentationMetadata();
|
||||||
let channel: NotificationChannel;
|
let channel: NotificationChannel;
|
||||||
let storage: jest.Mocked<NotificationChannelStorage>;
|
let storage: jest.Mocked<NotificationChannelStorage>;
|
||||||
let emitter: ActivityEmitter;
|
let emitter: ActivityEmitter;
|
||||||
@ -52,12 +54,12 @@ describe('A ListeningActivityHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls the NotificationHandler if there is an event.', async(): Promise<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();
|
await flushPromises();
|
||||||
|
|
||||||
expect(notificationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
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(logger.error).toHaveBeenCalledTimes(0);
|
||||||
expect(storage.update).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> => {
|
it('updates the lastEmit value of the channel if it has a rate limit.', async(): Promise<void> => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
channel.rate = 10 * 1000;
|
channel.rate = 10 * 1000;
|
||||||
emitter.emit('changed', topic, activity);
|
emitter.emit('changed', topic, activity, metadata);
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(notificationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
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(logger.error).toHaveBeenCalledTimes(0);
|
||||||
expect(storage.update).toHaveBeenCalledTimes(1);
|
expect(storage.update).toHaveBeenCalledTimes(1);
|
||||||
expect(storage.update).toHaveBeenLastCalledWith({
|
expect(storage.update).toHaveBeenLastCalledWith({
|
||||||
@ -84,7 +86,7 @@ describe('A ListeningActivityHandler', (): void => {
|
|||||||
channel.rate = 100000;
|
channel.rate = 100000;
|
||||||
channel.lastEmit = Date.now();
|
channel.lastEmit = Date.now();
|
||||||
|
|
||||||
emitter.emit('changed', topic, activity);
|
emitter.emit('changed', topic, activity, metadata);
|
||||||
|
|
||||||
await flushPromises();
|
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> => {
|
it('does not emit an event on channels if their start time has not been reached.', async(): Promise<void> => {
|
||||||
channel.startAt = Date.now() + 100000;
|
channel.startAt = Date.now() + 100000;
|
||||||
|
|
||||||
emitter.emit('changed', topic, activity);
|
emitter.emit('changed', topic, activity, metadata);
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@ -107,7 +109,7 @@ describe('A ListeningActivityHandler', (): void => {
|
|||||||
storage.getAll.mockResolvedValue([ channel.id, channel.id ]);
|
storage.getAll.mockResolvedValue([ channel.id, channel.id ]);
|
||||||
notificationHandler.handleSafe.mockRejectedValueOnce(new Error('bad input'));
|
notificationHandler.handleSafe.mockRejectedValueOnce(new Error('bad input'));
|
||||||
|
|
||||||
emitter.emit('changed', topic, activity);
|
emitter.emit('changed', topic, activity, metadata);
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@ -119,7 +121,7 @@ describe('A ListeningActivityHandler', (): void => {
|
|||||||
it('logs an error if something goes wrong handling the event.', async(): Promise<void> => {
|
it('logs an error if something goes wrong handling the event.', async(): Promise<void> => {
|
||||||
storage.getAll.mockRejectedValue(new Error('bad event'));
|
storage.getAll.mockRejectedValue(new Error('bad event'));
|
||||||
|
|
||||||
emitter.emit('changed', topic, activity);
|
emitter.emit('changed', topic, activity, metadata);
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(notificationHandler.handleSafe).toHaveBeenCalledTimes(0);
|
expect(notificationHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
@ -130,7 +132,7 @@ describe('A ListeningActivityHandler', (): void => {
|
|||||||
it('ignores undefined channels.', async(): Promise<void> => {
|
it('ignores undefined channels.', async(): Promise<void> => {
|
||||||
storage.get.mockResolvedValue(undefined);
|
storage.get.mockResolvedValue(undefined);
|
||||||
|
|
||||||
emitter.emit('changed', topic, activity);
|
emitter.emit('changed', topic, activity, metadata);
|
||||||
|
|
||||||
await flushPromises();
|
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 { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
|
||||||
import type { DataAccessor } from '../../../src/storage/accessors/DataAccessor';
|
import type { DataAccessor } from '../../../src/storage/accessors/DataAccessor';
|
||||||
import { BasicConditions } from '../../../src/storage/BasicConditions';
|
import { BasicConditions } from '../../../src/storage/BasicConditions';
|
||||||
|
|
||||||
import { DataAccessorBasedStore } from '../../../src/storage/DataAccessorBasedStore';
|
import { DataAccessorBasedStore } from '../../../src/storage/DataAccessorBasedStore';
|
||||||
import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
|
import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
|
||||||
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
||||||
@ -265,7 +264,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.add(RDF.terms.type, LDP.terms.Container);
|
representation.metadata.add(RDF.terms.type, LDP.terms.Container);
|
||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result.size).toBe(2);
|
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)!;
|
const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!;
|
||||||
expect(generatedID).toBeDefined();
|
expect(generatedID).toBeDefined();
|
||||||
@ -278,7 +277,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
|
|
||||||
expect(result.size).toBe(2);
|
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)!;
|
const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!;
|
||||||
expect(generatedID).toBeDefined();
|
expect(generatedID).toBeDefined();
|
||||||
@ -288,6 +287,8 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
await expect(arrayifyStream(accessor.data[generatedID.path].data)).resolves.toEqual([ resourceData ]);
|
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(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(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> => {
|
it('can write containers.', async(): Promise<void> => {
|
||||||
@ -296,7 +297,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
|
|
||||||
expect(result.size).toBe(2);
|
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)!;
|
const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!;
|
||||||
expect(generatedID).toBeDefined();
|
expect(generatedID).toBeDefined();
|
||||||
@ -317,7 +318,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result.size).toBe(2);
|
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);
|
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);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result.size).toBe(2);
|
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);
|
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);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result.size).toBe(2);
|
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);
|
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 resourceID = { path: `${root}resource` };
|
||||||
const result = await store.setRepresentation(resourceID, representation);
|
const result = await store.setRepresentation(resourceID, representation);
|
||||||
expect(result.size).toBe(2);
|
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);
|
expect(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
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());
|
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>.` ]);
|
representation.data = guardedStreamFrom([ `<${root}resource/> a <coolContainer>.` ]);
|
||||||
const result = await store.setRepresentation(resourceID, representation);
|
const result = await store.setRepresentation(resourceID, representation);
|
||||||
expect(result.size).toBe(2);
|
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(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||||
@ -489,7 +491,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
const resourceID = { path: `${root}resource` };
|
const resourceID = { path: `${root}resource` };
|
||||||
const result = await store.setRepresentation(resourceID, representation);
|
const result = await store.setRepresentation(resourceID, representation);
|
||||||
expect(result.size).toBe(2);
|
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(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
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());
|
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);
|
representation.metadata.add(namedNode('gen'), 'value', SOLID_META.terms.ResponseMetadata);
|
||||||
const result = await store.setRepresentation(resourceID, representation);
|
const result = await store.setRepresentation(resourceID, representation);
|
||||||
expect(result.size).toBe(2);
|
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(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Create);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||||
expect(accessor.data[resourceID.path].metadata.get(namedNode('notGen'))?.value).toBe('value');
|
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 resourceID = { path: `${root}a/b/resource` };
|
||||||
const result = await store.setRepresentation(resourceID, representation);
|
const result = await store.setRepresentation(resourceID, representation);
|
||||||
expect(result.size).toBe(4);
|
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/` })?.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/` })?.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);
|
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;
|
accessor.data[resourceID.path] = representation;
|
||||||
const result = await store.deleteResource(resourceID);
|
const result = await store.deleteResource(resourceID);
|
||||||
expect(result.size).toBe(2);
|
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(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||||
expect(accessor.data[resourceID.path]).toBeUndefined();
|
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
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);
|
auxiliaryStrategy.isRequiredInRoot = jest.fn().mockReturnValue(true);
|
||||||
const result = await store.deleteResource(auxResourceID);
|
const result = await store.deleteResource(auxResourceID);
|
||||||
expect(result.size).toBe(2);
|
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(result.get(auxResourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||||
expect(accessor.data[auxResourceID.path]).toBeUndefined();
|
expect(accessor.data[auxResourceID.path]).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -807,7 +810,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
const result = await store.deleteResource(resourceID);
|
const result = await store.deleteResource(resourceID);
|
||||||
expect(result.size).toBe(3);
|
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(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||||
expect(result.get(auxResourceID)?.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();
|
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||||
@ -830,7 +833,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
logger.error = jest.fn();
|
logger.error = jest.fn();
|
||||||
const result = await store.deleteResource(resourceID);
|
const result = await store.deleteResource(resourceID);
|
||||||
expect(result.size).toBe(2);
|
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(result.get(resourceID)?.get(SOLID_AS.terms.activity)).toEqual(AS.terms.Delete);
|
||||||
expect(accessor.data[resourceID.path]).toBeUndefined();
|
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||||
expect(accessor.data[auxResourceID.path]).toBeDefined();
|
expect(accessor.data[auxResourceID.path]).toBeDefined();
|
||||||
|
@ -10,26 +10,30 @@ describe('A MonitoringStore', (): void => {
|
|||||||
let store: MonitoringStore;
|
let store: MonitoringStore;
|
||||||
let source: ResourceStore;
|
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 changedCallback: () => void;
|
||||||
let createdCallback: () => void;
|
let createdCallback: () => void;
|
||||||
let updatedCallback: () => void;
|
let updatedCallback: () => void;
|
||||||
let deletedCallback: () => void;
|
let deletedCallback: () => void;
|
||||||
|
|
||||||
const addResourceReturnMock: ChangeMap = new IdentifierMap([
|
const addResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Create }) ],
|
[ idNew, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Create }) ],
|
||||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
[ id, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||||
]);
|
]);
|
||||||
const setRepresentationReturnMock: ChangeMap = new IdentifierMap([
|
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([
|
const deleteResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Delete }) ],
|
[ idNew, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Delete }) ],
|
||||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
[ id, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||||
]);
|
]);
|
||||||
const modifyResourceReturnMock: ChangeMap = new IdentifierMap([
|
const modifyResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||||
[{ path: 'http://example.org/foo/bar/old' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Delete }) ],
|
[ idOld, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Delete }) ],
|
||||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Create }) ],
|
[ idNew, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Create }) ],
|
||||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
[ id, new RepresentationMetadata({ [SOLID_AS.activity]: AS.terms.Update }) ],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -61,98 +65,98 @@ describe('A MonitoringStore', (): void => {
|
|||||||
|
|
||||||
it('does not fire a change event after getRepresentation.', async(): Promise<void> => {
|
it('does not fire a change event after getRepresentation.', async(): Promise<void> => {
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await store.getRepresentation({ path: 'http://example.org/foo/bar' }, {});
|
await store.getRepresentation(id, {});
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls addResource directly from the source.', async(): Promise<void> => {
|
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);
|
.resolves.toBe(addResourceReturnMock);
|
||||||
expect(source.addResource).toHaveBeenCalledTimes(1);
|
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> => {
|
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);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await result;
|
await result;
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
expect(changedCallback).toHaveBeenCalledTimes(2);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
expect(changedCallback).toHaveBeenCalledWith(id, AS.terms.Update, addResourceReturnMock.get(id));
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Create);
|
expect(changedCallback).toHaveBeenCalledWith(idNew, AS.terms.Create, addResourceReturnMock.get(idNew));
|
||||||
expect(createdCallback).toHaveBeenCalledTimes(1);
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
expect(updatedCallback).toHaveBeenCalledWith(id, addResourceReturnMock.get(id));
|
||||||
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls setRepresentation directly from the source.', async(): Promise<void> => {
|
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);
|
.resolves.toEqual(setRepresentationReturnMock);
|
||||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
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> => {
|
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);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await result;
|
await result;
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(1);
|
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(createdCallback).toHaveBeenCalledTimes(0);
|
||||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
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);
|
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls deleteResource directly from the source.', async(): Promise<void> => {
|
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);
|
.resolves.toEqual(deleteResourceReturnMock);
|
||||||
expect(source.deleteResource).toHaveBeenCalledTimes(1);
|
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> => {
|
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);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await result;
|
await result;
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
expect(changedCallback).toHaveBeenCalledTimes(2);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
expect(changedCallback).toHaveBeenCalledWith(id, AS.terms.Update, deleteResourceReturnMock.get(id));
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Delete);
|
expect(changedCallback).toHaveBeenCalledWith(idNew, AS.terms.Delete, deleteResourceReturnMock.get(idNew));
|
||||||
expect(createdCallback).toHaveBeenCalledTimes(0);
|
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
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).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> => {
|
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);
|
.resolves.toEqual(modifyResourceReturnMock);
|
||||||
expect(source.modifyResource).toHaveBeenCalledTimes(1);
|
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> => {
|
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);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await result;
|
await result;
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(3);
|
expect(changedCallback).toHaveBeenCalledTimes(3);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Create);
|
expect(changedCallback).toHaveBeenCalledWith(idNew, AS.terms.Create, modifyResourceReturnMock.get(idNew));
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
expect(changedCallback).toHaveBeenCalledWith(id, AS.terms.Update, modifyResourceReturnMock.get(id));
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' }, AS.terms.Delete);
|
expect(changedCallback).toHaveBeenCalledWith(idOld, AS.terms.Delete, modifyResourceReturnMock.get(idOld));
|
||||||
expect(createdCallback).toHaveBeenCalledTimes(1);
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
expect(updatedCallback).toHaveBeenCalledWith(id, modifyResourceReturnMock.get(id));
|
||||||
expect(deletedCallback).toHaveBeenCalledTimes(1);
|
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> => {
|
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).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> => {
|
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' }) ],
|
[{ 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(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
expect(createdCallback).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 topic - The topic of the notification.
|
||||||
* @param type - What type of notification is expected.
|
* @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 = {
|
const expected: any = {
|
||||||
'@context': [
|
'@context': [
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user