diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index be1d8892c9..cfb0b75917 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -277,7 +277,7 @@ export class Agent { } private async getMediationConnection(mediatorInvitationUrl: string) { - const outOfBandInvitation = await this.oob.parseInvitation(mediatorInvitationUrl) + const outOfBandInvitation = this.oob.parseInvitation(mediatorInvitationUrl) const outOfBandRecord = await this.oob.findByInvitationId(outOfBandInvitation.id) const [connection] = outOfBandRecord ? await this.connections.findAllByOutOfBandId(outOfBandRecord.id) : [] diff --git a/packages/core/src/agent/MessageReceiver.ts b/packages/core/src/agent/MessageReceiver.ts index acb651fc0c..8f2120b72e 100644 --- a/packages/core/src/agent/MessageReceiver.ts +++ b/packages/core/src/agent/MessageReceiver.ts @@ -13,7 +13,6 @@ import { ConnectionsModule } from '../modules/connections' import { ProblemReportError, ProblemReportMessage, ProblemReportReason } from '../modules/problem-reports' import { isValidJweStructure } from '../utils/JWE' import { JsonTransformer } from '../utils/JsonTransformer' -import { MessageValidator } from '../utils/MessageValidator' import { canHandleMessageType, parseMessageType, replaceLegacyDidSovPrefixOnMessage } from '../utils/messageType' import { AgentConfig } from './AgentConfig' @@ -168,7 +167,6 @@ export class MessageReceiver { let message: AgentMessage try { message = await this.transformMessage(plaintextMessage) - await this.validateMessage(message) } catch (error) { if (connection) await this.sendProblemReportMessage(error.message, connection, plaintextMessage) throw error @@ -209,25 +207,19 @@ export class MessageReceiver { } // Cast the plain JSON object to specific instance of Message extended from AgentMessage - return JsonTransformer.fromJSON(message, MessageClass) - } - - /** - * Validate an AgentMessage instance. - * @param message agent message to validate - */ - private async validateMessage(message: AgentMessage) { + let messageTransformed: AgentMessage try { - await MessageValidator.validate(message) + messageTransformed = JsonTransformer.fromJSON(message, MessageClass) } catch (error) { this.logger.error(`Error validating message ${message.type}`, { errors: error, - message: message.toJSON(), + message: JSON.stringify(message), }) throw new ProblemReportError(`Error validating message ${message.type}`, { problemCode: ProblemReportReason.MessageParseFailure, }) } + return messageTransformed } /** diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index 0bafa1c9c5..8a4d795969 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -313,7 +313,7 @@ export class MessageSender { } try { - await MessageValidator.validate(message) + MessageValidator.validateSync(message) } catch (error) { this.logger.error( `Aborting sending outbound message ${message.type} to ${service.serviceEndpoint}. Message validation failed`, diff --git a/packages/core/src/agent/__tests__/AgentMessage.test.ts b/packages/core/src/agent/__tests__/AgentMessage.test.ts index 66f4a1a8b9..ec9f8e3d7a 100644 --- a/packages/core/src/agent/__tests__/AgentMessage.test.ts +++ b/packages/core/src/agent/__tests__/AgentMessage.test.ts @@ -1,6 +1,6 @@ import { TestMessage } from '../../../tests/TestMessage' +import { ClassValidationError } from '../../error/ClassValidationError' import { JsonTransformer } from '../../utils' -import { MessageValidator } from '../../utils/MessageValidator' import { IsValidMessageType, parseMessageType } from '../../utils/messageType' import { AgentMessage } from '../AgentMessage' @@ -32,7 +32,7 @@ describe('AgentMessage', () => { const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) - await expect(MessageValidator.validate(message)).resolves.toBeUndefined() + expect(message).toBeInstanceOf(CustomProtocolMessage) }) it('successfully validates if the message type minor version is lower than the supported message type', async () => { @@ -43,10 +43,10 @@ describe('AgentMessage', () => { const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) - await expect(MessageValidator.validate(message)).resolves.toBeUndefined() + expect(message).toBeInstanceOf(CustomProtocolMessage) }) - it('successfully validates if the message type minor version is higher than the supported message type', async () => { + it('successfully validates if the message type minor version is higher than the supported message type', () => { const json = { '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', '@type': 'https://didcomm.org/fake-protocol/1.8/message', @@ -54,26 +54,45 @@ describe('AgentMessage', () => { const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) - await expect(MessageValidator.validate(message)).resolves.toBeUndefined() + expect(message).toBeInstanceOf(CustomProtocolMessage) }) - it('throws a validation error if the message type major version differs from the supported message type', async () => { - expect.assertions(1) - + it('throws a validation error if the message type major version differs from the supported message type', () => { const json = { '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', '@type': 'https://didcomm.org/fake-protocol/2.0/message', } - const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) - - await expect(MessageValidator.validate(message)).rejects.toMatchObject([ - { - constraints: { - isValidMessageType: 'type does not match the expected message type (only minor version may be lower)', + expect(() => JsonTransformer.fromJSON(json, CustomProtocolMessage)).toThrowError(ClassValidationError) + try { + JsonTransformer.fromJSON(json, CustomProtocolMessage) + } catch (error) { + const thrownError = error as ClassValidationError + expect(thrownError.message).toEqual( + 'CustomProtocolMessage: Failed to validate class.\nAn instance of CustomProtocolMessage has failed the validation:\n - property type has failed the following constraints: isValidMessageType \n' + ) + expect(thrownError.validationErrors).toMatchObject([ + { + target: { + appendedAttachments: undefined, + id: 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + l10n: undefined, + pleaseAck: undefined, + service: undefined, + thread: undefined, + timing: undefined, + transport: undefined, + type: 'https://didcomm.org/fake-protocol/2.0/message', + }, + value: 'https://didcomm.org/fake-protocol/2.0/message', + property: 'type', + children: [], + constraints: { + isValidMessageType: 'type does not match the expected message type (only minor version may be lower)', + }, }, - }, - ]) + ]) + } }) }) }) diff --git a/packages/core/src/decorators/ack/AckDecorator.test.ts b/packages/core/src/decorators/ack/AckDecorator.test.ts index a49bdcd03f..324d997d99 100644 --- a/packages/core/src/decorators/ack/AckDecorator.test.ts +++ b/packages/core/src/decorators/ack/AckDecorator.test.ts @@ -1,4 +1,5 @@ import { BaseMessage } from '../../agent/BaseMessage' +import { ClassValidationError } from '../../error/ClassValidationError' import { JsonTransformer } from '../../utils/JsonTransformer' import { MessageValidator } from '../../utils/MessageValidator' import { Compose } from '../../utils/mixins' @@ -25,22 +26,9 @@ describe('Decorators | AckDecoratorExtension', () => { }) test('transforms Json to AckDecorator class', () => { - const transformed = JsonTransformer.fromJSON({ '~please_ack': {} }, TestMessage) - - expect(transformed).toEqual({ - pleaseAck: { - on: ['RECEIPT'], - }, - }) - expect(transformed).toBeInstanceOf(TestMessage) - }) - - test('successfully transforms ack decorator with on field present', () => { const transformed = JsonTransformer.fromJSON( { - '~please_ack': { - on: ['RECEIPT'], - }, + '~please_ack': {}, '@id': '7517433f-1150-46f2-8495-723da61b872a', '@type': 'https://didcomm.org/test-protocol/1.0/test-message', }, @@ -88,45 +76,56 @@ describe('Decorators | AckDecoratorExtension', () => { TestMessage ) - await expect(MessageValidator.validate(transformedWithDefault)).resolves.toBeUndefined() + expect(MessageValidator.validateSync(transformedWithDefault)).toBeUndefined() + }) - const transformedWithoutDefault = JsonTransformer.fromJSON( - { - '~please_ack': { - on: ['OUTCOME'], + test('transforms Json to AckDecorator class', () => { + const transformed = () => + JsonTransformer.fromJSON( + { + '~please_ack': {}, + '@id': undefined, + '@type': undefined, }, - '@id': '7517433f-1150-46f2-8495-723da61b872a', - '@type': 'https://didcomm.org/test-protocol/1.0/test-message', - }, - TestMessage - ) - - await expect(MessageValidator.validate(transformedWithoutDefault)).resolves.toBeUndefined() + TestMessage + ) - const transformedWithIncorrectValue = JsonTransformer.fromJSON( - { - '~please_ack': { - on: ['NOT_A_VALID_VALUE'], + expect(() => transformed()).toThrow(ClassValidationError) + try { + transformed() + } catch (e) { + const caughtError = e as ClassValidationError + expect(caughtError.message).toEqual( + 'TestMessage: Failed to validate class.\nAn instance of TestMessage has failed the validation:\n - property id has failed the following constraints: matches \n\nAn instance of TestMessage has failed the validation:\n - property type has failed the following constraints: matches \n' + ) + expect(caughtError.validationErrors).toMatchObject([ + { + children: [], + constraints: { + matches: 'id must match /[-_./a-zA-Z0-9]{8,64}/ regular expression', + }, + property: 'id', + target: { + pleaseAck: { + on: ['RECEIPT'], + }, + }, + value: undefined, }, - '@id': '7517433f-1150-46f2-8495-723da61b872a', - '@type': 'https://didcomm.org/test-protocol/1.0/test-message', - }, - TestMessage - ) - - await expect(MessageValidator.validate(transformedWithIncorrectValue)).rejects.toMatchObject([ - { - children: [ - { - children: [], - constraints: { isEnum: 'each value in on must be a valid enum value' }, - property: 'on', - target: { on: ['NOT_A_VALID_VALUE'] }, - value: ['NOT_A_VALID_VALUE'], + { + children: [], + constraints: { + matches: 'type must match /(.*?)([a-zA-Z0-9._-]+)\\/(\\d[^/]*)\\/([a-zA-Z0-9._-]+)$/ regular expression', }, - ], - property: 'pleaseAck', - }, - ]) + property: 'type', + target: { + pleaseAck: { + on: ['RECEIPT'], + }, + }, + value: undefined, + }, + ]) + } }) }) diff --git a/packages/core/src/decorators/service/ServiceDecorator.test.ts b/packages/core/src/decorators/service/ServiceDecorator.test.ts index 9ee654bab5..69936286a3 100644 --- a/packages/core/src/decorators/service/ServiceDecorator.test.ts +++ b/packages/core/src/decorators/service/ServiceDecorator.test.ts @@ -25,7 +25,10 @@ describe('Decorators | ServiceDecoratorExtension', () => { }) test('transforms Json to ServiceDecorator class', () => { - const transformed = JsonTransformer.fromJSON({ '~service': service }, TestMessage) + const transformed = JsonTransformer.fromJSON( + { '@id': 'randomID', '@type': 'https://didcomm.org/fake-protocol/1.5/message', '~service': service }, + TestMessage + ) expect(transformed.service).toEqual(service) expect(transformed).toBeInstanceOf(TestMessage) diff --git a/packages/core/src/decorators/transport/TransportDecorator.test.ts b/packages/core/src/decorators/transport/TransportDecorator.test.ts index aed4feb9c1..edea4acd28 100644 --- a/packages/core/src/decorators/transport/TransportDecorator.test.ts +++ b/packages/core/src/decorators/transport/TransportDecorator.test.ts @@ -1,14 +1,14 @@ +import { ClassValidationError } from '../../error/ClassValidationError' import { JsonTransformer } from '../../utils/JsonTransformer' import { MessageValidator } from '../../utils/MessageValidator' import { TransportDecorator, ReturnRouteTypes } from './TransportDecorator' const validTransport = (transportJson: Record) => - MessageValidator.validate(JsonTransformer.fromJSON(transportJson, TransportDecorator)) -const expectValid = (transportJson: Record) => - expect(validTransport(transportJson)).resolves.toBeUndefined() + MessageValidator.validateSync(JsonTransformer.fromJSON(transportJson, TransportDecorator)) +const expectValid = (transportJson: Record) => expect(validTransport(transportJson)).toBeUndefined() const expectInvalid = (transportJson: Record) => - expect(validTransport(transportJson)).rejects.not.toBeNull() + expect(() => validTransport(transportJson)).toThrowError(ClassValidationError) const valid = { all: { @@ -60,18 +60,18 @@ describe('Decorators | TransportDecorator', () => { expect(json).toEqual(transformed) }) - it('should only allow correct return_route values', async () => { + it('should only allow correct return_route values', () => { expect.assertions(4) - await expectValid(valid.all) - await expectValid(valid.none) - await expectValid(valid.thread) - await expectInvalid(invalid.random) + expectValid(valid.all) + expectValid(valid.none) + expectValid(valid.thread) + expectInvalid(invalid.random) }) it('should require return_route_thread when return_route is thread', async () => { expect.assertions(3) - await expectValid(valid.thread) - await expectInvalid(invalid.invalidThreadId) - await expectInvalid(invalid.missingThreadId) + expectValid(valid.thread) + expectInvalid(invalid.invalidThreadId) + expectInvalid(invalid.missingThreadId) }) }) diff --git a/packages/core/src/error/ClassValidationError.ts b/packages/core/src/error/ClassValidationError.ts new file mode 100644 index 0000000000..e9205cab47 --- /dev/null +++ b/packages/core/src/error/ClassValidationError.ts @@ -0,0 +1,24 @@ +import type { ValidationError } from 'class-validator' + +import { AriesFrameworkError } from './AriesFrameworkError' + +export class ClassValidationError extends AriesFrameworkError { + public validationErrors: ValidationError[] + + public validationErrorsToString() { + return this.validationErrors?.map((error) => error.toString(true)).join('\n') ?? '' + } + + public constructor( + message: string, + { classType, cause, validationErrors }: { classType: string; cause?: Error; validationErrors?: ValidationError[] } + ) { + const validationErrorsStringified = validationErrors?.map((error) => error.toString()).join('\n') + super( + `${classType}: ${message} +${validationErrorsStringified}`, + { cause } + ) + this.validationErrors = validationErrors ?? [] + } +} diff --git a/packages/core/src/error/ValidationErrorUtils.ts b/packages/core/src/error/ValidationErrorUtils.ts new file mode 100644 index 0000000000..de16ea1330 --- /dev/null +++ b/packages/core/src/error/ValidationErrorUtils.ts @@ -0,0 +1,9 @@ +import { ValidationError } from 'class-validator' + +export function isValidationErrorArray(e: ValidationError[] | unknown): boolean { + if (Array.isArray(e)) { + const isErrorArray = e.length > 0 && e.every((err) => err instanceof ValidationError) + return isErrorArray + } + return false +} diff --git a/packages/core/src/error/__tests__/ValidationErrorUtils.test.ts b/packages/core/src/error/__tests__/ValidationErrorUtils.test.ts new file mode 100644 index 0000000000..fada3e6d5b --- /dev/null +++ b/packages/core/src/error/__tests__/ValidationErrorUtils.test.ts @@ -0,0 +1,24 @@ +import { ValidationError } from 'class-validator' + +import { isValidationErrorArray } from '../ValidationErrorUtils' + +describe('ValidationErrorUtils', () => { + test('returns true for an array of ValidationErrors', () => { + const error = new ValidationError() + const errorArray = [error, error] + const isErrorArray = isValidationErrorArray(errorArray) + expect(isErrorArray).toBeTruthy + }) + + test('returns false for an array of strings', () => { + const errorArray = ['hello', 'world'] + const isErrorArray = isValidationErrorArray(errorArray) + expect(isErrorArray).toBeFalsy + }) + + test('returns false for a non array', () => { + const error = new ValidationError() + const isErrorArray = isValidationErrorArray(error) + expect(isErrorArray).toBeFalsy + }) +}) diff --git a/packages/core/src/error/index.ts b/packages/core/src/error/index.ts index c0bea689fc..5098161d50 100644 --- a/packages/core/src/error/index.ts +++ b/packages/core/src/error/index.ts @@ -2,3 +2,4 @@ export * from './AriesFrameworkError' export * from './RecordNotFoundError' export * from './RecordDuplicateError' export * from './IndySdkError' +export * from './ClassValidationError' diff --git a/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts index d386a967c8..256bed4371 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts @@ -1,6 +1,7 @@ import { validateOrReject } from 'class-validator' import { parseUrl } from 'query-string' +import { ClassValidationError } from '../../../error/ClassValidationError' import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' import { ConnectionInvitationMessage } from '../messages/ConnectionInvitationMessage' @@ -15,7 +16,7 @@ describe('ConnectionInvitationMessage', () => { label: 'test', } const invitation = JsonTransformer.fromJSON(json, ConnectionInvitationMessage) - await expect(validateOrReject(invitation)).resolves.toBeUndefined() + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) }) it('should throw error if both did and inline keys / endpoint are missing', async () => { @@ -24,8 +25,8 @@ describe('ConnectionInvitationMessage', () => { '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', label: 'test', } - const invitation = JsonTransformer.fromJSON(json, ConnectionInvitationMessage) - await expect(validateOrReject(invitation)).rejects.not.toBeNull() + + expect(() => JsonTransformer.fromJSON(json, ConnectionInvitationMessage)).toThrowError(ClassValidationError) }) it('should replace legacy did:sov:BzCbsNYhMrjHiqZDTUASHg;spec prefix with https://didcomm.org in message type', async () => { @@ -42,7 +43,7 @@ describe('ConnectionInvitationMessage', () => { expect(invitation.type).toBe('https://didcomm.org/connections/1.0/invitation') // Assert validation also works with the transformation - await expect(validateOrReject(invitation)).resolves.toBeUndefined() + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) }) describe('toUrl', () => { @@ -87,27 +88,27 @@ describe('ConnectionInvitationMessage', () => { }) describe('fromUrl', () => { - it('should correctly convert a valid invitation url to a `ConnectionInvitationMessage` with `d_m` as parameter', async () => { + it('should correctly convert a valid invitation url to a `ConnectionInvitationMessage` with `d_m` as parameter', () => { const invitationUrl = 'https://trinsic.studio/link/?d_m=eyJsYWJlbCI6InRlc3QiLCJpbWFnZVVybCI6Imh0dHBzOi8vdHJpbnNpY2FwaWFzc2V0cy5henVyZWVkZ2UubmV0L2ZpbGVzL2IyODhkMTE3LTNjMmMtNGFjNC05MzVhLWE1MDBkODQzYzFlOV9kMGYxN2I0OS0wNWQ5LTQ4ZDAtODJlMy1jNjg3MGI4MjNjMTUucG5nIiwic2VydmljZUVuZHBvaW50IjoiaHR0cHM6Ly9hcGkucG9ydGFsLnN0cmVldGNyZWQuaWQvYWdlbnQvTVZob1VaQjlHdUl6bVJzSTNIWUNuZHpBcXVKY1ZNdFUiLCJyb3V0aW5nS2V5cyI6WyJCaFZRdEZHdGJ4NzZhMm13Y3RQVkJuZWtLaG1iMTdtUHdFMktXWlVYTDFNaSJdLCJyZWNpcGllbnRLZXlzIjpbIkcyOVF6bXBlVXN0dUVHYzlXNzlYNnV2aUhTUTR6UlV2VWFFOHpXV2VZYjduIl0sIkBpZCI6IjgxYzZiNDUzLWNkMTUtNDQwMC04MWU5LTkwZTJjM2NhY2I1NCIsIkB0eXBlIjoiZGlkOnNvdjpCekNic05ZaE1yakhpcVpEVFVBU0hnO3NwZWMvY29ubmVjdGlvbnMvMS4wL2ludml0YXRpb24ifQ%3D%3D&orig=https://trinsic.studio/url/6dd56daf-e153-40dd-b849-2b345b6853f6' - const invitation = await ConnectionInvitationMessage.fromUrl(invitationUrl) + const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl) - await expect(validateOrReject(invitation)).resolves.toBeUndefined() + expect(validateOrReject(invitation)).resolves.toBeUndefined() }) - it('should correctly convert a valid invitation url to a `ConnectionInvitationMessage` with `c_i` as parameter', async () => { + it('should correctly convert a valid invitation url to a `ConnectionInvitationMessage` with `c_i` as parameter', () => { const invitationUrl = 'https://example.com?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZmM3ODFlMDItMjA1YS00NGUzLWE5ZTQtYjU1Y2U0OTE5YmVmIiwgInNlcnZpY2VFbmRwb2ludCI6ICJodHRwczovL2RpZGNvbW0uZmFiZXIuYWdlbnQuYW5pbW8uaWQiLCAibGFiZWwiOiAiQW5pbW8gRmFiZXIgQWdlbnQiLCAicmVjaXBpZW50S2V5cyI6IFsiR0hGczFQdFRabjdmYU5LRGVnMUFzU3B6QVAyQmpVckVjZlR2bjc3SnBRTUQiXX0=' - const invitation = await ConnectionInvitationMessage.fromUrl(invitationUrl) + const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl) - await expect(validateOrReject(invitation)).resolves.toBeUndefined() + expect(validateOrReject(invitation)).resolves.toBeUndefined() }) - it('should throw error if url does not contain `c_i` or `d_m`', async () => { + it('should throw error if url does not contain `c_i` or `d_m`', () => { const invitationUrl = 'https://example.com?param=123' - await expect(ConnectionInvitationMessage.fromUrl(invitationUrl)).rejects.toThrowError() + expect(() => ConnectionInvitationMessage.fromUrl(invitationUrl)).toThrowError() }) }) }) diff --git a/packages/core/src/modules/connections/__tests__/ConnectionRequestMessage.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionRequestMessage.test.ts index 078d074f43..91d9e11955 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionRequestMessage.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionRequestMessage.test.ts @@ -1,10 +1,9 @@ +import { ClassValidationError } from '../../../error/ClassValidationError' import { MessageValidator } from '../../../utils/MessageValidator' import { ConnectionRequestMessage } from '../messages/ConnectionRequestMessage' describe('ConnectionRequestMessage', () => { - it('throws an error when the message does not contain a connection parameter', async () => { - expect.assertions(1) - + it('throws an error when the message does not contain a connection parameter', () => { const connectionRequest = new ConnectionRequestMessage({ did: 'did', label: 'test-label', @@ -14,6 +13,6 @@ describe('ConnectionRequestMessage', () => { // @ts-ignore delete connectionRequest.connection - return expect(MessageValidator.validate(connectionRequest)).rejects.not.toBeNull() + expect(() => MessageValidator.validateSync(connectionRequest)).toThrowError(ClassValidationError) }) }) diff --git a/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts b/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts index 004429f115..66067a15ce 100644 --- a/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts +++ b/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts @@ -6,7 +6,6 @@ import { AgentMessage } from '../../../agent/AgentMessage' import { AriesFrameworkError } from '../../../error' import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' -import { MessageValidator } from '../../../utils/MessageValidator' import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../utils/messageType' export interface BaseInvitationOptions { @@ -121,7 +120,7 @@ export class ConnectionInvitationMessage extends AgentMessage { * @throws Error when url can not be decoded to JSON, or decoded message is not a valid `ConnectionInvitationMessage` * @throws Error when the url does not contain c_i or d_m as parameter */ - public static async fromUrl(invitationUrl: string) { + public static fromUrl(invitationUrl: string) { const parsedUrl = parseUrl(invitationUrl).query const encodedInvitation = parsedUrl['c_i'] ?? parsedUrl['d_m'] @@ -129,8 +128,6 @@ export class ConnectionInvitationMessage extends AgentMessage { const invitationJson = JsonEncoder.fromBase64(encodedInvitation) const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage) - await MessageValidator.validate(invitation) - return invitation } else { throw new AriesFrameworkError( diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 209ccba0ce..9f160f3e18 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -18,7 +18,6 @@ import { InjectionSymbols } from '../../../constants' import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils' import { AriesFrameworkError } from '../../../error' import { JsonTransformer } from '../../../utils/JsonTransformer' -import { MessageValidator } from '../../../utils/MessageValidator' import { indyDidFromPublicKeyBase58 } from '../../../utils/did' import { Wallet } from '../../../wallet/Wallet' import { DidKey, Key, IndyAgentService } from '../../dids' @@ -286,11 +285,6 @@ export class ConnectionService { } const connection = JsonTransformer.fromJSON(connectionJson, Connection) - try { - await MessageValidator.validate(connection) - } catch (error) { - throw new Error(error) - } // Per the Connection RFC we must check if the key used to sign the connection~sig is the same key // as the recipient key(s) in the connection invitation message diff --git a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts index f0bb0cb44f..f34714fae6 100644 --- a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts +++ b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts @@ -112,7 +112,7 @@ export class IndyCredentialFormatService extends CredentialFormatService { expect(keyAgreement[1]).toBeInstanceOf(VerificationMethod) }) - it('validation should throw an error if the did document is invalid', async () => { - const didDocument = JsonTransformer.fromJSON(didExample456Invalid, DidDocument) - + it('validation should throw an error if the did document is invalid', () => { try { - await MessageValidator.validate(didDocument) + JsonTransformer.fromJSON(didExample456Invalid, DidDocument) } catch (error) { - expect(error).toMatchObject([ + expect(error).toBeInstanceOf(ClassValidationError) + expect(error.message).toContain('property type has failed the following constraints: isString') + expect(error.validationErrors).toMatchObject([ { - value: 'did:example:123', - property: 'alsoKnownAs', children: [], - constraints: { isArray: 'alsoKnownAs must be an array' }, - }, - { - value: [ - 'did:example:456#key-1', - { - id: 'did:example:456#key-2', - type: 'Ed25519VerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyBase58: '-----BEGIN PUBLIC 9...', - }, - { - id: 'did:example:456#key-3', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC A...', - }, - ], - property: 'verificationMethod', - children: [ - { - target: [ - 'did:example:456#key-1', - { - id: 'did:example:456#key-2', - type: 'Ed25519VerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyBase58: '-----BEGIN PUBLIC 9...', - }, - { - id: 'did:example:456#key-3', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC A...', - }, - ], - value: 'did:example:456#key-1', - property: '0', - children: [ - { - value: 'did:example:456#key-1', - property: 'verificationMethod', - constraints: { - nestedValidation: 'each value in nested property verificationMethod must be either object or array', - }, - }, - ], - }, - { - target: [ - 'did:example:456#key-1', - { - id: 'did:example:456#key-2', - type: 'Ed25519VerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyBase58: '-----BEGIN PUBLIC 9...', - }, - { - id: 'did:example:456#key-3', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC A...', - }, - ], - value: { - id: 'did:example:456#key-3', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC A...', - }, - property: '2', - children: [ - { - target: { - id: 'did:example:456#key-3', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC A...', - }, - property: 'type', - children: [], - constraints: { isString: 'type must be a string' }, - }, - ], - }, - ], + constraints: { + isString: 'type must be a string', + }, + property: 'type', + target: { + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + id: 'did:example:123#assertionMethod-1', + publicKeyPem: '-----BEGIN PUBLIC A...', + }, + value: undefined, }, ]) } diff --git a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts index ee8642326e..628b2eb177 100644 --- a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts +++ b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts @@ -5,7 +5,6 @@ import { Resolver } from 'did-resolver' import * as didWeb from 'web-did-resolver' import { JsonTransformer } from '../../../../utils/JsonTransformer' -import { MessageValidator } from '../../../../utils/MessageValidator' import { DidDocument } from '../../domain' export class WebDidResolver implements DidResolver { @@ -29,7 +28,6 @@ export class WebDidResolver implements DidResolver { let didDocument = null if (result.didDocument) { didDocument = JsonTransformer.fromJSON(result.didDocument, DidDocument) - await MessageValidator.validate(didDocument) } return { diff --git a/packages/core/src/modules/oob/OutOfBandModule.ts b/packages/core/src/modules/oob/OutOfBandModule.ts index 5245fc93ce..5bfa1236b8 100644 --- a/packages/core/src/modules/oob/OutOfBandModule.ts +++ b/packages/core/src/modules/oob/OutOfBandModule.ts @@ -263,8 +263,8 @@ export class OutOfBandModule { * @param config configuration of how out-of-band invitation should be processed * @returns out-of-band record and connection record if one has been created */ - public async receiveInvitationFromUrl(invitationUrl: string, config: ReceiveOutOfBandInvitationConfig = {}) { - const message = await this.parseInvitation(invitationUrl) + public receiveInvitationFromUrl(invitationUrl: string, config: ReceiveOutOfBandInvitationConfig = {}) { + const message = this.parseInvitation(invitationUrl) return this.receiveInvitation(message, config) } @@ -275,8 +275,8 @@ export class OutOfBandModule { * * @returns OutOfBandInvitation */ - public async parseInvitation(invitationUrl: string): Promise { - return await parseInvitationUrl(invitationUrl) + public parseInvitation(invitationUrl: string): OutOfBandInvitation { + return parseInvitationUrl(invitationUrl) } /** diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts index e91410aa6c..29de6b5af6 100644 --- a/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts +++ b/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts @@ -1,4 +1,4 @@ -import type { ValidationError } from 'class-validator' +import type { ClassValidationError } from '../../../error/ClassValidationError' import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' @@ -27,11 +27,11 @@ describe('OutOfBandInvitation', () => { }) describe('fromUrl', () => { - test('decode the URL containing the base64 encoded invitation as the oob parameter into an `OutOfBandInvitation`', async () => { + test('decode the URL containing the base64 encoded invitation as the oob parameter into an `OutOfBandInvitation`', () => { const invitationUrl = 'http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0K' - const invitation = await OutOfBandInvitation.fromUrl(invitationUrl) + const invitation = OutOfBandInvitation.fromUrl(invitationUrl) const json = JsonTransformer.toJSON(invitation) expect(json).toEqual({ '@type': 'https://didcomm.org/out-of-band/1.1/invitation', @@ -46,7 +46,7 @@ describe('OutOfBandInvitation', () => { }) describe('fromJson', () => { - test('create an instance of `OutOfBandInvitation` from JSON object', async () => { + test('create an instance of `OutOfBandInvitation` from JSON object', () => { const json = { '@type': 'https://didcomm.org/out-of-band/1.1/invitation', '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', @@ -57,13 +57,13 @@ describe('OutOfBandInvitation', () => { services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], } - const invitation = await OutOfBandInvitation.fromJson(json) + const invitation = OutOfBandInvitation.fromJson(json) expect(invitation).toBeDefined() expect(invitation).toBeInstanceOf(OutOfBandInvitation) }) - test('create an instance of `OutOfBandInvitation` from JSON object with inline service', async () => { + test('create an instance of `OutOfBandInvitation` from JSON object with inline service', () => { const json = { '@type': 'https://didcomm.org/out-of-band/1.1/invitation', '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', @@ -81,12 +81,12 @@ describe('OutOfBandInvitation', () => { ], } - const invitation = await OutOfBandInvitation.fromJson(json) + const invitation = OutOfBandInvitation.fromJson(json) expect(invitation).toBeDefined() expect(invitation).toBeInstanceOf(OutOfBandInvitation) }) - test('throw validation error when services attribute is empty', async () => { + test('throw validation error when services attribute is empty', () => { const json = { '@type': 'https://didcomm.org/out-of-band/1.1/invitation', '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', @@ -99,14 +99,24 @@ describe('OutOfBandInvitation', () => { expect.assertions(1) try { - await OutOfBandInvitation.fromJson(json) + OutOfBandInvitation.fromJson(json) } catch (error) { - const [firstError] = error as [ValidationError] - expect(firstError.constraints).toEqual({ arrayNotEmpty: 'services should not be empty' }) + const firstError = error as ClassValidationError + expect(firstError.validationErrors[0]).toMatchObject({ + children: [], + constraints: { arrayNotEmpty: 'services should not be empty' }, + property: 'services', + target: { + goal: 'To issue a Faber College Graduate credential', + label: 'Faber College', + services: [], + }, + value: [], + }) } }) - test('throw validation error when incorrect service object present in services attribute', async () => { + test('throw validation error when incorrect service object present in services attribute', () => { const json = { '@type': 'https://didcomm.org/out-of-band/1.1/invitation', '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', @@ -125,24 +135,23 @@ describe('OutOfBandInvitation', () => { expect.assertions(1) try { - await OutOfBandInvitation.fromJson(json) + OutOfBandInvitation.fromJson(json) } catch (error) { - const [firstError] = error as [ValidationError] - - expect(firstError).toMatchObject({ - children: [ - { - children: [ - { - constraints: { - arrayNotEmpty: 'recipientKeys should not be empty', - isDidKeyString: 'each value in recipientKeys must be a did:key string', - }, - }, - { constraints: { isDidKeyString: 'each value in routingKeys must be a did:key string' } }, - ], - }, - ], + const firstError = error as ClassValidationError + expect(firstError.validationErrors[0]).toMatchObject({ + children: [], + constraints: { + arrayNotEmpty: 'recipientKeys should not be empty', + isDidKeyString: 'each value in recipientKeys must be a did:key string', + }, + property: 'recipientKeys', + target: { + id: '#inline', + routingKeys: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + serviceEndpoint: 'https://example.com/ssi', + type: 'did-communication', + }, + value: undefined, }) } }) diff --git a/packages/core/src/modules/oob/__tests__/helpers.test.ts b/packages/core/src/modules/oob/__tests__/helpers.test.ts index debbc71821..e1920f4cae 100644 --- a/packages/core/src/modules/oob/__tests__/helpers.test.ts +++ b/packages/core/src/modules/oob/__tests__/helpers.test.ts @@ -58,12 +58,12 @@ describe('convertToNewInvitation', () => { label: 'a-label', imageUrl: 'https://my-image.com', }, - ConnectionInvitationMessage + ConnectionInvitationMessage, + // Don't validate because we want this to be mal-formatted + { validate: false } ) - expect(() => convertToNewInvitation(connectionInvitation)).toThrowError( - 'Missing required serviceEndpoint, routingKeys and/or did fields in connection invitation' - ) + expect(() => convertToNewInvitation(connectionInvitation)).toThrowError() }) }) diff --git a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts index bdb4312eef..8d631f4abe 100644 --- a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts +++ b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts @@ -11,7 +11,6 @@ import { Attachment, AttachmentData } from '../../../decorators/attachment/Attac import { AriesFrameworkError } from '../../../error' import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' -import { MessageValidator } from '../../../utils/MessageValidator' import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' import { IsStringOrInstance } from '../../../utils/validators' import { DidKey } from '../../dids' @@ -68,7 +67,7 @@ export class OutOfBandInvitation extends AgentMessage { return invitationUrl } - public static async fromUrl(invitationUrl: string) { + public static fromUrl(invitationUrl: string) { const parsedUrl = parseUrl(invitationUrl).query const encodedInvitation = parsedUrl['oob'] @@ -84,10 +83,8 @@ export class OutOfBandInvitation extends AgentMessage { } } - public static async fromJson(json: Record) { - const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation) - await MessageValidator.validate(invitation) - return invitation + public static fromJson(json: Record) { + return JsonTransformer.fromJSON(json, OutOfBandInvitation) } public get invitationDids() { diff --git a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts b/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts index 0d0b74cde8..4a94da5aa9 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts +++ b/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts @@ -1,3 +1,4 @@ +import { ClassValidationError } from '../../../error/ClassValidationError' import { JsonTransformer } from '../../../utils/JsonTransformer' import { MessageValidator } from '../../../utils/MessageValidator' import { ProofRequest } from '../models' @@ -35,42 +36,44 @@ describe('ProofRequest', () => { ProofRequest ) - expect(MessageValidator.validate(proofRequest)).resolves.not.toThrow() + expect(() => MessageValidator.validateSync(proofRequest)).not.toThrow() }) it('should throw an error if the proof request json contains an invalid structure', async () => { - const proofRequest = JsonTransformer.fromJSON( - { - name: 'ProofRequest', - version: '1.0', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', - requested_attributes: { - First: { - names: [], - restrictions: [ - { - schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', - }, - ], - }, + const proofRequest = { + name: 'ProofRequest', + version: '1.0', + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + requested_attributes: { + First: { + names: [], + restrictions: [ + { + schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + }, + ], }, - requested_predicates: [ - { - name: 'Timo', - p_type: '<=', - p_value: 10, - restrictions: [ - { - schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', - }, - ], - }, - ], }, - ProofRequest - ) + requested_predicates: [ + { + name: 'Timo', + p_type: '<=', + p_value: 10, + restrictions: [ + { + schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + }, + ], + }, + ], + } - // Expect 2 top level validation errors - expect(MessageValidator.validate(proofRequest)).rejects.toHaveLength(2) + expect(() => JsonTransformer.fromJSON(proofRequest, ProofRequest)).toThrowError(ClassValidationError) + try { + JsonTransformer.fromJSON(proofRequest, ProofRequest) + } catch (e) { + const caughtError = e as ClassValidationError + expect(caughtError.validationErrors).toHaveLength(2) + } }) }) diff --git a/packages/core/src/modules/proofs/models/RequestedProof.ts b/packages/core/src/modules/proofs/models/RequestedProof.ts index 0755e70119..a2f2a5cf85 100644 --- a/packages/core/src/modules/proofs/models/RequestedProof.ts +++ b/packages/core/src/modules/proofs/models/RequestedProof.ts @@ -1,5 +1,5 @@ import { Expose, Type } from 'class-transformer' -import { IsInstance, IsString, ValidateNested } from 'class-validator' +import { IsInstance, IsOptional, ValidateNested } from 'class-validator' import { ProofAttribute } from './ProofAttribute' @@ -18,6 +18,7 @@ export class RequestedProof { public revealedAttributes!: Map @Expose({ name: 'self_attested_attrs' }) - @IsString({ each: true }) - public selfAttestedAttributes!: Map + @IsOptional() + // Validation is relaxed/skipped because empty Map validation will fail on JSON transform validation + public selfAttestedAttributes: Map = new Map() } diff --git a/packages/core/src/utils/JsonTransformer.ts b/packages/core/src/utils/JsonTransformer.ts index 763e486957..eb65999ca3 100644 --- a/packages/core/src/utils/JsonTransformer.ts +++ b/packages/core/src/utils/JsonTransformer.ts @@ -1,5 +1,15 @@ +import type { Validate } from 'class-validator' + import { instanceToPlain, plainToInstance, instanceToInstance } from 'class-transformer' +import { ClassValidationError } from '../error/ClassValidationError' + +import { MessageValidator } from './MessageValidator' + +interface Validate { + validate?: boolean +} + export class JsonTransformer { public static toJSON(classInstance: T) { return instanceToPlain(classInstance, { @@ -7,9 +17,24 @@ export class JsonTransformer { }) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public static fromJSON(json: any, Class: { new (...args: any[]): T }): T { - return plainToInstance(Class, json, { exposeDefaultValues: true }) + public static fromJSON( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cls: { new (...args: any[]): T }, + { validate = true }: Validate = {} + ): T { + const instance = plainToInstance(cls, json, { exposeDefaultValues: true }) + + // Skip validation + if (!validate) return instance + + if (!instance) { + throw new ClassValidationError('Cannot validate instance of ', { classType: Object.getPrototypeOf(cls).name }) + } + MessageValidator.validateSync(instance) + + return instance } public static clone(classInstance: T): T { @@ -25,8 +50,12 @@ export class JsonTransformer { return JSON.stringify(this.toJSON(classInstance)) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public static deserialize(jsonString: string, Class: { new (...args: any[]): T }): T { - return this.fromJSON(JSON.parse(jsonString), Class) + public static deserialize( + jsonString: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cls: { new (...args: any[]): T }, + { validate = true }: Validate = {} + ): T { + return this.fromJSON(JSON.parse(jsonString), cls, { validate }) } } diff --git a/packages/core/src/utils/MessageValidator.ts b/packages/core/src/utils/MessageValidator.ts index 86d1283ccd..82e1afc408 100644 --- a/packages/core/src/utils/MessageValidator.ts +++ b/packages/core/src/utils/MessageValidator.ts @@ -1,4 +1,7 @@ -import { validateOrReject } from 'class-validator' +import { validateSync } from 'class-validator' + +import { ClassValidationError } from '../error' +import { isValidationErrorArray } from '../error/ValidationErrorUtils' export class MessageValidator { /** @@ -8,7 +11,19 @@ export class MessageValidator { * @throws array of validation errors {@link ValidationError} */ // eslint-disable-next-line @typescript-eslint/ban-types - public static validate(classInstance: T) { - return validateOrReject(classInstance) + public static validateSync(classInstance: T & {}) { + // NOTE: validateSync (strangely) return an Array of errors so we + // have to transform that into an error of choice and throw that. + const errors = validateSync(classInstance) + if (isValidationErrorArray(errors)) { + throw new ClassValidationError('Failed to validate class.', { + classType: classInstance.constructor.name, + validationErrors: errors, + }) + } else if (errors.length !== 0) { + throw new ClassValidationError('An unknown validation error occurred.', { + classType: Object.prototype.constructor(classInstance).name, + }) + } } } diff --git a/packages/core/src/utils/__tests__/JsonTransformer.test.ts b/packages/core/src/utils/__tests__/JsonTransformer.test.ts index 83dc18e489..0d7158fb80 100644 --- a/packages/core/src/utils/__tests__/JsonTransformer.test.ts +++ b/packages/core/src/utils/__tests__/JsonTransformer.test.ts @@ -72,7 +72,7 @@ describe('JsonTransformer', () => { it('transforms JSON string to nested class instance', () => { const didDocumentString = - '{"@context":["https://w3id.org/did/v1"],"id":"did:peer:1zQmRYBx1pL86DrsxoJ2ZD3w42d7Ng92ErPgFsCSqg8Q1h4i","keyAgreement":[{"id":"#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V","type":"Ed25519VerificationKey2018","publicKeyBase58":"ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7"}],"service":[{"id":"#service-0","type":"did-communication","serviceEndpoint":"https://example.com/endpoint","recipientKeys":["#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V"],"routingKeys":["did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"],"accept":["didcomm/v2","didcomm/aip2;env=rfc587"]}]}' + '{"@context":["https://w3id.org/did/v1"],"id":"did:peer:1zQmRYBx1pL86DrsxoJ2ZD3w42d7Ng92ErPgFsCSqg8Q1h4i","controller": "nowYouAreUnderMyControl", "keyAgreement":[{"id":"#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V", "controller": "#id", "type":"Ed25519VerificationKey2018","publicKeyBase58":"ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7"}],"service":[{"id":"#service-0","type":"did-communication","serviceEndpoint":"https://example.com/endpoint","recipientKeys":["#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V"],"routingKeys":["did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"],"accept":["didcomm/v2","didcomm/aip2;env=rfc587"]}]}' const didDocument = JsonTransformer.deserialize(didDocumentString, DidDocument) diff --git a/packages/core/src/utils/__tests__/MessageValidator.test.ts b/packages/core/src/utils/__tests__/MessageValidator.test.ts new file mode 100644 index 0000000000..b0c15e2491 --- /dev/null +++ b/packages/core/src/utils/__tests__/MessageValidator.test.ts @@ -0,0 +1,27 @@ +import { ClassValidationError } from '../../error/ClassValidationError' +import { ConnectionInvitationMessage } from '../../modules/connections' +import { MessageValidator } from '../MessageValidator' + +describe('MessageValidator', () => { + describe('validateSync', () => { + it('validates a class instance correctly', () => { + const invitation = new ConnectionInvitationMessage({ + did: 'did:sov:test1234', + id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + }) + + expect(MessageValidator.validateSync(invitation)).toBeUndefined() + }) + it('throws an error for invalid class instance', () => { + const invitation = new ConnectionInvitationMessage({ + did: 'did:sov:test1234', + id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + }) + invitation.did = undefined + + expect(() => MessageValidator.validateSync(invitation)).toThrow(ClassValidationError) + }) + }) +}) diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts index 72fabcdf3c..0421eb6b67 100644 --- a/packages/core/src/utils/parseInvitation.ts +++ b/packages/core/src/utils/parseInvitation.ts @@ -12,13 +12,13 @@ import { OutOfBandInvitation } from '../modules/oob/messages' * * @returns OutOfBandInvitation */ -export const parseInvitationUrl = async (invitationUrl: string): Promise => { +export const parseInvitationUrl = (invitationUrl: string): OutOfBandInvitation => { const parsedUrl = parseUrl(invitationUrl).query if (parsedUrl['oob']) { - const outOfBandInvitation = await OutOfBandInvitation.fromUrl(invitationUrl) + const outOfBandInvitation = OutOfBandInvitation.fromUrl(invitationUrl) return outOfBandInvitation } else if (parsedUrl['c_i'] || parsedUrl['d_m']) { - const invitation = await ConnectionInvitationMessage.fromUrl(invitationUrl) + const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl) return convertToNewInvitation(invitation) } throw new AriesFrameworkError(