From e261da0ed097721be36588f72a531a879734b2cb Mon Sep 17 00:00:00 2001 From: Emma Casolin Date: Fri, 25 Feb 2022 15:59:51 +1100 Subject: [PATCH] Fixes for failing tests on master --- src/utils/utils.ts | 4 +- tests/bin/utils.test.ts | 6 +- .../NotificationsManager.test.ts | 1227 ++++++++++------- tests/validation/utils.nodes.test.ts | 52 + 4 files changed, 804 insertions(+), 485 deletions(-) create mode 100644 tests/validation/utils.nodes.test.ts diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 636f92bd5..547e1c6b2 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -205,7 +205,7 @@ function arrayUnset(items: Array, item: T) { function debounce

( f: (...params: P) => any, - timeout: number = 0 + timeout: number = 0, ): (...param: P) => void { let timer: ReturnType; return function (this: any, ...args: any[]) { @@ -231,5 +231,5 @@ export { timerStop, arraySet, arrayUnset, - debounce + debounce, }; diff --git a/tests/bin/utils.test.ts b/tests/bin/utils.test.ts index 1b00ad9f0..9bfcbf9d1 100644 --- a/tests/bin/utils.test.ts +++ b/tests/bin/utils.test.ts @@ -15,7 +15,7 @@ describe('bin/utils', () => { type: 'json', data: ['Testing', 'the', 'list', 'output'], }), - ).toBe('["Testing","the","list","output"]'); + ).toBe('["Testing","the","list","output"]\n'); }); test('table in human and in json format', () => { // Table @@ -38,7 +38,7 @@ describe('bin/utils', () => { ], }), ).toBe( - '[{"key1":"value1","key2":"value2"},{"key1":"data1","key2":"data2"}]', + '[{"key1":"value1","key2":"value2"},{"key1":"data1","key2":"data2"}]\n', ); }); test('dict in human and in json format', () => { @@ -61,6 +61,6 @@ describe('bin/utils', () => { type: 'json', data: { key1: 'value1', key2: 'value2' }, }), - ).toBe('{"key1":"value1","key2":"value2"}'); + ).toBe('{"key1":"value1","key2":"value2"}\n'); }); }); diff --git a/tests/notifications/NotificationsManager.test.ts b/tests/notifications/NotificationsManager.test.ts index a1e23b564..d52aa0968 100644 --- a/tests/notifications/NotificationsManager.test.ts +++ b/tests/notifications/NotificationsManager.test.ts @@ -1,605 +1,872 @@ -import type { NodeId, NodeInfo, NodeAddress } from '@/nodes/types'; -import type { Host, Port, TLSConfig } from '@/network/types'; -import type { KeyPairPem, CertificatePem } from '@/keys/types'; +import type { NodeId } from '@/nodes/types'; +import type { Host, Port } from '@/network/types'; import type { VaultActions, VaultName } from '@/vaults/types'; -import type { NotificationData } from '@/notifications/types'; +import type { Notification, NotificationData } from '@/notifications/types'; import fs from 'fs'; import os from 'os'; import path from 'path'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; - -import { ACL } from '@/acl'; -import { Sigchain } from '@/sigchain'; -import { GRPCServer } from '@/grpc'; -import { KeyManager, utils as keysUtils } from '@/keys'; -import { VaultManager } from '@/vaults'; -import { GestaltGraph } from '@/gestalts'; -import { NodeConnectionManager, NodeGraph, NodeManager } from '@/nodes'; -import { NotificationsManager } from '@/notifications'; -import { ForwardProxy, ReverseProxy } from '@/network'; -import { AgentServiceService, createAgentService } from '@/agent'; -import { generateVaultId } from '@/vaults/utils'; -import { utils as nodesUtils } from '@/nodes'; +import { IdInternal } from '@matrixai/id'; +import PolykeyAgent from '@/PolykeyAgent'; +import ACL from '@/acl/ACL'; +import Sigchain from '@/sigchain/Sigchain'; +import KeyManager from '@/keys/KeyManager'; +import NodeConnectionManager from '@/nodes/NodeConnectionManager'; +import NodeGraph from '@/nodes/NodeGraph'; +import NodeManager from '@/nodes/NodeManager'; +import NotificationsManager from '@/notifications/NotificationsManager'; +import ForwardProxy from '@/network/ForwardProxy'; +import ReverseProxy from '@/network/ReverseProxy'; +import * as notificationsErrors from '@/notifications/errors'; +import * as vaultsUtils from '@/vaults/utils'; +import * as nodesUtils from '@/nodes/utils'; +import * as keysUtils from '@/keys/utils'; import * as testUtils from '../utils'; -// Mocks. -jest.mock('@/keys/utils', () => ({ - ...jest.requireActual('@/keys/utils'), - generateDeterministicKeyPair: - jest.requireActual('@/keys/utils').generateKeyPair, -})); - describe('NotificationsManager', () => { const password = 'password'; - const node: NodeInfo = { - id: nodesUtils.encodeNodeId(testUtils.generateRandomNodeId()), - chain: {}, - }; - const logger = new Logger('NotificationsManager Test', LogLevel.WARN, [ - new StreamHandler(), + const logger = new Logger( + `${NotificationsManager.name} Test`, + LogLevel.WARN, + [new StreamHandler()], + ); + const senderId = IdInternal.create([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 5, ]); - const authToken = 'AUTH'; - let senderFwdProxy: ForwardProxy; - let receiverRevProxy: ReverseProxy; - let fwdTLSConfig: TLSConfig; - - let keysDataDir: string; - let receiverDataDir: string; - let receiverKeyManager: KeyManager; - let receiverVaultManager: VaultManager; - let receiverNodeGraph: NodeGraph; - let receiverNodeManager: NodeManager; - let receiverNodeConnectionManager: NodeConnectionManager; - let receiverSigchain: Sigchain; - let receiverACL: ACL; - let receiverGestaltGraph: GestaltGraph; - let receiverDb: DB; - let receiverNotificationsManager: NotificationsManager; - - let senderDataDir: string; - let senderKeyManager: KeyManager; - let senderDb: DB; - let senderACL: ACL; - let senderSigchain: Sigchain; - let senderNodeGraph: NodeGraph; - let senderNodeConnectionManager: NodeConnectionManager; - let senderNodeManager: NodeManager; - - let senderNodeId: NodeId, receiverNodeId: NodeId; - let senderKeyPairPem: KeyPairPem, receiverKeyPairPem: KeyPairPem; - let senderCertPem: CertificatePem, receiverCertPem: CertificatePem; - - let agentService; - let agentServer: GRPCServer; - - // Keep IPs unique. ideally we'd use the generated IP and port. but this is good for now. - // If this fails again we shouldn't specify the port and IP. - const senderHost = '127.0.0.1' as Host; - const receiverHost = '127.0.0.2' as Host; - let receiverIngressPort: Port; - + const senderIdEncoded = nodesUtils.encodeNodeId( + IdInternal.create([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 5, + ]), + ); + let mockedGenerateKeyPair: jest.SpyInstance; + let mockedGenerateDeterministicKeyPair: jest.SpyInstance; + /** + * Shared ACL, DB, NodeManager, KeyManager for all tests + */ + let dataDir: string; + let acl: ACL; + let db: DB; + let nodeGraph: NodeGraph; + let nodeConnectionManager: NodeConnectionManager; + let nodeManager: NodeManager; + let keyManager: KeyManager; + let sigchain: Sigchain; + let fwdProxy: ForwardProxy; + let revProxy: ReverseProxy; + let receiver: PolykeyAgent; beforeAll(async () => { - keysDataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-server'), + const globalKeyPair = await testUtils.setupGlobalKeypair(); + mockedGenerateKeyPair = jest + .spyOn(keysUtils, 'generateKeyPair') + .mockResolvedValueOnce(globalKeyPair); + mockedGenerateDeterministicKeyPair = jest + .spyOn(keysUtils, 'generateDeterministicKeyPair') + .mockResolvedValueOnce(globalKeyPair); + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), ); - - receiverDataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-server'), - ); - - const senderKeysPath = path.join(keysDataDir, 'senderKeys'); - senderKeyManager = await KeyManager.createKeyManager({ + const keysPath = path.join(dataDir, 'keys'); + keyManager = await KeyManager.createKeyManager({ password, - keysPath: senderKeysPath, - fs, + keysPath, logger, }); - senderKeyPairPem = senderKeyManager.getRootKeyPairPem(); - senderCertPem = senderKeyManager.getRootCertPem(); - senderNodeId = keysUtils.certNodeId(senderKeyManager.getRootCert())!; - fwdTLSConfig = { - keyPrivatePem: senderKeyPairPem.privateKey, - certChainPem: senderCertPem, - }; - - const receiverKeysPath = path.join(keysDataDir, 'receiverKeys'); - receiverKeyManager = await KeyManager.createKeyManager({ - password, - keysPath: receiverKeysPath, - fs: fs, - logger: logger, - }); - receiverKeyPairPem = receiverKeyManager.getRootKeyPairPem(); - receiverCertPem = receiverKeyManager.getRootCertPem(); - const revTLSConfig = { - keyPrivatePem: receiverKeyPairPem.privateKey, - certChainPem: receiverCertPem, - }; - - // Server setup - const receiverVaultsPath = path.join(receiverDataDir, 'receiverVaults'); - const receiverDbPath = path.join(receiverDataDir, 'receiverDb'); - - receiverDb = await DB.createDB({ - dbPath: receiverDbPath, - fs: fs, - logger: logger, + const dbPath = path.join(dataDir, 'db'); + db = await DB.createDB({ + dbPath, + logger, crypto: { - key: receiverKeyManager.dbKey, + key: keyManager.dbKey, ops: { encrypt: keysUtils.encryptWithKey, decrypt: keysUtils.decryptWithKey, }, }, }); - receiverACL = await ACL.createACL({ - db: receiverDb, - logger: logger, - }); - receiverSigchain = await Sigchain.createSigchain({ - keyManager: receiverKeyManager, - db: receiverDb, - logger: logger, - }); - receiverGestaltGraph = await GestaltGraph.createGestaltGraph({ - db: receiverDb, - acl: receiverACL, - logger: logger, - }); - // Won't be used so don't need to start - const receiverFwdProxy = new ForwardProxy({ - authToken: '', - logger: logger, - }); - receiverRevProxy = new ReverseProxy({ - logger: logger, - }); - receiverNodeGraph = await NodeGraph.createNodeGraph({ - db: receiverDb, - keyManager: receiverKeyManager, - logger: logger, - }); - receiverNodeConnectionManager = new NodeConnectionManager({ - keyManager: receiverKeyManager, - nodeGraph: receiverNodeGraph, - fwdProxy: receiverFwdProxy, - revProxy: receiverRevProxy, + acl = await ACL.createACL({ + db, logger, }); - await receiverNodeConnectionManager.start(); - receiverNodeManager = new NodeManager({ - db: receiverDb, - sigchain: receiverSigchain, - keyManager: receiverKeyManager, - nodeGraph: receiverNodeGraph, - nodeConnectionManager: receiverNodeConnectionManager, - logger: logger, - }); - receiverVaultManager = await VaultManager.createVaultManager({ - keyManager: receiverKeyManager, - vaultsPath: receiverVaultsPath, - nodeConnectionManager: receiverNodeConnectionManager, - vaultsKey: receiverKeyManager.vaultKey, - db: receiverDb, - acl: receiverACL, - gestaltGraph: receiverGestaltGraph, - fs: fs, - logger: logger, - }); - receiverNotificationsManager = - await NotificationsManager.createNotificationsManager({ - acl: receiverACL, - db: receiverDb, - nodeConnectionManager: receiverNodeConnectionManager, - nodeManager: receiverNodeManager, - keyManager: receiverKeyManager, - messageCap: 5, - logger: logger, - }); - receiverNodeId = keysUtils.certNodeId(receiverKeyManager.getRootCert())!; - await receiverGestaltGraph.setNode(node); - - agentService = createAgentService({ - keyManager: receiverKeyManager, - vaultManager: receiverVaultManager, - nodeManager: receiverNodeManager, - nodeGraph: receiverNodeGraph, - sigchain: receiverSigchain, - nodeConnectionManager: receiverNodeConnectionManager, - notificationsManager: receiverNotificationsManager, - }); - agentServer = new GRPCServer({ - logger: logger, - }); - await agentServer.start({ - services: [[AgentServiceService, agentService]], - host: receiverHost, - }); - - await receiverRevProxy.start({ - serverHost: receiverHost, - serverPort: agentServer.port, - ingressHost: receiverHost, - tlsConfig: revTLSConfig, + sigchain = await Sigchain.createSigchain({ + db, + keyManager, + logger, }); - receiverIngressPort = receiverRevProxy.getIngressPort(); - }, global.polykeyStartupTimeout * 2); - - beforeEach(async () => { - senderDataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-sender'), - ); - const senderDbPath = await fs.promises.mkdtemp( - path.join(senderDataDir, 'senderDb'), - ); - // Won't be used so don't need to start - const senderRevProxy = new ReverseProxy({ - logger: logger, + fwdProxy = new ForwardProxy({ + authToken: 'abc123', + logger, }); - senderFwdProxy = new ForwardProxy({ - authToken: authToken, - logger: logger, + await fwdProxy.start({ + tlsConfig: { + keyPrivatePem: keyManager.getRootKeyPairPem().privateKey, + certChainPem: await keyManager.getRootCertChainPem(), + }, }); - senderDb = await DB.createDB({ - dbPath: senderDbPath, - fs, - logger, - crypto: { - key: senderKeyManager.dbKey, - ops: { - encrypt: keysUtils.encryptWithKey, - decrypt: keysUtils.decryptWithKey, - }, + revProxy = new ReverseProxy({ logger }); + await revProxy.start({ + serverHost: '127.0.0.1' as Host, + serverPort: 55555 as Port, + tlsConfig: { + keyPrivatePem: keyManager.getRootKeyPairPem().privateKey, + certChainPem: await keyManager.getRootCertChainPem(), }, }); - senderACL = await ACL.createACL({ db: senderDb, logger }); - senderSigchain = await Sigchain.createSigchain({ - keyManager: senderKeyManager, - db: senderDb, + nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, logger, }); - senderNodeGraph = await NodeGraph.createNodeGraph({ - db: senderDb, - keyManager: senderKeyManager, + nodeConnectionManager = new NodeConnectionManager({ + nodeGraph, + keyManager, + fwdProxy, + revProxy, logger, }); - senderNodeConnectionManager = new NodeConnectionManager({ - keyManager: senderKeyManager, - nodeGraph: senderNodeGraph, - fwdProxy: senderFwdProxy, - revProxy: senderRevProxy, + await nodeConnectionManager.start(); + nodeManager = new NodeManager({ + db, + keyManager, + sigchain, + nodeConnectionManager, + nodeGraph, logger, }); - await senderNodeConnectionManager.start(); - senderNodeManager = new NodeManager({ - db: senderDb, - sigchain: senderSigchain, - keyManager: senderKeyManager, - nodeGraph: senderNodeGraph, - nodeConnectionManager: senderNodeConnectionManager, + // Set up node for receiving notifications + receiver = await PolykeyAgent.createPolykeyAgent({ + password: password, + nodePath: path.join(dataDir, 'receiver'), + keysConfig: { + rootKeyPairBits: 1024, + }, logger, }); - - await senderACL.stop(); - await senderFwdProxy.start({ - tlsConfig: fwdTLSConfig, - proxyHost: senderHost, - // ProxyPort: senderPort, - egressHost: senderHost, - // EgressPort: senderPort, - }); - await senderNodeGraph.setNode(receiverNodeId, { - host: receiverHost, - port: receiverIngressPort, - } as NodeAddress); - - await receiverNotificationsManager.clearNotifications(); - expect(await receiverNotificationsManager.readNotifications()).toEqual([]); - }, global.polykeyStartupTimeout * 2); - - afterEach(async () => { - await senderNodeConnectionManager.stop(); - await senderNodeGraph.stop(); - await senderACL.stop(); - await senderFwdProxy.stop(); - await senderDb.stop(); - await fs.promises.rm(senderDataDir, { - force: true, - recursive: true, + await nodeGraph.setNode(receiver.keyManager.getNodeId(), { + host: receiver.revProxy.getIngressHost(), + port: receiver.revProxy.getIngressPort(), }); - }); - + }, global.defaultTimeout); afterAll(async () => { - await senderKeyManager.stop(); - await receiverACL.stop(); - await receiverSigchain.stop(); - await receiverGestaltGraph.stop(); - await receiverVaultManager.stop(); - await receiverNodeConnectionManager.stop(); - await receiverNodeGraph.stop(); - await receiverNotificationsManager.stop(); - await agentServer.stop(); - await receiverRevProxy.stop(); - await receiverKeyManager.stop(); - await receiverDb.stop(); - await fs.promises.rm(receiverDataDir, { + await receiver.stop(); + await nodeConnectionManager.stop(); + await nodeGraph.stop(); + await revProxy.stop(); + await fwdProxy.stop(); + await sigchain.stop(); + await acl.stop(); + await db.stop(); + await keyManager.stop(); + await fs.promises.rm(dataDir, { force: true, recursive: true, }); + mockedGenerateKeyPair.mockRestore(); + mockedGenerateDeterministicKeyPair.mockRestore(); }); - - test('can send notifications', async () => { - const senderNotificationsManager = + test('notifications manager readiness', async () => { + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, + logger, + }); + await expect(notificationsManager.destroy()).rejects.toThrow( + notificationsErrors.ErrorNotificationsRunning, + ); + // Should be a noop + await notificationsManager.start(); + await notificationsManager.stop(); + await notificationsManager.destroy(); + await expect(notificationsManager.start()).rejects.toThrow( + notificationsErrors.ErrorNotificationsDestroyed, + ); + await expect(async () => { + await notificationsManager.readNotifications(); + }).rejects.toThrow(notificationsErrors.ErrorNotificationsNotRunning); + await expect(async () => { + await notificationsManager.clearNotifications(); + }).rejects.toThrow(notificationsErrors.ErrorNotificationsNotRunning); + }); + test('can send notifications with permission', async () => { + const notificationsManager = await NotificationsManager.createNotificationsManager({ - acl: senderACL, - db: senderDb, - nodeConnectionManager: senderNodeConnectionManager, - nodeManager: senderNodeManager, - keyManager: senderKeyManager, + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, logger, }); - const notificationData: NotificationData = { + const generalNotification: NotificationData = { type: 'General', message: 'msg', }; - // Can send with permissions - await receiverACL.setNodePerm(senderNodeId, { + const gestaltNotification: NotificationData = { + type: 'GestaltInvite', + }; + const vaultNotification: NotificationData = { + type: 'VaultShare', + vaultId: vaultsUtils.generateVaultId().toString(), + vaultName: 'vaultName' as VaultName, + actions: { + clone: null, + pull: null, + } as VaultActions, + }; + await receiver.acl.setNodePerm(keyManager.getNodeId(), { gestalt: { notify: null, }, vaults: {}, }); - // Can send without permissions - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData, + await notificationsManager.sendNotification( + receiver.keyManager.getNodeId(), + generalNotification, ); - await receiverACL.setNodePerm(senderNodeId, { - gestalt: {}, - vaults: {}, - }); - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData, + await notificationsManager.sendNotification( + receiver.keyManager.getNodeId(), + gestaltNotification, + ); + await notificationsManager.sendNotification( + receiver.keyManager.getNodeId(), + vaultNotification, + ); + const receivedNotifications = + await receiver.notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(3); + expect(receivedNotifications[0].data).toEqual(vaultNotification); + expect(receivedNotifications[0].senderId).toBe( + nodesUtils.encodeNodeId(keyManager.getNodeId()), + ); + expect(receivedNotifications[1].data).toEqual(gestaltNotification); + expect(receivedNotifications[1].senderId).toBe( + nodesUtils.encodeNodeId(keyManager.getNodeId()), ); - await senderNotificationsManager.stop(); + expect(receivedNotifications[2].data).toEqual(generalNotification); + expect(receivedNotifications[2].senderId).toBe( + nodesUtils.encodeNodeId(keyManager.getNodeId()), + ); + // Reverse side-effects + await receiver.notificationsManager.clearNotifications(); + await receiver.acl.unsetNodePerm(keyManager.getNodeId()); + await notificationsManager.stop(); }); - - test('can receive and read sent notifications', async () => { - const senderNotificationsManager = + test('cannot send notifications without permission', async () => { + const notificationsManager = await NotificationsManager.createNotificationsManager({ - acl: senderACL, - db: senderDb, - nodeConnectionManager: senderNodeConnectionManager, - nodeManager: senderNodeManager, - keyManager: senderKeyManager, + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, logger, }); - - const notificationData: NotificationData = { + const generalNotification: NotificationData = { type: 'General', message: 'msg', }; - await receiverACL.setNodePerm(senderNodeId, { + const gestaltNotification: NotificationData = { + type: 'GestaltInvite', + }; + const vaultNotification: NotificationData = { + type: 'VaultShare', + vaultId: vaultsUtils.generateVaultId().toString(), + vaultName: 'vaultName' as VaultName, + actions: { + clone: null, + pull: null, + } as VaultActions, + }; + await expect(async () => + notificationsManager.sendNotification( + receiver.keyManager.getNodeId(), + generalNotification, + ), + ).rejects.toThrow( + notificationsErrors.ErrorNotificationsPermissionsNotFound, + ); + await expect(async () => + notificationsManager.sendNotification( + receiver.keyManager.getNodeId(), + gestaltNotification, + ), + ).rejects.toThrow( + notificationsErrors.ErrorNotificationsPermissionsNotFound, + ); + await expect(async () => + notificationsManager.sendNotification( + receiver.keyManager.getNodeId(), + vaultNotification, + ), + ).rejects.toThrow( + notificationsErrors.ErrorNotificationsPermissionsNotFound, + ); + const receivedNotifications = + await receiver.notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(0); + // Reverse side-effects + await notificationsManager.stop(); + }); + test('can receive notifications from senders with permission', async () => { + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, + logger, + }); + const notification1: Notification = { + data: { + type: 'General', + message: 'msg', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification2: Notification = { + data: { + type: 'GestaltInvite', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification3: Notification = { + data: { + type: 'VaultShare', + vaultId: vaultsUtils.generateVaultId().toString(), + vaultName: 'vaultName' as VaultName, + actions: { + clone: null, + pull: null, + } as VaultActions, + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { gestalt: { notify: null, }, vaults: {}, }); - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData, - ); - const notifs = await receiverNotificationsManager.readNotifications(); - expect(notifs[0].data).toEqual(notificationData); - expect(notifs[0].senderId).toEqual(nodesUtils.encodeNodeId(senderNodeId)); - expect(notifs[0].isRead).toBeTruthy(); - - await senderNotificationsManager.stop(); + await notificationsManager.receiveNotification(notification1); + await notificationsManager.receiveNotification(notification2); + await notificationsManager.receiveNotification(notification3); + const receivedNotifications = + await notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(3); + expect(receivedNotifications[0].data).toEqual(notification3.data); + expect(receivedNotifications[0].senderId).toEqual(senderIdEncoded); + expect(receivedNotifications[1].data).toEqual(notification2.data); + expect(receivedNotifications[1].senderId).toEqual(senderIdEncoded); + expect(receivedNotifications[2].data).toEqual(notification1.data); + expect(receivedNotifications[2].senderId).toEqual(senderIdEncoded); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); }); - - test('cannot receive notifications without notify permission', async () => { - const senderNotificationsManager = + test('cannot receive notifications from senders without permission', async () => { + const notificationsManager = await NotificationsManager.createNotificationsManager({ - acl: senderACL, - db: senderDb, - nodeConnectionManager: senderNodeConnectionManager, - nodeManager: senderNodeManager, - keyManager: senderKeyManager, + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, logger, }); - - const notificationData: NotificationData = { - type: 'General', - message: 'msg', + const notification: Notification = { + data: { + type: 'General', + message: 'msg', + }, + senderId: senderIdEncoded, + isRead: false, }; - await receiverACL.setNodePerm(senderNodeId, { + // No permissions + await expect(async () => + notificationsManager.receiveNotification(notification), + ).rejects.toThrow( + notificationsErrors.ErrorNotificationsPermissionsNotFound, + ); + let receivedNotifications = await notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(0); + // Missing permission + await acl.setNodePerm(senderId, { gestalt: {}, vaults: {}, }); - - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData, - ); - const notifs = await receiverNotificationsManager.readNotifications(); - expect(notifs).toEqual([]); - - await senderNotificationsManager.stop(); + await notificationsManager.receiveNotification(notification); + receivedNotifications = await notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(0); + // Reverse side-effects + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); }); - - test('notifications are read in order they were sent', async () => { - const senderNotificationsManager = + test('marks notifications as read', async () => { + const notificationsManager = await NotificationsManager.createNotificationsManager({ - acl: senderACL, - db: senderDb, - nodeConnectionManager: senderNodeConnectionManager, - nodeManager: senderNodeManager, - keyManager: senderKeyManager, + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, logger, }); - - const notificationData1: NotificationData = { - type: 'General', - message: 'msg1', + const notification: Notification = { + data: { + type: 'General', + message: 'msg', + }, + senderId: senderIdEncoded, + isRead: false, }; - const notificationData2: NotificationData = { - type: 'General', - message: 'msg2', + await acl.setNodePerm(senderId, { + gestalt: { + notify: null, + }, + vaults: {}, + }); + await notificationsManager.receiveNotification(notification); + const receivedNotifications = + await notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(1); + expect(receivedNotifications[0].isRead).toBeTruthy(); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); + }); + test('all notifications are read oldest to newest by default', async () => { + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, + logger, + }); + const notification1: Notification = { + data: { + type: 'General', + message: 'msg1', + }, + senderId: senderIdEncoded, + isRead: false, }; - const notificationData3: NotificationData = { - type: 'General', - message: 'msg3', + const notification2: Notification = { + data: { + type: 'General', + message: 'msg2', + }, + senderId: senderIdEncoded, + isRead: false, }; - - await receiverACL.setNodePerm(senderNodeId, { + const notification3: Notification = { + data: { + type: 'General', + message: 'msg3', + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { gestalt: { notify: null, }, vaults: {}, }); - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData1, - ); - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData2, - ); - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData3, - ); - const notifs = await receiverNotificationsManager.readNotifications(); - expect(notifs[0].data).toEqual(notificationData3); - expect(notifs[1].data).toEqual(notificationData2); - expect(notifs[2].data).toEqual(notificationData1); - - await senderNotificationsManager.stop(); + await notificationsManager.receiveNotification(notification1); + await notificationsManager.receiveNotification(notification2); + await notificationsManager.receiveNotification(notification3); + const receivedNotifications = + await notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(3); + expect(receivedNotifications[0].data['message']).toBe('msg3'); + expect(receivedNotifications[1].data['message']).toBe('msg2'); + expect(receivedNotifications[2].data['message']).toBe('msg1'); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); }); - - test('notifications can be capped', async () => { - const senderNotificationsManager = + test('can read only unread notifications', async () => { + const notificationsManager = await NotificationsManager.createNotificationsManager({ - acl: senderACL, - db: senderDb, - nodeConnectionManager: senderNodeConnectionManager, - nodeManager: senderNodeManager, - keyManager: senderKeyManager, + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, logger, }); - - await receiverACL.setNodePerm(senderNodeId, { + const notification1: Notification = { + data: { + type: 'General', + message: 'msg1', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification2: Notification = { + data: { + type: 'General', + message: 'msg2', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification3: Notification = { + data: { + type: 'General', + message: 'msg3', + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { gestalt: { notify: null, }, vaults: {}, }); - for (let i = 0; i <= 5; i++) { - const notificationData: NotificationData = { + await notificationsManager.receiveNotification(notification1); + await notificationsManager.receiveNotification(notification2); + await notificationsManager.receiveNotification(notification3); + await notificationsManager.readNotifications(); + const unreadNotifications = await notificationsManager.readNotifications({ + unread: true, + }); + expect(unreadNotifications).toHaveLength(0); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); + }); + test('can read a single notification', async () => { + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, + logger, + }); + const notification1: Notification = { + data: { type: 'General', - message: i.toString(), - }; - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData, - ); - } - const notifs = await receiverNotificationsManager.readNotifications(); - expect(notifs[0].data).toEqual({ - type: 'General', - message: '5', - }); - expect(notifs[1].data).toEqual({ - type: 'General', - message: '4', - }); - expect(notifs[2].data).toEqual({ - type: 'General', - message: '3', + message: 'msg1', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification2: Notification = { + data: { + type: 'General', + message: 'msg2', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification3: Notification = { + data: { + type: 'General', + message: 'msg3', + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { + gestalt: { + notify: null, + }, + vaults: {}, }); - expect(notifs[3].data).toEqual({ - type: 'General', - message: '2', + await notificationsManager.receiveNotification(notification1); + await notificationsManager.receiveNotification(notification2); + await notificationsManager.receiveNotification(notification3); + const lastNotification = await notificationsManager.readNotifications({ + number: 1, + }); + expect(lastNotification).toHaveLength(1); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); + }); + test('can read notifications in reverse order', async () => { + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, + logger, + }); + const notification1: Notification = { + data: { + type: 'General', + message: 'msg1', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification2: Notification = { + data: { + type: 'General', + message: 'msg2', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification3: Notification = { + data: { + type: 'General', + message: 'msg3', + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { + gestalt: { + notify: null, + }, + vaults: {}, }); - expect(notifs[4].data).toEqual({ - type: 'General', - message: '1', + await notificationsManager.receiveNotification(notification1); + await notificationsManager.receiveNotification(notification2); + await notificationsManager.receiveNotification(notification3); + const reversedNotifications = await notificationsManager.readNotifications({ + order: 'oldest', + }); + expect(reversedNotifications).toHaveLength(3); + expect(reversedNotifications[0].data['message']).toBe('msg1'); + expect(reversedNotifications[1].data['message']).toBe('msg2'); + expect(reversedNotifications[2].data['message']).toBe('msg3'); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); + }); + test('notifications can be capped and oldest notifications deleted', async () => { + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, + messageCap: 2, + logger, + }); + const notification1: Notification = { + data: { + type: 'General', + message: 'msg1', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification2: Notification = { + data: { + type: 'General', + message: 'msg2', + }, + senderId: senderIdEncoded, + isRead: false, + }; + const notification3: Notification = { + data: { + type: 'General', + message: 'msg3', + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { + gestalt: { + notify: null, + }, + vaults: {}, }); - - await senderNotificationsManager.stop(); + await notificationsManager.receiveNotification(notification1); + await notificationsManager.receiveNotification(notification2); + await notificationsManager.receiveNotification(notification3); + const receivedNotifications = + await notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(2); + expect(receivedNotifications[0].data['message']).toBe('msg3'); + expect(receivedNotifications[1].data['message']).toBe('msg2'); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); }); - - test('can send and receive Gestalt Invite notifications', async () => { - const senderNotificationsManager = + test('can find a gestalt invite notification', async () => { + const notificationsManager = await NotificationsManager.createNotificationsManager({ - acl: senderACL, - db: senderDb, - nodeConnectionManager: senderNodeConnectionManager, - nodeManager: senderNodeManager, - keyManager: senderKeyManager, + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, logger, }); - - const notificationData: NotificationData = { - type: 'GestaltInvite', + const notification: Notification = { + data: { + type: 'GestaltInvite', + }, + senderId: senderIdEncoded, + isRead: false, }; - await receiverACL.setNodePerm(senderNodeId, { + await acl.setNodePerm(senderId, { gestalt: { notify: null, }, vaults: {}, }); - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData, + await notificationsManager.receiveNotification(notification); + const receivedInvite = await notificationsManager.findGestaltInvite( + senderId, ); - const notifs = await receiverNotificationsManager.readNotifications(); - expect(notifs[0].data).toEqual(notificationData); - expect(notifs[0].senderId).toEqual(nodesUtils.encodeNodeId(senderNodeId)); - expect(notifs[0].isRead).toBeTruthy(); - - await senderNotificationsManager.stop(); + expect(receivedInvite).toEqual(notification); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); }); - - test('can send and receive Vault Share notifications', async () => { - const senderNotificationsManager = + test('clears notifications', async () => { + const notificationsManager = await NotificationsManager.createNotificationsManager({ - acl: senderACL, - db: senderDb, - nodeConnectionManager: senderNodeConnectionManager, - nodeManager: senderNodeManager, - keyManager: senderKeyManager, + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, logger, }); - - await receiverACL.setNodePerm(senderNodeId, { + const notification: Notification = { + data: { + type: 'General', + message: 'msg', + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { gestalt: { notify: null, }, vaults: {}, }); - - const notificationData: NotificationData = { - type: 'VaultShare', - vaultId: generateVaultId().toString(), - vaultName: 'vaultName' as VaultName, - actions: { - clone: null, - pull: null, - } as VaultActions, + await notificationsManager.receiveNotification(notification); + await notificationsManager.clearNotifications(); + const receivedNotifications = + await notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(0); + // Reverse side-effects + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); + }); + test('notifications are persistent across restarts', async () => { + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, + logger, + }); + const notification1: Notification = { + data: { + type: 'General', + message: 'msg1', + }, + senderId: senderIdEncoded, + isRead: false, }; - - await senderNotificationsManager.sendNotification( - receiverNodeId, - notificationData, - ); - const notifs = await receiverNotificationsManager.readNotifications(); - expect(notifs[0].data).toEqual(notificationData); - expect(notifs[0].senderId).toEqual(nodesUtils.encodeNodeId(senderNodeId)); - expect(notifs[0].isRead).toBeTruthy(); - - await senderNotificationsManager.stop(); + const notification2: Notification = { + data: { + type: 'General', + message: 'msg2', + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { + gestalt: { + notify: null, + }, + vaults: {}, + }); + await notificationsManager.receiveNotification(notification1); + await notificationsManager.receiveNotification(notification2); + await notificationsManager.readNotifications({ number: 1 }); + await notificationsManager.stop(); + await notificationsManager.start(); + const unreadNotifications = await notificationsManager.readNotifications({ + unread: true, + }); + expect(unreadNotifications).toHaveLength(1); + expect(unreadNotifications[0].data).toEqual(notification1.data); + expect(unreadNotifications[0].senderId).toBe(notification1.senderId); + const latestNotification = await notificationsManager.readNotifications({ + number: 1, + }); + expect(latestNotification).toHaveLength(1); + expect(latestNotification[0].data).toEqual(notification2.data); + expect(latestNotification[0].senderId).toBe(notification2.senderId); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); + }); + test('creating fresh notifications manager', async () => { + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeConnectionManager, + nodeManager, + keyManager, + logger, + }); + const notification: Notification = { + data: { + type: 'General', + message: 'msg', + }, + senderId: senderIdEncoded, + isRead: false, + }; + await acl.setNodePerm(senderId, { + gestalt: { + notify: null, + }, + vaults: {}, + }); + await notificationsManager.receiveNotification(notification); + await notificationsManager.stop(); + await notificationsManager.start({ fresh: true }); + const receivedNotifications = + await notificationsManager.readNotifications(); + expect(receivedNotifications).toHaveLength(0); + // Reverse side-effects + await notificationsManager.clearNotifications(); + await acl.unsetNodePerm(senderId); + await notificationsManager.stop(); }); }); diff --git a/tests/validation/utils.nodes.test.ts b/tests/validation/utils.nodes.test.ts new file mode 100644 index 000000000..8374312ef --- /dev/null +++ b/tests/validation/utils.nodes.test.ts @@ -0,0 +1,52 @@ +import * as validationUtils from '@/validation/utils'; +import * as validationErrors from '@/validation/errors'; + +describe('nodes validationUtils', () => { + const nodeIdEncoded1 = + 'vrsc24a1er424epq77dtoveo93meij0pc8ig4uvs9jbeld78n9nl0'; + const nodeIdEncoded2 = + 'vrcacp9vsb4ht25hds6s4lpp2abfaso0mptcfnh499n35vfcn2gkg'; + const nodeIdEncoded3 = + 'vi3et1hrpv2m2lrplcm7cu913kr45v51cak54vm68anlbvuf83ra0'; + const hostname = 'testnet.polykey.io'; + const hostIPv4 = '127.0.0.1'; + const hostIPv6 = '[2001:db8:85a3:8d3:1319:8a2e:370:7348]'; + const port1 = 1314; + const port2 = 1315; + const port3 = 1316; + test('parseSeedNodes - valid seed nodes (using hostname, IPv4, IPv6)', () => { + const rawSeedNodes = + `${nodeIdEncoded1}@${hostname}:${port1};` + + `${nodeIdEncoded2}@${hostIPv4}:${port2};` + + `${nodeIdEncoded3}@${hostIPv6}:${port3};`; + const parsed = validationUtils.parseSeedNodes(rawSeedNodes); + const seeds = parsed[0]; + expect(seeds[nodeIdEncoded1]).toStrictEqual({ + host: hostname, + port: port1, + }); + expect(seeds[nodeIdEncoded2]).toStrictEqual({ + host: hostIPv4, + port: port2, + }); + expect(seeds[nodeIdEncoded3]).toStrictEqual({ + host: hostIPv6.replace(/\[|\]/g, ''), + port: port3, + }); + expect(parsed[1]).toBeFalsy(); + }); + test('parseSeedNodes - invalid node ID', () => { + const rawSeedNodes = `INVALIDNODEID@${hostname}:${port1}`; + expect(() => validationUtils.parseSeedNodes(rawSeedNodes)).toThrow( + validationErrors.ErrorParse, + ); + }); + test('parseSeedNodes - invalid hostname', () => { + const rawSeedNodes = `${nodeIdEncoded1}@$invalidHost:${port1}`; + expect(() => validationUtils.parseSeedNodes(rawSeedNodes)).toThrow( + validationErrors.ErrorParse, + ); + }); + test.todo('parseSeedNodes - invalid port'); + test.todo('parseSeedNodes - invalid structure'); +});