diff --git a/docs/spec/patches.md b/docs/spec/patches.md index dfa1edfbe..83b0608a1 100644 --- a/docs/spec/patches.md +++ b/docs/spec/patches.md @@ -142,7 +142,7 @@ The `add-services` _Patch Action_ describes the addition of [Service Endpoints]( 3. Each service being added ****MUST**** be represented by an entry in the `services` array, and each entry must be an object composed as follows: 1. The object ****MUST**** include an `id` property, and its value ****MUST**** be a string with a length of no more than fifty (50) Base64URL encoded characters. If the value is not of the correct type or exceeds the specified length, the entire _Patch Action_ ****MUST**** be discarded, without any of it being used to modify the DID's state. 2. The object ****MUST**** include a `type` property, and its value ****MUST**** be a string with a length of no more than thirty (30) Base64URL encoded characters. If the value is not a string or exceeds the specified length, the entire _Patch Action_ ****MUST**** be discarded, without any of it being used to modify the DID's state. - 3. The object ****MUST**** include a `serviceEndpoint` property, and its value ****MUST**** be either a valid URI string (including a scheme segment: i.e. http://, git://) or a JSON object with properties that describe the Service Endpoint further. If the values do not adhere to these constraints, the entire _Patch Action_ ****MUST**** be discarded, without any of it being used to modify the DID's state. + 3. The object ****MUST**** include a `serviceEndpoint` property, and its value ****MUST**** be either a valid URI string (including a scheme segment: i.e. http://, git://), a JSON object with properties that describe the Service Endpoint further, or an array of such URI strings or JSON objects. If the values do not adhere to these constraints, the entire _Patch Action_ ****MUST**** be discarded, without any of it being used to modify the DID's state. #### `remove-services` diff --git a/lib/core/versions/1.0/DocumentComposer.ts b/lib/core/versions/1.0/DocumentComposer.ts index 673d23c9e..631490ff0 100644 --- a/lib/core/versions/1.0/DocumentComposer.ts +++ b/lib/core/versions/1.0/DocumentComposer.ts @@ -345,22 +345,26 @@ export default class DocumentComposer { } // `serviceEndpoint` validations. - const serviceEndpoint = service.serviceEndpoint; - if (typeof serviceEndpoint === 'string') { - const uri = URI.parse(service.serviceEndpoint); - if (uri.error !== undefined) { - throw new SidetreeError( - ErrorCode.DocumentComposerPatchServiceEndpointStringNotValidUri, - `Service endpoint string '${serviceEndpoint}' is not a valid URI.` - ); - } - } else if (typeof serviceEndpoint === 'object') { - // Allow `object` type only if it is not an array. - if (Array.isArray(serviceEndpoint)) { - throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointCannotBeAnArray); + // transform URI strings and JSON objects into array so that we can run validations more easily + const serviceEndpointValueAsArray = Array.isArray(service.serviceEndpoint) ? service.serviceEndpoint : [service.serviceEndpoint]; + for (const serviceEndpoint of serviceEndpointValueAsArray) { + // serviceEndpoint itself must be URI string or non-array object + if (typeof serviceEndpoint === 'string') { + const uri = URI.parse(serviceEndpoint); + if (uri.error !== undefined) { + throw new SidetreeError( + ErrorCode.DocumentComposerPatchServiceEndpointStringNotValidUri, + `Service endpoint string '${serviceEndpoint}' is not a valid URI.` + ); + } + } else if (typeof serviceEndpoint === 'object') { + // Allow `object` type only if it is not an array. + if (Array.isArray(serviceEndpoint)) { + throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointCannotBeAnArray); + } + } else { + throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointMustBeStringOrNonArrayObject); } - } else { - throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointMustBeStringOrNonArrayObject); } } } diff --git a/lib/core/versions/1.0/models/ServiceModel.ts b/lib/core/versions/1.0/models/ServiceModel.ts index 6429897ea..45c81bf3a 100644 --- a/lib/core/versions/1.0/models/ServiceModel.ts +++ b/lib/core/versions/1.0/models/ServiceModel.ts @@ -4,5 +4,5 @@ export default interface ServiceModel { id: string; type: string; - serviceEndpoint: string | object; + serviceEndpoint: string | object | Array; } diff --git a/lib/core/versions/latest/DocumentComposer.ts b/lib/core/versions/latest/DocumentComposer.ts index 673d23c9e..631490ff0 100644 --- a/lib/core/versions/latest/DocumentComposer.ts +++ b/lib/core/versions/latest/DocumentComposer.ts @@ -345,22 +345,26 @@ export default class DocumentComposer { } // `serviceEndpoint` validations. - const serviceEndpoint = service.serviceEndpoint; - if (typeof serviceEndpoint === 'string') { - const uri = URI.parse(service.serviceEndpoint); - if (uri.error !== undefined) { - throw new SidetreeError( - ErrorCode.DocumentComposerPatchServiceEndpointStringNotValidUri, - `Service endpoint string '${serviceEndpoint}' is not a valid URI.` - ); - } - } else if (typeof serviceEndpoint === 'object') { - // Allow `object` type only if it is not an array. - if (Array.isArray(serviceEndpoint)) { - throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointCannotBeAnArray); + // transform URI strings and JSON objects into array so that we can run validations more easily + const serviceEndpointValueAsArray = Array.isArray(service.serviceEndpoint) ? service.serviceEndpoint : [service.serviceEndpoint]; + for (const serviceEndpoint of serviceEndpointValueAsArray) { + // serviceEndpoint itself must be URI string or non-array object + if (typeof serviceEndpoint === 'string') { + const uri = URI.parse(serviceEndpoint); + if (uri.error !== undefined) { + throw new SidetreeError( + ErrorCode.DocumentComposerPatchServiceEndpointStringNotValidUri, + `Service endpoint string '${serviceEndpoint}' is not a valid URI.` + ); + } + } else if (typeof serviceEndpoint === 'object') { + // Allow `object` type only if it is not an array. + if (Array.isArray(serviceEndpoint)) { + throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointCannotBeAnArray); + } + } else { + throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointMustBeStringOrNonArrayObject); } - } else { - throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointMustBeStringOrNonArrayObject); } } } diff --git a/lib/core/versions/latest/models/ServiceModel.ts b/lib/core/versions/latest/models/ServiceModel.ts index 6429897ea..45c81bf3a 100644 --- a/lib/core/versions/latest/models/ServiceModel.ts +++ b/lib/core/versions/latest/models/ServiceModel.ts @@ -4,5 +4,5 @@ export default interface ServiceModel { id: string; type: string; - serviceEndpoint: string | object; + serviceEndpoint: string | object | Array; } diff --git a/tests/core/DocumentComposer.spec.ts b/tests/core/DocumentComposer.spec.ts index e064159b0..43a5a7e45 100644 --- a/tests/core/DocumentComposer.spec.ts +++ b/tests/core/DocumentComposer.spec.ts @@ -469,7 +469,7 @@ describe('DocumentComposer', async () => { DocumentComposer['validateAddServicesPatch'](patch); }); - it('should throw error if `serviceEndpoint` is an array.', () => { + it('should allow an array as `serviceEndpoint`.', () => { const patch = { action: PatchAction.AddServices, services: [{ @@ -478,6 +478,20 @@ describe('DocumentComposer', async () => { serviceEndpoint: [] }] }; + + // Expecting this call to succeed without errors. + DocumentComposer['validateAddServicesPatch'](patch); + }); + + it('should throw error if `serviceEndpoint` is an array that includes an array.', () => { + const patch = { + action: PatchAction.AddServices, + services: [{ + id: 'someId', + type: 'someType', + serviceEndpoint: [[]] // array must contain URI strings or objects but not arrays + }] + }; const expectedError = new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointCannotBeAnArray); expect(() => { DocumentComposer['validateAddServicesPatch'](patch); }).toThrow(expectedError); }); @@ -495,6 +509,19 @@ describe('DocumentComposer', async () => { expect(() => { DocumentComposer['validateAddServicesPatch'](patch); }).toThrow(expectedError); }); + it('should throw error if `serviceEndpoint` has an invalid type (inside an array).', () => { + const patch = { + action: PatchAction.AddServices, + services: [{ + id: 'someId', + type: 'someType', + serviceEndpoint: [123] // Invalid serviceEndpoint type. + }] + }; + const expectedError = new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointMustBeStringOrNonArrayObject); + expect(() => { DocumentComposer['validateAddServicesPatch'](patch); }).toThrow(expectedError); + }); + it('Should throw if `serviceEndpoint` is not valid URI.', () => { const patch = { action: PatchAction.AddServices, @@ -510,6 +537,22 @@ describe('DocumentComposer', async () => { ErrorCode.DocumentComposerPatchServiceEndpointStringNotValidUri ); }); + + it('Should throw if `serviceEndpoint` is not valid URI (inside an array).', () => { + const patch = { + action: PatchAction.AddServices, + services: [{ + id: 'someId', + type: 'someType', + serviceEndpoint: ['http://'] // Invalid URI. + }] + }; + + JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( + () => DocumentComposer['validateAddServicesPatch'](patch), + ErrorCode.DocumentComposerPatchServiceEndpointStringNotValidUri + ); + }); }); describe('validateDocumentPatches()', async () => {