Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add validation to JSON transformer #830

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3d91c49
feat: wip add validation to JSON transformer
Jun 2, 2022
0c350cf
feat: wip address code review feedback.
Jun 3, 2022
5de2d99
chore: merge branch 'main' into feat/json-tranform-validator
Jun 3, 2022
19a8f48
chore: wip fix validation issues
Jun 3, 2022
89e691a
chore: merge remote-tracking branch 'upstream/main' into feat/json-tr…
Jun 3, 2022
29d26f5
chore: fixing more issues
Jun 3, 2022
29f4e6d
chore: fix som emore errors
Jun 3, 2022
587d438
chore: code review feedback
Jun 6, 2022
8d235b5
chore: come more code review feedback
Jun 6, 2022
9ae21e3
chore: fix type check
Jun 6, 2022
e6f8637
chore: fix tests
Jun 6, 2022
f474eba
chore: fix TransportDecorator test
Jun 7, 2022
1eb8def
chore: enable validate for ConnectionService
Jun 7, 2022
71aca28
chore: code review feedback
Jun 9, 2022
f189eca
chore: add test for message validator
Jun 9, 2022
8721bb7
chore: merge branch 'main' into feat/json-tranform-validator
Jun 9, 2022
d13c3c9
chore: fix accDecorator tests
Jun 9, 2022
686e0fe
chore: add tests for validating error array
Jun 9, 2022
ea761dc
chore: wip this fails
Jun 9, 2022
f3e9023
chore: code review feedback
Jun 9, 2022
793117c
chore: refactor
Jun 9, 2022
502244e
chore: wip code review feedback fixing tests
Jun 10, 2022
c2c70b7
chore: fix tests
Jun 10, 2022
7824356
chore: merge remote-tracking branch 'upstream/main' into feat/json-tr…
Jun 10, 2022
3cabb43
chore: fix typo
Jun 10, 2022
50365d7
chore: fix newline in in template literal
Jun 13, 2022
9e04a1b
chore: code review feedback
Jun 13, 2022
c0e45c7
chore: code review feedback
Jun 14, 2022
7695eaa
chore: merge branch 'main' into feat/json-tranform-validator
Jun 15, 2022
992862a
chore: code review feedback
Jun 15, 2022
bdab6bc
chore: code review feedback
Jun 15, 2022
80fee1c
chore: merge remote-tracking branch 'upstream/main' into feat/json-tr…
Jun 15, 2022
cbb0f62
chore: code review feedback
Jun 15, 2022
5a4a9bf
chore: merge remote-tracking branch 'upstream/main' into feat/json-tr…
Jun 15, 2022
d5eecb9
chore: merge remote-tracking branch 'upstream/main' into feat/json-tr…
Jun 16, 2022
8504119
chore: code review feedback
Jun 16, 2022
e71fe1c
chore: merge remote-tracking branch 'upstream/main' into feat/json-tr…
Jun 16, 2022
cee5211
chore: remove unused es-lint ignore line
Jun 16, 2022
d8174fc
fix: variable declaration
Jun 17, 2022
ff330a2
Merge remote-tracking branch 'upstream/main' into feat/json-tranform-…
Jun 17, 2022
baf038b
merge remote-tracking branch 'upstream/main' into feat/json-tranform-…
Jun 20, 2022
3291848
Merge branch 'main' into feat/json-tranform-validator
TimoGlastra Jun 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/agent/MessageReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export class MessageReceiver {
}

// Cast the plain JSON object to specific instance of Message extended from AgentMessage
return JsonTransformer.fromJSON(message, MessageClass)
return JsonTransformer.fromJSON(message, MessageClass, { validate: true })
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
39 changes: 29 additions & 10 deletions packages/core/src/agent/__tests__/AgentMessage.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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 () => {
Expand All @@ -43,32 +43,51 @@ describe('AgentMessage', () => {

const message = JsonTransformer.fromJSON(json, CustomProtocolMessage)

await expect(MessageValidator.validate(message)).resolves.toBeUndefined()
await 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',
}

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)

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([
expect(() => JsonTransformer.fromJSON(json, CustomProtocolMessage)).toThrowError(ClassValidationError)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let thrownError: any
try {
JsonTransformer.fromJSON(json, CustomProtocolMessage)
} catch (e) {
thrownError = e
}
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
expect(thrownError.message).toContain('Failed to validate class.')
expect(thrownError.validationErrors).toMatchObject([
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
{
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)',
},
Expand Down
139 changes: 45 additions & 94 deletions packages/core/src/decorators/ack/AckDecorator.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { wordSize } from 'bn.js'
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved

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'

import { AckDecorated } from './AckDecoratorExtension'
Expand All @@ -25,107 +27,56 @@ 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'],
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
)
TestMessage,
{
validate: true,
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
}
)

expect(transformed).toEqual({
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
id: '7517433f-1150-46f2-8495-723da61b872a',
type: 'https://didcomm.org/test-protocol/1.0/test-message',
pleaseAck: {
on: ['RECEIPT'],
},
})
expect(transformed).toBeInstanceOf(TestMessage)
})

// this covers the pre-aip 2 please ack decorator
test('sets `on` value to `receipt` if `on` is not present in ack decorator', () => {
const transformed = JsonTransformer.fromJSON(
{
'~please_ack': {},
'@id': '7517433f-1150-46f2-8495-723da61b872a',
'@type': 'https://didcomm.org/test-protocol/1.0/test-message',
},
TestMessage
)

expect(transformed).toEqual({
id: '7517433f-1150-46f2-8495-723da61b872a',
type: 'https://didcomm.org/test-protocol/1.0/test-message',
pleaseAck: {
on: ['RECEIPT'],
},
})
expect(transformed).toBeInstanceOf(TestMessage)
})

test('successfully validates please ack decorator', async () => {
const transformedWithDefault = JsonTransformer.fromJSON(
{
'~please_ack': {},
'@id': '7517433f-1150-46f2-8495-723da61b872a',
'@type': 'https://didcomm.org/test-protocol/1.0/test-message',
},
TestMessage
)

await expect(MessageValidator.validate(transformedWithDefault)).resolves.toBeUndefined()

const transformedWithoutDefault = JsonTransformer.fromJSON(
expect(() => transformed()).toThrow(ClassValidationError)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let caughtError: any
try {
transformed()
} catch (e) {
caughtError = e
}
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
expect(caughtError.message.includes('An instance of TestMessage has failed the validation')).toBeTruthy()
expect(caughtError.message.includes('property id has failed the following constraints: matches')).toBeTruthy()
expect(caughtError.message.includes('- property type has failed the following constraints: matches')).toBeTruthy()
expect(caughtError.validationErrors).toMatchObject([
{
'~please_ack': {
on: ['OUTCOME'],
children: [],
constraints: {
matches: 'id must match /[-_./a-zA-Z0-9]{8,64}/ regular expression',
},
'@id': '7517433f-1150-46f2-8495-723da61b872a',
'@type': 'https://didcomm.org/test-protocol/1.0/test-message',
},
TestMessage
)

await expect(MessageValidator.validate(transformedWithoutDefault)).resolves.toBeUndefined()

const transformedWithIncorrectValue = JsonTransformer.fromJSON(
{
'~please_ack': {
on: ['NOT_A_VALID_VALUE'],
property: 'id',
target: {
pleaseAck: {
on: ['RECEIPT'],
},
},
'@id': '7517433f-1150-46f2-8495-723da61b872a',
'@type': 'https://didcomm.org/test-protocol/1.0/test-message',
value: undefined,
},
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: 'type',
target: {
pleaseAck: {
on: ['RECEIPT'],
},
],
property: 'pleaseAck',
},
value: undefined,
},
])
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('Decorators | ServiceDecoratorExtension', () => {
})

test('transforms Json to ServiceDecorator class', () => {
const transformed = JsonTransformer.fromJSON({ '~service': service }, TestMessage)
const transformed = JsonTransformer.fromJSON({ '~service': service }, TestMessage, { validate: false })
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved

expect(transformed.service).toEqual(service)
expect(transformed).toBeInstanceOf(TestMessage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { MessageValidator } from '../../utils/MessageValidator'
import { TransportDecorator, ReturnRouteTypes } from './TransportDecorator'

const validTransport = (transportJson: Record<string, unknown>) =>
MessageValidator.validate(JsonTransformer.fromJSON(transportJson, TransportDecorator))
MessageValidator.validate(JsonTransformer.fromJSON(transportJson, TransportDecorator, { validate: true }))
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
const expectValid = (transportJson: Record<string, unknown>) =>
expect(validTransport(transportJson)).resolves.toBeUndefined()
const expectInvalid = (transportJson: Record<string, unknown>) =>
expect(validTransport(transportJson)).rejects.not.toBeNull()
expect(() => validTransport(transportJson)).toThrowError('Failed to validate class.')

const valid = {
all: {
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/error/ClassValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ValidationError } from 'class-validator'

import { AriesFrameworkError } from './AriesFrameworkError'

export class ClassValidationError extends AriesFrameworkError {
public validationErrors?: ValidationError[] | undefined

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}\n${validationErrorsStringified}`, { cause })
this.validationErrors = validationErrors
}
}
14 changes: 14 additions & 0 deletions packages/core/src/error/ValidationErrorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ValidationError } from 'class-validator'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isValidationErrorArray(e: any): boolean {
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
if (Array.isArray(e)) {
const isErrorArray =
e.length > 0 &&
e.every((err) => {
return err instanceof ValidationError
})
return isErrorArray
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
}
return false
}
1 change: 1 addition & 0 deletions packages/core/src/error/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './AriesFrameworkError'
export * from './RecordNotFoundError'
export * from './RecordDuplicateError'
export * from './IndySdkError'
export * from './ClassValidationError'
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ export class DidExchangeProtocol {
const payload = JsonEncoder.toBuffer(json)
const { isValid, signerVerkeys } = await this.jwsService.verifyJws({ jws, payload })

const didDocument = JsonTransformer.fromJSON(json, DidDocument)
const didDocument = JsonTransformer.fromJSON(json, DidDocument, { validate: false })
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
const didDocumentKeysBase58 = didDocument.authentication
?.map((authentication) => {
const verificationMethod =
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -127,9 +126,7 @@ export class ConnectionInvitationMessage extends AgentMessage {

if (typeof encodedInvitation === 'string') {
const invitationJson = JsonEncoder.fromBase64(encodedInvitation)
const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage)

await MessageValidator.validate(invitation)
const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage, { validate: false })
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved

return invitation
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -285,12 +284,7 @@ export class ConnectionService {
throw error
}

const connection = JsonTransformer.fromJSON(connectionJson, Connection)
try {
morrieinmaas marked this conversation as resolved.
Show resolved Hide resolved
await MessageValidator.validate(connection)
} catch (error) {
throw new Error(error)
}
const connection = JsonTransformer.fromJSON(connectionJson, Connection, { validate: true })

// 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
Expand Down
Loading