diff --git a/config/http/handler/default.json b/config/http/handler/default.json index 34bd40352..b5be5b6ad 100644 --- a/config/http/handler/default.json +++ b/config/http/handler/default.json @@ -1,7 +1,6 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", "import": [ - "css:config/http/handler/handlers/notifications.json", "css:config/http/handler/handlers/oidc.json", "css:config/http/handler/handlers/storage-description.json" ], diff --git a/config/http/notifications/all.json b/config/http/notifications/all.json index f8e2cf1ff..22a288f74 100644 --- a/config/http/notifications/all.json +++ b/config/http/notifications/all.json @@ -3,6 +3,7 @@ "import": [ "css:config/http/notifications/base/description.json", "css:config/http/notifications/base/handler.json", + "css:config/http/notifications/base/http.json", "css:config/http/notifications/base/listener.json", "css:config/http/notifications/base/storage.json", "css:config/http/notifications/websockets/handler.json", diff --git a/config/http/handler/handlers/notifications.json b/config/http/notifications/base/http.json similarity index 57% rename from config/http/handler/handlers/notifications.json rename to config/http/notifications/base/http.json index 2d33b5d9e..e99654173 100644 --- a/config/http/handler/handlers/notifications.json +++ b/config/http/notifications/base/http.json @@ -16,6 +16,21 @@ "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, "operationHandler": { + "@type": "WaterfallHandler", + "handlers": [ + { "@id": "urn:solid-server:default:NotificationReadWriteHandler" }, + { "@id": "urn:solid-server:default:NotificationDeleteHandler" } + ] + } + }, + + { + "comment": "Handles reading and writing notification subscriptions and channels.", + "@id": "urn:solid-server:default:NotificationReadWriteHandler", + "@type": "OperationRouterHandler", + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "allowedMethods": [ "HEAD", "GET", "POST" ], + "handler": { "comment": "Converts outgoing responses based on the user preferences", "@type": "ConvertingOperationHttpHandler", "converter": { "@id": "urn:solid-server:default:RepresentationConverter" }, @@ -26,6 +41,18 @@ "handlers": [ ] } } + }, + + { + "comment": "Handles deleting notification channels.", + "@id": "urn:solid-server:default:NotificationDeleteHandler", + "@type": "OperationRouterHandler", + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "allowedMethods": [ "DELETE" ], + "handler": { + "@type": "NotificationUnsubscriber", + "storage": { "@id": "urn:solid-server:default:SubscriptionStorage" } + } } ] } diff --git a/config/http/notifications/disabled.json b/config/http/notifications/disabled.json index cdcc167f9..9a28390db 100644 --- a/config/http/notifications/disabled.json +++ b/config/http/notifications/disabled.json @@ -2,7 +2,9 @@ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", "@graph": [ { - "comment": "Disable notifications by not attaching a notification listener." + "comment": "Disables notification routing.", + "@id": "urn:solid-server:default:NotificationHttpHandler", + "@type": "UnsupportedAsyncHandler" } ] } diff --git a/config/http/notifications/legacy-websockets.json b/config/http/notifications/legacy-websockets.json index 936cc1f27..c9f5188be 100644 --- a/config/http/notifications/legacy-websockets.json +++ b/config/http/notifications/legacy-websockets.json @@ -1,6 +1,11 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", "@graph": [ + { + "comment": "Disables notification routing.", + "@id": "urn:solid-server:default:NotificationHttpHandler", + "@type": "UnsupportedAsyncHandler" + }, { "@id": "urn:solid-server:default:ServerConfigurator", "@type": "ParallelHandler", diff --git a/config/http/notifications/webhooks.json b/config/http/notifications/webhooks.json index 23d374acc..88e26ea00 100644 --- a/config/http/notifications/webhooks.json +++ b/config/http/notifications/webhooks.json @@ -3,6 +3,7 @@ "import": [ "css:config/http/notifications/base/description.json", "css:config/http/notifications/base/handler.json", + "css:config/http/notifications/base/http.json", "css:config/http/notifications/base/listener.json", "css:config/http/notifications/base/storage.json", "css:config/http/notifications/webhooks/handler.json", diff --git a/config/http/notifications/webhooks/routes.json b/config/http/notifications/webhooks/routes.json index 603330a11..ee595f9c4 100644 --- a/config/http/notifications/webhooks/routes.json +++ b/config/http/notifications/webhooks/routes.json @@ -14,20 +14,6 @@ "relativePath": "/webId" }, - - { - "comment": "Handles unsubscribing from a WebHookSubscription2021.", - "@id": "urn:solid-server:default:WebHookUnsubscriber", - "@type": "OperationRouterHandler", - "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, - "allowedMethods": [ "DELETE" ], - "allowedPathNames": [ "/WebHookSubscription2021/" ], - "handler": { - "@type": "WebHookUnsubscriber", - "credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" }, - "storage": { "@id": "urn:solid-server:default:SubscriptionStorage" } - } - }, { "comment": "Handles the WebHookSubscription2021 WebID.", "@id": "urn:solid-server:default:WebHookWebId", @@ -45,7 +31,6 @@ "@type": "WaterfallHandler", "handlers": [ { "@id": "urn:solid-server:default:WebHookRouter" }, - { "@id": "urn:solid-server:default:WebHookUnsubscriber" }, { "@id": "urn:solid-server:default:WebHookWebId" } ] } diff --git a/config/http/notifications/websockets.json b/config/http/notifications/websockets.json index 8c8cfce93..4d778e2fc 100644 --- a/config/http/notifications/websockets.json +++ b/config/http/notifications/websockets.json @@ -3,6 +3,7 @@ "import": [ "css:config/http/notifications/base/description.json", "css:config/http/notifications/base/handler.json", + "css:config/http/notifications/base/http.json", "css:config/http/notifications/base/listener.json", "css:config/http/notifications/base/storage.json", "css:config/http/notifications/websockets/handler.json", diff --git a/documentation/markdown/usage/notifications.md b/documentation/markdown/usage/notifications.md index 32d43dc6c..8d942ad29 100644 --- a/documentation/markdown/usage/notifications.md +++ b/documentation/markdown/usage/notifications.md @@ -135,6 +135,19 @@ The `unsubscribe_endpoint` field is new here. Once created, the notification channel can be removed and notifications stopped by sending a `DELETE` request to the URL found in that field. +## Unsubscribing from a notification channel + +!!! note + This feature is not part of the Solid Notification v0.2 specification + so might be changed or removed in the future. + +If you no longer want to receive notifications on the channel you created, +you can send a `DELETE` request to the channel to remove it. +Use the value found in the `id` field of the subscription response. +There is no way to retrieve this identifier later on, +so make sure to keep track of it just in case you want to unsubscribe at some point. +No authorization is needed for this request. + ## Notification format Below is an example notification that would be sent when a resource changes: @@ -203,13 +216,13 @@ The available fields are: ## Important note for server owners -There is not much restriction on who can create a new notification channel, +There is not much restriction on who can create a new notification channel; only `Read` permissions on the target resource are required. It is therefore possible for the server to accumulate created channels. As these channels still get used every time their corresponding resource changes, this could degrade server performance. -For this reason, the server is by default configured to always remove notification channels after 2 weeks. +For this reason, the default server configuration removes notification channels after two weeks (20160 minutes). You can modify this behaviour by adding the following block to your configuration: ```json @@ -222,5 +235,5 @@ You can modify this behaviour by adding the following block to your configuratio `maxDuration` defines after how many minutes every channel will be removed. Setting this value to 0 will allow channels to exist forever. -Similarly, for changing the maximum duration of webhook channels you can use the identifier +Similarly, to change the maximum duration of webhook channels you can use the identifier `urn:solid-server:default:WebHookSubscriber`. diff --git a/src/index.ts b/src/index.ts index 04c6c35fb..b2886dfe6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -324,7 +324,6 @@ export * from './server/notifications/serialize/NotificationSerializer'; // Server/Notifications/WebHookSubscription2021 export * from './server/notifications/WebHookSubscription2021/WebHookEmitter'; export * from './server/notifications/WebHookSubscription2021/WebHookSubscription2021'; -export * from './server/notifications/WebHookSubscription2021/WebHookUnsubscriber'; export * from './server/notifications/WebHookSubscription2021/WebHookWebId'; // Server/Notifications/WebSocketChannel2023 @@ -350,6 +349,7 @@ export * from './server/notifications/NotificationDescriber'; export * from './server/notifications/NotificationEmitter'; export * from './server/notifications/NotificationHandler'; export * from './server/notifications/NotificationSubscriber'; +export * from './server/notifications/NotificationUnsubscriber'; export * from './server/notifications/StateHandler'; export * from './server/notifications/TypedNotificationHandler'; diff --git a/src/server/notifications/KeyValueChannelStorage.ts b/src/server/notifications/KeyValueChannelStorage.ts index bb11fb82e..b15684278 100644 --- a/src/server/notifications/KeyValueChannelStorage.ts +++ b/src/server/notifications/KeyValueChannelStorage.ts @@ -74,13 +74,14 @@ export class KeyValueChannelStorage implements NotificationChannelStorage { }); } - public async delete(id: string): Promise { - return this.locker.withWriteLock(this.getLockKey(id), async(): Promise => { + public async delete(id: string): Promise { + return this.locker.withWriteLock(this.getLockKey(id), async(): Promise => { const channel = await this.get(id); if (!channel) { - return; + return false; } await this.deleteChannel(channel); + return true; }); } diff --git a/src/server/notifications/NotificationChannelStorage.ts b/src/server/notifications/NotificationChannelStorage.ts index 01a5dbd50..796df6be2 100644 --- a/src/server/notifications/NotificationChannelStorage.ts +++ b/src/server/notifications/NotificationChannelStorage.ts @@ -37,7 +37,8 @@ export interface NotificationChannelStorage { /** * Deletes the given notification channel from the storage. + * Returns true if the channel existed. * @param id - The identifier of the notification channel */ - delete: (id: string) => Promise; + delete: (id: string) => Promise; } diff --git a/src/server/notifications/NotificationUnsubscriber.ts b/src/server/notifications/NotificationUnsubscriber.ts new file mode 100644 index 000000000..7d2dfd296 --- /dev/null +++ b/src/server/notifications/NotificationUnsubscriber.ts @@ -0,0 +1,34 @@ +import { ResetResponseDescription } from '../../http/output/response/ResetResponseDescription'; +import type { ResponseDescription } from '../../http/output/response/ResponseDescription'; +import { getLoggerFor } from '../../logging/LogUtil'; +import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; +import type { OperationHttpHandlerInput } from '../OperationHttpHandler'; +import { OperationHttpHandler } from '../OperationHttpHandler'; +import type { NotificationChannelStorage } from './NotificationChannelStorage'; + +/** + * Allows clients to unsubscribe from notification channels. + * Should be wrapped in a route handler that only allows `DELETE` operations. + */ +export class NotificationUnsubscriber extends OperationHttpHandler { + protected readonly logger = getLoggerFor(this); + + private readonly storage: NotificationChannelStorage; + + public constructor(storage: NotificationChannelStorage) { + super(); + this.storage = storage; + } + + public async handle({ operation }: OperationHttpHandlerInput): Promise { + const id = operation.target.path; + + const existed = await this.storage.delete(id); + if (!existed) { + throw new NotFoundHttpError(); + } + this.logger.debug(`Deleted notification channel ${id}`); + + return new ResetResponseDescription(); + } +} diff --git a/src/server/notifications/WebHookSubscription2021/WebHookUnsubscriber.ts b/src/server/notifications/WebHookSubscription2021/WebHookUnsubscriber.ts deleted file mode 100644 index 7ee882582..000000000 --- a/src/server/notifications/WebHookSubscription2021/WebHookUnsubscriber.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CredentialsExtractor } from '../../../authentication/CredentialsExtractor'; -import { ResetResponseDescription } from '../../../http/output/response/ResetResponseDescription'; -import type { ResponseDescription } from '../../../http/output/response/ResponseDescription'; -import { getLoggerFor } from '../../../logging/LogUtil'; -import { ForbiddenHttpError } from '../../../util/errors/ForbiddenHttpError'; -import { NotFoundHttpError } from '../../../util/errors/NotFoundHttpError'; -import type { OperationHttpHandlerInput } from '../../OperationHttpHandler'; -import { OperationHttpHandler } from '../../OperationHttpHandler'; -import type { NotificationChannelStorage } from '../NotificationChannelStorage'; -import { isWebHook2021Channel } from './WebHookSubscription2021'; - -/** - * Allows clients to unsubscribe from a WebHookSubscription2021. - * Should be wrapped in a route handler that only allows `DELETE` operations. - */ -export class WebHookUnsubscriber extends OperationHttpHandler { - protected readonly logger = getLoggerFor(this); - - private readonly credentialsExtractor: CredentialsExtractor; - private readonly storage: NotificationChannelStorage; - - public constructor(credentialsExtractor: CredentialsExtractor, storage: NotificationChannelStorage) { - super(); - this.credentialsExtractor = credentialsExtractor; - this.storage = storage; - } - - public async handle({ operation, request }: OperationHttpHandlerInput): Promise { - const id = operation.target.path; - const channel = await this.storage.get(id); - if (!channel || !isWebHook2021Channel(channel)) { - throw new NotFoundHttpError(); - } - - const credentials = await this.credentialsExtractor.handleSafe(request); - if (channel.webId !== credentials.agent?.webId) { - throw new ForbiddenHttpError(); - } - - this.logger.debug(`Deleting WebHook notification channel ${id}`); - await this.storage.delete(id); - - return new ResetResponseDescription(); - } -} diff --git a/src/server/notifications/WebSocketChannel2023/WebSocket2023Storer.ts b/src/server/notifications/WebSocketChannel2023/WebSocket2023Storer.ts index e6a035fc3..a0bdb3ae4 100644 --- a/src/server/notifications/WebSocketChannel2023/WebSocket2023Storer.ts +++ b/src/server/notifications/WebSocketChannel2023/WebSocket2023Storer.ts @@ -49,7 +49,8 @@ export class WebSocket2023Storer extends WebSocket2023Handler { const result = await this.storage.get(id); if (!result) { for (const socket of sockets) { - // Due to the attached listener this also deletes the entries + // Due to the attached listener, this also deletes the entries in the `socketMap` + socket.send(`Notification channel has expired`); socket.close(); } } diff --git a/test/integration/WebHookSubscription2021.test.ts b/test/integration/WebHookSubscription2021.test.ts index 07b6d4bc3..77258619f 100644 --- a/test/integration/WebHookSubscription2021.test.ts +++ b/test/integration/WebHookSubscription2021.test.ts @@ -5,9 +5,7 @@ import { createRemoteJWKSet, jwtVerify } from 'jose'; import type { NamedNode } from 'n3'; import { DataFactory, Parser, Store } from 'n3'; import type { App } from '../../src/init/App'; -import type { - WebHookSubscription2021Channel, -} from '../../src/server/notifications/WebHookSubscription2021/WebHookSubscription2021'; + import { matchesAuthorizationScheme } from '../../src/util/HeaderUtil'; import { joinUrl, trimTrailingSlashes } from '../../src/util/PathUtil'; import { readJsonStream } from '../../src/util/StreamUtil'; @@ -185,14 +183,12 @@ describe.each(stores)('A server supporting WebHookSubscription2021 using %s', (n response.end(); }); - it('allows a user to unsubscribe.', async(): Promise => { - const json = await subscribe(notificationType, webId, subscriptionUrl, topic, { [NOTIFY.target]: target }); - const unsubscribeUrl = (json as WebHookSubscription2021Channel).unsubscribe_endpoint; - let response = await fetch(unsubscribeUrl, { method: 'DELETE' }); - expect(response.status).toBe(403); - response = await fetch(unsubscribeUrl, { method: 'DELETE', headers: { authorization: `WebID ${webId}` }}); + it('can remove notification channels.', async(): Promise => { + const { id } = await subscribe(notificationType, webId, subscriptionUrl, topic, { [NOTIFY.target]: target }) as any; + + const response = await fetch(id, { method: 'DELETE' }); expect(response.status).toBe(205); - response = await fetch(joinUrl(subscriptionUrl, 'abc'), { method: 'DELETE' }); - expect(response.status).toBe(404); + + // Expired WebSockets only get removed every hour so not feasible to test in integration test }); }); diff --git a/test/integration/WebSocketChannel2023.test.ts b/test/integration/WebSocketChannel2023.test.ts index e48202fc5..df8872bb1 100644 --- a/test/integration/WebSocketChannel2023.test.ts +++ b/test/integration/WebSocketChannel2023.test.ts @@ -308,4 +308,13 @@ describe.each(stores)('A server supporting WebSocketChannel2023 using %s', (name expect(quads.getObjects(null, NOTIFY.terms.topic, null)).toEqual([ namedNode(topic) ]); expect(quads.countQuads(null, NOTIFY.terms.receiveFrom, null, null)).toBe(1); }); + + it('can remove notification channels.', async(): Promise => { + const { id } = await subscribe(notificationType, webId, subscriptionUrl, topic) as any; + + const response = await fetch(id, { method: 'DELETE' }); + expect(response.status).toBe(205); + + // Expired WebSockets only get removed every hour so not feasible to test in integration test + }); }); diff --git a/test/unit/server/notifications/KeyValueChannelStorage.test.ts b/test/unit/server/notifications/KeyValueChannelStorage.test.ts index 2082e9a20..bd1dbab75 100644 --- a/test/unit/server/notifications/KeyValueChannelStorage.test.ts +++ b/test/unit/server/notifications/KeyValueChannelStorage.test.ts @@ -130,7 +130,7 @@ describe('A KeyValueChannelStorage', (): void => { await storage.add(channel); await storage.add(channel2); expect(internalMap.size).toBe(3); - await expect(storage.delete(channel.id)).resolves.toBeUndefined(); + await expect(storage.delete(channel.id)).resolves.toBe(true); expect(internalMap.size).toBe(2); expect([ ...internalMap.values() ]).toEqual(expect.arrayContaining([ [ channel2.id ], @@ -140,12 +140,12 @@ describe('A KeyValueChannelStorage', (): void => { it('removes the references for an identifier if the array is empty.', async(): Promise => { await storage.add(channel); - await expect(storage.delete(channel.id)).resolves.toBeUndefined(); + await expect(storage.delete(channel.id)).resolves.toBe(true); expect(internalMap.size).toBe(0); }); it('does nothing if the target does not exist.', async(): Promise => { - await expect(storage.delete(channel.id)).resolves.toBeUndefined(); + await expect(storage.delete(channel.id)).resolves.toBe(false); }); it('logs an error if the target can not be found in the list of references.', async(): Promise => { @@ -153,7 +153,7 @@ describe('A KeyValueChannelStorage', (): void => { // Looking for the key so this test doesn't depend on the internal keys used const id = [ ...internalMap.entries() ].find((entry): boolean => Array.isArray(entry[1]))![0]; internalMap.set(id, []); - await expect(storage.delete(channel.id)).resolves.toBeUndefined(); + await expect(storage.delete(channel.id)).resolves.toBe(true); expect(logger.error).toHaveBeenCalledTimes(2); }); }); diff --git a/test/unit/server/notifications/NotificationUnsubscriber.test.ts b/test/unit/server/notifications/NotificationUnsubscriber.test.ts new file mode 100644 index 000000000..4cf54d909 --- /dev/null +++ b/test/unit/server/notifications/NotificationUnsubscriber.test.ts @@ -0,0 +1,45 @@ +import type { Operation } from '../../../../src/http/Operation'; +import { ResetResponseDescription } from '../../../../src/http/output/response/ResetResponseDescription'; +import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation'; +import type { HttpRequest } from '../../../../src/server/HttpRequest'; +import type { HttpResponse } from '../../../../src/server/HttpResponse'; +import type { NotificationChannelStorage } from '../../../../src/server/notifications/NotificationChannelStorage'; +import { NotificationUnsubscriber } from '../../../../src/server/notifications/NotificationUnsubscriber'; +import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; + +describe('A NotificationUnsubscriber', (): void => { + const request: HttpRequest = {} as any; + const response: HttpResponse = {} as any; + let operation: Operation; + let storage: jest.Mocked; + let unsubscriber: NotificationUnsubscriber; + + beforeEach(async(): Promise => { + operation = { + method: 'DELETE', + target: { path: 'http://example.com/.notifications/channeltype/134' }, + preferences: {}, + body: new BasicRepresentation(), + }; + + storage = { + delete: jest.fn().mockResolvedValue(true), + } as any; + + unsubscriber = new NotificationUnsubscriber(storage); + }); + + it('rejects if the id does not match any stored channel.', async(): Promise => { + storage.delete.mockResolvedValue(false); + await expect(unsubscriber.handle({ operation, request, response })).rejects.toThrow(NotFoundHttpError); + expect(storage.delete).toHaveBeenCalledTimes(1); + expect(storage.delete).toHaveBeenLastCalledWith('http://example.com/.notifications/channeltype/134'); + }); + + it('deletes the corresponding channel.', async(): Promise => { + await expect(unsubscriber.handle({ operation, request, response })) + .resolves.toEqual(new ResetResponseDescription()); + expect(storage.delete).toHaveBeenCalledTimes(1); + expect(storage.delete).toHaveBeenLastCalledWith('http://example.com/.notifications/channeltype/134'); + }); +}); diff --git a/test/unit/server/notifications/WebHookSubscription2021/WebHookUnsubscriber.test.ts b/test/unit/server/notifications/WebHookSubscription2021/WebHookUnsubscriber.test.ts deleted file mode 100644 index 6a5df42d7..000000000 --- a/test/unit/server/notifications/WebHookSubscription2021/WebHookUnsubscriber.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { CredentialsExtractor } from '../../../../../src/authentication/CredentialsExtractor'; -import type { Operation } from '../../../../../src/http/Operation'; -import { ResetResponseDescription } from '../../../../../src/http/output/response/ResetResponseDescription'; -import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation'; -import type { HttpRequest } from '../../../../../src/server/HttpRequest'; -import type { HttpResponse } from '../../../../../src/server/HttpResponse'; -import type { NotificationChannelStorage } from '../../../../../src/server/notifications/NotificationChannelStorage'; - -import { - WebHookUnsubscriber, -} from '../../../../../src/server/notifications/WebHookSubscription2021/WebHookUnsubscriber'; -import { ForbiddenHttpError } from '../../../../../src/util/errors/ForbiddenHttpError'; -import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError'; -import { NOTIFY } from '../../../../../src/util/Vocabularies'; - -describe('A WebHookUnsubscriber', (): void => { - const request: HttpRequest = {} as any; - const response: HttpResponse = {} as any; - let operation: Operation; - const webId = 'http://example.com/alice'; - let credentialsExtractor: jest.Mocked; - let storage: jest.Mocked; - let unsubscriber: WebHookUnsubscriber; - - beforeEach(async(): Promise => { - operation = { - method: 'DELETE', - target: { path: 'http://example.com/.notifications/webhooks/134' }, - preferences: {}, - body: new BasicRepresentation(), - }; - - credentialsExtractor = { - handleSafe: jest.fn().mockResolvedValue({ agent: { webId }}), - } as any; - - storage = { - get: jest.fn().mockResolvedValue({ type: NOTIFY.WebHookSubscription2021, webId }), - delete: jest.fn(), - } as any; - - unsubscriber = new WebHookUnsubscriber(credentialsExtractor, storage); - }); - - it('rejects if the id does not match any stored channel.', async(): Promise => { - storage.get.mockResolvedValue(undefined); - await expect(unsubscriber.handle({ operation, request, response })).rejects.toThrow(NotFoundHttpError); - expect(storage.delete).toHaveBeenCalledTimes(0); - }); - - it('rejects if credentials are wrong.', async(): Promise => { - credentialsExtractor.handleSafe.mockResolvedValue({ agent: { webId: 'http://example.com/bob' }}); - await expect(unsubscriber.handle({ operation, request, response })).rejects.toThrow(ForbiddenHttpError); - expect(storage.delete).toHaveBeenCalledTimes(0); - }); - - it('deletes the corresponding channel.', async(): Promise => { - await expect(unsubscriber.handle({ operation, request, response })) - .resolves.toEqual(new ResetResponseDescription()); - expect(storage.delete).toHaveBeenCalledTimes(1); - expect(storage.delete).toHaveBeenLastCalledWith('http://example.com/.notifications/webhooks/134'); - }); -}); diff --git a/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Storer.test.ts b/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Storer.test.ts index 1b49be672..699cd0b0f 100644 --- a/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Storer.test.ts +++ b/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Storer.test.ts @@ -25,6 +25,7 @@ describe('A WebSocket2023Storer', (): void => { beforeEach(async(): Promise => { webSocket = new EventEmitter() as any; + webSocket.send = jest.fn(); webSocket.close = jest.fn(); storage = { @@ -63,8 +64,10 @@ describe('A WebSocket2023Storer', (): void => { storer = new WebSocket2023Storer(storage, socketMap); const webSocket2: jest.Mocked = new EventEmitter() as any; + webSocket2.send = jest.fn(); webSocket2.close = jest.fn(); const webSocketOther: jest.Mocked = new EventEmitter() as any; + webSocketOther.send = jest.fn(); webSocketOther.close = jest.fn(); const channelOther: NotificationChannel = { ...channel,