From aa42d6953c03a093503916ecd84f53e4a5c0ef0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rib=C3=B3?= Date: Wed, 24 Apr 2024 19:43:46 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20optin=20to=20websockets=20for=20the=20me?= =?UTF-8?q?diator=20live=20mode=20as=20an=20experiment,=E2=80=A6=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 4 +- src/prism-agent/Agent.ts | 28 ++- .../connectionsManager/ConnectionsManager.ts | 15 +- src/prism-agent/helpers/Task.ts | 4 - src/prism-agent/types/index.ts | 8 + tests/agent/Agent.ConnectionsManager.test.ts | 225 ++++++++++++++++++ tests/agent/Agent.test.ts | 36 ++- tests/agent/mocks/ConnectionManagerMock.ts | 80 ++++++- 8 files changed, 370 insertions(+), 30 deletions(-) create mode 100644 tests/agent/Agent.ConnectionsManager.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 3559b54a1..a3a25278d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -28,8 +28,10 @@ "${fileBasenameNoExtension}", "--colors", "--workerThreads", + "--detectOpenHandles", "--maxWorkers", - "1" + "1", + "./tests" ], "skipFiles": [ "${workspaceRoot}/../../node_modules/**/*", diff --git a/src/prism-agent/Agent.ts b/src/prism-agent/Agent.ts index 44ac8f8a5..74f8569f0 100644 --- a/src/prism-agent/Agent.ts +++ b/src/prism-agent/Agent.ts @@ -11,6 +11,7 @@ import { AgentCredentials as AgentCredentialsClass, AgentDIDHigherFunctions as AgentDIDHigherFunctionsClass, AgentInvitations as AgentInvitationsClass, + AgentOptions, EventCallback, InvitationType, ListenerKey, @@ -86,8 +87,10 @@ export default class Agent public readonly mediationHandler: MediatorHandler, public readonly connectionManager: ConnectionsManager, public readonly seed: Domain.Seed = apollo.createRandomSeed().seed, - public readonly api: Domain.Api = new ApiImpl() + public readonly api: Domain.Api = new ApiImpl(), + options?: AgentOptions ) { + this.pollux = new Pollux(castor); this.agentCredentials = new AgentCredentials( apollo, @@ -104,7 +107,8 @@ export default class Agent pluto, this.agentCredentials, mediationHandler, - [] + [], + options ); @@ -147,6 +151,7 @@ export default class Agent castor?: Domain.Castor; mercury?: Domain.Mercury; seed?: Domain.Seed; + options?: AgentOptions }): Agent { const mediatorDID = Domain.DID.from(params.mediatorDID); const pluto = params.pluto; @@ -170,7 +175,15 @@ export default class Agent pollux, seed ); - const manager = new ConnectionsManager(castor, mercury, pluto, agentCredentials, handler); + const manager = new ConnectionsManager( + castor, + mercury, + pluto, + agentCredentials, + handler, + [], + params.options + ); const agent = new Agent( apollo, @@ -180,7 +193,8 @@ export default class Agent handler, manager, seed, - api + api, + params.options ); return agent; @@ -217,7 +231,8 @@ export default class Agent mercury: Domain.Mercury, connectionManager: ConnectionsManager, seed?: Domain.Seed, - api?: Domain.Api + api?: Domain.Api, + options?: AgentOptions ) { return new Agent( apollo, @@ -227,7 +242,8 @@ export default class Agent connectionManager.mediationHandler, connectionManager, seed ? seed : apollo.createRandomSeed().seed, - api ? api : new ApiImpl() + api ? api : new ApiImpl(), + options ); } diff --git a/src/prism-agent/connectionsManager/ConnectionsManager.ts b/src/prism-agent/connectionsManager/ConnectionsManager.ts index 5ac7e7c9c..fafb6d91b 100644 --- a/src/prism-agent/connectionsManager/ConnectionsManager.ts +++ b/src/prism-agent/connectionsManager/ConnectionsManager.ts @@ -1,4 +1,3 @@ -import { uuid } from "@stablelib/uuid"; import { DID, Message, MessageDirection, Pollux } from "../../domain"; import { Castor } from "../../domain/buildingBlocks/Castor"; import { Mercury } from "../../domain/buildingBlocks/Mercury"; @@ -10,6 +9,7 @@ import { CancellableTask } from "../helpers/Task"; import { AgentCredentials, AgentMessageEvents as AgentMessageEventsClass, + AgentOptions, ConnectionsManager as ConnectionsManagerClass, ListenerKey, MediatorHandler, @@ -72,11 +72,16 @@ export class ConnectionsManager implements ConnectionsManagerClass { public pluto: Pluto, public agentCredentials: AgentCredentials, public mediationHandler: MediatorHandler, - public pairings: DIDPair[] = [] + public pairings: DIDPair[] = [], + public options?: AgentOptions ) { this.events = new AgentMessageEvents(); } + get withWebsocketsExperiment() { + return this.options?.experiments?.liveMode === true + } + /** * Asyncronously Start the mediator, just checking if we had one stored in Database and * setting that one as default during the Agent start @@ -263,8 +268,10 @@ export class ConnectionsManager implements ConnectionsManagerClass { const currentMediator = this.mediationHandler.mediator.mediatorDID; const resolvedMediator = await this.castor.resolveDID(currentMediator.toString()); const hasWebsocket = resolvedMediator.services.find(({ serviceEndpoint: { uri } }) => - uri.startsWith("ws://") || - uri.startsWith("wss://") + ( + uri.startsWith("ws://") || + uri.startsWith("wss://") + ) && this.withWebsocketsExperiment ); if (!hasWebsocket) { const timeInterval = Math.max(iterationPeriod, 5) * 1000; diff --git a/src/prism-agent/helpers/Task.ts b/src/prism-agent/helpers/Task.ts index 52c37453e..f273c136f 100644 --- a/src/prism-agent/helpers/Task.ts +++ b/src/prism-agent/helpers/Task.ts @@ -1,9 +1,5 @@ type Task = (signal: AbortSignal) => Promise; - - - - export class CancellableTask { private period?: number; private controller: AbortController; diff --git a/src/prism-agent/types/index.ts b/src/prism-agent/types/index.ts index 7bb565942..e65dd1c45 100644 --- a/src/prism-agent/types/index.ts +++ b/src/prism-agent/types/index.ts @@ -28,6 +28,13 @@ export enum InvitationTypes { PRISM_ONBOARD, } + +export type AgentOptions = { + experiments?: { + liveMode?: boolean + } +} + export type InvitationType = PrismOnboardingInvitation | OutOfBandInvitation; export class PrismOnboardingInvitation implements InvitationInterface { @@ -111,6 +118,7 @@ export interface ConnectionsManager { // eslint-disable-next-line @typescript-eslint/no-explicit-any cancellables: CancellableTask[]; + withWebsocketsExperiment: boolean; stopAllEvents(): void; addConnection(paired: DIDPair): Promise; diff --git a/tests/agent/Agent.ConnectionsManager.test.ts b/tests/agent/Agent.ConnectionsManager.test.ts new file mode 100644 index 000000000..e766c82d9 --- /dev/null +++ b/tests/agent/Agent.ConnectionsManager.test.ts @@ -0,0 +1,225 @@ +/** + * @jest-environment node + */ +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import SinonChai from "sinon-chai"; +import { Apollo, BasicMediatorHandler, Castor, ConnectionsManager, MediatorStore, Pluto } from "../../src"; +import { Curve, KeyTypes, Mercury, Service, ServiceEndpoint } from "../../src/domain"; +import { MercuryStub } from "./mocks/MercuryMock"; +import { AgentCredentials } from "../../src/prism-agent/Agent.Credentials"; +import { AgentOptions } from "../../src/prism-agent/types"; + +chai.use(SinonChai); +chai.use(chaiAsPromised); + +const store: MediatorStore = null as any; +const mercury: Mercury = new MercuryStub(); + +const apollo = new Apollo(); +const castor = new Castor(apollo) +const pluto: Pluto = null as any; +const agentCredentials: AgentCredentials = null as any; + + +async function createBasicMediationHandler( + ConnectionsManager: any, + BasicMediatorHandler: any, + services: Service[], + options?: AgentOptions +): Promise< + { + manager: ConnectionsManager, + handler: BasicMediatorHandler + } +> { + + const seed = apollo.createRandomSeed().seed; + const keypair = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.SECP256K1, + seed: Buffer.from(seed.value).toString("hex"), + }); + const mediatorDID = await castor.createPrismDID(keypair.publicKey(), services); + const handler = new BasicMediatorHandler( + mediatorDID, + mercury, + store + ); + handler.mediator = { + hostDID: mediatorDID, + routingDID: mediatorDID, + mediatorDID: mediatorDID + } + const manager = new ConnectionsManager( + castor, + mercury, + pluto, + agentCredentials, + handler, + [], + options + ) + return { + manager, + handler + } +} + + +describe("ConnectionsManager tests", () => { + + beforeEach(() => { + jest.mock('isows', () => ({ + WebSocket: jest.fn(() => ({ + addEventListener: jest.fn(), + send: jest.fn(), + close: jest.fn(), + })), + })); + }) + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("Should use websockets if the mediator's did endpoint uri contains ws or wss and agent options have the opt in", async () => { + const services = [ + new Service( + "#didcomm-1", + ["DIDCommMessaging"], + new ServiceEndpoint("ws://localhost:12346") + ) + ]; + const ConnectionsManager = jest.requireActual('../../src/prism-agent/connectionsManager/ConnectionsManager').ConnectionsManager; + const BasicMediatorHandler = jest.requireMock('../../src/prism-agent/mediator/BasicMediatorHandler').BasicMediatorHandler; + const { manager, handler } = await createBasicMediationHandler( + ConnectionsManager, + BasicMediatorHandler, + services, + { + experiments: { + liveMode: true + } + } + ); + const listenUnread = jest.spyOn(handler, 'listenUnreadMessages') + expect(manager).toHaveProperty('withWebsocketsExperiment', true); + await manager.startFetchingMessages(1) + expect(listenUnread).toHaveBeenCalled(); + + manager.stopFetchingMessages() + }) + + it("Should not use websockets even if the mediator's did endpoint uri contains ws or wss if the agent options don't have the opt-in", async () => { + const services = [ + new Service( + "#didcomm-1", + ["DIDCommMessaging"], + new ServiceEndpoint("ws://localhost:12346") + ) + ]; + const ConnectionsManager = jest.requireActual('../../src/prism-agent/connectionsManager/ConnectionsManager').ConnectionsManager; + const BasicMediatorHandler = jest.requireMock('../../src/prism-agent/mediator/BasicMediatorHandler').BasicMediatorHandler; + const { manager, handler } = await createBasicMediationHandler( + ConnectionsManager, + BasicMediatorHandler, + services + ); + const listenUnread = jest.spyOn(handler, 'listenUnreadMessages') + expect(manager).toHaveProperty('withWebsocketsExperiment', false); + + await manager.startFetchingMessages(1) + + expect(listenUnread).not.toHaveBeenCalled() + manager.stopFetchingMessages() + }) + + it("Should not use websockets even if the mediator's did endpoint uri contains ws or wss if the agent options don't have the opt-in 1", async () => { + const services = [ + new Service( + "#didcomm-1", + ["DIDCommMessaging"], + new ServiceEndpoint("ws://localhost:12346") + ) + ]; + const ConnectionsManager = jest.requireActual('../../src/prism-agent/connectionsManager/ConnectionsManager').ConnectionsManager; + const BasicMediatorHandler = jest.requireMock('../../src/prism-agent/mediator/BasicMediatorHandler').BasicMediatorHandler; + const { manager, handler } = await createBasicMediationHandler( + ConnectionsManager, + BasicMediatorHandler, + services, + { + experiments: { + + } + } + ); + const listenUnread = jest.spyOn(handler, 'listenUnreadMessages') + expect(manager).toHaveProperty('withWebsocketsExperiment', false); + + await manager.startFetchingMessages(1) + + expect(listenUnread).not.toHaveBeenCalled() + manager.stopFetchingMessages() + }) + + it("Should not use websockets even if the mediator's did endpoint uri contains ws or wss if the agent options don't have the opt-in 2", async () => { + const services = [ + new Service( + "#didcomm-1", + ["DIDCommMessaging"], + new ServiceEndpoint("ws://localhost:12346") + ) + ]; + const ConnectionsManager = jest.requireActual('../../src/prism-agent/connectionsManager/ConnectionsManager').ConnectionsManager; + const BasicMediatorHandler = jest.requireMock('../../src/prism-agent/mediator/BasicMediatorHandler').BasicMediatorHandler; + const { manager, handler } = await createBasicMediationHandler( + ConnectionsManager, + BasicMediatorHandler, + services, + { + experiments: { + liveMode: false + } + } + ); + const listenUnread = jest.spyOn(handler, 'listenUnreadMessages') + expect(manager).toHaveProperty('withWebsocketsExperiment', false); + + await manager.startFetchingMessages(1) + + expect(listenUnread).not.toHaveBeenCalled() + manager.stopFetchingMessages() + }) + + it("Should not use websockets if the mediator'd did endpoint uri does not contain ws or wss for more than the agent has opted in", async () => { + const services = [ + new Service( + "#didcomm-1", + ["DIDCommMessaging"], + new ServiceEndpoint("http://localhost:12346") + ) + ]; + const ConnectionsManager = jest.requireActual('../../src/prism-agent/connectionsManager/ConnectionsManager').ConnectionsManager; + const BasicMediatorHandler = jest.requireMock('../../src/prism-agent/mediator/BasicMediatorHandler').BasicMediatorHandler; + const { manager, handler } = await createBasicMediationHandler( + ConnectionsManager, + BasicMediatorHandler, + services, + { + experiments: { + liveMode: true + } + } + ); + const listenUnread = jest.spyOn(handler, 'listenUnreadMessages') + expect(manager).toHaveProperty('withWebsocketsExperiment', true); + + await manager.startFetchingMessages(1) + + expect(listenUnread).not.toHaveBeenCalled() + manager.stopFetchingMessages() + }) + +}) \ No newline at end of file diff --git a/tests/agent/Agent.test.ts b/tests/agent/Agent.test.ts index 462159b5f..8eccfb003 100644 --- a/tests/agent/Agent.test.ts +++ b/tests/agent/Agent.test.ts @@ -60,10 +60,7 @@ let pluto: IPluto; let pollux: Pollux; let castor: Castor; let sandbox: sinon.SinonSandbox; -let store: Pluto.Store -// jest.mock("../apollo/utils/jwt/JWT", () => () => ({ -// sign: jest.fn(() => "") -// })); +let store: Pluto.Store; describe("Agent Tests", () => { @@ -76,7 +73,13 @@ describe("Agent Tests", () => { beforeEach(async () => { jest.useFakeTimers(); - + jest.mock('isows', () => ({ + WebSocket: jest.fn(() => ({ + addEventListener: jest.fn(), + send: jest.fn(), + close: jest.fn(), + })), + })); sandbox = sinon.createSandbox(); const apollo: Apollo = new Apollo(); castor = CastorMock; @@ -105,16 +108,31 @@ describe("Agent Tests", () => { apollo.createRandomSeed().seed ) - const connectionsManager = new ConnectionsManagerMock( - castor, mercury, pluto, agentCredentials + const connectionsManager = ConnectionsManagerMock.buildMock({ + castor, + mercury, + pluto, + agentCredentials, + options: { + experiments: { + liveMode: false + } + } + }) + - ); agent = Agent.instanceFromConnectionManager( apollo, castor, pluto, mercury, - connectionsManager + connectionsManager, + undefined, undefined, + { + experiments: { + liveMode: false + } + } ); await polluxInstance.start(); diff --git a/tests/agent/mocks/ConnectionManagerMock.ts b/tests/agent/mocks/ConnectionManagerMock.ts index 7f0627bbd..5bc3b5306 100644 --- a/tests/agent/mocks/ConnectionManagerMock.ts +++ b/tests/agent/mocks/ConnectionManagerMock.ts @@ -1,6 +1,8 @@ import { AgentCredentials, + AgentOptions, ConnectionsManager as ConnectionsManagerClass, + EventCallback, MediatorHandler, } from "../../../src/prism-agent/types"; import { Castor } from "../../../src/domain/buildingBlocks/Castor"; @@ -8,24 +10,37 @@ import { Mercury } from "../../../src/domain/buildingBlocks/Mercury"; import { Pluto } from "../../../src/domain/buildingBlocks/Pluto"; import { DIDPair } from "../../../src/domain/models/DIDPair"; import { CancellableTask } from "../../../src/prism-agent/helpers/Task"; -import { DID, Message, Pollux } from "../../../src/domain"; +import { DID, Mediator, Message, Pollux } from "../../../src/domain"; import { AgentMessageEvents } from "../../../src/prism-agent/Agent.MessageEvents"; import { ConnectionsManager } from "../../../src"; + + +type ConnectionMockConstructor = { + castor: Castor, + mercury: Mercury, + pluto: Pluto, + agentCredentials: AgentCredentials, + mediationHandler: MediatorHandler, + pairings?: DIDPair[], + options?: AgentOptions +} + + export class ConnectionsManagerMock implements ConnectionsManagerClass { private manager: ConnectionsManagerClass; + public options?: AgentOptions constructor( - castor: Castor, - mercury: Mercury, - pluto: Pluto, - agentCredentials: AgentCredentials + params: ConnectionMockConstructor ) { + const { castor, mercury, pluto, agentCredentials, options } = params this.castor = castor; this.mercury = mercury; this.pluto = pluto; this.agentCredentials = agentCredentials; + this.options = options; const connManager = new ConnectionsManager( this.castor, @@ -33,12 +48,65 @@ export class ConnectionsManagerMock implements ConnectionsManagerClass { this.pluto, this.agentCredentials, this.mediationHandler, - this.pairings + [], + options ) this.manager = connManager; this.mediationHandler = this.manager.mediationHandler; } + + static buildMock(params: Partial): ConnectionsManagerMock { + const mediationHandler: MediatorHandler = { + registerMessagesAsRead: async () => { }, + updateKeyListWithDIDs: async () => { }, + mediator: { + mediatorDID: new DID( + "did", + "peer", + "2.Ez6LSms555YhFthn1WV8ciDBpZm86hK9tp83WojJUmxPGk1hZ.Vz6MkmdBjMyB4TS5UbbQw54szm8yvMMf1ftGV2sQVYAxaeWhE.SeyJpZCI6Im5ldy1pZCIsInQiOiJkbSIsInMiOnsidXJpIjoiaHR0cHM6Ly9tZWRpYXRvci5yb290c2lkLmNsb3VkIiwiYSI6WyJkaWRjb21tL3YyIl19fQ" + ), + hostDID: new DID( + "did", + "peer", + "2.Ez6LSms555YhFthn1WV8ciDBpZm86hK9tp83WojJUmxPGk1hZ.Vz6MkmdBjMyB4TS5UbbQw54szm8yvMMf1ftGV2sQVYAxaeWhE.SeyJpZCI6Im5ldy1pZCIsInQiOiJkbSIsInMiOnsidXJpIjoiaHR0cHM6Ly9tZWRpYXRvci5yb290c2lkLmNsb3VkIiwiYSI6WyJkaWRjb21tL3YyIl19fQ" + ), + routingDID: new DID( + "did", + "peer", + "2.Ez6LSms555YhFthn1WV8ciDBpZm86hK9tp83WojJUmxPGk1hZ.Vz6MkmdBjMyB4TS5UbbQw54szm8yvMMf1ftGV2sQVYAxaeWhE.SeyJpZCI6Im5ldy1pZCIsInQiOiJkbSIsInMiOnsidXJpIjoiaHR0cHM6Ly9tZWRpYXRvci5yb290c2lkLmNsb3VkIiwiYSI6WyJkaWRjb21tL3YyIl19fQ" + ), + }, + mediatorDID: new DID( + "did", + "peer", + "2.Ez6LSms555YhFthn1WV8ciDBpZm86hK9tp83WojJUmxPGk1hZ.Vz6MkmdBjMyB4TS5UbbQw54szm8yvMMf1ftGV2sQVYAxaeWhE.SeyJpZCI6Im5ldy1pZCIsInQiOiJkbSIsInMiOnsidXJpIjoiaHR0cHM6Ly9tZWRpYXRvci5yb290c2lkLmNsb3VkIiwiYSI6WyJkaWRjb21tL3YyIl19fQ" + ), + bootRegisteredMediator: function (): Promise { + throw new Error("Mock bootRegisteredMediator Function not implemented."); + }, + achieveMediation: function (host: DID): Promise { + throw new Error("Mock achieveMediation Function not implemented."); + }, + pickupUnreadMessages: function (limit: number): Promise<{ attachmentId: string; message: Message; }[]> { + throw new Error("Mock pickupUnreadMessages Function not implemented."); + }, + listenUnreadMessages: function (signal: AbortSignal, serviceEndpointUri: string, onMessage: EventCallback): void { + throw new Error("Mock listenUnreadMessages Function not implemented."); + } + }; + + params.mediationHandler = mediationHandler; + + return new ConnectionsManagerMock( + params as ConnectionMockConstructor + ) + } + + get withWebsocketsExperiment() { + return this.options?.experiments?.liveMode === true + } + processMessages(messages: { attachmentId: string; message: Message; }[]): Promise { return this.manager.processMessages(messages) }