From 1e724a40007cd0a7181b65200db35b3380791649 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 3 Jul 2022 15:57:57 +0200 Subject: [PATCH 01/12] feat: add contextCorrelationId to events Signed-off-by: Timo Glastra --- packages/core/src/agent/EventEmitter.ts | 11 +++- packages/core/src/agent/Events.ts | 5 ++ .../src/agent/__tests__/EventEmitter.test.ts | 59 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/agent/__tests__/EventEmitter.test.ts diff --git a/packages/core/src/agent/EventEmitter.ts b/packages/core/src/agent/EventEmitter.ts index 3dfe6205b3..3d30e5fa32 100644 --- a/packages/core/src/agent/EventEmitter.ts +++ b/packages/core/src/agent/EventEmitter.ts @@ -10,6 +10,8 @@ import { injectable, inject } from '../plugins' import { AgentDependencies } from './AgentDependencies' +type EmitEvent = Omit + @injectable() export class EventEmitter { private eventEmitter: NativeEventEmitter @@ -24,8 +26,13 @@ export class EventEmitter { } // agentContext is currently not used, but already making required as it will be used soon - public emit(agentContext: AgentContext, data: T) { - this.eventEmitter.emit(data.type, data) + public emit(agentContext: AgentContext, data: EmitEvent) { + this.eventEmitter.emit(data.type, { + ...data, + metadata: { + contextCorrelationId: agentContext.contextCorrelationId, + }, + }) } public on(event: T['type'], listener: (data: T) => void | Promise) { diff --git a/packages/core/src/agent/Events.ts b/packages/core/src/agent/Events.ts index 9c34620ca4..ad4faf8ed2 100644 --- a/packages/core/src/agent/Events.ts +++ b/packages/core/src/agent/Events.ts @@ -6,9 +6,14 @@ export enum AgentEventTypes { AgentMessageProcessed = 'AgentMessageProcessed', } +export interface EventMetadata { + contextCorrelationId: string +} + export interface BaseEvent { type: string payload: Record + metadata: EventMetadata } export interface AgentMessageReceivedEvent extends BaseEvent { diff --git a/packages/core/src/agent/__tests__/EventEmitter.test.ts b/packages/core/src/agent/__tests__/EventEmitter.test.ts new file mode 100644 index 0000000000..480ccbedbb --- /dev/null +++ b/packages/core/src/agent/__tests__/EventEmitter.test.ts @@ -0,0 +1,59 @@ +import type { EventEmitter as NativeEventEmitter } from 'events' + +import { Subject } from 'rxjs' + +import { agentDependencies, getAgentContext } from '../../../tests/helpers' +import { EventEmitter } from '../EventEmitter' + +const mockEmit = jest.fn() +const mockOn = jest.fn() +const mockOff = jest.fn() +const mock = jest.fn().mockImplementation(() => { + return { emit: mockEmit, on: mockOn, off: mockOff } +}) as jest.Mock + +const eventEmitter = new EventEmitter( + { ...agentDependencies, EventEmitterClass: mock as unknown as typeof NativeEventEmitter }, + new Subject() +) +const agentContext = getAgentContext({}) + +describe('EventEmitter', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('emit', () => { + test("calls 'emit' on native event emitter instance", () => { + eventEmitter.emit(agentContext, { + payload: { some: 'payload' }, + type: 'some-event', + }) + + expect(mockEmit).toHaveBeenCalledWith('some-event', { + payload: { some: 'payload' }, + type: 'some-event', + metadata: { + contextCorrelationId: agentContext.contextCorrelationId, + }, + }) + }) + }) + + describe('on', () => { + test("calls 'on' on native event emitter instance", () => { + const listener = jest.fn() + eventEmitter.on('some-event', listener) + + expect(mockOn).toHaveBeenCalledWith('some-event', listener) + }) + }) + describe('off', () => { + test("calls 'off' on native event emitter instance", () => { + const listener = jest.fn() + eventEmitter.off('some-event', listener) + + expect(mockOff).toHaveBeenCalledWith('some-event', listener) + }) + }) +}) From 6d4b3ce3f2ceb9ad97e30d4dbd57bf0cddf29cdd Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 3 Jul 2022 15:59:40 +0200 Subject: [PATCH 02/12] fix: protected type string in jwe Signed-off-by: Timo Glastra --- packages/core/src/utils/JWE.ts | 10 +++++++++- packages/core/src/utils/__tests__/JWE.test.ts | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/core/src/utils/JWE.ts b/packages/core/src/utils/JWE.ts index d8d7909b65..f925987b4b 100644 --- a/packages/core/src/utils/JWE.ts +++ b/packages/core/src/utils/JWE.ts @@ -2,5 +2,13 @@ import type { EncryptedMessage } from '../types' // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isValidJweStructure(message: any): message is EncryptedMessage { - return message && typeof message === 'object' && message.protected && message.iv && message.ciphertext && message.tag + return ( + message && + typeof message === 'object' && + message !== null && + typeof message.protected === 'string' && + message.iv && + message.ciphertext && + message.tag + ) } diff --git a/packages/core/src/utils/__tests__/JWE.test.ts b/packages/core/src/utils/__tests__/JWE.test.ts index 80d1af2ae8..02ed3b5a55 100644 --- a/packages/core/src/utils/__tests__/JWE.test.ts +++ b/packages/core/src/utils/__tests__/JWE.test.ts @@ -3,7 +3,7 @@ import { isValidJweStructure } from '../JWE' describe('ValidJWEStructure', () => { test('throws error when the response message has an invalid JWE structure', async () => { const responseMessage = 'invalid JWE structure' - await expect(isValidJweStructure(responseMessage)).toBeFalsy() + expect(isValidJweStructure(responseMessage)).toBe(false) }) test('valid JWE structure', async () => { @@ -14,6 +14,6 @@ describe('ValidJWEStructure', () => { ciphertext: 'mwRMpVg9wkF4rIZcBeWLcc0fWhs=', tag: '0yW0Lx8-vWevj3if91R06g==', } - await expect(isValidJweStructure(responseMessage)).toBeTruthy() + expect(isValidJweStructure(responseMessage)).toBe(true) }) }) From de5e75174a849e2efec3c53dbc40cb3b1c70725e Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 3 Jul 2022 16:01:29 +0200 Subject: [PATCH 03/12] feat: add generate wallet key method Signed-off-by: Timo Glastra --- packages/core/src/wallet/IndyWallet.ts | 9 +++++++++ packages/core/src/wallet/Wallet.ts | 1 + packages/core/tests/mocks/MockWallet.ts | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/packages/core/src/wallet/IndyWallet.ts b/packages/core/src/wallet/IndyWallet.ts index e99143dc7b..92870f7d70 100644 --- a/packages/core/src/wallet/IndyWallet.ts +++ b/packages/core/src/wallet/IndyWallet.ts @@ -344,6 +344,7 @@ export class IndyWallet implements Wallet { * @throws {WalletError} if the wallet is already closed or another error occurs */ public async close(): Promise { + this.logger.debug(`Closing wallet ${this.walletConfig?.id}`) if (!this.walletHandle) { throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no `walletHandle`.') } @@ -634,4 +635,12 @@ export class IndyWallet implements Wallet { throw isIndyError(error) ? new IndySdkError(error) : error } } + + public async generateWalletKey() { + try { + return await this.indy.generateWalletKey() + } catch (error) { + throw new WalletError('Error generating wallet key', { cause: error }) + } + } } diff --git a/packages/core/src/wallet/Wallet.ts b/packages/core/src/wallet/Wallet.ts index 06a8899c4c..102c25e213 100644 --- a/packages/core/src/wallet/Wallet.ts +++ b/packages/core/src/wallet/Wallet.ts @@ -31,6 +31,7 @@ export interface Wallet { pack(payload: Record, recipientKeys: string[], senderVerkey?: string): Promise unpack(encryptedMessage: EncryptedMessage): Promise generateNonce(): Promise + generateWalletKey(): Promise } export interface DidInfo { diff --git a/packages/core/tests/mocks/MockWallet.ts b/packages/core/tests/mocks/MockWallet.ts index 83132e1303..864454edb6 100644 --- a/packages/core/tests/mocks/MockWallet.ts +++ b/packages/core/tests/mocks/MockWallet.ts @@ -71,4 +71,8 @@ export class MockWallet implements Wallet { public generateNonce(): Promise { throw new Error('Method not implemented.') } + + public generateWalletKey(): Promise { + throw new Error('Method not implemented.') + } } From 7d1e6df88bbf53056486d80c9229df3c3212eafb Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 3 Jul 2022 16:03:37 +0200 Subject: [PATCH 04/12] refactor!: dependency manager as agent param Signed-off-by: Timo Glastra --- packages/core/src/agent/Agent.ts | 12 ++------ packages/core/src/agent/BaseAgent.ts | 12 +++----- packages/core/src/index.ts | 3 +- .../storage/migration/__tests__/0.1.test.ts | 29 +++++++++---------- .../__tests__/UpdateAssistant.test.ts | 13 ++++----- 5 files changed, 27 insertions(+), 42 deletions(-) diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index 8b2d4ce997..9d92caf1cf 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -1,15 +1,12 @@ -import type { DependencyManager } from '../plugins' import type { InboundTransport } from '../transport/InboundTransport' import type { OutboundTransport } from '../transport/OutboundTransport' import type { InitConfig } from '../types' import type { AgentDependencies } from './AgentDependencies' import type { AgentMessageReceivedEvent } from './Events' import type { Subscription } from 'rxjs' -import type { DependencyContainer } from 'tsyringe' import { Subject } from 'rxjs' import { concatMap, takeUntil } from 'rxjs/operators' -import { container as baseContainer } from 'tsyringe' import { CacheRepository } from '../cache' import { InjectionSymbols } from '../constants' @@ -29,6 +26,7 @@ import { QuestionAnswerModule } from '../modules/question-answer/QuestionAnswerM import { MediatorModule } from '../modules/routing/MediatorModule' import { RecipientModule } from '../modules/routing/RecipientModule' import { W3cVcModule } from '../modules/vc/module' +import { DependencyManager } from '../plugins' import { DidCommMessageRepository, StorageUpdateService, StorageVersionRepository } from '../storage' import { InMemoryMessageRepository } from '../storage/InMemoryMessageRepository' import { IndyStorageService } from '../storage/IndyStorageService' @@ -52,11 +50,11 @@ export class Agent extends BaseAgent { public constructor( initialConfig: InitConfig, dependencies: AgentDependencies, - injectionContainer?: DependencyContainer + dependencyManager?: DependencyManager ) { // NOTE: we can't create variables before calling super as TS will complain that the super call must be the // the first statement in the constructor. - super(new AgentConfig(initialConfig, dependencies), injectionContainer ?? baseContainer.createChildContainer()) + super(new AgentConfig(initialConfig, dependencies), dependencyManager ?? new DependencyManager()) const stop$ = this.dependencyManager.resolve>(InjectionSymbols.Stop$) @@ -95,10 +93,6 @@ export class Agent extends BaseAgent { return this.eventEmitter } - public get isInitialized() { - return this._isInitialized && this.wallet.isInitialized - } - public async initialize() { const { connectToIndyLedgersOnStartup, mediatorConnectionsInvite } = this.agentConfig diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index eea807e245..c078796a35 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -1,7 +1,7 @@ import type { Logger } from '../logger' +import type { DependencyManager } from '../plugins' import type { AgentConfig } from './AgentConfig' import type { TransportSession } from './TransportService' -import type { DependencyContainer } from 'tsyringe' import { AriesFrameworkError } from '../error' import { BasicMessagesModule } from '../modules/basic-messages/BasicMessagesModule' @@ -16,7 +16,6 @@ import { ProofsModule } from '../modules/proofs/ProofsModule' import { QuestionAnswerModule } from '../modules/question-answer/QuestionAnswerModule' import { MediatorModule } from '../modules/routing/MediatorModule' import { RecipientModule } from '../modules/routing/RecipientModule' -import { DependencyManager } from '../plugins' import { StorageUpdateService } from '../storage' import { UpdateAssistant } from '../storage/migration/UpdateAssistant' import { DEFAULT_UPDATE_CONFIG } from '../storage/migration/updates' @@ -54,17 +53,14 @@ export abstract class BaseAgent { public readonly wallet: WalletModule public readonly oob: OutOfBandModule - public constructor(agentConfig: AgentConfig, container: DependencyContainer) { - this.dependencyManager = new DependencyManager(container) + public constructor(agentConfig: AgentConfig, dependencyManager: DependencyManager) { + this.dependencyManager = dependencyManager this.agentConfig = agentConfig this.logger = this.agentConfig.logger this.logger.info('Creating agent with config', { - ...agentConfig, - // Prevent large object being logged. - // Will display true/false to indicate if value is present in config - logger: agentConfig.logger != undefined, + agentConfig: agentConfig.toString(), }) if (!this.agentConfig.walletConfig) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5d831b06ae..751360137b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,9 +1,10 @@ // reflect-metadata used for class-transformer + class-validator import 'reflect-metadata' -export { AgentContext } from './agent' export { MessageReceiver } from './agent/MessageReceiver' export { Agent } from './agent/Agent' +export { BaseAgent } from './agent/BaseAgent' +export * from './agent' export { EventEmitter } from './agent/EventEmitter' export { Handler, HandlerInboundMessage } from './agent/Handler' export { InboundMessageContext } from './agent/models/InboundMessageContext' diff --git a/packages/core/src/storage/migration/__tests__/0.1.test.ts b/packages/core/src/storage/migration/__tests__/0.1.test.ts index 87a6d030c0..35c70a101d 100644 --- a/packages/core/src/storage/migration/__tests__/0.1.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.1.test.ts @@ -3,12 +3,12 @@ import type { V0_1ToV0_2UpdateConfig } from '../updates/0.1-0.2' import { unlinkSync, readFileSync } from 'fs' import path from 'path' -import { container as baseContainer } from 'tsyringe' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' import { Agent } from '../../../../src' import { agentDependencies } from '../../../../tests/helpers' import { InjectionSymbols } from '../../../constants' +import { DependencyManager } from '../../../plugins' import * as uuid from '../../../utils/uuid' import { UpdateAssistant } from '../UpdateAssistant' @@ -36,9 +36,9 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { ) for (const mediationRoleUpdateStrategy of mediationRoleUpdateStrategies) { - const container = baseContainer.createChildContainer() + const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() - container.registerInstance(InjectionSymbols.StorageService, storageService) + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) const agent = new Agent( { @@ -46,7 +46,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { walletConfig, }, agentDependencies, - container + dependencyManager ) const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) @@ -96,10 +96,9 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { 'utf8' ) - const container = baseContainer.createChildContainer() + const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() - - container.registerInstance(InjectionSymbols.StorageService, storageService) + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) const agent = new Agent( { @@ -107,7 +106,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { walletConfig, }, agentDependencies, - container + dependencyManager ) const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) @@ -159,10 +158,9 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { 'utf8' ) - const container = baseContainer.createChildContainer() + const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() - - container.registerInstance(InjectionSymbols.StorageService, storageService) + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) const agent = new Agent( { @@ -171,7 +169,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { autoUpdateStorageOnStartup: true, }, agentDependencies, - container + dependencyManager ) const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) @@ -210,10 +208,9 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { 'utf8' ) - const container = baseContainer.createChildContainer() + const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() - - container.registerInstance(InjectionSymbols.StorageService, storageService) + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) const agent = new Agent( { @@ -222,7 +219,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { autoUpdateStorageOnStartup: true, }, agentDependencies, - container + dependencyManager ) const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) diff --git a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts index 316d78d648..148442b8dc 100644 --- a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts +++ b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts @@ -1,12 +1,10 @@ import type { BaseRecord } from '../../BaseRecord' -import type { DependencyContainer } from 'tsyringe' - -import { container as baseContainer } from 'tsyringe' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' import { getBaseConfig } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { InjectionSymbols } from '../../../constants' +import { DependencyManager } from '../../../plugins' import { UpdateAssistant } from '../UpdateAssistant' const { agentDependencies, config } = getBaseConfig('UpdateAssistant') @@ -14,15 +12,14 @@ const { agentDependencies, config } = getBaseConfig('UpdateAssistant') describe('UpdateAssistant', () => { let updateAssistant: UpdateAssistant let agent: Agent - let container: DependencyContainer let storageService: InMemoryStorageService beforeEach(async () => { - container = baseContainer.createChildContainer() - storageService = new InMemoryStorageService() - container.registerInstance(InjectionSymbols.StorageService, storageService) + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - agent = new Agent(config, agentDependencies, container) + agent = new Agent(config, agentDependencies, dependencyManager) updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { From 95bf678f07b1e0517b5647cc78c3d5fd3327e131 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 3 Jul 2022 16:04:00 +0200 Subject: [PATCH 05/12] style: remove unnesary optional chainig ? Signed-off-by: Timo Glastra --- .../modules/connections/handlers/DidExchangeRequestHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts b/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts index 3c18a8dc84..20fa4437ec 100644 --- a/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts +++ b/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts @@ -68,7 +68,7 @@ export class DidExchangeRequestHandler implements Handler { const connectionRecord = await this.didExchangeProtocol.processRequest(messageContext, outOfBandRecord) - if (connectionRecord?.autoAcceptConnection ?? messageContext.agentContext.config.autoAcceptConnections) { + if (connectionRecord.autoAcceptConnection ?? messageContext.agentContext.config.autoAcceptConnections) { // TODO We should add an option to not pass routing and therefore do not rotate keys and use the keys from the invitation // TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable const routing = outOfBandRecord.reusable From 22bf4a3620e529f6a173a2069fdfa883800b18e5 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 3 Jul 2022 16:13:12 +0200 Subject: [PATCH 06/12] feat: get agent context by correlation id Signed-off-by: Timo Glastra --- .../src/agent/context/AgentContextProvider.ts | 8 +++++- .../context/DefaultAgentContextProvider.ts | 22 ++++++++++++++- .../DefaultAgentContextProvider.test.ts | 28 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agent/context/AgentContextProvider.ts b/packages/core/src/agent/context/AgentContextProvider.ts index 09047d38b6..c9f1c81296 100644 --- a/packages/core/src/agent/context/AgentContextProvider.ts +++ b/packages/core/src/agent/context/AgentContextProvider.ts @@ -2,7 +2,7 @@ import type { AgentContext } from './AgentContext' export interface AgentContextProvider { /** - * Find the agent context based for an inbound message. It's possible to provide a contextCorrelationId to make it + * Get the agent context for an inbound message. It's possible to provide a contextCorrelationId to make it * easier for the context provider implementation to correlate inbound messages to the correct context. This can be useful if * a plaintext message is passed and the context provider can't determine the context based on the recipient public keys * of the inbound message. @@ -14,4 +14,10 @@ export interface AgentContextProvider { inboundMessage: unknown, options?: { contextCorrelationId?: string } ): Promise + + /** + * Get the agent context for a context correlation id. Will throw an error if no AgentContext could be retrieved + * for the specified contextCorrelationId. + */ + getAgentContextForContextCorrelationId(contextCorrelationId: string): Promise } diff --git a/packages/core/src/agent/context/DefaultAgentContextProvider.ts b/packages/core/src/agent/context/DefaultAgentContextProvider.ts index 3227dadc55..87df3fb03d 100644 --- a/packages/core/src/agent/context/DefaultAgentContextProvider.ts +++ b/packages/core/src/agent/context/DefaultAgentContextProvider.ts @@ -1,5 +1,6 @@ import type { AgentContextProvider } from './AgentContextProvider' +import { AriesFrameworkError } from '../../error' import { injectable } from '../../plugins' import { AgentContext } from './AgentContext' @@ -18,7 +19,26 @@ export class DefaultAgentContextProvider implements AgentContextProvider { this.agentContext = agentContext } - public async getContextForInboundMessage(): Promise { + public async getAgentContextForContextCorrelationId(contextCorrelationId: string): Promise { + if (contextCorrelationId !== this.agentContext.contextCorrelationId) { + throw new AriesFrameworkError( + `Could not get agent context for contextCorrelationId '${contextCorrelationId}'. Only contextCorrelationId '${this.agentContext.contextCorrelationId}' is supported.` + ) + } + + return this.agentContext + } + + public async getContextForInboundMessage( + // We don't need to look at the message as we always use the same context in the default agent context provider + _: unknown, + options?: { contextCorrelationId?: string } + ): Promise { + // This will throw an error if the contextCorrelationId does not match with the contextCorrelationId of the agent context property of this class. + if (options?.contextCorrelationId) { + return this.getAgentContextForContextCorrelationId(options.contextCorrelationId) + } + return this.agentContext } } diff --git a/packages/core/src/agent/context/__tests__/DefaultAgentContextProvider.test.ts b/packages/core/src/agent/context/__tests__/DefaultAgentContextProvider.test.ts index f7faa58c78..510848d00f 100644 --- a/packages/core/src/agent/context/__tests__/DefaultAgentContextProvider.test.ts +++ b/packages/core/src/agent/context/__tests__/DefaultAgentContextProvider.test.ts @@ -14,5 +14,33 @@ describe('DefaultAgentContextProvider', () => { await expect(agentContextProvider.getContextForInboundMessage(message)).resolves.toBe(agentContext) }) + + test('throws an error if the provided contextCorrelationId does not match with the contextCorrelationId from the constructor agent context', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + + const message = {} + + await expect( + agentContextProvider.getContextForInboundMessage(message, { contextCorrelationId: 'wrong' }) + ).rejects.toThrowError( + `Could not get agent context for contextCorrelationId 'wrong'. Only contextCorrelationId 'mock' is supported.` + ) + }) + }) + + describe('getAgentContextForContextCorrelationId()', () => { + test('returns the agent context provided in the constructor if contextCorrelationId matches', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + + await expect(agentContextProvider.getAgentContextForContextCorrelationId('mock')).resolves.toBe(agentContext) + }) + + test('throws an error if the contextCorrelationId does not match with the contextCorrelationId from the constructor agent context', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + + await expect(agentContextProvider.getAgentContextForContextCorrelationId('wrong')).rejects.toThrowError( + `Could not get agent context for contextCorrelationId 'wrong'. Only contextCorrelationId 'mock' is supported.` + ) + }) }) }) From ee9b23b06fbb3fbb23cea6464a2fe437a5979556 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 3 Jul 2022 16:14:14 +0200 Subject: [PATCH 07/12] feat(tenants): base tenant module package Signed-off-by: Timo Glastra --- packages/module-tenants/README.md | 31 +++++++++++++++++++ packages/module-tenants/jest.config.ts | 13 ++++++++ packages/module-tenants/package.json | 34 +++++++++++++++++++++ packages/module-tenants/tsconfig.build.json | 9 ++++++ packages/module-tenants/tsconfig.json | 6 ++++ 5 files changed, 93 insertions(+) create mode 100644 packages/module-tenants/README.md create mode 100644 packages/module-tenants/jest.config.ts create mode 100644 packages/module-tenants/package.json create mode 100644 packages/module-tenants/tsconfig.build.json create mode 100644 packages/module-tenants/tsconfig.json diff --git a/packages/module-tenants/README.md b/packages/module-tenants/README.md new file mode 100644 index 0000000000..e0ec4b3ad4 --- /dev/null +++ b/packages/module-tenants/README.md @@ -0,0 +1,31 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript - Tenant Module

+

+ License + typescript + @aries-framework/module-tenants version + +

+
+ +Aries Framework JavaScript Tenant Module provides an optional addon to Aries Framework JavaScript to use an agent with multiple tenants. diff --git a/packages/module-tenants/jest.config.ts b/packages/module-tenants/jest.config.ts new file mode 100644 index 0000000000..ce53584ebf --- /dev/null +++ b/packages/module-tenants/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + name: packageJson.name, + displayName: packageJson.name, +} + +export default config diff --git a/packages/module-tenants/package.json b/packages/module-tenants/package.json new file mode 100644 index 0000000000..708a4e6a1a --- /dev/null +++ b/packages/module-tenants/package.json @@ -0,0 +1,34 @@ +{ + "name": "@aries-framework/module-tenants", + "main": "build/index", + "types": "build/index", + "version": "0.2.0", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/module-tenants", + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "packages/module-tenants" + }, + "scripts": { + "build": "yarn run clean && yarn run compile", + "clean": "rimraf -rf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "yarn run build", + "test": "jest" + }, + "dependencies": { + "@aries-framework/core": "0.2.0" + }, + "devDependencies": { + "rimraf": "~3.0.2", + "typescript": "~4.3.0", + "@aries-framework/node": "0.2.0" + } +} diff --git a/packages/module-tenants/tsconfig.build.json b/packages/module-tenants/tsconfig.build.json new file mode 100644 index 0000000000..9c30e30bd2 --- /dev/null +++ b/packages/module-tenants/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "outDir": "./build" + }, + + "include": ["src/**/*"] +} diff --git a/packages/module-tenants/tsconfig.json b/packages/module-tenants/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/module-tenants/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} From 5edb09b2f0d34a753df9ce25629904f213450a52 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 5 Jul 2022 09:29:29 +0200 Subject: [PATCH 08/12] feat: toJSON convience methods for better logging Signed-off-by: Timo Glastra --- packages/core/src/agent/AgentConfig.ts | 6 ++---- packages/core/src/agent/BaseAgent.ts | 2 +- packages/core/src/agent/context/AgentContext.ts | 6 ++++++ .../core/src/agent/models/InboundMessageContext.ts | 10 ++++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/core/src/agent/AgentConfig.ts b/packages/core/src/agent/AgentConfig.ts index e43b17c183..df12417754 100644 --- a/packages/core/src/agent/AgentConfig.ts +++ b/packages/core/src/agent/AgentConfig.ts @@ -120,14 +120,12 @@ export class AgentConfig { ) } - public toString() { - const config = { + public toJSON() { + return { ...this.initConfig, logger: this.logger !== undefined, agentDependencies: this.agentDependencies != undefined, label: this.label, } - - return JSON.stringify(config, null, 2) } } diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index c078796a35..cd57486557 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -60,7 +60,7 @@ export abstract class BaseAgent { this.logger = this.agentConfig.logger this.logger.info('Creating agent with config', { - agentConfig: agentConfig.toString(), + agentConfig: agentConfig.toJSON(), }) if (!this.agentConfig.walletConfig) { diff --git a/packages/core/src/agent/context/AgentContext.ts b/packages/core/src/agent/context/AgentContext.ts index ef425377b2..9c3e000c1b 100644 --- a/packages/core/src/agent/context/AgentContext.ts +++ b/packages/core/src/agent/context/AgentContext.ts @@ -46,4 +46,10 @@ export class AgentContext { public get wallet() { return this.dependencyManager.resolve(InjectionSymbols.Wallet) } + + public toJSON() { + return { + contextCorrelationId: this.contextCorrelationId, + } + } } diff --git a/packages/core/src/agent/models/InboundMessageContext.ts b/packages/core/src/agent/models/InboundMessageContext.ts index c3f3628e09..8a6e800160 100644 --- a/packages/core/src/agent/models/InboundMessageContext.ts +++ b/packages/core/src/agent/models/InboundMessageContext.ts @@ -45,4 +45,14 @@ export class InboundMessageContext { return this.connection } + + public toJSON() { + return { + message: this.message, + recipientKey: this.recipientKey?.fingerprint, + senderKey: this.senderKey?.fingerprint, + sessionId: this.sessionId, + agentContext: this.agentContext.toJSON(), + } + } } From fe0338bb664a95f66544f179c3abf5a7bd1ffc09 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 5 Jul 2022 09:29:54 +0200 Subject: [PATCH 09/12] test: auto set subject in subject transport Signed-off-by: Timo Glastra --- tests/transport/SubjectInboundTransport.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/transport/SubjectInboundTransport.ts b/tests/transport/SubjectInboundTransport.ts index 572784fb17..a9736a1bfb 100644 --- a/tests/transport/SubjectInboundTransport.ts +++ b/tests/transport/SubjectInboundTransport.ts @@ -1,7 +1,9 @@ import type { InboundTransport, Agent } from '../../packages/core/src' import type { TransportSession } from '../../packages/core/src/agent/TransportService' import type { EncryptedMessage } from '../../packages/core/src/types' -import type { Subject, Subscription } from 'rxjs' +import type { Subscription } from 'rxjs' + +import { Subject } from 'rxjs' import { MessageReceiver } from '../../packages/core/src' import { TransportService } from '../../packages/core/src/agent/TransportService' @@ -10,10 +12,10 @@ import { uuid } from '../../packages/core/src/utils/uuid' export type SubjectMessage = { message: EncryptedMessage; replySubject?: Subject } export class SubjectInboundTransport implements InboundTransport { - private ourSubject: Subject + public readonly ourSubject: Subject private subscription?: Subscription - public constructor(ourSubject: Subject) { + public constructor(ourSubject = new Subject()) { this.ourSubject = ourSubject } From c2042d053cf788203f407adf163fbdbb1c79808e Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 5 Jul 2022 14:58:26 +0200 Subject: [PATCH 10/12] feat: initial tenants module Signed-off-by: Timo Glastra --- .eslintrc.js | 11 +- packages/core/src/agent/Agent.ts | 6 + packages/core/src/agent/BaseAgent.ts | 6 +- packages/core/src/index.ts | 4 +- packages/module-tenants/jest.config.ts | 1 + packages/module-tenants/package.json | 8 +- packages/module-tenants/src/TenantAgent.ts | 23 ++ packages/module-tenants/src/TenantsApi.ts | 56 +++++ .../module-tenants/src/TenantsApiOptions.ts | 9 + packages/module-tenants/src/TenantsModule.ts | 31 +++ .../src/__tests__/TenantAgent.test.ts | 30 +++ .../src/__tests__/TenantsApi.test.ts | 127 +++++++++++ .../src/__tests__/TenantsModule.test.ts | 33 +++ .../src/context/TenantAgentContextProvider.ts | 144 +++++++++++++ .../src/context/TenantSessionCoordinator.ts | 117 ++++++++++ .../TenantAgentContextProvider.test.ts | 151 +++++++++++++ .../TenantSessionCoordinator.test.ts | 126 +++++++++++ packages/module-tenants/src/context/types.ts | 0 packages/module-tenants/src/index.ts | 4 + .../module-tenants/src/models/TenantConfig.ts | 5 + .../src/repository/TenantRecord.ts | 37 ++++ .../src/repository/TenantRepository.ts | 13 ++ .../src/repository/TenantRoutingRecord.ts | 47 ++++ .../src/repository/TenantRoutingRepository.ts | 21 ++ .../repository/__tests__/TenantRecord.test.ts | 87 ++++++++ .../__tests__/TenantRoutingRecord.test.ts | 77 +++++++ .../__tests__/TenantRoutingRepository.test.ts | 38 ++++ .../module-tenants/src/repository/index.ts | 4 + .../src/services/TenantService.ts | 83 ++++++++ .../services/__tests__/TenantService.test.ts | 151 +++++++++++++ packages/module-tenants/src/services/index.ts | 1 + packages/module-tenants/tests/setup.ts | 1 + .../module-tenants/tests/tenants.e2e.test.ts | 201 ++++++++++++++++++ yarn.lock | 9 +- 34 files changed, 1651 insertions(+), 11 deletions(-) create mode 100644 packages/module-tenants/src/TenantAgent.ts create mode 100644 packages/module-tenants/src/TenantsApi.ts create mode 100644 packages/module-tenants/src/TenantsApiOptions.ts create mode 100644 packages/module-tenants/src/TenantsModule.ts create mode 100644 packages/module-tenants/src/__tests__/TenantAgent.test.ts create mode 100644 packages/module-tenants/src/__tests__/TenantsApi.test.ts create mode 100644 packages/module-tenants/src/__tests__/TenantsModule.test.ts create mode 100644 packages/module-tenants/src/context/TenantAgentContextProvider.ts create mode 100644 packages/module-tenants/src/context/TenantSessionCoordinator.ts create mode 100644 packages/module-tenants/src/context/__tests__/TenantAgentContextProvider.test.ts create mode 100644 packages/module-tenants/src/context/__tests__/TenantSessionCoordinator.test.ts create mode 100644 packages/module-tenants/src/context/types.ts create mode 100644 packages/module-tenants/src/index.ts create mode 100644 packages/module-tenants/src/models/TenantConfig.ts create mode 100644 packages/module-tenants/src/repository/TenantRecord.ts create mode 100644 packages/module-tenants/src/repository/TenantRepository.ts create mode 100644 packages/module-tenants/src/repository/TenantRoutingRecord.ts create mode 100644 packages/module-tenants/src/repository/TenantRoutingRepository.ts create mode 100644 packages/module-tenants/src/repository/__tests__/TenantRecord.test.ts create mode 100644 packages/module-tenants/src/repository/__tests__/TenantRoutingRecord.test.ts create mode 100644 packages/module-tenants/src/repository/__tests__/TenantRoutingRepository.test.ts create mode 100644 packages/module-tenants/src/repository/index.ts create mode 100644 packages/module-tenants/src/services/TenantService.ts create mode 100644 packages/module-tenants/src/services/__tests__/TenantService.test.ts create mode 100644 packages/module-tenants/src/services/index.ts create mode 100644 packages/module-tenants/tests/setup.ts create mode 100644 packages/module-tenants/tests/tenants.e2e.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 75c94ae5c6..e45d4d2cad 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -99,7 +99,16 @@ module.exports = { }, }, { - files: ['*.test.ts', '**/__tests__/**', '**/tests/**', 'jest.*.ts', 'samples/**', 'demo/**', 'scripts/**'], + files: [ + '*.test.ts', + '**/__tests__/**', + '**/tests/**', + 'jest.*.ts', + 'samples/**', + 'demo/**', + 'scripts/**', + '**/tests/**', + ], env: { jest: true, node: false, diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index 9d92caf1cf..ea85fafad5 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -140,7 +140,13 @@ export class Agent extends BaseAgent { const transportPromises = allTransports.map((transport) => transport.stop()) await Promise.all(transportPromises) + // close wallet if still initialized + if (this.wallet.isInitialized) { + await this.wallet.close() + } + await super.shutdown() + this._isInitialized = false } protected registerDependencies(dependencyManager: DependencyManager) { diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index cd57486557..67c32965b5 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -149,11 +149,7 @@ export abstract class BaseAgent { } public async shutdown() { - // close wallet if still initialized - if (this.wallet.isInitialized) { - await this.wallet.close() - } - this._isInitialized = false + // No logic required at the moment } public get publicDid() { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 751360137b..55e9764719 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -42,11 +42,13 @@ export * from './modules/ledger' export * from './modules/routing' export * from './modules/question-answer' export * from './modules/oob' +export * from './wallet/WalletModule' export * from './modules/dids' -export * from './utils/JsonTransformer' +export { JsonEncoder, JsonTransformer, isJsonObject, isValidJweStructure } from './utils' export * from './logger' export * from './error' export * from './wallet/error' +export { Key, KeyType } from './crypto' export { parseMessageType, IsValidMessageType } from './utils/messageType' export * from './agent/Events' diff --git a/packages/module-tenants/jest.config.ts b/packages/module-tenants/jest.config.ts index ce53584ebf..55c67d70a6 100644 --- a/packages/module-tenants/jest.config.ts +++ b/packages/module-tenants/jest.config.ts @@ -8,6 +8,7 @@ const config: Config.InitialOptions = { ...base, name: packageJson.name, displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], } export default config diff --git a/packages/module-tenants/package.json b/packages/module-tenants/package.json index 708a4e6a1a..dae63f0858 100644 --- a/packages/module-tenants/package.json +++ b/packages/module-tenants/package.json @@ -24,11 +24,13 @@ "test": "jest" }, "dependencies": { - "@aries-framework/core": "0.2.0" + "@aries-framework/core": "0.2.0", + "async-mutex": "^0.3.2" }, "devDependencies": { + "@aries-framework/node": "0.2.0", + "reflect-metadata": "^0.1.13", "rimraf": "~3.0.2", - "typescript": "~4.3.0", - "@aries-framework/node": "0.2.0" + "typescript": "~4.3.0" } } diff --git a/packages/module-tenants/src/TenantAgent.ts b/packages/module-tenants/src/TenantAgent.ts new file mode 100644 index 0000000000..fbeb5f9723 --- /dev/null +++ b/packages/module-tenants/src/TenantAgent.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { BaseAgent } from '@aries-framework/core' + +export class TenantAgent extends BaseAgent { + public constructor(agentContext: AgentContext) { + super(agentContext.config, agentContext.dependencyManager) + } + + public async initialize() { + await super.initialize() + this._isInitialized = true + } + + public async shutdown() { + await super.shutdown() + this._isInitialized = false + } + + protected registerDependencies() { + // Nothing to do here + } +} diff --git a/packages/module-tenants/src/TenantsApi.ts b/packages/module-tenants/src/TenantsApi.ts new file mode 100644 index 0000000000..526f7c66d9 --- /dev/null +++ b/packages/module-tenants/src/TenantsApi.ts @@ -0,0 +1,56 @@ +import type { CreateTenantOptions, GetTenantAgentOptions } from './TenantsApiOptions' + +import { AgentContext, inject, InjectionSymbols, AgentContextProvider, injectable } from '@aries-framework/core' + +import { TenantAgent } from './TenantAgent' +import { TenantService } from './services' + +@injectable() +export class TenantsApi { + private agentContext: AgentContext + private tenantService: TenantService + private agentContextProvider: AgentContextProvider + + public constructor( + tenantService: TenantService, + agentContext: AgentContext, + @inject(InjectionSymbols.AgentContextProvider) agentContextProvider: AgentContextProvider + ) { + this.tenantService = tenantService + this.agentContext = agentContext + this.agentContextProvider = agentContextProvider + } + + public async getTenantAgent({ tenantId }: GetTenantAgentOptions): Promise { + const tenantContext = await this.agentContextProvider.getAgentContextForContextCorrelationId(tenantId) + + const tenantAgent = new TenantAgent(tenantContext) + await tenantAgent.initialize() + + return tenantAgent + } + + public async createTenant(options: CreateTenantOptions) { + const tenantRecord = await this.tenantService.createTenant(this.agentContext, options.config) + + // This initializes the tenant agent, creates the wallet etc... + const tenantAgent = await this.getTenantAgent({ tenantId: tenantRecord.id }) + await tenantAgent.shutdown() + + return tenantRecord + } + + public async getTenantById(tenantId: string) { + return this.tenantService.getTenantById(this.agentContext, tenantId) + } + + public async deleteTenantById(tenantId: string) { + // TODO: force remove context from the context provider (or session manager) + const tenantAgent = await this.getTenantAgent({ tenantId }) + + await tenantAgent.wallet.delete() + await tenantAgent.shutdown() + + return this.tenantService.deleteTenantById(this.agentContext, tenantId) + } +} diff --git a/packages/module-tenants/src/TenantsApiOptions.ts b/packages/module-tenants/src/TenantsApiOptions.ts new file mode 100644 index 0000000000..df9fa10324 --- /dev/null +++ b/packages/module-tenants/src/TenantsApiOptions.ts @@ -0,0 +1,9 @@ +import type { TenantConfig } from './models/TenantConfig' + +export interface GetTenantAgentOptions { + tenantId: string +} + +export interface CreateTenantOptions { + config: Omit +} diff --git a/packages/module-tenants/src/TenantsModule.ts b/packages/module-tenants/src/TenantsModule.ts new file mode 100644 index 0000000000..72545a8382 --- /dev/null +++ b/packages/module-tenants/src/TenantsModule.ts @@ -0,0 +1,31 @@ +import type { DependencyManager } from '@aries-framework/core' + +import { InjectionSymbols, module } from '@aries-framework/core' + +import { TenantsApi } from './TenantsApi' +import { TenantAgentContextProvider } from './context/TenantAgentContextProvider' +import { TenantSessionCoordinator } from './context/TenantSessionCoordinator' +import { TenantRepository, TenantRoutingRepository } from './repository' +import { TenantService } from './services' + +@module() +export class TenantsModule { + /** + * Registers the dependencies of the tenants module on the dependency manager. + */ + public static register(dependencyManager: DependencyManager) { + // Api + // NOTE: this is a singleton because tenants can't have their own tenants. This makes sure the tenants api is always used in the root agent context. + dependencyManager.registerSingleton(TenantsApi) + + // Services + dependencyManager.registerSingleton(TenantService) + + // Repositories + dependencyManager.registerSingleton(TenantRepository) + dependencyManager.registerSingleton(TenantRoutingRepository) + + dependencyManager.registerSingleton(InjectionSymbols.AgentContextProvider, TenantAgentContextProvider) + dependencyManager.registerSingleton(TenantSessionCoordinator) + } +} diff --git a/packages/module-tenants/src/__tests__/TenantAgent.test.ts b/packages/module-tenants/src/__tests__/TenantAgent.test.ts new file mode 100644 index 0000000000..ee97bd4b90 --- /dev/null +++ b/packages/module-tenants/src/__tests__/TenantAgent.test.ts @@ -0,0 +1,30 @@ +import { Agent } from '@aries-framework/core' + +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../core/tests/helpers' +import { TenantAgent } from '../TenantAgent' + +describe('TenantAgent', () => { + test('possible to construct a TenantAgent instance', () => { + const agent = new Agent( + { + label: 'test', + walletConfig: { + id: 'Wallet: TenantAgentRoot', + key: 'Wallet: TenantAgentRoot', + }, + }, + agentDependencies + ) + + const tenantDependencyManager = agent.dependencyManager.createChild() + + const agentContext = getAgentContext({ + agentConfig: getAgentConfig('TenantAgent'), + dependencyManager: tenantDependencyManager, + }) + + const tenantAgent = new TenantAgent(agentContext) + + expect(tenantAgent).toBeInstanceOf(TenantAgent) + }) +}) diff --git a/packages/module-tenants/src/__tests__/TenantsApi.test.ts b/packages/module-tenants/src/__tests__/TenantsApi.test.ts new file mode 100644 index 0000000000..8fe48593d7 --- /dev/null +++ b/packages/module-tenants/src/__tests__/TenantsApi.test.ts @@ -0,0 +1,127 @@ +import { Agent, AgentContext } from '@aries-framework/core' + +import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../core/tests/helpers' +import { TenantAgent } from '../TenantAgent' +import { TenantsApi } from '../TenantsApi' +import { TenantAgentContextProvider } from '../context/TenantAgentContextProvider' +import { TenantRecord } from '../repository' +import { TenantService } from '../services/TenantService' + +jest.mock('../services/TenantService') +const TenantServiceMock = TenantService as jest.Mock + +jest.mock('../context/TenantAgentContextProvider') +const AgentContextProviderMock = TenantAgentContextProvider as jest.Mock + +const tenantService = new TenantServiceMock() +const agentContextProvider = new AgentContextProviderMock() +const agentConfig = getAgentConfig('TenantsApi') +const rootAgent = new Agent(agentConfig, agentDependencies) + +const tenantsApi = new TenantsApi(tenantService, rootAgent.context, agentContextProvider) + +describe('TenantsApi', () => { + describe('getTenantAgent', () => { + test('gets context from agent context provider and initializes tenant agent instance', async () => { + const tenantDependencyManager = rootAgent.dependencyManager.createChild() + const tenantAgentContext = getAgentContext({ + contextCorrelationId: 'tenant-id', + dependencyManager: tenantDependencyManager, + agentConfig: agentConfig.extend({ + label: 'tenant-agent', + walletConfig: { + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }, + }), + }) + tenantDependencyManager.registerInstance(AgentContext, tenantAgentContext) + + mockFunction(agentContextProvider.getAgentContextForContextCorrelationId).mockResolvedValue(tenantAgentContext) + + const tenantAgent = await tenantsApi.getTenantAgent({ tenantId: 'tenant-id' }) + + expect(tenantAgent.isInitialized).toBe(true) + expect(tenantAgent.wallet.walletConfig).toEqual({ + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }) + + expect(agentContextProvider.getAgentContextForContextCorrelationId).toBeCalledWith('tenant-id') + expect(tenantAgent).toBeInstanceOf(TenantAgent) + expect(tenantAgent.context).toBe(tenantAgentContext) + + await tenantAgent.wallet.delete() + await tenantAgent.shutdown() + }) + }) + + describe('createTenant', () => { + test('create tenant in the service and get the tenant agent to initialize', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + config: { + label: 'test', + walletConfig: { + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }, + }, + }) + + const tenantAgentMock = { + wallet: { + delete: jest.fn(), + }, + shutdown: jest.fn(), + } as unknown as TenantAgent + + mockFunction(tenantService.createTenant).mockResolvedValue(tenantRecord) + const getTenantAgentSpy = jest.spyOn(tenantsApi, 'getTenantAgent').mockResolvedValue(tenantAgentMock) + + const createdTenantRecord = await tenantsApi.createTenant({ + config: { + label: 'test', + }, + }) + + expect(getTenantAgentSpy).toHaveBeenCalledWith({ tenantId: 'tenant-id' }) + expect(createdTenantRecord).toBe(tenantRecord) + expect(tenantAgentMock.shutdown).toHaveBeenCalled() + expect(tenantService.createTenant).toHaveBeenCalledWith(rootAgent.context, { + label: 'test', + }) + }) + }) + + describe('getTenantById', () => { + test('calls get tenant by id on tenant service', async () => { + const tenantRecord = jest.fn() as unknown as TenantRecord + mockFunction(tenantService.getTenantById).mockResolvedValue(tenantRecord) + + const actualTenantRecord = await tenantsApi.getTenantById('tenant-id') + + expect(tenantService.getTenantById).toHaveBeenCalledWith(rootAgent.context, 'tenant-id') + expect(actualTenantRecord).toBe(tenantRecord) + }) + }) + + describe('deleteTenantById', () => { + test('deletes the tenant and removes the wallet', async () => { + const tenantAgentMock = { + wallet: { + delete: jest.fn(), + }, + shutdown: jest.fn(), + } as unknown as TenantAgent + const getTenantAgentSpy = jest.spyOn(tenantsApi, 'getTenantAgent').mockResolvedValue(tenantAgentMock) + + await tenantsApi.deleteTenantById('tenant-id') + + expect(getTenantAgentSpy).toHaveBeenCalledWith({ tenantId: 'tenant-id' }) + expect(tenantAgentMock.wallet.delete).toHaveBeenCalled() + expect(tenantAgentMock.shutdown).toHaveBeenCalled() + expect(tenantService.deleteTenantById).toHaveBeenCalledWith(rootAgent.context, 'tenant-id') + }) + }) +}) diff --git a/packages/module-tenants/src/__tests__/TenantsModule.test.ts b/packages/module-tenants/src/__tests__/TenantsModule.test.ts new file mode 100644 index 0000000000..aeee314335 --- /dev/null +++ b/packages/module-tenants/src/__tests__/TenantsModule.test.ts @@ -0,0 +1,33 @@ +import { InjectionSymbols } from '@aries-framework/core' + +import { DependencyManager } from '../../../core/src/plugins/DependencyManager' +import { TenantsApi } from '../TenantsApi' +import { TenantsModule } from '../TenantsModule' +import { TenantAgentContextProvider } from '../context/TenantAgentContextProvider' +import { TenantSessionCoordinator } from '../context/TenantSessionCoordinator' +import { TenantRepository, TenantRoutingRepository } from '../repository' +import { TenantService } from '../services' + +jest.mock('../../../core/src/plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +describe('TenantsModule', () => { + test('registers dependencies on the dependency manager', () => { + TenantsModule.register(dependencyManager) + + expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(TenantsApi) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(5) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantRoutingRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + InjectionSymbols.AgentContextProvider, + TenantAgentContextProvider + ) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantSessionCoordinator) + }) +}) diff --git a/packages/module-tenants/src/context/TenantAgentContextProvider.ts b/packages/module-tenants/src/context/TenantAgentContextProvider.ts new file mode 100644 index 0000000000..8b32e1142d --- /dev/null +++ b/packages/module-tenants/src/context/TenantAgentContextProvider.ts @@ -0,0 +1,144 @@ +import type { AgentContextProvider, RoutingCreatedEvent, EncryptedMessage } from '@aries-framework/core' + +import { + AriesFrameworkError, + injectable, + AgentContext, + EventEmitter, + inject, + Logger, + RoutingEventTypes, + InjectionSymbols, + KeyType, + Key, + isValidJweStructure, + JsonEncoder, + isJsonObject, +} from '@aries-framework/core' + +import { TenantService } from '../services' + +import { TenantSessionCoordinator } from './TenantSessionCoordinator' + +@injectable() +export class TenantAgentContextProvider implements AgentContextProvider { + private tenantService: TenantService + private rootAgentContext: AgentContext + private eventEmitter: EventEmitter + private logger: Logger + private tenantSessionCoordinator: TenantSessionCoordinator + + public constructor( + tenantService: TenantService, + rootAgentContext: AgentContext, + eventEmitter: EventEmitter, + tenantSessionCoordinator: TenantSessionCoordinator, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.tenantService = tenantService + this.rootAgentContext = rootAgentContext + this.eventEmitter = eventEmitter + this.tenantSessionCoordinator = tenantSessionCoordinator + this.logger = logger + + // Start listener for newly created routing keys, so we can register a mapping for each new key for the tenant + this.listenForRoutingKeyCreatedEvents() + } + + public async getAgentContextForContextCorrelationId(tenantId: string) { + // TODO: maybe we can look at not having to retrieve the tenant record if there's already a context available. + const tenantRecord = await this.tenantService.getTenantById(this.rootAgentContext, tenantId) + const agentContext = this.tenantSessionCoordinator.getContextForSession(tenantRecord) + + this.logger.debug(`Created tenant agent context for tenant '${tenantId}'`) + + return agentContext + } + + public async getContextForInboundMessage(inboundMessage: unknown, options?: { contextCorrelationId?: string }) { + this.logger.debug('Getting context for inbound message in tenant agent context provider', { + contextCorrelationId: options?.contextCorrelationId, + }) + + let tenantId = options?.contextCorrelationId + let recipientKeys: Key[] = [] + + if (!tenantId && isValidJweStructure(inboundMessage)) { + this.logger.trace("Inbound message is a JWE, extracting tenant id from JWE's protected header") + recipientKeys = this.getRecipientKeysFromEncryptedMessage(inboundMessage) + + this.logger.trace(`Found ${recipientKeys.length} recipient keys in JWE's protected header`) + + // FIXME: what if there are multiple recipients in the same agent? If we receive the messages twice we will process it for + // the first found recipient multiple times. This is however a case I've never seen before and will add quite some complexity + // to resolve. I think we're fine to ignore this case for now. + for (const recipientKey of recipientKeys) { + const tenantRoutingRecord = await this.tenantService.findTenantRoutingRecordByRecipientKey( + this.rootAgentContext, + recipientKey + ) + + if (tenantRoutingRecord) { + this.logger.debug(`Found tenant routing record for recipient key ${recipientKeys[0].fingerprint}`, { + tenantId: tenantRoutingRecord.tenantId, + }) + tenantId = tenantRoutingRecord.tenantId + break + } + } + } + + if (!tenantId) { + this.logger.error("Couldn't determine tenant id for inbound message. Unable to create context", { + inboundMessage, + recipientKeys: recipientKeys.map((key) => key.fingerprint), + }) + throw new AriesFrameworkError("Couldn't determine tenant id for inbound message. Unable to create context") + } + + const agentContext = await this.getAgentContextForContextCorrelationId(tenantId) + + return agentContext + } + + private getRecipientKeysFromEncryptedMessage(jwe: EncryptedMessage): Key[] { + const jweProtected = JsonEncoder.fromBase64(jwe.protected) + if (!Array.isArray(jweProtected.recipients)) return [] + + const recipientKeys: Key[] = [] + + for (const recipient of jweProtected.recipients) { + // Check if recipient.header.kid is a string + if (isJsonObject(recipient) && isJsonObject(recipient.header) && typeof recipient.header.kid === 'string') { + // This won't work with other key types, we should detect what the encoding is of kid, and based on that + // determine how we extract the key from the message + const key = Key.fromPublicKeyBase58(recipient.header.kid, KeyType.Ed25519) + recipientKeys.push(key) + } + } + + return recipientKeys + } + + private async registerRecipientKeyForTenant(tenantId: string, recipientKey: Key) { + this.logger.debug(`Registering recipient key ${recipientKey.fingerprint} for tenant ${tenantId}`) + const tenantRecord = await this.tenantService.getTenantById(this.rootAgentContext, tenantId) + await this.tenantService.addTenantRoutingRecord(this.rootAgentContext, tenantRecord.id, recipientKey) + } + + private listenForRoutingKeyCreatedEvents() { + this.logger.debug('Listening for routing key created events in tenant agent context provider') + this.eventEmitter.on(RoutingEventTypes.RoutingCreatedEvent, async (event) => { + const contextCorrelationId = event.metadata.contextCorrelationId + const recipientKey = event.payload.routing.recipientKey + + // We don't want to register the key if it's for the root agent context + if (contextCorrelationId === this.rootAgentContext.contextCorrelationId) return + + this.logger.debug( + `Received routing key created event for tenant ${contextCorrelationId}, registering recipient key ${recipientKey.fingerprint} in base wallet` + ) + await this.registerRecipientKeyForTenant(contextCorrelationId, recipientKey) + }) + } +} diff --git a/packages/module-tenants/src/context/TenantSessionCoordinator.ts b/packages/module-tenants/src/context/TenantSessionCoordinator.ts new file mode 100644 index 0000000000..2fe8097f86 --- /dev/null +++ b/packages/module-tenants/src/context/TenantSessionCoordinator.ts @@ -0,0 +1,117 @@ +import type { TenantRecord } from '../repository' + +import { AgentConfig, AgentContext, AriesFrameworkError, injectable, WalletModule } from '@aries-framework/core' +import { Mutex } from 'async-mutex' + +/** + * Coordinates all agent context instance for tenant sessions. + * + * NOTE: the implementation in temporary and doesn't correctly handle the lifecycle of sessions, it's just an implementation to make + * multi-tenancy work. It will keep opening wallets over time, taking up more and more resources. The implementation will be improved in the near future. + * It does however handle race conditions on initialization of wallets (so two requests for the same tenant being processed in parallel) + */ +@injectable() +export class TenantSessionCoordinator { + private rootAgentContext: AgentContext + private tenantAgentContextMapping: TenantAgentContextMapping = {} + + public constructor(rootAgentContext: AgentContext) { + this.rootAgentContext = rootAgentContext + } + + // FIXME: add timeouts to the lock acquire (to prevent deadlocks) + public async getContextForSession(tenantRecord: TenantRecord): Promise { + let tenantContextMapping = this.tenantAgentContextMapping[tenantRecord.id] + + // TODO: we should probably create a new context (but with the same dependency manager / wallet) for each session. + // This way we can add a `.dispose()` on the agent context, which means that agent context isn't usable anymore. However + // the wallet won't be closed. + + // If we already have a context with sessions in place return the context and increment + // the session count. + if (isTenantContextSessions(tenantContextMapping)) { + tenantContextMapping.sessionCount++ + return tenantContextMapping.agentContext + } + + // TODO: look at semaphores to manage the total number of wallets + // If the context is currently being initialized, wait for it to complete. + else if (isTenantAgentContextInitializing(tenantContextMapping)) { + // Wait for the wallet to finish initializing, then try to + return await tenantContextMapping.mutex.runExclusive(() => { + tenantContextMapping = this.tenantAgentContextMapping[tenantRecord.id] + + // There should always be a context now, if this isn't the case we must error out + // TODO: handle the case where the previous initialization failed (the value is undefined) + // We can just open a new session in that case, but for now we'll ignore this flow + if (!isTenantContextSessions(tenantContextMapping)) { + throw new AriesFrameworkError('Tenant context is not ready yet') + } + + tenantContextMapping.sessionCount++ + return tenantContextMapping.agentContext + }) + } + // No value for this tenant exists yet, initialize a new session. + else { + // Set a mutex on the agent context mapping so other requests can wait for it to be initialized. + const mutex = new Mutex() + this.tenantAgentContextMapping[tenantRecord.id] = { + mutex, + } + + return await mutex.runExclusive(async () => { + const tenantDependencyManager = this.rootAgentContext.dependencyManager.createChild() + const tenantConfig = this.rootAgentContext.config.extend(tenantRecord.config) + + const agentContext = new AgentContext({ + contextCorrelationId: tenantRecord.id, + dependencyManager: tenantDependencyManager, + }) + + tenantDependencyManager.registerInstance(AgentContext, agentContext) + tenantDependencyManager.registerInstance(AgentConfig, tenantConfig) + + tenantContextMapping = { + agentContext, + sessionCount: 1, + } + + // NOTE: we're using the wallet module here because that correctly handle creating if it doesn't exist yet + // and will also write the storage version to the storage, which is needed by the update assistant. We either + // need to move this out of the module, or just keep using the module here. + const walletModule = agentContext.dependencyManager.resolve(WalletModule) + await walletModule.initialize(tenantRecord.config.walletConfig) + + this.tenantAgentContextMapping[tenantRecord.id] = tenantContextMapping + + return agentContext + }) + } + } +} + +interface TenantContextSessions { + sessionCount: number + agentContext: AgentContext +} + +interface TenantContextInitializing { + mutex: Mutex +} + +export interface TenantAgentContextMapping { + [tenantId: string]: TenantContextSessions | TenantContextInitializing | undefined +} + +function isTenantAgentContextInitializing( + contextMapping: TenantContextSessions | TenantContextInitializing | undefined +): contextMapping is TenantContextInitializing { + return contextMapping !== undefined && (contextMapping as TenantContextInitializing).mutex !== undefined +} + +function isTenantContextSessions( + contextMapping: TenantContextSessions | TenantContextInitializing | undefined +): contextMapping is TenantContextSessions { + return contextMapping !== undefined && (contextMapping as TenantContextSessions).sessionCount !== undefined +} diff --git a/packages/module-tenants/src/context/__tests__/TenantAgentContextProvider.test.ts b/packages/module-tenants/src/context/__tests__/TenantAgentContextProvider.test.ts new file mode 100644 index 0000000000..8b57626900 --- /dev/null +++ b/packages/module-tenants/src/context/__tests__/TenantAgentContextProvider.test.ts @@ -0,0 +1,151 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Key } from '@aries-framework/core' + +import { EventEmitter } from '../../../../core/src/agent/EventEmitter' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { TenantRecord, TenantRoutingRecord } from '../../repository' +import { TenantService } from '../../services/TenantService' +import { TenantAgentContextProvider } from '../TenantAgentContextProvider' +import { TenantSessionCoordinator } from '../TenantSessionCoordinator' + +jest.mock('../../../../core/src/agent/EventEmitter') +jest.mock('../../services/TenantService') +jest.mock('../TenantSessionCoordinator') + +const EventEmitterMock = EventEmitter as jest.Mock +const TenantServiceMock = TenantService as jest.Mock +const TenantSessionCoordinatorMock = TenantSessionCoordinator as jest.Mock + +const tenantService = new TenantServiceMock() +const tenantSessionCoordinator = new TenantSessionCoordinatorMock() + +const rootAgentContext = getAgentContext() +const agentConfig = getAgentConfig('TenantAgentContextProvider') +const eventEmitter = new EventEmitterMock() + +const tenantAgentContextProvider = new TenantAgentContextProvider( + tenantService, + rootAgentContext, + eventEmitter, + tenantSessionCoordinator, + agentConfig.logger +) + +const inboundMessage = { + protected: + 'eyJlbmMiOiJ4Y2hhY2hhMjBwb2x5MTMwNV9pZXRmIiwidHlwIjoiSldNLzEuMCIsImFsZyI6IkF1dGhjcnlwdCIsInJlY2lwaWVudHMiOlt7ImVuY3J5cHRlZF9rZXkiOiIta3AzRlREbzdNTnlqSVlvWkhFdFhzMzRLTlpEODBIc2tEVTcxSVg5ejJpdVphUy1PVHhNc21KUWNCeDN1Y1lVIiwiaGVhZGVyIjp7ImtpZCI6IjdQd0ZrMXB2V2JOTkUyUDRDTFlnWW5jUXJMc0VSRUx2cDZmQVRaTktVZnpXIiwiaXYiOiJBSElTQk94MWhrWk5obkVwWndJNFlWZ09HNnQ3RDhlQiIsInNlbmRlciI6IjRMTnFHWGJ3SGlPU01uQThsV1M3bEpocER6aGY5UUIyYjNPUVZyWkkyeEctWWJkT1BUamR6WWRGamdpbFo4MF95bXhKSHpoWmp1bHhPeEVvek81VUhDQzJ3bnV3MHo3OVRRWjE5MFgzUFI2WWlrSUVIcms2N3A4V09WTT0ifX1dfQ==', + iv: 'CfsDEiS63uOJRZa-', + ciphertext: + 'V6V23nNdKSn2a0jqrjQoU8cj6Ks9w9_4eqE0_856hjd_gxYxqT4W0M9sZ5ov1zlOptrBz6wGDK-BoEbOzvgNqHmiUS5h_VvVuEIevpp9xYrCLlNigrXJEtoDGWkVVpYq3i14l5XGMYCu2zTL7QJxHqDrzRAG-5Iqht0FY45u4L471CMvj31XuZps6I_wl-TJWfeZbAS1Knp_dEnElqtbkcctOKjnvaosk2WYaIrQXRiJxk-4URGnmMAQxjSSt5KuzE3LQ_fa_u5PQLC0EaOsg5M9fYBSIB1_090fQ0QTPXLB69pyiFzLmb016vHGIG5nAbqNKS7fmkhxo7iMkOBlR5d5FpCAguXln4Fg4Q4tZgEaPXVkqmayTvLyeJqXa22dbNDhaPGrvjlNimn8moP8qSC0Avoozn4BDdLrSQxs5daYcH0JYhqz7VII2Mrb2gp2LMsQsoy-UrChTZSBeyjWdapqOzMc8yOhKYXwA_du0RfSaPFe8VJyMo4X73LC6-i1QU5xg3fZKiKJWRrTUazLvdNEXm79DV76LxylodY7OeCGH6E2amF1k10VC2eYwNCI3CfXS8uvjDEIQGgsVETCqwclWKxD-AhQEwZFRlNhZjlfbUyOKH8WAoikloN75T2AZiTivzlE5ZvnPUU_z4LJI02z-vpIMEjkHKcgjx0jDFi3VkfLPiOG4P6btZpxjISfZvWcCiebAhs5jAGX2zNjYiPErJx33zOclHYS8KEZB3fdAdpAZKdYlPyAOFpN8r21kn6HPYjm-3NmTqrHAjDIMgt0mJ6AI58XOnoqRWN7Hl1HWhy9qkt0AqRVJZIIoyFefvKRJvotsv9aj1ZGPqnrkR2Hpj7u-K8VOKreIg4kWYyVbAF8Oift9CrqJ4olOOSUOQZ_NL36qGJc1RCR_wRnTWikoRR_o4h4fDZtxTQG9nUgbAoaAumJAbp5mxrYBW6KVZ-Jm9rhdNnRRnvvd1e_uW-X66_9B5g0GM3BmsXK-ARpJP6ZYmpQYiVFjrDxOSrvq1gD3aPi0SCP6mYoNvemGoXFhGTPMTGQvy1RAwY9t_BosZNEMZMfYTzHxWhN55yXd0861uv5nFe_aLKQcdin8QySW-FS0jcExnRostY922fqT5JYPBINqAr59u8gpzX-N9DgczL1WjuKkwyezLrcCR1IaG9gZrEIJxLDRGHvBno6ZkqmLiuAx3LZxgrT5yN2fI7WjO5HHQMVLn7rVF-THmpLNTZmmsoJ2ZU9ZGeAMKBpcfIYIHgKHF1vumr_h2uCbvxlwqigm5A-dSmto0Fv9xewfDhZ5TvE-TKwHpwmb0OG4kZqC3CnMmzh_oSOK0Cc6ovldiVOUvXdVZJiSD9KEFxn1YmDNbsdMDP9GAAWAknFmdBp5x7DCCt6sMjCVuw1hbELAGXusipfdvfb4htSN5FR4k72efenEr0glFtDk7s5EvWTWsBZyv92P5et-70MjTKGtMJaC4uCBL3li3ty397yKKcJly2Fog5N0phqPHPHg_-CGZ8YpkcM_q3Ijcc8db701K2TShiG97AjOdCZCSgK8OGv_UFXxXXxiwrdQOM0Jfg0TCz_ESxQLAlepK4JQplE_kR8k3jDf5nH4SMueobioPfkLQ92lCFXBOCX3ugoJJnnb49CbQfi-49PAHsGaTopLXxZoEdf6kgJ8phFakBoMmbLE1zIV43oVR8T-zZYsr377q6c6LY46PyYusP7CB5wgXbG4nyJZ_zGZHvY_hnbcE2-EuysmzQV4-6rJdLdT8FSyX_Xo-K2ZmX-riFUcKamoFWmO3CDtexn-ZgtAIJpdjAApWHFxZWLI6xx67OgHl8GT2HIs_BdoetFvmj4tJ_Aw8_Mmb9W37B4Esom1Tg3XxxfLqj24s7UlgUwYFblkYtB1L9-9DkNlZZWkYJz-A28WW6OSqIYNw0ASyNDEp3Mwy0SHDUYh10NUmQ4C476QRNmr32Jv_6AiTGj1thibFg_Ewd_kdvvo0E7VL6gktZNh9kIT-EPgFAobR5IpG0_V1dJ7pEQPKN-n7nc6gWgry7kxNIfS4LcbPwVDsUzJiJ4Qlw=', + tag: 'goWiDaoxy4mHHRnkPiux4Q==', +} + +describe('TenantAgentContextProvider', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('getAgentContextForContextCorrelationId', () => { + test('retrieves the tenant and calls tenant session coordinator', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + }) + + const tenantAgentContext = jest.fn() as unknown as AgentContext + + mockFunction(tenantService.getTenantById).mockResolvedValue(tenantRecord) + mockFunction(tenantSessionCoordinator.getContextForSession).mockResolvedValue(tenantAgentContext) + + const returnedAgentContext = await tenantAgentContextProvider.getAgentContextForContextCorrelationId('tenant1') + + expect(tenantService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') + expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord) + expect(returnedAgentContext).toBe(tenantAgentContext) + }) + }) + + describe('getContextForInboundMessage', () => { + test('directly calls get agent context if tenant id has been provided in the contextCorrelationId', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + }) + + const tenantAgentContext = jest.fn() as unknown as AgentContext + + mockFunction(tenantService.getTenantById).mockResolvedValue(tenantRecord) + mockFunction(tenantSessionCoordinator.getContextForSession).mockResolvedValue(tenantAgentContext) + + const returnedAgentContext = await tenantAgentContextProvider.getContextForInboundMessage( + {}, + { contextCorrelationId: 'tenant1' } + ) + + expect(tenantService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') + expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord) + expect(returnedAgentContext).toBe(tenantAgentContext) + expect(tenantService.findTenantRoutingRecordByRecipientKey).not.toHaveBeenCalled() + }) + + test('throws an error if not contextCorrelationId is provided and no tenant id could be extracted from the inbound message', async () => { + // no routing records found + mockFunction(tenantService.findTenantRoutingRecordByRecipientKey).mockResolvedValue(null) + + await expect(tenantAgentContextProvider.getContextForInboundMessage(inboundMessage)).rejects.toThrowError( + "Couldn't determine tenant id for inbound message. Unable to create context" + ) + }) + + test('finds the tenant id based on the inbound message recipient keys and calls get agent context', async () => { + const tenantRoutingRecord = new TenantRoutingRecord({ + recipientKeyFingerprint: 'z6MkkrCJLG5Mr8rqLXDksuWXPtAQfv95q7bHW7a6HqLLPtmt', + tenantId: 'tenant1', + }) + + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + }) + + const tenantAgentContext = jest.fn() as unknown as AgentContext + mockFunction(tenantService.findTenantRoutingRecordByRecipientKey).mockResolvedValue(tenantRoutingRecord) + + mockFunction(tenantService.getTenantById).mockResolvedValue(tenantRecord) + mockFunction(tenantSessionCoordinator.getContextForSession).mockResolvedValue(tenantAgentContext) + + const returnedAgentContext = await tenantAgentContextProvider.getContextForInboundMessage(inboundMessage) + + expect(tenantService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') + expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord) + expect(returnedAgentContext).toBe(tenantAgentContext) + expect(tenantService.findTenantRoutingRecordByRecipientKey).toHaveBeenCalledWith( + rootAgentContext, + expect.any(Key) + ) + + const actualKey = mockFunction(tenantService.findTenantRoutingRecordByRecipientKey).mock.calls[0][1] + // Based on the recipient key from the inboundMessage protected header above + expect(actualKey.fingerprint).toBe('z6MkkrCJLG5Mr8rqLXDksuWXPtAQfv95q7bHW7a6HqLLPtmt') + }) + }) +}) diff --git a/packages/module-tenants/src/context/__tests__/TenantSessionCoordinator.test.ts b/packages/module-tenants/src/context/__tests__/TenantSessionCoordinator.test.ts new file mode 100644 index 0000000000..deaf159d1c --- /dev/null +++ b/packages/module-tenants/src/context/__tests__/TenantSessionCoordinator.test.ts @@ -0,0 +1,126 @@ +import type { TenantAgentContextMapping } from '../TenantSessionCoordinator' +import type { AgentContext } from '@aries-framework/core' + +import { WalletModule } from '@aries-framework/core' +import { Mutex } from 'async-mutex' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { TenantRecord } from '../../repository' +import { TenantSessionCoordinator } from '../TenantSessionCoordinator' + +// tenantAgentContextMapping is private, but we need to access it to properly test this class. Adding type override to +// make sure we don't get a lot of type errors. +type PublicTenantAgentContextMapping = Omit & { + tenantAgentContextMapping: TenantAgentContextMapping +} + +const wallet = { + initialize: jest.fn(), +} as unknown as WalletModule + +const agentContext = getAgentContext({ + agentConfig: getAgentConfig('TenantSessionCoordinator'), +}) + +agentContext.dependencyManager.registerInstance(WalletModule, wallet) +const tenantSessionCoordinator = new TenantSessionCoordinator( + agentContext +) as unknown as PublicTenantAgentContextMapping + +describe('TenantSessionCoordinator', () => { + afterEach(() => { + tenantSessionCoordinator.tenantAgentContextMapping = {} + jest.clearAllMocks() + }) + + describe('getContextForSession', () => { + test('returns the context from the tenantAgentContextMapping and increases the session count if already available', async () => { + const tenant1AgentContext = jest.fn() as unknown as AgentContext + + const tenant1 = { + agentContext: tenant1AgentContext, + sessionCount: 1, + } + tenantSessionCoordinator.tenantAgentContextMapping = { + tenant1, + } + + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + }) + + const tenantAgentContext = await tenantSessionCoordinator.getContextForSession(tenantRecord) + expect(tenantAgentContext).toBe(tenant1AgentContext) + expect(tenant1.sessionCount).toBe(2) + }) + + test('creates a new agent context, initializes the wallet and stores it in the tenant agent context mapping', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + }) + + const tenantAgentContext = await tenantSessionCoordinator.getContextForSession(tenantRecord) + + expect(wallet.initialize).toHaveBeenCalledWith(tenantRecord.config.walletConfig) + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + agentContext: tenantAgentContext, + sessionCount: 1, + }) + + expect(tenantAgentContext.contextCorrelationId).toBe('tenant1') + }) + + test('locks and waits for lock to release when initialization is already in progress', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + }) + + // Add timeout to mock the initialization and we can test that the mutex is used. + mockFunction(wallet.initialize).mockReturnValueOnce(new Promise((resolve) => setTimeout(resolve, 100))) + + // Start two context session creations (but don't await). It should set the mutex property on the tenant agent context mapping. + const tenantAgentContext1Promise = tenantSessionCoordinator.getContextForSession(tenantRecord) + const tenantAgentContext2Promise = tenantSessionCoordinator.getContextForSession(tenantRecord) + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + mutex: expect.any(Mutex), + }) + + // Await both context value promises + const tenantAgentContext1 = await tenantAgentContext1Promise + const tenantAgentContext2 = await tenantAgentContext2Promise + + // There should be two sessions active now + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + agentContext: tenantAgentContext1, + sessionCount: 2, + }) + + // Initialize should only be called once + expect(wallet.initialize).toHaveBeenCalledTimes(1) + expect(wallet.initialize).toHaveBeenCalledWith(tenantRecord.config.walletConfig) + + expect(tenantAgentContext1).toBe(tenantAgentContext2) + }) + }) +}) diff --git a/packages/module-tenants/src/context/types.ts b/packages/module-tenants/src/context/types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/module-tenants/src/index.ts b/packages/module-tenants/src/index.ts new file mode 100644 index 0000000000..e4f3378296 --- /dev/null +++ b/packages/module-tenants/src/index.ts @@ -0,0 +1,4 @@ +export { TenantRecord, TenantRecordProps } from './repository/TenantRecord' +export * from './TenantsModule' +export * from './TenantsApi' +export * from './TenantsApiOptions' diff --git a/packages/module-tenants/src/models/TenantConfig.ts b/packages/module-tenants/src/models/TenantConfig.ts new file mode 100644 index 0000000000..d8849e73fe --- /dev/null +++ b/packages/module-tenants/src/models/TenantConfig.ts @@ -0,0 +1,5 @@ +import type { InitConfig, WalletConfig } from '@aries-framework/core' + +export type TenantConfig = Pick & { + walletConfig: Pick +} diff --git a/packages/module-tenants/src/repository/TenantRecord.ts b/packages/module-tenants/src/repository/TenantRecord.ts new file mode 100644 index 0000000000..6c49689d9e --- /dev/null +++ b/packages/module-tenants/src/repository/TenantRecord.ts @@ -0,0 +1,37 @@ +import type { TenantConfig } from '../models/TenantConfig' +import type { RecordTags, TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export type TenantRecordTags = RecordTags + +export interface TenantRecordProps { + id?: string + createdAt?: Date + config: TenantConfig + tags?: TagsBase +} + +export class TenantRecord extends BaseRecord { + public static readonly type = 'TenantRecord' + public readonly type = TenantRecord.type + + public config!: TenantConfig + + public constructor(props: TenantRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + this.config = props.config + } + } + + public getTags() { + return { + ...this._tags, + } + } +} diff --git a/packages/module-tenants/src/repository/TenantRepository.ts b/packages/module-tenants/src/repository/TenantRepository.ts new file mode 100644 index 0000000000..abfd0da287 --- /dev/null +++ b/packages/module-tenants/src/repository/TenantRepository.ts @@ -0,0 +1,13 @@ +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@aries-framework/core' + +import { TenantRecord } from './TenantRecord' + +@injectable() +export class TenantRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(TenantRecord, storageService, eventEmitter) + } +} diff --git a/packages/module-tenants/src/repository/TenantRoutingRecord.ts b/packages/module-tenants/src/repository/TenantRoutingRecord.ts new file mode 100644 index 0000000000..62987290b0 --- /dev/null +++ b/packages/module-tenants/src/repository/TenantRoutingRecord.ts @@ -0,0 +1,47 @@ +import type { RecordTags, TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export type TenantRoutingRecordTags = RecordTags + +type DefaultTenantRoutingRecordTags = { + tenantId: string + recipientKeyFingerprint: string +} + +export interface TenantRoutingRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + + tenantId: string + recipientKeyFingerprint: string +} + +export class TenantRoutingRecord extends BaseRecord { + public static readonly type = 'TenantRoutingRecord' + public readonly type = TenantRoutingRecord.type + + public tenantId!: string + public recipientKeyFingerprint!: string + + public constructor(props: TenantRoutingRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + this.tenantId = props.tenantId + this.recipientKeyFingerprint = props.recipientKeyFingerprint + } + } + + public getTags() { + return { + ...this._tags, + tenantId: this.tenantId, + recipientKeyFingerprint: this.recipientKeyFingerprint, + } + } +} diff --git a/packages/module-tenants/src/repository/TenantRoutingRepository.ts b/packages/module-tenants/src/repository/TenantRoutingRepository.ts new file mode 100644 index 0000000000..1696aeb9fb --- /dev/null +++ b/packages/module-tenants/src/repository/TenantRoutingRepository.ts @@ -0,0 +1,21 @@ +import type { AgentContext, Key } from '@aries-framework/core' + +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@aries-framework/core' + +import { TenantRoutingRecord } from './TenantRoutingRecord' + +@injectable() +export class TenantRoutingRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(TenantRoutingRecord, storageService, eventEmitter) + } + + public findByRecipientKey(agentContext: AgentContext, key: Key) { + return this.findSingleByQuery(agentContext, { + recipientKeyFingerprint: key.fingerprint, + }) + } +} diff --git a/packages/module-tenants/src/repository/__tests__/TenantRecord.test.ts b/packages/module-tenants/src/repository/__tests__/TenantRecord.test.ts new file mode 100644 index 0000000000..7c6e311704 --- /dev/null +++ b/packages/module-tenants/src/repository/__tests__/TenantRecord.test.ts @@ -0,0 +1,87 @@ +import { JsonTransformer } from '../../../../core/src' +import { TenantRecord } from '../TenantRecord' + +describe('TenantRecord', () => { + test('sets the values passed in the constructor on the record', () => { + const createdAt = new Date() + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + createdAt, + tags: { + some: 'tag', + }, + config: { + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }, + }) + + expect(tenantRecord.type).toBe('TenantRecord') + expect(tenantRecord.id).toBe('tenant-id') + expect(tenantRecord.createdAt).toBe(createdAt) + expect(tenantRecord.config).toMatchObject({ + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }) + expect(tenantRecord.getTags()).toMatchObject({ + some: 'tag', + }) + }) + + test('serializes and deserializes', () => { + const createdAt = new Date('2022-02-02') + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + createdAt, + tags: { + some: 'tag', + }, + config: { + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }, + }) + + const json = tenantRecord.toJSON() + expect(json).toEqual({ + id: 'tenant-id', + createdAt: '2022-02-02T00:00:00.000Z', + metadata: {}, + _tags: { + some: 'tag', + }, + config: { + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }, + }) + + const instance = JsonTransformer.fromJSON(json, TenantRecord) + + expect(instance.type).toBe('TenantRecord') + expect(instance.id).toBe('tenant-id') + expect(instance.createdAt.getTime()).toBe(createdAt.getTime()) + expect(instance.config).toMatchObject({ + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }) + expect(instance.getTags()).toMatchObject({ + some: 'tag', + }) + }) +}) diff --git a/packages/module-tenants/src/repository/__tests__/TenantRoutingRecord.test.ts b/packages/module-tenants/src/repository/__tests__/TenantRoutingRecord.test.ts new file mode 100644 index 0000000000..1d47b2bd18 --- /dev/null +++ b/packages/module-tenants/src/repository/__tests__/TenantRoutingRecord.test.ts @@ -0,0 +1,77 @@ +import { JsonTransformer } from '../../../../core/src' +import { TenantRoutingRecord } from '../TenantRoutingRecord' + +describe('TenantRoutingRecord', () => { + test('sets the values passed in the constructor on the record', () => { + const createdAt = new Date() + const tenantRoutingRecord = new TenantRoutingRecord({ + id: 'record-id', + createdAt, + tags: { + some: 'tag', + }, + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + + expect(tenantRoutingRecord.type).toBe('TenantRoutingRecord') + expect(tenantRoutingRecord.id).toBe('record-id') + expect(tenantRoutingRecord.tenantId).toBe('tenant-id') + expect(tenantRoutingRecord.createdAt).toBe(createdAt) + expect(tenantRoutingRecord.recipientKeyFingerprint).toBe('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + expect(tenantRoutingRecord.getTags()).toMatchObject({ + some: 'tag', + }) + }) + + test('returns the default tags', () => { + const tenantRoutingRecord = new TenantRoutingRecord({ + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + + expect(tenantRoutingRecord.getTags()).toMatchObject({ + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + }) + + test('serializes and deserializes', () => { + const createdAt = new Date('2022-02-02') + const tenantRoutingRecord = new TenantRoutingRecord({ + id: 'record-id', + createdAt, + tags: { + some: 'tag', + }, + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + + const json = tenantRoutingRecord.toJSON() + expect(json).toEqual({ + id: 'record-id', + createdAt: '2022-02-02T00:00:00.000Z', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + tenantId: 'tenant-id', + metadata: {}, + _tags: { + some: 'tag', + }, + }) + + const instance = JsonTransformer.fromJSON(json, TenantRoutingRecord) + + expect(instance.type).toBe('TenantRoutingRecord') + expect(instance.id).toBe('record-id') + expect(instance.createdAt.getTime()).toBe(createdAt.getTime()) + expect(instance.recipientKeyFingerprint).toBe('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + expect(instance.tenantId).toBe('tenant-id') + + expect(instance.getTags()).toMatchObject({ + some: 'tag', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + tenantId: 'tenant-id', + }) + }) +}) diff --git a/packages/module-tenants/src/repository/__tests__/TenantRoutingRepository.test.ts b/packages/module-tenants/src/repository/__tests__/TenantRoutingRepository.test.ts new file mode 100644 index 0000000000..ed22ee31ab --- /dev/null +++ b/packages/module-tenants/src/repository/__tests__/TenantRoutingRepository.test.ts @@ -0,0 +1,38 @@ +import type { StorageService, EventEmitter } from '../../../../core/src' + +import { Key } from '../../../../core/src' +import { getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { TenantRoutingRecord } from '../TenantRoutingRecord' +import { TenantRoutingRepository } from '../TenantRoutingRepository' + +const storageServiceMock = { + findByQuery: jest.fn(), +} as unknown as StorageService +const eventEmitter = jest.fn() as unknown as EventEmitter +const agentContext = getAgentContext() + +const tenantRoutingRepository = new TenantRoutingRepository(storageServiceMock, eventEmitter) + +describe('TenantRoutingRepository', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('findByRecipientKey', () => { + test('it should correctly transform the key to a fingerprint and return the routing record', async () => { + const key = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const tenantRoutingRecord = new TenantRoutingRecord({ + recipientKeyFingerprint: key.fingerprint, + tenantId: 'tenant-id', + }) + + mockFunction(storageServiceMock.findByQuery).mockResolvedValue([tenantRoutingRecord]) + const returnedRecord = await tenantRoutingRepository.findByRecipientKey(agentContext, key) + + expect(storageServiceMock.findByQuery).toHaveBeenCalledWith(agentContext, TenantRoutingRecord, { + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + expect(returnedRecord).toBe(tenantRoutingRecord) + }) + }) +}) diff --git a/packages/module-tenants/src/repository/index.ts b/packages/module-tenants/src/repository/index.ts new file mode 100644 index 0000000000..99ac579c10 --- /dev/null +++ b/packages/module-tenants/src/repository/index.ts @@ -0,0 +1,4 @@ +export * from './TenantRecord' +export * from './TenantRepository' +export * from './TenantRoutingRecord' +export * from './TenantRoutingRepository' diff --git a/packages/module-tenants/src/services/TenantService.ts b/packages/module-tenants/src/services/TenantService.ts new file mode 100644 index 0000000000..be6858d164 --- /dev/null +++ b/packages/module-tenants/src/services/TenantService.ts @@ -0,0 +1,83 @@ +import type { TenantConfig } from '../models/TenantConfig' +import type { AgentContext, Key } from '@aries-framework/core' + +import { injectable, utils } from '@aries-framework/core' + +import { TenantRepository, TenantRecord, TenantRoutingRepository, TenantRoutingRecord } from '../repository' + +@injectable() +export class TenantService { + private tenantRepository: TenantRepository + private tenantRoutingRepository: TenantRoutingRepository + + public constructor(tenantRepository: TenantRepository, tenantRoutingRepository: TenantRoutingRepository) { + this.tenantRepository = tenantRepository + this.tenantRoutingRepository = tenantRoutingRepository + } + + public async createTenant(agentContext: AgentContext, config: Omit) { + const tenantId = utils.uuid() + + const walletId = `tenant-${tenantId}` + const walletKey = await agentContext.wallet.generateWalletKey() + + const tenantRecord = new TenantRecord({ + id: tenantId, + config: { + ...config, + walletConfig: { + id: walletId, + key: walletKey, + }, + }, + }) + + await this.tenantRepository.save(agentContext, tenantRecord) + + return tenantRecord + } + + public async getTenantById(agentContext: AgentContext, tenantId: string) { + return this.tenantRepository.getById(agentContext, tenantId) + } + + public async deleteTenantById(agentContext: AgentContext, tenantId: string) { + const tenantRecord = await this.getTenantById(agentContext, tenantId) + + const tenantRoutingRecords = await this.tenantRoutingRepository.findByQuery(agentContext, { + tenantId: tenantRecord.id, + }) + + // Delete all tenant routing records + await Promise.all( + tenantRoutingRecords.map((tenantRoutingRecord) => + this.tenantRoutingRepository.delete(agentContext, tenantRoutingRecord) + ) + ) + + // Delete tenant record + await this.tenantRepository.delete(agentContext, tenantRecord) + } + + public async findTenantRoutingRecordByRecipientKey( + agentContext: AgentContext, + recipientKey: Key + ): Promise { + return this.tenantRoutingRepository.findByRecipientKey(agentContext, recipientKey) + } + + public async addTenantRoutingRecord( + agentContext: AgentContext, + tenantId: string, + recipientKey: Key + ): Promise { + const tenantRoutingRecord = new TenantRoutingRecord({ + tenantId, + recipientKeyFingerprint: recipientKey.fingerprint, + }) + + await this.tenantRoutingRepository.save(agentContext, tenantRoutingRecord) + + return tenantRoutingRecord + } +} diff --git a/packages/module-tenants/src/services/__tests__/TenantService.test.ts b/packages/module-tenants/src/services/__tests__/TenantService.test.ts new file mode 100644 index 0000000000..2d5e86f09f --- /dev/null +++ b/packages/module-tenants/src/services/__tests__/TenantService.test.ts @@ -0,0 +1,151 @@ +import type { Wallet } from '@aries-framework/core' + +import { Key } from '@aries-framework/core' + +import { getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { TenantRecord, TenantRoutingRecord } from '../../repository' +import { TenantRepository } from '../../repository/TenantRepository' +import { TenantRoutingRepository } from '../../repository/TenantRoutingRepository' +import { TenantService } from '../TenantService' + +jest.mock('../repository/TenantRepository') +const TenantRepositoryMock = TenantRepository as jest.Mock +jest.mock('../repository/TenantRoutingRepository') +const TenantRoutingRepositoryMock = TenantRoutingRepository as jest.Mock + +const wallet = { + generateWalletKey: jest.fn(() => Promise.resolve('walletKey')), +} as unknown as Wallet + +const tenantRepository = new TenantRepositoryMock() +const tenantRoutingRepository = new TenantRoutingRepositoryMock() +const agentContext = getAgentContext({ wallet }) + +const tenantService = new TenantService(tenantRepository, tenantRoutingRepository) + +describe('TenantService', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('createTenant', () => { + test('creates a tenant record and stores it in the tenant repository', async () => { + const tenantRecord = await tenantService.createTenant(agentContext, { + label: 'Test Tenant', + connectionImageUrl: 'https://example.com/connection.png', + }) + + expect(tenantRecord).toMatchObject({ + id: expect.any(String), + config: { + label: 'Test Tenant', + connectionImageUrl: 'https://example.com/connection.png', + walletConfig: { + id: expect.any(String), + key: 'walletKey', + }, + }, + }) + + expect(agentContext.wallet.generateWalletKey).toHaveBeenCalled() + expect(tenantRepository.save).toHaveBeenCalledWith(agentContext, tenantRecord) + }) + }) + + describe('getTenantById', () => { + test('returns value from tenant repository get by id', async () => { + const tenantRecord = jest.fn() as unknown as TenantRecord + mockFunction(tenantRepository.getById).mockResolvedValue(tenantRecord) + const returnedTenantRecord = await tenantService.getTenantById(agentContext, 'tenantId') + + expect(returnedTenantRecord).toBe(tenantRecord) + }) + }) + + describe('deleteTenantById', () => { + test('retrieves the tenant record and calls delete on the tenant repository', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'tenant-wallet-id', + key: 'tenant-wallet-key', + }, + }, + }) + mockFunction(tenantRepository.getById).mockResolvedValue(tenantRecord) + mockFunction(tenantRoutingRepository.findByQuery).mockResolvedValue([]) + + await tenantService.deleteTenantById(agentContext, 'tenant-id') + + expect(tenantRepository.delete).toHaveBeenCalledWith(agentContext, tenantRecord) + }) + + test('deletes associated tenant routing records', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'tenant-wallet-id', + key: 'tenant-wallet-key', + }, + }, + }) + const tenantRoutingRecords = [ + new TenantRoutingRecord({ + recipientKeyFingerprint: '1', + tenantId: 'tenant-id', + }), + new TenantRoutingRecord({ + recipientKeyFingerprint: '2', + tenantId: 'tenant-id', + }), + ] + + mockFunction(tenantRepository.getById).mockResolvedValue(tenantRecord) + mockFunction(tenantRoutingRepository.findByQuery).mockResolvedValue(tenantRoutingRecords) + + await tenantService.deleteTenantById(agentContext, 'tenant-id') + + expect(tenantRoutingRepository.findByQuery).toHaveBeenCalledWith(agentContext, { + tenantId: 'tenant-id', + }) + + expect(tenantRoutingRepository.delete).toHaveBeenCalledTimes(2) + expect(tenantRoutingRepository.delete).toHaveBeenNthCalledWith(1, agentContext, tenantRoutingRecords[0]) + expect(tenantRoutingRepository.delete).toHaveBeenNthCalledWith(2, agentContext, tenantRoutingRecords[1]) + }) + }) + + describe('findTenantRoutingRecordByRecipientKey', () => { + test('returns value from tenant routing repository findByRecipientKey', async () => { + const tenantRoutingRecord = jest.fn() as unknown as TenantRoutingRecord + mockFunction(tenantRoutingRepository.findByRecipientKey).mockResolvedValue(tenantRoutingRecord) + + const recipientKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const returnedTenantRoutingRecord = await tenantService.findTenantRoutingRecordByRecipientKey( + agentContext, + recipientKey + ) + + expect(tenantRoutingRepository.findByRecipientKey).toHaveBeenCalledWith(agentContext, recipientKey) + expect(returnedTenantRoutingRecord).toBe(tenantRoutingRecord) + }) + }) + + describe('addTenantRoutingRecord', () => { + test('creates a tenant routing record and stores it in the tenant routing repository', async () => { + const recipientKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const tenantRoutingRecord = await tenantService.addTenantRoutingRecord(agentContext, 'tenant-id', recipientKey) + + expect(tenantRoutingRepository.save).toHaveBeenCalledWith(agentContext, tenantRoutingRecord) + expect(tenantRoutingRecord).toMatchObject({ + id: expect.any(String), + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + }) + }) +}) diff --git a/packages/module-tenants/src/services/index.ts b/packages/module-tenants/src/services/index.ts new file mode 100644 index 0000000000..8f1c72138f --- /dev/null +++ b/packages/module-tenants/src/services/index.ts @@ -0,0 +1 @@ +export * from './TenantService' diff --git a/packages/module-tenants/tests/setup.ts b/packages/module-tenants/tests/setup.ts new file mode 100644 index 0000000000..3f425aaf8f --- /dev/null +++ b/packages/module-tenants/tests/setup.ts @@ -0,0 +1 @@ +import 'reflect-metadata' diff --git a/packages/module-tenants/tests/tenants.e2e.test.ts b/packages/module-tenants/tests/tenants.e2e.test.ts new file mode 100644 index 0000000000..c95c4c815d --- /dev/null +++ b/packages/module-tenants/tests/tenants.e2e.test.ts @@ -0,0 +1,201 @@ +import type { InitConfig } from '@aries-framework/core' + +import { Agent, DependencyManager } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import testLogger from '../../core/tests/logger' +import { TenantsApi, TenantsModule } from '../src' + +jest.setTimeout(2000000) + +const agent1Config: InitConfig = { + label: 'Tenant Agent 1', + walletConfig: { + id: 'Wallet: tenants e2e agent 1', + key: 'Wallet: tenants e2e agent 1', + }, + logger: testLogger, + endpoints: ['rxjs:tenant-agent1'], + autoAcceptConnections: true, +} + +const agent2Config: InitConfig = { + label: 'Tenant Agent 2', + walletConfig: { + id: 'Wallet: tenants e2e agent 2', + key: 'Wallet: tenants e2e agent 2', + }, + logger: testLogger, + endpoints: ['rxjs:tenant-agent2'], + autoAcceptConnections: true, +} + +// Register tenant module. For now we need to create a custom dependency manager +// and register all plugins before initializing the agent. Later, we can add the module registration +// to the agent constructor. +const agent1DependencyManager = new DependencyManager() +agent1DependencyManager.registerModules(TenantsModule) + +const agent2DependencyManager = new DependencyManager() +agent2DependencyManager.registerModules(TenantsModule) + +// Create multi-tenant agents +const agent1 = new Agent(agent1Config, agentDependencies, agent1DependencyManager) +const agent2 = new Agent(agent2Config, agentDependencies, agent2DependencyManager) + +// Register inbound and outbound transports (so we can communicate with ourselves) +const agent1InboundTransport = new SubjectInboundTransport() +const agent2InboundTransport = new SubjectInboundTransport() + +agent1.registerInboundTransport(agent1InboundTransport) +agent2.registerInboundTransport(agent2InboundTransport) + +agent1.registerOutboundTransport( + new SubjectOutboundTransport({ + 'rxjs:tenant-agent1': agent1InboundTransport.ourSubject, + 'rxjs:tenant-agent2': agent2InboundTransport.ourSubject, + }) +) +agent2.registerOutboundTransport( + new SubjectOutboundTransport({ + 'rxjs:tenant-agent1': agent1InboundTransport.ourSubject, + 'rxjs:tenant-agent2': agent2InboundTransport.ourSubject, + }) +) + +const agent1TenantsApi = agent1.dependencyManager.resolve(TenantsApi) +const agent2TenantsApi = agent2.dependencyManager.resolve(TenantsApi) + +describe('Tenants E2E', () => { + beforeAll(async () => { + await agent1.initialize() + await agent2.initialize() + }) + + afterAll(async () => { + await agent1.wallet.delete() + await agent1.shutdown() + await agent2.wallet.delete() + await agent2.shutdown() + }) + + test('create get and delete a tenant', async () => { + // Create tenant + let tenantRecord1 = await agent1TenantsApi.createTenant({ + config: { + label: 'Tenant 1', + }, + }) + + // Retrieve tenant record from storage + tenantRecord1 = await agent1TenantsApi.getTenantById(tenantRecord1.id) + + // Get tenant agent + const tenantAgent = await agent1TenantsApi.getTenantAgent({ + tenantId: tenantRecord1.id, + }) + await tenantAgent.shutdown() + + // Delete tenant agent + await agent1TenantsApi.deleteTenantById(tenantRecord1.id) + + // Can not get tenant agent again + await expect(agent1TenantsApi.getTenantAgent({ tenantId: tenantRecord1.id })).rejects.toThrow( + `TenantRecord: record with id ${tenantRecord1.id} not found.` + ) + }) + + test('create a connection between two tenants within the same agent', async () => { + // Create tenants + const tenantRecord1 = await agent1TenantsApi.createTenant({ + config: { + label: 'Tenant 1', + }, + }) + const tenantRecord2 = await agent1TenantsApi.createTenant({ + config: { + label: 'Tenant 2', + }, + }) + + const tenantAgent1 = await agent1TenantsApi.getTenantAgent({ + tenantId: tenantRecord1.id, + }) + const tenantAgent2 = await agent1TenantsApi.getTenantAgent({ + tenantId: tenantRecord2.id, + }) + + // Create and receive oob invitation in scope of tenants + const outOfBandRecord = await tenantAgent1.oob.createInvitation() + const { connectionRecord: tenant2ConnectionRecord } = await tenantAgent2.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation + ) + + // Retrieve all oob records for the base and tenant agent, only the + // tenant agent should have a record. + const baseAgentOutOfBandRecords = await agent1.oob.getAll() + const tenantAgent1OutOfBandRecords = await tenantAgent1.oob.getAll() + const tenantAgent2OutOfBandRecords = await tenantAgent2.oob.getAll() + + expect(baseAgentOutOfBandRecords.length).toBe(0) + expect(tenantAgent1OutOfBandRecords.length).toBe(1) + expect(tenantAgent2OutOfBandRecords.length).toBe(1) + + if (!tenant2ConnectionRecord) throw new Error('Receive invitation did not return connection record') + await tenantAgent2.connections.returnWhenIsConnected(tenant2ConnectionRecord.id) + + // Find the connection record for the created oob invitation + const [connectionRecord] = await tenantAgent1.connections.findAllByOutOfBandId(outOfBandRecord.id) + await tenantAgent1.connections.returnWhenIsConnected(connectionRecord.id) + + await tenantAgent1.shutdown() + await tenantAgent1.shutdown() + + // Delete tenants (will also delete wallets) + await agent1TenantsApi.deleteTenantById(tenantAgent1.context.contextCorrelationId) + await agent1TenantsApi.deleteTenantById(tenantAgent2.context.contextCorrelationId) + }) + + test('create a connection between two tenants within different agents', async () => { + // Create tenants + const tenantRecord1 = await agent1TenantsApi.createTenant({ + config: { + label: 'Agent 1 Tenant 1', + }, + }) + const tenantAgent1 = await agent1TenantsApi.getTenantAgent({ + tenantId: tenantRecord1.id, + }) + + const tenantRecord2 = await agent2TenantsApi.createTenant({ + config: { + label: 'Agent 2 Tenant 1', + }, + }) + const tenantAgent2 = await agent2TenantsApi.getTenantAgent({ + tenantId: tenantRecord2.id, + }) + + // Create and receive oob invitation in scope of tenants + const outOfBandRecord = await tenantAgent1.oob.createInvitation() + const { connectionRecord: tenant2ConnectionRecord } = await tenantAgent2.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation + ) + + if (!tenant2ConnectionRecord) throw new Error('Receive invitation did not return connection record') + await tenantAgent2.connections.returnWhenIsConnected(tenant2ConnectionRecord.id) + + // Find the connection record for the created oob invitation + const [connectionRecord] = await tenantAgent1.connections.findAllByOutOfBandId(outOfBandRecord.id) + await tenantAgent1.connections.returnWhenIsConnected(connectionRecord.id) + + await tenantAgent1.shutdown() + await tenantAgent1.shutdown() + + // Delete tenants (will also delete wallets) + await agent1TenantsApi.deleteTenantById(tenantAgent1.context.contextCorrelationId) + await agent2TenantsApi.deleteTenantById(tenantAgent2.context.contextCorrelationId) + }) +}) diff --git a/yarn.lock b/yarn.lock index a9d7bc2f49..38fa54ba92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3104,6 +3104,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-mutex@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df" + integrity sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA== + dependencies: + tslib "^2.3.1" + async@^2.4.0: version "2.6.4" resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" @@ -10521,7 +10528,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== From c69de1bdbe39a89f8c094cc9da3b671c1a017ef8 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 5 Jul 2022 18:59:54 +0200 Subject: [PATCH 11/12] test: fix tests Signed-off-by: Timo Glastra --- .../RevocationNotificationService.test.ts | 6 ++++++ .../v1/__tests__/V1CredentialServiceCred.test.ts | 9 +++++++++ .../V1CredentialServiceProposeOffer.test.ts | 9 +++++++++ .../v2/__tests__/V2CredentialServiceCred.test.ts | 9 +++++++++ .../v2/__tests__/V2CredentialServiceOffer.test.ts | 6 ++++++ .../modules/proofs/__tests__/ProofService.test.ts | 3 +++ .../__tests__/QuestionAnswerService.test.ts | 6 ++++++ .../services/__tests__/RoutingService.test.ts | 3 +++ .../core/src/storage/__tests__/Repository.test.ts | 9 +++++++++ .../migration/__tests__/UpdateAssistant.test.ts | 2 +- packages/core/src/utils/JWE.ts | 14 +++++++------- packages/core/tests/oob.test.ts | 9 +++++++++ .../{postgres.test.ts => postgres.e2e.test.ts} | 0 .../src/__tests__/TenantsModule.test.ts | 6 ++---- .../src/services/__tests__/TenantService.test.ts | 4 ++-- 15 files changed, 81 insertions(+), 14 deletions(-) rename packages/core/tests/{postgres.test.ts => postgres.e2e.test.ts} (100%) diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts index 9222b3fdcf..c0b3e57904 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts @@ -103,6 +103,9 @@ describe('RevocationNotificationService', () => { expect(eventListenerMock).toHaveBeenCalledWith({ type: 'RevocationNotificationReceived', + metadata: { + contextCorrelationId: 'mock', + }, payload: { credentialRecord: expect.any(CredentialExchangeRecord), }, @@ -208,6 +211,9 @@ describe('RevocationNotificationService', () => { expect(eventListenerMock).toHaveBeenCalledWith({ type: 'RevocationNotificationReceived', + metadata: { + contextCorrelationId: 'mock', + }, payload: { credentialRecord: expect.any(CredentialExchangeRecord), }, diff --git a/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialServiceCred.test.ts b/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialServiceCred.test.ts index 7f2760a501..343751b4d5 100644 --- a/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialServiceCred.test.ts +++ b/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialServiceCred.test.ts @@ -482,6 +482,9 @@ describe('V1CredentialService', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: CredentialState.RequestReceived, credentialRecord: expect.objectContaining({ @@ -608,6 +611,9 @@ describe('V1CredentialService', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: CredentialState.CredentialReceived, credentialRecord: expect.objectContaining({ @@ -934,6 +940,9 @@ describe('V1CredentialService', () => { const [[event]] = eventListenerMock.mock.calls expect(event).toMatchObject({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: CredentialState.OfferReceived, credentialRecord: expect.objectContaining({ diff --git a/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialServiceProposeOffer.test.ts b/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialServiceProposeOffer.test.ts index 7ebe9467aa..3fd459ece5 100644 --- a/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialServiceProposeOffer.test.ts +++ b/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialServiceProposeOffer.test.ts @@ -182,6 +182,9 @@ describe('V1CredentialServiceProposeOffer', () => { expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: null, credentialRecord: expect.objectContaining({ @@ -289,6 +292,9 @@ describe('V1CredentialServiceProposeOffer', () => { expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: null, credentialRecord: expect.objectContaining({ @@ -385,6 +391,9 @@ describe('V1CredentialServiceProposeOffer', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: null, credentialRecord: expect.objectContaining({ diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialServiceCred.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialServiceCred.test.ts index 2168342054..619658db7d 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialServiceCred.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialServiceCred.test.ts @@ -484,6 +484,9 @@ describe('CredentialService', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: CredentialState.RequestReceived, credentialRecord: expect.objectContaining({ @@ -590,6 +593,9 @@ describe('CredentialService', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: CredentialState.CredentialReceived, credentialRecord: expect.objectContaining({ @@ -887,6 +893,9 @@ describe('CredentialService', () => { const [[event]] = eventListenerMock.mock.calls expect(event).toMatchObject({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: CredentialState.OfferReceived, credentialRecord: expect.objectContaining({ diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialServiceOffer.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialServiceOffer.test.ts index 89670711fe..4bb5ba769d 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialServiceOffer.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialServiceOffer.test.ts @@ -161,6 +161,9 @@ describe('V2CredentialServiceOffer', () => { expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: null, credentialRecord: expect.objectContaining({ @@ -248,6 +251,9 @@ describe('V2CredentialServiceOffer', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: null, credentialRecord: expect.objectContaining({ diff --git a/packages/core/src/modules/proofs/__tests__/ProofService.test.ts b/packages/core/src/modules/proofs/__tests__/ProofService.test.ts index 554856172e..b64ded9b1c 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofService.test.ts +++ b/packages/core/src/modules/proofs/__tests__/ProofService.test.ts @@ -175,6 +175,9 @@ describe('ProofService', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'ProofStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: null, proofRecord: expect.objectContaining({ diff --git a/packages/core/src/modules/question-answer/__tests__/QuestionAnswerService.test.ts b/packages/core/src/modules/question-answer/__tests__/QuestionAnswerService.test.ts index c940c1e30c..1820debc06 100644 --- a/packages/core/src/modules/question-answer/__tests__/QuestionAnswerService.test.ts +++ b/packages/core/src/modules/question-answer/__tests__/QuestionAnswerService.test.ts @@ -99,6 +99,9 @@ describe('QuestionAnswerService', () => { expect(eventListenerMock).toHaveBeenCalledWith({ type: 'QuestionAnswerStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: null, questionAnswerRecord: expect.objectContaining({ @@ -146,6 +149,9 @@ describe('QuestionAnswerService', () => { expect(eventListenerMock).toHaveBeenCalledWith({ type: 'QuestionAnswerStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, payload: { previousState: QuestionAnswerState.QuestionReceived, questionAnswerRecord: expect.objectContaining({ diff --git a/packages/core/src/modules/routing/services/__tests__/RoutingService.test.ts b/packages/core/src/modules/routing/services/__tests__/RoutingService.test.ts index c1360ed1df..504da2f0b2 100644 --- a/packages/core/src/modules/routing/services/__tests__/RoutingService.test.ts +++ b/packages/core/src/modules/routing/services/__tests__/RoutingService.test.ts @@ -66,6 +66,9 @@ describe('RoutingService', () => { expect(routing).toEqual(routing) expect(routingListener).toHaveBeenCalledWith({ type: RoutingEventTypes.RoutingCreatedEvent, + metadata: { + contextCorrelationId: 'mock', + }, payload: { routing, }, diff --git a/packages/core/src/storage/__tests__/Repository.test.ts b/packages/core/src/storage/__tests__/Repository.test.ts index 6ff81c3c64..cea72b3c22 100644 --- a/packages/core/src/storage/__tests__/Repository.test.ts +++ b/packages/core/src/storage/__tests__/Repository.test.ts @@ -61,6 +61,9 @@ describe('Repository', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'RecordSaved', + metadata: { + contextCorrelationId: 'mock', + }, payload: { record: expect.objectContaining({ id: 'test-id', @@ -91,6 +94,9 @@ describe('Repository', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'RecordUpdated', + metadata: { + contextCorrelationId: 'mock', + }, payload: { record: expect.objectContaining({ id: 'test-id', @@ -121,6 +127,9 @@ describe('Repository', () => { // then expect(eventListenerMock).toHaveBeenCalledWith({ type: 'RecordDeleted', + metadata: { + contextCorrelationId: 'mock', + }, payload: { record: expect.objectContaining({ id: 'test-id', diff --git a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts index 148442b8dc..4cc59315e2 100644 --- a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts +++ b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts @@ -16,7 +16,7 @@ describe('UpdateAssistant', () => { beforeEach(async () => { const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() + storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) agent = new Agent(config, agentDependencies, dependencyManager) diff --git a/packages/core/src/utils/JWE.ts b/packages/core/src/utils/JWE.ts index f925987b4b..f0c7c6049f 100644 --- a/packages/core/src/utils/JWE.ts +++ b/packages/core/src/utils/JWE.ts @@ -2,13 +2,13 @@ import type { EncryptedMessage } from '../types' // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isValidJweStructure(message: any): message is EncryptedMessage { - return ( + return Boolean( message && - typeof message === 'object' && - message !== null && - typeof message.protected === 'string' && - message.iv && - message.ciphertext && - message.tag + typeof message === 'object' && + message !== null && + typeof message.protected === 'string' && + message.iv && + message.ciphertext && + message.tag ) } diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index 03e33462ea..b3fc66c71d 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -244,6 +244,9 @@ describe('out of band', () => { expect(eventListener).toHaveBeenCalledWith({ type: OutOfBandEventTypes.OutOfBandStateChanged, + metadata: { + contextCorrelationId: 'default', + }, payload: { outOfBandRecord, previousState: null, @@ -583,6 +586,9 @@ describe('out of band', () => { // Receiving the invitation expect(eventListener).toHaveBeenNthCalledWith(1, { type: OutOfBandEventTypes.OutOfBandStateChanged, + metadata: { + contextCorrelationId: 'default', + }, payload: { outOfBandRecord: expect.objectContaining({ state: OutOfBandState.Initial }), previousState: null, @@ -592,6 +598,9 @@ describe('out of band', () => { // Accepting the invitation expect(eventListener).toHaveBeenNthCalledWith(2, { type: OutOfBandEventTypes.OutOfBandStateChanged, + metadata: { + contextCorrelationId: 'default', + }, payload: { outOfBandRecord, previousState: OutOfBandState.Initial, diff --git a/packages/core/tests/postgres.test.ts b/packages/core/tests/postgres.e2e.test.ts similarity index 100% rename from packages/core/tests/postgres.test.ts rename to packages/core/tests/postgres.e2e.test.ts diff --git a/packages/module-tenants/src/__tests__/TenantsModule.test.ts b/packages/module-tenants/src/__tests__/TenantsModule.test.ts index aeee314335..0e815072a3 100644 --- a/packages/module-tenants/src/__tests__/TenantsModule.test.ts +++ b/packages/module-tenants/src/__tests__/TenantsModule.test.ts @@ -17,10 +17,8 @@ describe('TenantsModule', () => { test('registers dependencies on the dependency manager', () => { TenantsModule.register(dependencyManager) - expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) - expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(TenantsApi) - - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(5) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(6) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantsApi) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantRepository) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantRoutingRepository) diff --git a/packages/module-tenants/src/services/__tests__/TenantService.test.ts b/packages/module-tenants/src/services/__tests__/TenantService.test.ts index 2d5e86f09f..edbb44f9cd 100644 --- a/packages/module-tenants/src/services/__tests__/TenantService.test.ts +++ b/packages/module-tenants/src/services/__tests__/TenantService.test.ts @@ -8,9 +8,9 @@ import { TenantRepository } from '../../repository/TenantRepository' import { TenantRoutingRepository } from '../../repository/TenantRoutingRepository' import { TenantService } from '../TenantService' -jest.mock('../repository/TenantRepository') +jest.mock('../../repository/TenantRepository') const TenantRepositoryMock = TenantRepository as jest.Mock -jest.mock('../repository/TenantRoutingRepository') +jest.mock('../../repository/TenantRoutingRepository') const TenantRoutingRepositoryMock = TenantRoutingRepository as jest.Mock const wallet = { From a1613b1f39a554b8d5d31a1ec888946a2d5c4eec Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 11 Jul 2022 09:41:50 +0200 Subject: [PATCH 12/12] style: remove unnesary import Signed-off-by: Timo Glastra --- packages/core/src/modules/connections/ConnectionsModule.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/modules/connections/ConnectionsModule.ts b/packages/core/src/modules/connections/ConnectionsModule.ts index d81459cdad..91a7a9e90f 100644 --- a/packages/core/src/modules/connections/ConnectionsModule.ts +++ b/packages/core/src/modules/connections/ConnectionsModule.ts @@ -1,4 +1,3 @@ -import type { Key } from '../../crypto' import type { DependencyManager } from '../../plugins' import type { OutOfBandRecord } from '../oob/repository' import type { ConnectionRecord } from './repository/ConnectionRecord'