Skip to content

Commit

Permalink
fix: add notification configuration, modify notification subscription…
Browse files Browse the repository at this point in the history
… handler behaviour
  • Loading branch information
TamSzaGot committed Dec 3, 2021
1 parent 3bde06c commit b5935c8
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 28 deletions.
1 change: 1 addition & 0 deletions config/file.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/notification/default.json",
"files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json",
Expand Down
29 changes: 29 additions & 0 deletions config/http/handler/notification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/init/initializers/root.json"
],
"@graph": [
{
"comment": "These are all the handlers a request will go through until it is handled.",
"@id": "urn:solid-server:default:HttpHandler",
"@type": "SequenceHandler",
"handlers": [
{ "@id": "urn:solid-server:default:Middleware" },
{
"@type": "WaterfallHandler",
"handlers": [
{ "@id": "urn:solid-server:default:StaticAssetHandler" },
{ "@id": "urn:solid-server:default:SetupHandler" },
{ "@id": "urn:solid-server:default:NotificationWellKnownHandler" },
{ "@id": "urn:solid-server:default:NotificationGatewayHandler" },
{ "@id": "urn:solid-server:default:NotificationSubscriptionHandler" },
{ "@id": "urn:solid-server:default:AuthResourceHttpHandler" },
{ "@id": "urn:solid-server:default:IdentityProviderHandler" },
{ "@id": "urn:solid-server:default:LdpHandler" }
]
}
]
}
]
}
40 changes: 40 additions & 0 deletions config/notification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/initialize-prefilled-root.json",
"files-scs:config/app/setup/optional.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.json",
"files-scs:config/identity/access/public.json",
"files-scs:config/identity/email/default.json",
"files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json",
"files-scs:config/identity/pod/static.json",
"files-scs:config/identity/registration/enabled.json",
"files-scs:config/ldp/authentication/dpop-bearer.json",
"files-scs:config/ldp/authorization/webacl.json",
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/notification/default.json",
"files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json",
"files-scs:config/util/identifiers/suffix.json",
"files-scs:config/util/index/default.json",
"files-scs:config/util/logging/winston.json",
"files-scs:config/util/representation-conversion/default.json",
"files-scs:config/util/resource-locker/memory.json",
"files-scs:config/util/variables/default.json"
],
"@graph": [
{
"comment": "A single-pod server that stores its resources in memory."
}
]
}
99 changes: 99 additions & 0 deletions config/notification/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Handles the notification well-known solid endpoint.",
"@id": "urn:solid-server:default:NotificationWellKnownHandler",
"@type": "RouterHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"args_allowedMethods": [ "GET" ],
"args_allowedPathNames": [ "^/\\.well-known/solid" ],
"args_handler": { "@id": "urn:solid-server:default:NotificationWellKnownParsingHandler" }
},
{
"comment": "Handles well-known endpoint input parsing.",
"@id": "urn:solid-server:default:NotificationWellKnownParsingHandler",
"@type": "ParsingHttpHandler",
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"args_metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"args_operationHandler": {
"comment": "Handles notification well-known solid behaviour.",
"@id": "urn:solid-server:default:NotificationWellKnownHttpHandler",
"@type": "NotificationWellKnownHttpHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
}
},
{
"comment": "Handles the notification gateway endpoint.",
"@id": "urn:solid-server:default:NotificationGatewayHandler",
"@type": "RouterHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"args_allowedMethods": [ "POST" ],
"args_allowedPathNames": [ "^/gateway"],
"args_handler": { "@id": "urn:solid-server:default:NotificationGatewayParsingHandler" }
},
{
"comment": "Handles notification gateway input parsing.",
"@id": "urn:solid-server:default:NotificationGatewayParsingHandler",
"@type": "ParsingHttpHandler",
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"args_metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"args_operationHandler": {
"comment": "Handles notification handler behaviour.",
"@id": "urn:solid-server:default:NotificationGatewayHttpHandler",
"@type": "NotificationGatewayHttpHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_subscriptionPath": "subscription",
"args_subscriptionHandler": { "@id": "urn:solid-server:default:NotificationSubscriptionHttpHandler"}
}
},
{
"comment": "Handles the notification subscription endpoint.",
"@id": "urn:solid-server:default:NotificationSubscriptionHandler",
"@type": "RouterHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"args_allowedMethods": [ "POST" ],
"args_allowedPathNames": [ "^/subscription" ],
"args_handler": { "@id": "urn:solid-server:default:NotificationSubscriptionParsingHandler" }
},
{
"comment": "Handles notification subscription input parsing.",
"@id": "urn:solid-server:default:NotificationSubscriptionParsingHandler",
"@type": "ParsingHttpHandler",
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"args_metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"args_operationHandler": {
"comment": "Handles notification subscription behaviour.",
"@id": "urn:solid-server:default:NotificationSubscriptionHttpHandler",
"@type": "NotificationSubscriptionHttpHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_wsEndpoint": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
"args_permissionReader": { "@id": "urn:solid-server:default:PermissionReader" },
"args_notificationStorage": { "@id": "urn:solid-server:default:NotificationStorage"},
"args_handlers": [{ "@id": "urn:solid-server:default:WebHookSubscription2021Handler"}],
"args_source": { "@id": "urn:solid-server:default:ResourceStore"}
}
},
{
"comment": "Handles WebHookSubscription2021 subscriptions",
"@id": "urn:solid-server:default:WebHookSubscription2021Handler",
"@type": "WebHookSubscription2021Handler",
"httpClient": { "@id": "urn:solid-server:default:BaseHttpClient" }
},
{
"comment": "Handles http calls",
"@id": "urn:solid-server:default:BaseHttpClient",
"@type": "BaseHttpClient"
}
]
}
5 changes: 5 additions & 0 deletions config/storage/key-value/memory.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
"comment": "Storage used by setup components.",
"@id": "urn:solid-server:default:SetupStorage",
"@type": "MemoryMapStorage"
},
{
"comment": "Storage used for notification management.",
"@id": "urn:solid-server:default:NotificationStorage",
"@type": "MemoryMapStorage"
}
]
}
8 changes: 8 additions & 0 deletions config/storage/key-value/resource-store.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/.internal/setup/"
},
{
"comment": "Storage used for notification management.",
"@id": "urn:solid-server:default:NotificationStorage",
"@type": "JsonResourceStorage",
"source": { "@id": "urn:solid-server:default:ResourceStore" },
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/.internal/notification/"
},
{
"comment": "Block external access to the storage containers to avoid exposing internal data.",
"@id": "urn:solid-server:default:PathBasedReader",
Expand Down
23 changes: 15 additions & 8 deletions src/http/NotificationSubscriptionHttpHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,21 @@ export class NotificationSubscriptionHttpHandler extends OperationHttpHandler {
private async onResourceChanged(
resources: ModifiedResource[],
): Promise<void> {
const modified = resources.pop();
const topic = await this.notificationStorage.get(modified!.resource.path);
const { subscriptions } = topic!;
// eslint-disable-next-line guard-for-in
for (const key in subscriptions) {
const subscription = subscriptions[key];
const subscriptionHandler = this.subscriptionHandlers.get(subscription.type);
await subscriptionHandler!.onResourcesChanged(resources, subscription);
const orgResources = [ ...resources ];
for (const modified of orgResources) {
// Aconst modified = resources[0];
let topic = await this.notificationStorage.get(modified.resource.path);
if (!topic) {
topic = { subscriptions: {}};
}
const { subscriptions } = topic;
// eslint-disable-next-line guard-for-in
for (const key in subscriptions) {
const subscription = subscriptions[key];
const subscriptionHandler = this.subscriptionHandlers.get(subscription.type);
await subscriptionHandler!.onResourcesChanged(resources, subscription);
}
resources.shift();
}
}
}
49 changes: 29 additions & 20 deletions test/unit/http/NotificationSubscriptionHttpHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { EventEmitter } from 'events';
import type { Readable } from 'stream';
import { ResponseDescription } from '../../../src';
import type { Credential, CredentialGroup } from '../../../src/authentication/Credentials';
import { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
import type { PermissionReaderInput } from '../../../src/authorization/PermissionReader';
import { PermissionReader } from '../../../src/authorization/PermissionReader';
import type { AccessMode } from '../../../src/authorization/permissions/Permissions';
import type { NotificationSubscriptionHttpHandlerArgs } from '../../../src/http/NotificationSubscriptionHttpHandler';
import { NotificationSubscriptionHttpHandler } from '../../../src/http/NotificationSubscriptionHttpHandler';
// Iimport type { ResponseDescription } from '../../../src/http/output/response/ResponseDescription';
import { ResponseDescription } from '../../../src/http/output/response/ResponseDescription';
import { createResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
import type { Subscription, SubscriptionHandler } from '../../../src/notification/SubscriptionHandler';
import type { HttpRequest } from '../../../src/server/HttpRequest';
Expand All @@ -20,8 +19,8 @@ import { NotImplementedHttpError } from '../../../src/util/errors/NotImplemented
import type { Guarded } from '../../../src/util/GuardedStream';

class WhateverSubscriptionHandler implements SubscriptionHandler {
private readonly subscriptionHandeled: () => Promise<void>;
public constructor(subscriptionHandled: () => Promise<void>) {
private readonly subscriptionHandeled: (resources: ModifiedResource[]) => Promise<void>;
public constructor(subscriptionHandled: (resources: ModifiedResource[]) => Promise<void>) {
this.subscriptionHandeled = subscriptionHandled;
}

Expand All @@ -38,7 +37,7 @@ class WhateverSubscriptionHandler implements SubscriptionHandler {
};

public onResourcesChanged: (resources: ModifiedResource[], subscription: Subscription) => Promise<void> =
async(): Promise<void> => this.subscriptionHandeled();
async(resources: ModifiedResource[]): Promise<void> => this.subscriptionHandeled(resources);
}

class MockCredentialsExtractor extends CredentialsExtractor {
Expand Down Expand Up @@ -99,6 +98,8 @@ class MockPermissionReader extends PermissionReader {
case 'https://pod.example/PublicDenyAgentDeny': return { public: { read: false }, agent: { read: false }};
case 'https://pod.example/AgentDeny': return { agent: { read: false }};
case 'https://pod.example/AgentAllow': return { agent: { read: true }};
case 'https://pod.example/foo/': return { agent: { read: true }};
case 'https://pod.example/bar/AgentAllow': return { agent: { read: true }};
default: return {};
}
}
Expand Down Expand Up @@ -128,14 +129,22 @@ class AllowPermissionReader extends PermissionReader {
}

describe('A NotificationSubscriptionHttpHandler', (): void => {
const source = new EventEmitter();
const eventsource = new EventEmitter();
const countHandling = jest.fn();
async function handleSubscription(): Promise<void> {
countHandling();
let countHandling = jest.fn();

beforeEach((): void => {
countHandling = jest.fn();
});

afterEach((): void => {
countHandling.mockClear();
});

async function handleSubscription(resources: ModifiedResource[]): Promise<void> {
countHandling(resources);
return new Promise<void>((resolve): void => resolve());
}

const source = new EventEmitter();
const eventsource = new EventEmitter();
const subscriptionHandler = new WhateverSubscriptionHandler(handleSubscription);
const subscriptionArgs: NotificationSubscriptionHttpHandlerArgs = {
handlers: [ subscriptionHandler ],
Expand Down Expand Up @@ -266,7 +275,6 @@ describe('A NotificationSubscriptionHttpHandler', (): void => {
await expect(promise).resolves.not.toBeNull();
});
it('shoud handle notification event when resource is modified.', async(): Promise<void> => {
countHandling.mockReset();
subscriptionArgs.permissionReader = new AllowPermissionReader();
const json = {
'@context': [ 'https://www.w3.org/ns/solid/notification/v1' ],
Expand All @@ -281,7 +289,7 @@ describe('A NotificationSubscriptionHttpHandler', (): void => {
expect(res).toBeInstanceOf(ResponseDescription);
const modifiedResources: ModifiedResource[] = [
createModifiedResource(createResourceIdentifier('https://pod.example/'), ModificationType.changed),
createModifiedResource(createResourceIdentifier('https://pod.example/AgentAllow'), ModificationType.created),
createModifiedResource(createResourceIdentifier('https://pod.example/AgentAllow'), ModificationType.changed),
];
expect(countHandling).toHaveBeenCalledTimes(0);
eventsource.emit('changed', modifiedResources);
Expand All @@ -296,12 +304,11 @@ describe('A NotificationSubscriptionHttpHandler', (): void => {
expect(countHandling).toHaveBeenCalledTimes(1);
});
it('shoud skip notification event when resource is not subscribed.', async(): Promise<void> => {
countHandling.mockReset();
subscriptionArgs.permissionReader = new AllowPermissionReader();
const json = {
'@context': [ 'https://www.w3.org/ns/solid/notification/v1' ],
type: 'WhateverSubscriptionHandler',
topic: 'https://pod.example/',
topic: 'https://pod.example/foo/',
};
// eslint-disable-next-line func-style
const readFn: () => Promise<any> = async function(): Promise<any> {
Expand All @@ -310,8 +317,8 @@ describe('A NotificationSubscriptionHttpHandler', (): void => {
const res = await authenticatedHandler1.handle({ operation: { method: 'POST' }, request: { read: readFn }} as any);
expect(res).toBeInstanceOf(ResponseDescription);
const modifiedResources: ModifiedResource[] = [
createModifiedResource(createResourceIdentifier('https://pod.example/'), ModificationType.changed),
createModifiedResource(createResourceIdentifier('https://pod.example/AgentAllow'), ModificationType.created),
createModifiedResource(createResourceIdentifier('https://pod.example/bar/'), ModificationType.changed),
createModifiedResource(createResourceIdentifier('https://pod.example/bar/AgentAllow'), ModificationType.changed),
];
expect(countHandling).toHaveBeenCalledTimes(0);
eventsource.emit('changed', modifiedResources);
Expand All @@ -323,16 +330,15 @@ describe('A NotificationSubscriptionHttpHandler', (): void => {
};
await aSecond();

expect(countHandling).toHaveBeenCalledTimes(1);
expect(countHandling).toHaveBeenCalledTimes(0);
});

it('shoud use stored topic.', async(): Promise<void> => {
countHandling.mockReset();
subscriptionArgs.permissionReader = new AllowPermissionReader();
const json = {
'@context': [ 'https://www.w3.org/ns/solid/notification/v1' ],
type: 'WhateverSubscriptionHandler',
topic: 'https://pod.example/',
topic: 'https://pod.example/AgentAllow',
};
// eslint-disable-next-line func-style
const readFn: () => Promise<any> = async function(): Promise<any> {
Expand All @@ -354,6 +360,9 @@ describe('A NotificationSubscriptionHttpHandler', (): void => {
};
await aSecond();

// The assertion beneath doen't work!? While debugging the mock is being called with the expected value!
// Aexpect(countHandling).toHaveBeenLastCalledWith([{ resource: { path: 'https://pod.example/AgentAllow' }, modificationType: 'CREATED' }]);
expect(countHandling).toHaveBeenCalledTimes(1);
});
});

0 comments on commit b5935c8

Please sign in to comment.