diff --git a/npmDepsHash b/npmDepsHash index 629b0383..771b49fb 100644 --- a/npmDepsHash +++ b/npmDepsHash @@ -1 +1 @@ -sha256-pckkN6xZ+tj+srTB5iHCwuHb3EFv0KiTPvFIGTTy1nQ= +sha256-z97XzEPI3Dwl+Ld26FdKVzkstLbTWRgLiKVT8Dh1uZY= diff --git a/package-lock.json b/package-lock.json index 30980998..1c3bafd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "nexpect": "^0.6.0", "node-gyp-build": "^4.4.0", "nodemon": "^3.0.1", - "polykey": "^1.2.3", + "polykey": "^1.4.0", "prettier": "^3.0.0", "shelljs": "^0.8.5", "shx": "^0.3.4", @@ -7569,9 +7569,9 @@ } }, "node_modules/polykey": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.2.3.tgz", - "integrity": "sha512-PwsXsLMVZvv+yR+ry5+9Bzu10yXSvTczFM58GM4zpR1BxoLD7bwUj4gwJGnToAcufdUismpwHhb7CjT6QYYK/A==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.4.0.tgz", + "integrity": "sha512-xurnvH8aJENtjvVbYszDCr0f2KfucQgPfLhpmXHho35gz1KvVDEHoOmaA+qIK0pSksaPVcOxEPt5oQ+Apsde8Q==", "dev": true, "dependencies": { "@matrixai/async-cancellable": "^1.1.1", diff --git a/package.json b/package.json index 677c6fe9..c43fe0d9 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "nexpect": "^0.6.0", "node-gyp-build": "^4.4.0", "nodemon": "^3.0.1", - "polykey": "^1.2.3", + "polykey": "^1.4.0", "prettier": "^3.0.0", "shelljs": "^0.8.5", "shx": "^0.3.4", diff --git a/src/notifications/CommandNotifications.ts b/src/notifications/CommandNotifications.ts index aff48da1..2a88fe6e 100644 --- a/src/notifications/CommandNotifications.ts +++ b/src/notifications/CommandNotifications.ts @@ -1,5 +1,5 @@ -import CommandClear from './CommandClear'; -import CommandRead from './CommandRead'; +import CommandInbox from './inbox'; +import CommandOutbox from './outbox'; import CommandSend from './CommandSend'; import CommandPolykey from '../CommandPolykey'; @@ -8,8 +8,8 @@ class CommandNotifications extends CommandPolykey { super(...args); this.name('notifications'); this.description('Notifications Operations'); - this.addCommand(new CommandClear(...args)); - this.addCommand(new CommandRead(...args)); + this.addCommand(new CommandInbox(...args)); + this.addCommand(new CommandOutbox(...args)); this.addCommand(new CommandSend(...args)); } } diff --git a/src/notifications/CommandSend.ts b/src/notifications/CommandSend.ts index 0a85d635..ec001af2 100644 --- a/src/notifications/CommandSend.ts +++ b/src/notifications/CommandSend.ts @@ -17,6 +17,10 @@ class CommandSend extends CommandPolykey { binParsers.parseNodeId, ); this.argument('', 'Message to send'); + this.option( + '-r, --retries [number]', + '(optional) Number of retries that should be attempted before giving up', + ); this.addOption(binOptions.nodeId); this.addOption(binOptions.clientHost); this.addOption(binOptions.clientPort); @@ -52,12 +56,14 @@ class CommandSend extends CommandPolykey { }, logger: this.logger.getChild(PolykeyClient.name), }); + const retries = parseInt(options.retries); await binUtils.retryAuthentication( (auth) => pkClient.rpcClient.methods.notificationsSend({ metadata: auth, nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), message: message, + retries: Number.isNaN(retries) ? undefined : retries, }), auth, ); diff --git a/src/notifications/CommandClear.ts b/src/notifications/inbox/CommandClear.ts similarity index 83% rename from src/notifications/CommandClear.ts rename to src/notifications/inbox/CommandClear.ts index 16077b0c..1f8fc153 100644 --- a/src/notifications/CommandClear.ts +++ b/src/notifications/inbox/CommandClear.ts @@ -1,14 +1,14 @@ import type PolykeyClient from 'polykey/dist/PolykeyClient'; -import CommandPolykey from '../CommandPolykey'; -import * as binUtils from '../utils'; -import * as binOptions from '../utils/options'; -import * as binProcessors from '../utils/processors'; +import CommandPolykey from '../../CommandPolykey'; +import * as binUtils from '../../utils'; +import * as binOptions from '../../utils/options'; +import * as binProcessors from '../../utils/processors'; class CommandClear extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); this.name('clear'); - this.description('Clear all Notifications'); + this.description('Clear Inbox Notifications'); this.addOption(binOptions.nodeId); this.addOption(binOptions.clientHost); this.addOption(binOptions.clientPort); @@ -45,7 +45,7 @@ class CommandClear extends CommandPolykey { }); await binUtils.retryAuthentication( (auth) => - pkClient.rpcClient.methods.notificationsClear({ + pkClient.rpcClient.methods.notificationsInboxClear({ metadata: auth, }), auth, diff --git a/src/notifications/CommandRead.ts b/src/notifications/inbox/CommandRead.ts similarity index 86% rename from src/notifications/CommandRead.ts rename to src/notifications/inbox/CommandRead.ts index 4b2811e9..c603eb92 100644 --- a/src/notifications/CommandRead.ts +++ b/src/notifications/inbox/CommandRead.ts @@ -1,23 +1,22 @@ import type { Notification } from 'polykey/dist/notifications/types'; import type PolykeyClient from 'polykey/dist/PolykeyClient'; -import CommandPolykey from '../CommandPolykey'; -import * as binUtils from '../utils'; -import * as binOptions from '../utils/options'; -import * as binProcessors from '../utils/processors'; +import CommandPolykey from '../../CommandPolykey'; +import * as binUtils from '../../utils'; +import * as binOptions from '../../utils/options'; +import * as binProcessors from '../../utils/processors'; class CommandRead extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); this.name('read'); - this.description('Display Notifications'); + this.description('Display Inbox Notifications'); this.option( '-u, --unread', '(optional) Flag to only display unread notifications', ); this.option( - '-n, --number [number]', + '-l, --limit [number]', '(optional) Number of notifications to read', - 'all', ); this.option( '-o, --order [order]', @@ -63,14 +62,13 @@ class CommandRead extends CommandPolykey { }); const notificationReadMessages = await binUtils.retryAuthentication( async (auth) => { - const response = await pkClient.rpcClient.methods.notificationsRead( - { + const response = + await pkClient.rpcClient.methods.notificationsInboxRead({ metadata: auth, unread: options.unread, - number: options.number, - order: options.order, - }, - ); + limit: parseInt(options.limit), + order: options.order === 'newest' ? 'desc' : 'asc', + }); const notificationReadMessages: Array<{ notification: Notification; }> = []; diff --git a/src/notifications/inbox/CommandRemove.ts b/src/notifications/inbox/CommandRemove.ts new file mode 100644 index 00000000..c189088e --- /dev/null +++ b/src/notifications/inbox/CommandRemove.ts @@ -0,0 +1,69 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import * as notificationsUtils from 'polykey/dist/notifications/utils'; +import CommandPolykey from '../../CommandPolykey'; +import * as binUtils from '../../utils'; +import * as binOptions from '../../utils/options'; +import * as binProcessors from '../../utils/processors'; +import * as binParsers from '../../utils/parsers'; + +class CommandRemove extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('remove'); + this.description('Remove a Notification in the Inbox'); + this.argument( + '', + 'Id of the notification to remove', + binParsers.parseNotificationId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (notificationId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + }); + try { + pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + options: { + nodePath: options.nodePath, + }, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClient.methods.notificationsInboxRemove({ + notificationIdEncoded: + notificationsUtils.encodeNotificationId(notificationId), + metadata: auth, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + } + }); + } +} + +export default CommandRemove; diff --git a/src/notifications/inbox/index.ts b/src/notifications/inbox/index.ts new file mode 100644 index 00000000..3783ebf4 --- /dev/null +++ b/src/notifications/inbox/index.ts @@ -0,0 +1,17 @@ +import CommandClear from './CommandClear'; +import CommandRead from './CommandRead'; +import CommandRemove from './CommandRemove'; +import CommandPolykey from '../../CommandPolykey'; + +class CommandInbox extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('inbox'); + this.description('Notifications Inbox Operations'); + this.addCommand(new CommandClear(...args)); + this.addCommand(new CommandRead(...args)); + this.addCommand(new CommandRemove(...args)); + } +} + +export default CommandInbox; diff --git a/src/notifications/outbox/CommandClear.ts b/src/notifications/outbox/CommandClear.ts new file mode 100644 index 00000000..28d5ac37 --- /dev/null +++ b/src/notifications/outbox/CommandClear.ts @@ -0,0 +1,60 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import CommandPolykey from '../../CommandPolykey'; +import * as binUtils from '../../utils'; +import * as binOptions from '../../utils/options'; +import * as binProcessors from '../../utils/processors'; + +class CommandClear extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('clear'); + this.description('Clear Outbox Notifications'); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + }); + try { + pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + options: { + nodePath: options.nodePath, + }, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClient.methods.notificationsOutboxClear({ + metadata: auth, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + } + }); + } +} + +export default CommandClear; diff --git a/src/notifications/outbox/CommandRead.ts b/src/notifications/outbox/CommandRead.ts new file mode 100644 index 00000000..035acf12 --- /dev/null +++ b/src/notifications/outbox/CommandRead.ts @@ -0,0 +1,115 @@ +import type { Notification } from 'polykey/dist/notifications/types'; +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type { NotificationOutboxMessage } from 'polykey/dist/client/types'; +import CommandPolykey from '../../CommandPolykey'; +import * as binUtils from '../../utils'; +import * as binOptions from '../../utils/options'; +import * as binProcessors from '../../utils/processors'; + +class CommandRead extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('read'); + this.description('Display Outbox Notifications'); + this.option( + '-l, --limit [number]', + '(optional) Number of notifications to read', + ); + this.option( + '-o, --order [order]', + '(optional) Order to read notifications', + 'newest', + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const notificationsUtils = await import( + 'polykey/dist/notifications/utils' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + }); + try { + pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + options: { + nodePath: options.nodePath, + }, + logger: this.logger.getChild(PolykeyClient.name), + }); + const notificationReadMessages = await binUtils.retryAuthentication( + async (auth) => { + const response = + await pkClient.rpcClient.methods.notificationsOutboxRead({ + metadata: auth, + limit: parseInt(options.limit), + order: options.order === 'newest' ? 'desc' : 'asc', + }); + const notificationReadMessages: Array<{ + notification: Notification; + taskMetadata: NotificationOutboxMessage['taskMetadata']; + }> = []; + for await (const notificationMessage of response) { + const notification = notificationsUtils.parseNotification( + notificationMessage.notification, + ); + notificationReadMessages.push({ + notification, + taskMetadata: notificationMessage.taskMetadata, + }); + } + return notificationReadMessages; + }, + meta, + ); + if (notificationReadMessages.length === 0) { + process.stderr.write('No notifications pending\n'); + } + if (options.format === 'json') { + process.stdout.write( + binUtils.outputFormatter({ + type: 'json', + data: notificationReadMessages, + }), + ); + } else { + for (const notificationReadMessage of notificationReadMessages) { + process.stdout.write( + binUtils.outputFormatter({ + type: 'dict', + data: { + notificiation: notificationReadMessage.notification, + taskMetadata: notificationReadMessage.taskMetadata, + }, + }), + ); + } + } + } finally { + if (pkClient! != null) await pkClient.stop(); + } + }); + } +} + +export default CommandRead; diff --git a/src/notifications/outbox/CommandRemove.ts b/src/notifications/outbox/CommandRemove.ts new file mode 100644 index 00000000..8861c748 --- /dev/null +++ b/src/notifications/outbox/CommandRemove.ts @@ -0,0 +1,69 @@ +import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import * as notificationsUtils from 'polykey/dist/notifications/utils'; +import CommandPolykey from '../../CommandPolykey'; +import * as binUtils from '../../utils'; +import * as binOptions from '../../utils/options'; +import * as binProcessors from '../../utils/processors'; +import * as binParsers from '../../utils/parsers'; + +class CommandRemove extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('remove'); + this.description('Remove a Pending Notification to be Sent in the Outbox'); + this.argument( + '', + 'Id of the notification to remove', + binParsers.parseNotificationId, + ); + this.addOption(binOptions.nodeId); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.action(async (notificationId, options) => { + const { default: PolykeyClient } = await import( + 'polykey/dist/PolykeyClient' + ); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + const auth = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); + }); + try { + pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + options: { + nodePath: options.nodePath, + }, + logger: this.logger.getChild(PolykeyClient.name), + }); + await binUtils.retryAuthentication( + (auth) => + pkClient.rpcClient.methods.notificationsOutboxRemove({ + notificationIdEncoded: + notificationsUtils.encodeNotificationId(notificationId), + metadata: auth, + }), + auth, + ); + } finally { + if (pkClient! != null) await pkClient.stop(); + } + }); + } +} + +export default CommandRemove; diff --git a/src/notifications/outbox/index.ts b/src/notifications/outbox/index.ts new file mode 100644 index 00000000..5c3fbe96 --- /dev/null +++ b/src/notifications/outbox/index.ts @@ -0,0 +1,17 @@ +import CommandClear from './CommandClear'; +import CommandRead from './CommandRead'; +import CommandRemove from './CommandRemove'; +import CommandPolykey from '../../CommandPolykey'; + +class CommandOutbox extends CommandPolykey { + constructor(...args: ConstructorParameters) { + super(...args); + this.name('outbox'); + this.description('Notifications Outbox Operations'); + this.addCommand(new CommandClear(...args)); + this.addCommand(new CommandRead(...args)); + this.addCommand(new CommandRemove(...args)); + } +} + +export default CommandOutbox; diff --git a/src/utils/parsers.ts b/src/utils/parsers.ts index 307d0c45..1dc5b513 100644 --- a/src/utils/parsers.ts +++ b/src/utils/parsers.ts @@ -105,6 +105,9 @@ const parseNodeId: (data: string) => ids.NodeId = validateParserToArgParser( ids.parseNodeId, ); +const parseNotificationId: (data: string) => ids.NotificationId = + validateParserToArgParser(ids.parseNotificationId); + const parseGestaltId: (data: string) => ids.GestaltId = validateParserToArgParser(ids.parseGestaltId); @@ -153,6 +156,7 @@ export { parseInteger, parseNumber, parseNodeId, + parseNotificationId, parseGestaltId, parseGestaltIdentityId, parseGestaltAction, diff --git a/tests/nodes/claim.test.ts b/tests/nodes/claim.test.ts index b33a0c12..f0fe71a3 100644 --- a/tests/nodes/claim.test.ts +++ b/tests/nodes/claim.test.ts @@ -96,8 +96,11 @@ describe('claim', () => { expect(stderr).toContain(remoteIdEncoded); }); test('sends a gestalt invite (force invite)', async () => { - await remoteNode.notificationsManager.sendNotification(localId, { - type: 'GestaltInvite', + await remoteNode.notificationsManager.sendNotification({ + nodeId: localId, + data: { + type: 'GestaltInvite', + }, }); const { exitCode, stdout, stderr } = await testUtils.pkStdio( ['nodes', 'claim', remoteIdEncoded, '--force-invite', '--format', 'json'], @@ -114,8 +117,11 @@ describe('claim', () => { expect(stderr).toContain(nodesUtils.encodeNodeId(remoteId)); }); test('claims a node', async () => { - await remoteNode.notificationsManager.sendNotification(localId, { - type: 'GestaltInvite', + await remoteNode.notificationsManager.sendNotification({ + nodeId: localId, + data: { + type: 'GestaltInvite', + }, }); const { exitCode, stdout, stderr } = await testUtils.pkStdio( ['nodes', 'claim', remoteIdEncoded, '--format', 'json'], diff --git a/tests/nodes/connections.test.ts b/tests/nodes/connections.test.ts index fe41375d..52ab1154 100644 --- a/tests/nodes/connections.test.ts +++ b/tests/nodes/connections.test.ts @@ -87,8 +87,11 @@ describe('connections', () => { }); }); test('Correctly list connection information, and not list auth data', async () => { - await remoteNode.notificationsManager.sendNotification(localId, { - type: 'GestaltInvite', + await remoteNode.notificationsManager.sendNotification({ + nodeId: localId, + data: { + type: 'GestaltInvite', + }, }); const { exitCode } = await testUtils.pkStdio( ['nodes', 'claim', remoteIdEncoded, '--force-invite'], diff --git a/tests/notifications/sendReadClear.test.ts b/tests/notifications/inbox/sendReadRemoveClear.test.ts similarity index 77% rename from tests/notifications/sendReadClear.test.ts rename to tests/notifications/inbox/sendReadRemoveClear.test.ts index 17ecc18f..36b4e593 100644 --- a/tests/notifications/sendReadClear.test.ts +++ b/tests/notifications/inbox/sendReadRemoveClear.test.ts @@ -5,12 +5,14 @@ import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import * as nodesUtils from 'polykey/dist/nodes/utils'; -import * as testUtils from '../utils'; +import * as testUtils from '../../utils'; describe('send/read/claim', () => { - const logger = new Logger('send/read/clear test', LogLevel.WARN, [ - new StreamHandler(), - ]); + const logger = new Logger( + 'inbox send/read/remove/clear test', + LogLevel.WARN, + [new StreamHandler()], + ); let dataDir: string; let senderId: NodeId; let senderHost: string; @@ -161,9 +163,9 @@ describe('send/read/claim', () => { }, )); expect(exitCode).toBe(0); - // Read notifications + // Read inbox notifications ({ exitCode, stdout } = await testUtils.pkExec( - ['notifications', 'read', '--format', 'json'], + ['notifications', 'inbox', 'read', '--format', 'json'], { env: { PK_NODE_PATH: receiverAgentDir, @@ -202,9 +204,9 @@ describe('send/read/claim', () => { sub: nodesUtils.encodeNodeId(receiverId), isRead: true, }); - // Read only unread (none) + // Read inbox only unread (none) ({ exitCode, stdout } = await testUtils.pkExec( - ['notifications', 'read', '--unread', '--format', 'json'], + ['notifications', 'inbox', 'read', '--unread', '--format', 'json'], { env: { PK_NODE_PATH: receiverAgentDir, @@ -216,9 +218,16 @@ describe('send/read/claim', () => { expect(exitCode).toBe(0); readNotificationMessages = JSON.parse(stdout); expect(readNotificationMessages).toHaveLength(0); - // Read notifications on reverse order + // Read inbox notifications on reverse order ({ exitCode, stdout } = await testUtils.pkExec( - ['notifications', 'read', '--order=oldest', '--format', 'json'], + [ + 'notifications', + 'inbox', + 'read', + '--order=oldest', + '--format', + 'json', + ], { env: { PK_NODE_PATH: receiverAgentDir, @@ -257,9 +266,9 @@ describe('send/read/claim', () => { sub: nodesUtils.encodeNodeId(receiverId), isRead: true, }); - // Read only one notification + // Read only one inbox notification ({ exitCode, stdout } = await testUtils.pkExec( - ['notifications', 'read', '--number=1', '--format', 'json'], + ['notifications', 'inbox', 'read', '--limit', '1', '--format', 'json'], { env: { PK_NODE_PATH: receiverAgentDir, @@ -280,17 +289,67 @@ describe('send/read/claim', () => { sub: nodesUtils.encodeNodeId(receiverId), isRead: true, }); - // Clear notifications - await testUtils.pkExec(['notifications', 'clear'], { + // Get a notificationId to remove + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'inbox', 'read', '--format', 'json'], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + }, + )); + expect(exitCode).toBe(0); + readNotificationMessages = JSON.parse(stdout); + expect(readNotificationMessages).toHaveLength(3); + const deletedNotificationIdEncoded = + readNotificationMessages[0].notification.notificationIdEncoded; + // Remove inbox notificataions + await testUtils.pkExec( + ['notifications', 'inbox', 'remove', deletedNotificationIdEncoded], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + }, + ); + // Check that the notification no longer exists + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'inbox', 'read', '--format', 'json'], + { + env: { + PK_NODE_PATH: receiverAgentDir, + PK_PASSWORD: receiverAgentPassword, + }, + cwd: receiverAgentDir, + }, + )); + expect(exitCode).toBe(0); + readNotificationMessages = JSON.parse(stdout); + expect(readNotificationMessages).toHaveLength(2); + expect(readNotificationMessages).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + notification: { + notificationIdEncoded: deletedNotificationIdEncoded, + }, + }), + ]), + ); + // Clear inbox notifications + await testUtils.pkExec(['notifications', 'inbox', 'clear'], { env: { PK_NODE_PATH: receiverAgentDir, PK_PASSWORD: receiverAgentPassword, }, cwd: receiverAgentDir, }); - // Check there are no more notifications + // Check there are no more inbox notifications ({ exitCode, stdout } = await testUtils.pkExec( - ['notifications', 'read', '--format', 'json'], + ['notifications', 'inbox', 'read', '--format', 'json'], { env: { PK_NODE_PATH: receiverAgentDir, diff --git a/tests/notifications/outbox/sendReadRemoveClear.test.ts b/tests/notifications/outbox/sendReadRemoveClear.test.ts new file mode 100644 index 00000000..ef206a32 --- /dev/null +++ b/tests/notifications/outbox/sendReadRemoveClear.test.ts @@ -0,0 +1,262 @@ +import type { NodeId } from 'polykey/dist/ids/types'; +import type { Notification } from 'polykey/dist/notifications/types'; +import type { StatusLive } from 'polykey/dist/status/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as nodesUtils from 'polykey/dist/nodes/utils'; +import * as testUtils from '../../utils'; + +describe('send/read/claim', () => { + const logger = new Logger( + 'outbox send/read/remove/clear test', + LogLevel.WARN, + [new StreamHandler()], + ); + let dataDir: string; + let senderId: NodeId; + let senderAgentStatus: StatusLive; + let senderAgentClose: () => Promise; + let senderAgentDir: string; + let senderAgentPassword: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + ({ + agentStatus: senderAgentStatus, + agentClose: senderAgentClose, + agentDir: senderAgentDir, + agentPassword: senderAgentPassword, + } = await testUtils.setupTestAgent(logger)); + senderId = senderAgentStatus.data.nodeId; + }); + afterEach(async () => { + await senderAgentClose(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test( + 'sends, receives, and clears notifications', + async () => { + const receiverId = + 'v0000000000000000000000000000000000000000000000000000'; + let exitCode: number, stdout: string; + let readNotificationMessages: Array<{ notification: Notification }>; + // Send some notifications + ({ exitCode } = await testUtils.pkExec( + ['notifications', 'send', receiverId, 'test message 1'], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + ({ exitCode } = await testUtils.pkExec( + ['notifications', 'send', receiverId, 'test message 2'], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + ({ exitCode } = await testUtils.pkExec( + ['notifications', 'send', receiverId, 'test message 3'], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + // Read outbox notifications + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'outbox', 'read', '--format', 'json'], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + readNotificationMessages = JSON.parse(stdout); + expect(readNotificationMessages).toHaveLength(3); + expect(readNotificationMessages[0].notification).toMatchObject({ + data: { + type: 'General', + message: 'test message 3', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: receiverId, + }); + expect(readNotificationMessages[1].notification).toMatchObject({ + data: { + type: 'General', + message: 'test message 2', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: receiverId, + }); + expect(readNotificationMessages[2].notification).toMatchObject({ + data: { + type: 'General', + message: 'test message 1', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: receiverId, + }); + // Read outbox notifications on reverse order + ({ exitCode, stdout } = await testUtils.pkExec( + [ + 'notifications', + 'outbox', + 'read', + '--order=oldest', + '--format', + 'json', + ], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + readNotificationMessages = JSON.parse(stdout); + expect(readNotificationMessages).toHaveLength(3); + expect(readNotificationMessages[0].notification).toMatchObject({ + data: { + type: 'General', + message: 'test message 1', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: receiverId, + }); + expect(readNotificationMessages[1].notification).toMatchObject({ + data: { + type: 'General', + message: 'test message 2', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: receiverId, + }); + expect(readNotificationMessages[2].notification).toMatchObject({ + data: { + type: 'General', + message: 'test message 3', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: receiverId, + }); + // Read only one outbox notification + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'outbox', 'read', '--limit', '1', '--format', 'json'], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + readNotificationMessages = JSON.parse(stdout); + expect(readNotificationMessages).toHaveLength(1); + expect(readNotificationMessages[0].notification).toMatchObject({ + data: { + type: 'General', + message: 'test message 3', + }, + iss: nodesUtils.encodeNodeId(senderId), + sub: receiverId, + }); + // Get a notificationId to remove + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'outbox', 'read', '--format', 'json'], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + readNotificationMessages = JSON.parse(stdout); + expect(readNotificationMessages).toHaveLength(3); + const deletedNotificationIdEncoded = + readNotificationMessages[0].notification.notificationIdEncoded; + // Remove outbox notificataions + await testUtils.pkExec( + ['notifications', 'outbox', 'remove', deletedNotificationIdEncoded], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + ); + // Check that the notification no longer exists + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'outbox', 'read', '--format', 'json'], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + readNotificationMessages = JSON.parse(stdout); + expect(readNotificationMessages).toHaveLength(2); + expect(readNotificationMessages).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + notification: { + notificationIdEncoded: deletedNotificationIdEncoded, + }, + }), + ]), + ); + // Clear outbox notifications + await testUtils.pkExec(['notifications', 'outbox', 'clear'], { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }); + // Check there are no more outbox notifications + ({ exitCode, stdout } = await testUtils.pkExec( + ['notifications', 'outbox', 'read', '--format', 'json'], + { + env: { + PK_NODE_PATH: senderAgentDir, + PK_PASSWORD: senderAgentPassword, + }, + cwd: senderAgentDir, + }, + )); + expect(exitCode).toBe(0); + readNotificationMessages = JSON.parse(stdout); + expect(readNotificationMessages).toHaveLength(0); + }, + globalThis.defaultTimeout * 3, + ); +}); diff --git a/tests/vaults/share.test.ts b/tests/vaults/share.test.ts index c7e9f04f..a4905593 100644 --- a/tests/vaults/share.test.ts +++ b/tests/vaults/share.test.ts @@ -90,7 +90,10 @@ describe('commandShare', () => { ); try { // We don't want to actually send a notification - mockedSendNotification.mockImplementation(async (_) => {}); + mockedSendNotification.mockResolvedValue({ + notificationId: ids.generateNotificationIdFromTimestamp(Date.now()), + sendP: Promise.resolve(), + }); const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); const targetNodeId = nodeIdGenerator();