From d92adbad5b846039c0f0a55905bc5c5d779f00de Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Mon, 10 Feb 2025 16:09:35 -0500 Subject: [PATCH] test: add unit tests (#77) --- package.json | 2 +- packages/client/package.json | 2 +- packages/main/package.json | 2 +- packages/nestjs-client/package.json | 4 +- packages/nestjs-client/src/app.module.spec.ts | 45 +++ .../connections/connection.controller.spec.ts | 69 +++++ .../connections/connection.service.spec.ts | 112 +++++++ .../credentials/credential.service.spec.ts | 290 ++++++++++++++++++ .../src/messages/message.controller.spec.ts | 100 ++++++ .../src/messages/message.service.spec.ts | 170 ++++++++++ tsconfig.build.json | 1 + 11 files changed, 792 insertions(+), 5 deletions(-) create mode 100644 packages/nestjs-client/src/app.module.spec.ts create mode 100644 packages/nestjs-client/src/connections/connection.controller.spec.ts create mode 100644 packages/nestjs-client/src/connections/connection.service.spec.ts create mode 100644 packages/nestjs-client/src/credentials/credential.service.spec.ts create mode 100644 packages/nestjs-client/src/messages/message.controller.spec.ts create mode 100644 packages/nestjs-client/src/messages/message.service.spec.ts diff --git a/package.json b/package.json index a02ddbe..7b48fed 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "check-types:build": "yarn workspaces run tsc --noEmit -p tsconfig.build.json", "format": "prettier \"packages/*/src/**/*.ts\" --write", "check-format": "prettier -c \"packages/*/src/**/*.ts\"", - "test": "yarn workspaces run test --passWithNoTests", + "test": "yarn workspaces run test", "lint": "eslint \"{packages,apps,libs}/**/*.ts\" --fix", "validate": "yarn lint && yarn check-types && yarn check-format" }, diff --git a/packages/client/package.json b/packages/client/package.json index 4fadd38..48cde49 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -15,7 +15,7 @@ "clean": "rimraf -rf ./build", "compile": "tsc -p tsconfig.build.json", "prepublishOnly": "yarn run build", - "test": "jest" + "test": "jest --passWithNoTests" }, "dependencies": { "@2060.io/service-agent-model": "*", diff --git a/packages/main/package.json b/packages/main/package.json index b213bd3..d981dea 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -50,7 +50,7 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "test": "jest", + "test": "jest --passWithNoTests", "postinstall": "patch-package" }, "dependencies": { diff --git a/packages/nestjs-client/package.json b/packages/nestjs-client/package.json index 2e10e60..3824ce7 100644 --- a/packages/nestjs-client/package.json +++ b/packages/nestjs-client/package.json @@ -20,8 +20,8 @@ }, "dependencies": { "@2060.io/credo-ts-didcomm-mrtd": "^0.0.12", - "@2060.io/service-agent-model": "*", "@2060.io/service-agent-client": "*", + "@2060.io/service-agent-model": "*", "@credo-ts/core": "^0.5.11", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", @@ -34,8 +34,8 @@ "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@nestjs/swagger": "^8.0.7", + "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.13", "credo-ts-receipts": "^0.0.1-alpha.5", "source-map-support": "^0.5.21", diff --git a/packages/nestjs-client/src/app.module.spec.ts b/packages/nestjs-client/src/app.module.spec.ts new file mode 100644 index 0000000..5451ae9 --- /dev/null +++ b/packages/nestjs-client/src/app.module.spec.ts @@ -0,0 +1,45 @@ +import { ApiVersion } from '@2060.io/service-agent-client' + +import { ConnectionsEventModule, EventsModule, MessageEventModule } from '../src' + +describe('EventsModule', () => { + it('should register in EventModule some modules', () => { + const module = EventsModule.register({ + modules: { messages: true, connections: true, credentials: false }, + options: { + eventHandler: jest.fn(), + url: 'http://example.com', + version: ApiVersion.V1, + }, + }) + + expect(module.imports).toEqual( + expect.arrayContaining([ + MessageEventModule.forRoot({ + eventHandler: expect.any(Function), + imports: [], + url: 'http://example.com', + version: ApiVersion.V1, + }), + ConnectionsEventModule.forRoot({ + eventHandler: expect.any(Function), + imports: [], + useMessages: true, + }), + ]), + ) + }) + + it('should throw an error if eventHandler is not provided for MessageEventModule or ConnectionsEventModule', () => { + expect(() => { + EventsModule.register({ + modules: { messages: true, connections: true, credentials: false }, + options: { + // eventHandler is missing + url: 'http://example.com', + version: ApiVersion.V1, + }, + }) + }).toThrow(new Error('Event handler is required but not provided.')) + }) +}) diff --git a/packages/nestjs-client/src/connections/connection.controller.spec.ts b/packages/nestjs-client/src/connections/connection.controller.spec.ts new file mode 100644 index 0000000..3cffa1b --- /dev/null +++ b/packages/nestjs-client/src/connections/connection.controller.spec.ts @@ -0,0 +1,69 @@ +import { HttpUtils } from '@2060.io/service-agent-client' +import { ConnectionStateUpdated, ExtendedDidExchangeState } from '@2060.io/service-agent-model' +import { Logger } from '@nestjs/common' +import { Test, TestingModule } from '@nestjs/testing' + +import { ConnectionsEventController } from './connection.controller' +import { ConnectionsEventService } from './connection.service' + +jest.mock('@2060.io/service-agent-client', () => ({ + HttpUtils: { + handleException: jest.fn(), + }, +})) + +describe('ConnectionsEventController', () => { + let controller: ConnectionsEventController + let service: ConnectionsEventService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConnectionsEventController], + providers: [ + { + provide: ConnectionsEventService, + useValue: { + update: jest.fn(), + }, + }, + ], + }).compile() + + controller = module.get(ConnectionsEventController) + service = module.get(ConnectionsEventService) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) + + describe('update', () => { + const mockBody = new ConnectionStateUpdated({ + connectionId: '123', + invitationId: '456', + state: ExtendedDidExchangeState.Completed, + }) + + it('should call service.update and return success message', async () => { + jest.spyOn(service, 'update').mockResolvedValue(undefined) + + const response = await controller.update(mockBody) + + expect(service.update).toHaveBeenCalledWith(mockBody) + expect(response).toEqual({ message: 'Connection state updated successfully' }) + }) + + it('should handle exceptions and call HttpUtils.handleException', async () => { + const error = new Error('Test error') + jest.spyOn(service, 'update').mockRejectedValue(error) + + await controller.update(mockBody) + + expect(HttpUtils.handleException).toHaveBeenCalledWith( + expect.any(Logger), + error, + 'Failed to update connection state', + ) + }) + }) +}) diff --git a/packages/nestjs-client/src/connections/connection.service.spec.ts b/packages/nestjs-client/src/connections/connection.service.spec.ts new file mode 100644 index 0000000..d563225 --- /dev/null +++ b/packages/nestjs-client/src/connections/connection.service.spec.ts @@ -0,0 +1,112 @@ +import { ConnectionStateUpdated, ExtendedDidExchangeState } from '@2060.io/service-agent-model' +import { Test, TestingModule } from '@nestjs/testing' + +import { ConnectionsRepository, EventHandler, ConnectionsEventService } from '../../src' + +describe('ConnectionsEventService', () => { + let service: ConnectionsEventService + let mockEventHandler: Partial + let mockRepository: Partial + + beforeEach(async () => { + jest.clearAllMocks() + + mockEventHandler = { + closeConnection: jest.fn(), + newConnection: jest.fn(), + } + + mockRepository = { + updateMetadata: jest.fn(), + create: jest.fn(), + updateStatus: jest.fn(), + isCompleted: jest.fn().mockResolvedValue(false), + } + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ConnectionsEventService, + { + provide: 'GLOBAL_MODULE_OPTIONS', + useValue: { useMessages: false }, + }, + { + provide: 'CONNECTIONS_EVENT', + useValue: mockEventHandler, + }, + { + provide: ConnectionsRepository, + useValue: mockRepository, + }, + ], + }).compile() + + service = module.get(ConnectionsEventService) + }) + + it('should be defined', () => { + expect(service['messageEvent']).toBe(false) + }) + + describe('update', () => { + it('should update state to Terminated and call closeConnection', async () => { + const event = new ConnectionStateUpdated({ + state: ExtendedDidExchangeState.Terminated, + connectionId: '123', + }) + + await service.update(event) + + expect(mockRepository.updateStatus).toHaveBeenCalledWith('123', ExtendedDidExchangeState.Terminated) + expect(mockEventHandler.closeConnection).toHaveBeenCalledWith('123') + }) + + it('should update state to completed', async () => { + const event = new ConnectionStateUpdated({ + state: ExtendedDidExchangeState.Completed, + connectionId: '123', + metadata: { key: 'value' }, + }) + + await service.update(event) + + expect(mockRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: '123', + status: ExtendedDidExchangeState.Start, + metadata: event.metadata, + }), + ) + }) + + it('should update state to Updated and call handleNewConnection', async () => { + const event = new ConnectionStateUpdated({ + state: ExtendedDidExchangeState.Updated, + connectionId: '123', + metadata: { key: 'value' }, + }) + + jest.spyOn(service, 'handleNewConnection').mockImplementation(jest.fn()) + + await service.update(event) + + expect(service.handleNewConnection).toHaveBeenCalledWith('123') + }) + }) + + describe('handleNewConnection', () => { + it('should not call newConnection when isCompleted returns false', async () => { + mockRepository.isCompleted = jest.fn().mockResolvedValue(false) + await service.handleNewConnection('123') + expect(mockRepository.isCompleted).toHaveBeenCalledWith('123', false) + expect(mockEventHandler.newConnection).not.toHaveBeenCalled() + }) + + it('should call newConnection when isCompleted returns true', async () => { + mockRepository.isCompleted = jest.fn().mockResolvedValue(true) + await service.handleNewConnection('123') + expect(mockRepository.isCompleted).toHaveBeenCalledWith('123', false) + expect(mockEventHandler.newConnection).toHaveBeenCalledWith('123') + }) + }) +}) diff --git a/packages/nestjs-client/src/credentials/credential.service.spec.ts b/packages/nestjs-client/src/credentials/credential.service.spec.ts new file mode 100644 index 0000000..0ca0f23 --- /dev/null +++ b/packages/nestjs-client/src/credentials/credential.service.spec.ts @@ -0,0 +1,290 @@ +import { Claim } from '@2060.io/service-agent-model' +import { Test, TestingModule } from '@nestjs/testing' +import { getRepositoryToken } from '@nestjs/typeorm' +import { Repository, EntityManager } from 'typeorm' + +import { CredentialStatus } from '../types' + +import { CredentialEntity } from './credential.entity' +import { CredentialService } from './credential.service' +import { RevocationRegistryEntity } from './revocation-registry.entity' + +// Mock for external API client +const mockSend = jest.fn().mockResolvedValue({ id: 'mocked-id' }) +const mockCreate = jest.fn().mockResolvedValue({ + id: 'test-id', + name: 'TestCred', + version: '1.0', + revocationSupported: true, +}) +const mockGetAll = jest.fn().mockResolvedValue([ + { + id: 'test-def-id', + name: 'TestCred', + version: '1.0', + revocationSupported: true, + }, +]) +jest.mock('@2060.io/service-agent-client', () => ({ + ApiClient: jest.fn().mockImplementation(() => ({ + credentialTypes: { + getAll: mockGetAll, + create: mockCreate, + }, + messages: { + send: mockSend, + }, + revocationRegistries: { + create: jest.fn().mockResolvedValue('rev-registry-id'), + }, + })), + ApiVersion: { + V1: 'v1', + }, +})) + +describe('CredentialService', () => { + let service: CredentialService + let credentialRepository: Repository + let revocationRepository: Repository + let entityManager: EntityManager + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CredentialService, + { + provide: getRepositoryToken(CredentialEntity), + useClass: Repository, + }, + { + provide: getRepositoryToken(RevocationRegistryEntity), + useClass: Repository, + }, + { + provide: EntityManager, + useValue: { + transaction: jest.fn(), + }, + }, + { + provide: 'GLOBAL_MODULE_OPTIONS', + useValue: { url: 'http://example.com' }, + }, + ], + }).compile() + + service = module.get(CredentialService) + credentialRepository = module.get(getRepositoryToken(CredentialEntity)) + revocationRepository = module.get(getRepositoryToken(RevocationRegistryEntity)) + entityManager = module.get(EntityManager) + }) + + describe('createType', () => { + it('should create a credential type when getAll returns empty array', async () => { + const getAllMock = jest.fn().mockResolvedValue([]) + service['apiClient'].credentialTypes.getAll = getAllMock + const createRevocationRegistryMock = jest + .spyOn(service as any, 'createRevocationRegistry') + .mockResolvedValue({}) + + jest.spyOn(service as any, 'createRevocationRegistry').mockResolvedValue({}) + + await service.createType('TestCred', '1.0', ['email'], { supportRevocation: true }) + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'TestCred', + version: '1.0', + attributes: ['email'], + supportRevocation: true, + }), + ) + expect(createRevocationRegistryMock).toHaveBeenCalledTimes(2) + expect(createRevocationRegistryMock).toHaveBeenCalledWith('test-id', undefined) + }) + + it('should not create a credential type when it already exists', async () => { + const getAllMock = jest.fn().mockResolvedValue([ + { + name: 'TestCred', + version: '1.0', + attributes: ['email'], + revocationSupported: true, + }, + ]) + const createMock = jest.fn() + const createRevocationRegistryMock = jest + .spyOn(service as any, 'createRevocationRegistry') + .mockResolvedValue({}) + + service['apiClient'].credentialTypes.getAll = getAllMock + service['apiClient'].credentialTypes.create = createMock + + await service.createType('TestCred', '1.0', ['email']) + + expect(createMock).not.toHaveBeenCalled() + expect(createRevocationRegistryMock).not.toHaveBeenCalled() + }) + }) + + describe('issue', () => { + const mockClaims = [{ name: 'name', value: 'John Doe' } as Claim] + const mockRevocationRegistry = { + revocationDefinitionId: 'rev-def-id', + currentIndex: 0, + maximumCredentialNumber: 5, + } as RevocationRegistryEntity + const mockFindCredential = { + id: 'cred-123', + connectionId: 'conn-123', + revocationRegistry: { + revocationDefinitionId: 'rev-def-id', + currentIndex: 0, + maximumCredentialNumber: 5, + }, + } as CredentialEntity + const mockCredential = { + id: 'cred-123', + connectionId: 'conn-123', + status: CredentialStatus.OFFERED, + revocationRegistry: { + revocationDefinitionId: 'rev-def-id', + currentIndex: 0, + maximumCredentialNumber: 5, + }, + } as CredentialEntity + it('should issue a credential successfully', async () => { + const getAllMock = jest.fn().mockResolvedValue([ + { + id: 'def-id', + name: 'TestCred', + version: '1.0', + attributes: ['email'], + revocationSupported: true, + }, + ]) + + service['apiClient'].credentialTypes.getAll = getAllMock + + jest.spyOn(credentialRepository, 'find').mockResolvedValue([]) + jest.spyOn(entityManager, 'transaction').mockResolvedValue(mockFindCredential) + + jest.spyOn(credentialRepository, 'save').mockResolvedValue(mockCredential) + jest.spyOn(revocationRepository, 'save').mockResolvedValue(mockRevocationRegistry) + jest.spyOn(service, 'revoke').mockResolvedValue(undefined) + + await service.issue('conn-123', mockClaims, { credentialDefinitionId: 'def-id' }) + + expect(credentialRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ threadId: 'mocked-id', status: CredentialStatus.OFFERED }), + ) + expect(revocationRepository.save).toHaveBeenCalledWith(expect.objectContaining({ currentIndex: 1 })) + expect(service.revoke).not.toHaveBeenCalled() + }) + + it('should throw an error if no credential definitions are found', async () => { + service['apiClient'].credentialTypes.getAll = jest.fn().mockResolvedValue([]) + + await expect(service.issue('conn-123', [], { credentialDefinitionId: 'def-id' })).rejects.toThrow( + 'No credential definitions found. Please configure a credential using the create method before proceeding.', + ) + }) + + it('should revoke existing credentials if revokeIfAlreadyIssued is true', async () => { + const existingCreds = [{ threadId: 'thread-123' } as CredentialEntity] + const getAllMock = jest.fn().mockResolvedValue([ + { + id: 'def-id', + name: 'TestCred', + version: '1.0', + attributes: ['email'], + revocationSupported: true, + }, + ]) + + service['apiClient'].credentialTypes.getAll = getAllMock + jest.spyOn(credentialRepository, 'find').mockResolvedValue(existingCreds) + jest.spyOn(entityManager, 'transaction').mockResolvedValue(mockFindCredential) + + jest.spyOn(credentialRepository, 'save').mockResolvedValue(mockCredential) + jest.spyOn(revocationRepository, 'save').mockResolvedValue(mockRevocationRegistry) + jest.spyOn(service, 'revoke').mockResolvedValue(undefined) + + await service.issue('conn-123', mockClaims, { + credentialDefinitionId: 'def-id', + revokeIfAlreadyIssued: true, + }) + + expect(service.revoke).toHaveBeenCalledWith('conn-123', existingCreds[0].threadId) + }) + }) + + describe('handleAcceptance', () => { + it('should update credential status to ACCEPTED', async () => { + const mockCredential = { + threadId: 'thread-123', + status: CredentialStatus.OFFERED, + } as CredentialEntity + + jest.spyOn(credentialRepository, 'findOne').mockResolvedValue(mockCredential) + jest.spyOn(credentialRepository, 'save').mockResolvedValue(mockCredential) + + await service.handleAcceptance('thread-123') + + expect(credentialRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ status: CredentialStatus.ACCEPTED }), + ) + }) + }) + + describe('handleRejection', () => { + it('should update credential status to ACCEPTED', async () => { + const mockCredential = { + threadId: 'thread-123', + status: CredentialStatus.OFFERED, + } as CredentialEntity + + jest.spyOn(credentialRepository, 'findOne').mockResolvedValue(mockCredential) + jest.spyOn(credentialRepository, 'save').mockResolvedValue(mockCredential) + + await service.handleRejection('thread-123') + + expect(credentialRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ status: CredentialStatus.REJECTED }), + ) + }) + }) + + describe('revoke', () => { + it('should revoke a credential successfully with threadId', async () => { + const mockCredential = { + id: 'cred-123', + connectionId: 'conn-123', + threadId: 'thread-123', + status: CredentialStatus.ACCEPTED, + revocationRegistry: { + credentialDefinitionId: 'def-id', + }, + } as CredentialEntity + + jest.spyOn(credentialRepository, 'findOne').mockResolvedValue(mockCredential) + jest.spyOn(credentialRepository, 'save').mockResolvedValue(mockCredential) + + await service.revoke('conn-123', 'thread-123') + + expect(credentialRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ status: CredentialStatus.REVOKED }), + ) + expect(mockSend).toHaveBeenCalledWith(expect.objectContaining({ connectionId: 'conn-123' })) + }) + + it('should throw an error if credential is not found', async () => { + jest.spyOn(credentialRepository, 'findOne').mockResolvedValue(null) + + await expect(service.revoke('conn-123', 'thread-123')).rejects.toThrow( + 'Credential not found with threadId "thread-123" or connectionId "conn-123".', + ) + }) + }) +}) diff --git a/packages/nestjs-client/src/messages/message.controller.spec.ts b/packages/nestjs-client/src/messages/message.controller.spec.ts new file mode 100644 index 0000000..62097cf --- /dev/null +++ b/packages/nestjs-client/src/messages/message.controller.spec.ts @@ -0,0 +1,100 @@ +import { HttpUtils } from '@2060.io/service-agent-client' +import { MessageReceived, MessageStateUpdated, TextMessage } from '@2060.io/service-agent-model' +import { Logger } from '@nestjs/common' +import { Test, TestingModule } from '@nestjs/testing' +import { MessageState } from 'credo-ts-receipts' + +import { MessageEventController } from './message.controller' +import { MessageEventService } from './message.service' + +jest.mock('@2060.io/service-agent-client', () => ({ + HttpUtils: { + handleException: jest.fn(), + }, +})) + +describe('MessageEventController', () => { + let controller: MessageEventController + let messageService: MessageEventService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MessageEventController], + providers: [ + { + provide: MessageEventService, + useValue: { + received: jest.fn(), + updated: jest.fn(), + }, + }, + ], + }).compile() + + controller = module.get(MessageEventController) + messageService = module.get(MessageEventService) + }) + + describe('received', () => { + const mockBody = new MessageReceived({ + message: { + type: 'text', + connectionId: 'conn1', + threadId: 'thread1', + content: 'Hello', + } as TextMessage, + }) + it('should successfully process received message', async () => { + const result = await controller.received(mockBody) + + expect(messageService.received).toHaveBeenCalledWith(mockBody) + expect(result).toEqual({ message: 'Message received updated successfully' }) + }) + + it('should handle error in received message processing', async () => { + const error = new Error('Test error') + jest.spyOn(messageService, 'received').mockRejectedValue(error) + await controller.received(mockBody) + + expect(HttpUtils.handleException).toHaveBeenCalledWith( + expect.any(Logger), + error, + 'Failed to received message state', + ) + }) + }) + + describe('updated', () => { + it('should successfully process updated message state', async () => { + const mockBody = new MessageStateUpdated({ + connectionId: 'conn-1', + messageId: 'msg-1', + state: MessageState.Submitted, + }) + + const result = await controller.updated(mockBody) + + expect(messageService.updated).toHaveBeenCalled() + expect(result).toEqual({ message: 'Message state updated successfully' }) + }) + + it('should handle error in message state update', async () => { + const error = new Error('Test error') + jest.spyOn(messageService, 'updated').mockRejectedValue(error) + jest.spyOn(HttpUtils, 'handleException') + + const mockBody = new MessageStateUpdated({ + connectionId: 'conn-1', + messageId: 'msg-1', + state: MessageState.Submitted, + }) + await controller.updated(mockBody) + + expect(HttpUtils.handleException).toHaveBeenCalledWith( + expect.any(Logger), + error, + 'Failed to update message state', + ) + }) + }) +}) diff --git a/packages/nestjs-client/src/messages/message.service.spec.ts b/packages/nestjs-client/src/messages/message.service.spec.ts new file mode 100644 index 0000000..60503bf --- /dev/null +++ b/packages/nestjs-client/src/messages/message.service.spec.ts @@ -0,0 +1,170 @@ +import { ApiVersion } from '@2060.io/service-agent-client' +import { + CredentialReceptionMessage, + MessageReceived, + ProfileMessage, + TextMessage, +} from '@2060.io/service-agent-model' +import { CredentialState } from '@credo-ts/core' +import { Test, TestingModule } from '@nestjs/testing' +import { MessageState } from 'credo-ts-receipts' + +import { + ConnectionsEventService, + ConnectionsRepository, + CredentialService, + EventHandler, + MessageEventService, +} from '../../src' + +const mockSend = jest.fn().mockResolvedValue({ id: 'mocked-id' }) +jest.mock('@2060.io/service-agent-client', () => ({ + ApiClient: jest.fn().mockImplementation(() => ({ + messages: { + send: mockSend, + }, + })), + ApiVersion: { + V1: 'v1', + }, +})) + +describe('MessageEventService', () => { + let service: MessageEventService + let mockCredentialService: Partial + let mockEventHandler: Partial + let mockConnectionsRepository: Partial + let mockConnectionsEventService: Partial + + beforeEach(async () => { + jest.clearAllMocks() + mockCredentialService = { + handleAcceptance: jest.fn(), + handleRejection: jest.fn(), + } + + mockEventHandler = { + inputMessage: jest.fn(), + } + + mockConnectionsRepository = { + updateUserProfile: jest.fn(), + } + + mockConnectionsEventService = { + handleNewConnection: jest.fn(), + } + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MessageEventService, + { + provide: 'GLOBAL_MODULE_OPTIONS', + useValue: { url: 'http://example.com', version: ApiVersion.V1 }, + }, + { + provide: 'MESSAGE_EVENT', + useValue: mockEventHandler, + }, + { + provide: CredentialService, + useValue: mockCredentialService, + }, + { + provide: ConnectionsRepository, + useValue: mockConnectionsRepository, + }, + { + provide: ConnectionsEventService, + useValue: mockConnectionsEventService, + }, + ], + }).compile() + + service = module.get(MessageEventService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) + + it('should throw an error if url is not provided', () => { + expect(() => new MessageEventService({})).toThrow( + new Error('For this module to be used the value url must be added'), + ) + }) + + it('should select version by default', () => { + service = new MessageEventService({ url: 'http://example.com' }) + expect(Reflect.get(service, 'version')).toBe(ApiVersion.V1) + }) + + describe('received', () => { + it('should send a receipt message and call eventHandler, but not call credential handlers', async () => { + const messageReceived = new MessageReceived({ + message: { + type: 'text', + connectionId: 'conn1', + threadId: 'thread1', + content: 'Hello', + } as TextMessage, + }) + + await service.received(messageReceived) + + expect(mockSend).toHaveBeenCalledWith( + expect.objectContaining({ + connectionId: 'conn1', + receipts: expect.arrayContaining([ + expect.objectContaining({ + state: MessageState.Viewed, + }), + ]), + }), + ) + + expect(mockEventHandler.inputMessage).toHaveBeenCalledWith(messageReceived.message) + expect(mockCredentialService.handleAcceptance).not.toHaveBeenCalled() + expect(mockCredentialService.handleRejection).not.toHaveBeenCalled() + }) + + it('should handle CredentialReceptionMessage with Done state', async () => { + const messageReceived = new MessageReceived({ + message: { + type: 'credential-reception', + connectionId: 'conn1', + threadId: 'thread1', + state: CredentialState.Done, + } as CredentialReceptionMessage, + }) + + await service.received(messageReceived) + + expect(mockCredentialService.handleAcceptance).toHaveBeenCalledWith('thread1') + expect(mockCredentialService.handleRejection).not.toHaveBeenCalled() + expect(mockEventHandler.inputMessage).toHaveBeenCalledWith(messageReceived.message) + expect(mockSend).toHaveBeenCalled() + }) + + it('should handle ProfileMessage and update user profile', async () => { + const messageReceived = new MessageReceived({ + message: { + type: 'profile', + connectionId: 'conn1', + displayName: 'Test', + preferredLanguage: 'en', + } as ProfileMessage, + }) + + await service.received(messageReceived) + + expect(mockConnectionsRepository.updateUserProfile).toHaveBeenCalledWith( + 'conn1', + expect.objectContaining(messageReceived.message), + ) + expect(mockConnectionsEventService.handleNewConnection).toHaveBeenCalledWith('conn1') + expect(mockEventHandler.inputMessage).toHaveBeenCalledWith(messageReceived.message) + expect(mockSend).toHaveBeenCalled() + }) + }) +}) diff --git a/tsconfig.build.json b/tsconfig.build.json index e1f1428..2dc1840 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -20,6 +20,7 @@ "exclude": [ "node_modules", "build", + "**/*.spec.ts", "**/*.test.ts", "**/__tests__/*.ts", "**/__mocks__/*.ts",