From f2adfab6b8fa192cef609ab8d0dc0c5192fd2d91 Mon Sep 17 00:00:00 2001 From: Dave Kelsey Date: Mon, 4 Feb 2019 14:49:54 +0000 Subject: [PATCH] [Master] Move experimental features to 0.20 (#4593) Signed-off-by: Dave Kelsey --- packages/composer-common/api.txt | 8 +- packages/composer-common/changelog.txt | 6 + packages/composer-common/index.js | 1 + .../lib/businessnetworkdefinition.js | 2 + .../lib/introspect/property.js | 34 +- .../composer-common/lib/readonlydecorator.js | 50 +++ .../lib/readonlydecoratorfactory.js | 42 ++ packages/composer-common/lib/serializer.js | 42 +- .../lib/serializer/instancegenerator.js | 1 - .../lib/serializer/jsonpopulator.js | 24 +- packages/composer-common/lib/util.js | 1 - .../test/businessnetworkdefinition.js | 13 + .../test/introspect/property_ex.js | 7 +- .../composer-common/test/readonlydecorator.js | 48 ++ .../test/readonlydecoratorfactory.js | 52 +++ packages/composer-common/test/serializer.js | 37 ++ .../test/embeddedconnection.js | 7 +- .../test/embeddedcontext.js | 13 +- .../composer-runtime-web/test/webcontext.js | 11 +- packages/composer-runtime/lib/context.js | 1 + packages/composer-runtime/lib/engine.js | 2 +- .../lib/engine.transactions.js | 41 +- .../lib/installedbusinessnetwork.js | 9 + packages/composer-runtime/lib/registry.js | 2 +- .../composer-runtime/lib/registrymanager.js | 4 +- .../composer-runtime/lib/transactionlogger.js | 6 +- packages/composer-runtime/test/api.js | 56 +-- packages/composer-runtime/test/context.js | 14 + .../test/engine.transactions.js | 102 ++++- .../test/installedbusinessnetwork.js | 69 +++ .../systest/accesscontrols.js | 17 +- .../systest/nohistorian.js | 422 ++++++++++++++++++ 32 files changed, 1032 insertions(+), 112 deletions(-) create mode 100644 packages/composer-common/lib/readonlydecorator.js create mode 100644 packages/composer-common/lib/readonlydecoratorfactory.js create mode 100644 packages/composer-common/test/readonlydecorator.js create mode 100644 packages/composer-common/test/readonlydecoratorfactory.js create mode 100644 packages/composer-runtime/test/installedbusinessnetwork.js create mode 100644 packages/composer-tests-functional/systest/nohistorian.js diff --git a/packages/composer-common/api.txt b/packages/composer-common/api.txt index fc5aec73f2..6ec6107523 100644 --- a/packages/composer-common/api.txt +++ b/packages/composer-common/api.txt @@ -258,6 +258,12 @@ class ModelManager { + DecoratorFactory[] getDecoratorFactories() + void addDecoratorFactory(DecoratorFactory) } +class ReadOnlyDecorator extends Decorator { + + void constructor(Object) throws IllegalModelException +} +class ReadOnlyDecoratorFactory extends DecoratorFactory { + + Decorator newDecorator(Object) +} class ReturnsDecorator extends Decorator { + void constructor(Object) throws IllegalModelException + string getType() @@ -272,7 +278,7 @@ class ReturnsDecoratorFactory extends DecoratorFactory { class Serializer { + void constructor(Factory,ModelManager) + void setDefaultOptions(Object) - + Object toJSON(Resource,Object,boolean,boolean,boolean,boolean) throws Error + + Object toJSON(Resource,Object,boolean,boolean,boolean,boolean,boolean) throws Error + Resource fromJSON(Object,Object,boolean,boolean) } class Wallet { diff --git a/packages/composer-common/changelog.txt b/packages/composer-common/changelog.txt index e5500b61a6..94df571d36 100644 --- a/packages/composer-common/changelog.txt +++ b/packages/composer-common/changelog.txt @@ -24,6 +24,12 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 0.19.20 {308d962120667e982b2600107d0b8b13} 2019-01-29 +- Modify readonly decorator to be parameterless + +Version 0.19.20 {5ff216eab17b2d990c43636c51a585b5} 2018-12-18 +- Add readonly decorator factory + Version 0.19.11 {765772c9b73656c289a15398bccc76fd} 2018-06-29 - Add decorator factories - Add returns decorator factory diff --git a/packages/composer-common/index.js b/packages/composer-common/index.js index 61acbf19de..fcdb9221e8 100644 --- a/packages/composer-common/index.js +++ b/packages/composer-common/index.js @@ -92,6 +92,7 @@ module.exports.Query = require('./lib/query/query'); module.exports.QueryAnalyzer = require('./lib/query/queryanalyzer.js'); module.exports.QueryFile = require('./lib/query/queryfile'); module.exports.QueryManager = require('./lib/querymanager'); +module.exports.ReadOnlyDecoratorFactory = require('./lib/readonlydecoratorfactory'); module.exports.Relationship = require('./lib/model/relationship'); module.exports.RelationshipDeclaration = require('./lib/introspect/relationshipdeclaration'); module.exports.Resource = require('./lib/model/resource'); diff --git a/packages/composer-common/lib/businessnetworkdefinition.js b/packages/composer-common/lib/businessnetworkdefinition.js index 74ef3b45de..5cc3194c45 100644 --- a/packages/composer-common/lib/businessnetworkdefinition.js +++ b/packages/composer-common/lib/businessnetworkdefinition.js @@ -27,6 +27,7 @@ const minimatch = require('minimatch'); const ModelManager = require('./modelmanager'); const QueryFile = require('./query/queryfile'); const QueryManager = require('./querymanager'); +const ReadOnlyDecoratorFactory = require('./readonlydecoratorfactory'); const ReturnsDecoratorFactory = require('./returnsdecoratorfactory'); const ScriptManager = require('./scriptmanager'); const semver = require('semver'); @@ -117,6 +118,7 @@ class BusinessNetworkDefinition { this.modelManager = new ModelManager(); this.modelManager.addDecoratorFactory(new CommitDecoratorFactory()); + this.modelManager.addDecoratorFactory(new ReadOnlyDecoratorFactory()); this.modelManager.addDecoratorFactory(new ReturnsDecoratorFactory()); this.factory = this.modelManager.getFactory(); this.serializer = this.modelManager.getSerializer(); diff --git a/packages/composer-common/lib/introspect/property.js b/packages/composer-common/lib/introspect/property.js index c493e4669d..84b15662e7 100644 --- a/packages/composer-common/lib/introspect/property.js +++ b/packages/composer-common/lib/introspect/property.js @@ -124,25 +124,35 @@ class Property extends Decorated { * @return {string} the fully qualified type of this property */ getFullyQualifiedTypeName() { + if(this.isPrimitive()) { return this.type; } - const parent = this.getParent(); - if(!parent) { - throw new Error('Property ' + this.name + ' does not have a parent.'); - } - const modelFile = parent.getModelFile(); - if(!modelFile) { - throw new Error('Parent of property ' + this.name + ' does not have a ModelFile!'); - } + if (this.fullyQualifiedTypeName){ + return this.fullyQualifiedTypeName; + } else { + const parent = this.getParent(); + if(!parent) { + throw new Error('Property ' + this.name + ' does not have a parent.'); + } + const modelFile = parent.getModelFile(); + if(!modelFile) { + throw new Error('Parent of property ' + this.name + ' does not have a ModelFile!'); + } + + const result = modelFile.getFullyQualifiedTypeName(this.type); + + if(!result) { + throw new Error('Failed to find fully qualified type name for property ' + this.name + ' with type ' + this.type ); + } + + this.fullyQualifiedTypeName = result; + Object.defineProperty(this, 'fullyQualifiedTypeName', { enumerable: false }); - const result = modelFile.getFullyQualifiedTypeName(this.type); - if(!result) { - throw new Error('Failed to find fully qualified type name for property ' + this.name + ' with type ' + this.type ); + return result; } - return result; } /** diff --git a/packages/composer-common/lib/readonlydecorator.js b/packages/composer-common/lib/readonlydecorator.js new file mode 100644 index 0000000000..b8747044e2 --- /dev/null +++ b/packages/composer-common/lib/readonlydecorator.js @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Decorator = require('./introspect/decorator'); +const IllegalModelException = require('./introspect/illegalmodelexception'); + +/** + * Specialised decorator implementation for the @readonly decorator. + */ +class ReadOnlyDecorator extends Decorator { + + /** + * Create a Decorator. + * @param {ClassDeclaration | Property} parent - the owner of this property + * @param {Object} ast - The AST created by the parser + * @throws {IllegalModelException} + */ + constructor(parent, ast) { + super(parent, ast); + } + + /** + * Process the AST and build the model + * @throws {IllegalModelException} + * @private + */ + process() { + super.process(); + const args = this.getArguments(); + if (args.length !== 0) { + throw new IllegalModelException(`@readonly decorator expects 0 arguments, but ${args.length} arguments were specified.`, this.parent.getModelFile(), this.ast.location); + } + } + +} + +module.exports = ReadOnlyDecorator; diff --git a/packages/composer-common/lib/readonlydecoratorfactory.js b/packages/composer-common/lib/readonlydecoratorfactory.js new file mode 100644 index 0000000000..c27cd06347 --- /dev/null +++ b/packages/composer-common/lib/readonlydecoratorfactory.js @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const DecoratorFactory = require('./introspect/decoratorfactory'); +const ReadOnlyDecorator = require('./readonlydecorator'); + +/** + * A decorator factory for the @readonly decorator. + */ +class ReadOnlyDecoratorFactory extends DecoratorFactory { + + /** + * Process the decorator, and return a specific implementation class for that + * decorator, or return null if this decorator is not handled by this processor. + * @abstract + * @param {ClassDeclaration | Property} parent - the owner of this property + * @param {Object} ast - The AST created by the parser + * @return {Decorator} The decorator. + */ + newDecorator(parent, ast) { + if (ast.name !== 'readonly') { + return null; + } + return new ReadOnlyDecorator(parent, ast); + } + +} + +module.exports = ReadOnlyDecoratorFactory; diff --git a/packages/composer-common/lib/serializer.js b/packages/composer-common/lib/serializer.js index 816ce40662..3b79e2472e 100644 --- a/packages/composer-common/lib/serializer.js +++ b/packages/composer-common/lib/serializer.js @@ -68,6 +68,22 @@ class Serializer { this.defaultOptions = Object.assign({}, baseDefaultOptions, newDefaultOptions); } + + /** + * Generate validation parameters for a Resource + * @private + * @param {Resource} resource - The instance being worked + * @returns {Object} parameters used for validation + */ + validationParameters(resource){ + const parameters = {}; + parameters.stack = new TypedStack(resource); + parameters.modelManager = this.modelManager; + parameters.seenResources = new Set(); + parameters.dedupeResources = new Set(); + return parameters; + } + /** *

* Convert a {@link Resource} to a JavaScript object suitable for long-term @@ -84,6 +100,9 @@ class Serializer { * @param {boolean} [options.deduplicateResources] - Generate $id for resources and * if a resources appears multiple times in the object graph only the first instance is * serialized in full, subsequent instances are replaced with a reference to the $id + * @param {boolean} [options.useOriginal] - shortcut the generation of the JSON structure from + * the resource and directly return the $original if present from the creation of the resource + * through the fromJSON method * @return {Object} - The Javascript Object that represents the resource * @throws {Error} - throws an exception if resource is not an instance of * Resource or fails validation. @@ -94,15 +113,18 @@ class Serializer { throw new Error(Globalize.formatMessage('serializer-tojson-notcobject')); } - const parameters = {}; - parameters.stack = new TypedStack(resource); - parameters.modelManager = this.modelManager; - parameters.seenResources = new Set(); - parameters.dedupeResources = new Set(); + // Assign options + options = options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions; + + // Enable shortcut retrieval of original JSON stored during serializer.fromJSON() method call + if (resource.$original && options.useOriginal) { + return resource.$original; + } + + const parameters = this.validationParameters(resource); const classDeclaration = this.modelManager.getType( resource.getFullyQualifiedType() ); - // validate the resource against the model - options = options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions; + // conditionally validate the resource against the model if(options.validate) { const validator = new ResourceValidator(options); classDeclaration.accept(validator, parameters); @@ -120,6 +142,7 @@ class Serializer { // this performs the conversion of the resouce into a standard JSON object let result = classDeclaration.accept(generator, parameters); return result; + } /** @@ -184,6 +207,11 @@ class Serializer { resource.validate(); } + // Store the original JSON object to enable consditional retrieval later + delete jsonObject.$networkId; + resource.$original = jsonObject; + Object.defineProperty(resource, '$original', { enumerable: false }); + return resource; } } diff --git a/packages/composer-common/lib/serializer/instancegenerator.js b/packages/composer-common/lib/serializer/instancegenerator.js index 675b52db9d..32547bce5c 100644 --- a/packages/composer-common/lib/serializer/instancegenerator.js +++ b/packages/composer-common/lib/serializer/instancegenerator.js @@ -17,7 +17,6 @@ const ClassDeclaration = require('../introspect/classdeclaration'); const EnumDeclaration = require('../introspect/enumdeclaration'); const Field = require('../introspect/field'); -// const leftPad = require('left-pad'); const padStart = require('lodash.padstart'); const ModelUtil = require('../modelutil'); const RelationshipDeclaration = require('../introspect/relationshipdeclaration'); diff --git a/packages/composer-common/lib/serializer/jsonpopulator.js b/packages/composer-common/lib/serializer/jsonpopulator.js index 19dd2fd0da..beb9022829 100644 --- a/packages/composer-common/lib/serializer/jsonpopulator.js +++ b/packages/composer-common/lib/serializer/jsonpopulator.js @@ -39,21 +39,25 @@ function isSystemProperty(name) { * @private */ function getAssignableProperties(resourceData) { - return Object.keys(resourceData).filter((property) => { - return !isSystemProperty(property) && !Util.isNull(resourceData[property]); - }); + const assignable = []; + for (const property in resourceData){ + if(!isSystemProperty(property) && !Util.isNull(resourceData[property])){ + assignable.push(property); + } + } + return assignable; } /** * Assert that all resource properties exist in a given class declaration. - * @param {Array} properties Property names. + * @param {Array} propertyNames Property names. * @param {ClassDeclaration} classDeclaration class declaration. * @throws {ValidationException} if any properties are not defined by the class declaration. * @private */ -function validateProperties(properties, classDeclaration) { +function validateProperties(propertyNames, classDeclaration) { const expectedProperties = classDeclaration.getProperties().map((property) => property.getName()); - const invalidProperties = properties.filter((property) => !expectedProperties.includes(property)); + const invalidProperties = propertyNames.filter((property) => !expectedProperties.includes(property)); if (invalidProperties.length > 0) { const errorText = `Unexpected properties for type ${classDeclaration.getFullyQualifiedName()}: ` + invalidProperties.join(', '); @@ -117,12 +121,12 @@ class JSONPopulator { const properties = getAssignableProperties(jsonObj); validateProperties(properties, classDeclaration); - properties.forEach((property) => { + for (const property of properties) { const value = jsonObj[property]; parameters.jsonStack.push(value); const classProperty = classDeclaration.getProperty(property); - resourceObj[property] = classProperty.accept(this,parameters); - }); + resourceObj[property] = classProperty.accept(this, parameters); + } return resourceObj; } @@ -194,7 +198,7 @@ class JSONPopulator { classDeclaration.accept(this, parameters); } else { - result = this.convertToObject(field,jsonItem); + result = this.convertToObject(field, jsonItem); } return result; diff --git a/packages/composer-common/lib/util.js b/packages/composer-common/lib/util.js index 0333f64b8d..0bdb59b5fd 100644 --- a/packages/composer-common/lib/util.js +++ b/packages/composer-common/lib/util.js @@ -116,7 +116,6 @@ class Util { if (transaction instanceof Resource){ transaction.setIdentifier(txId.idStr); json = serializer.toJSON(transaction); - } else { transaction.transactionId = txId.idStr; json=transaction; diff --git a/packages/composer-common/test/businessnetworkdefinition.js b/packages/composer-common/test/businessnetworkdefinition.js index 6eb8f953d8..e582d67540 100644 --- a/packages/composer-common/test/businessnetworkdefinition.js +++ b/packages/composer-common/test/businessnetworkdefinition.js @@ -23,6 +23,7 @@ const moxios = require('moxios'); const nodeUtil = require('util'); const os = require('os'); const path = require('path'); +const ReadOnlyDecorator = require('../lib/readonlydecorator'); const ReturnsDecorator = require('../lib/returnsdecorator'); const rimraf = nodeUtil.promisify(require('rimraf')); @@ -516,6 +517,18 @@ describe('BusinessNetworkDefinition', () => { decorator.should.be.an.instanceOf(ReturnsDecorator); }); + it('should install the decorator processor for @readonly', () => { + const bnd = new BusinessNetworkDefinition('id@1.0.0', 'description', null, 'readme'); + const modelManager = bnd.getModelManager(); + modelManager.addModelFile(` + namespace org.acme + @readonly + transaction T{ }`); + const transactionDeclaration = modelManager.getType('org.acme.T'); + const decorator = transactionDeclaration.getDecorator('readonly'); + decorator.should.be.an.instanceOf(ReadOnlyDecorator); + }); + }); }); diff --git a/packages/composer-common/test/introspect/property_ex.js b/packages/composer-common/test/introspect/property_ex.js index 4c0a2dc703..d28d7dc1ae 100644 --- a/packages/composer-common/test/introspect/property_ex.js +++ b/packages/composer-common/test/introspect/property_ex.js @@ -53,7 +53,7 @@ describe('Property', function () { const field = person.getProperty('owner'); // stub the getType method to return null sinon.stub(field, 'getParent', function(){return null;}); - + field.fullyQualifiedTypeName = null; (function () { field.getFullyQualifiedTypeName(); }).should.throw(/Property owner does not have a parent./); @@ -63,7 +63,7 @@ describe('Property', function () { const field = person.getProperty('owner'); // stub the getType method to return null sinon.stub(person, 'getModelFile', function(){return null;}); - + field.fullyQualifiedTypeName = null; (function () { field.getFullyQualifiedTypeName(); }).should.throw(/Parent of property owner does not have a ModelFile!/); @@ -73,7 +73,7 @@ describe('Property', function () { const field = person.getProperty('owner'); // stub the getType method to return null sinon.stub(person.getModelFile(), 'getFullyQualifiedTypeName', function(){return null;}); - + field.fullyQualifiedTypeName = null; (function () { field.getFullyQualifiedTypeName(); }).should.throw(/Failed to find fully qualified type name for property owner with type Person/); @@ -83,5 +83,6 @@ describe('Property', function () { const field = person.getProperty('owner'); field.toString().should.equal('RelationshipDeclaration {name=owner, type=org.acme.l1.Person, array=false, optional=false}'); }); + }); }); diff --git a/packages/composer-common/test/readonlydecorator.js b/packages/composer-common/test/readonlydecorator.js new file mode 100644 index 0000000000..5aaacc4a22 --- /dev/null +++ b/packages/composer-common/test/readonlydecorator.js @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const ReadOnlyDecorator = require('../lib/readonlydecorator'); +const ReadOnlyDecoratorDecoratorFactory = require('../lib/readonlydecoratorfactory'); +const ModelManager = require('../lib/modelmanager'); + +require('chai').should(); + +describe('ReadOnlyDecorator', () => { + + let modelManager; + let transactionDeclaration; + + beforeEach(() => { + modelManager = new ModelManager(); + modelManager.addDecoratorFactory(new ReadOnlyDecoratorDecoratorFactory()); + modelManager.addModelFile(` + namespace org.acme + transaction T { } + `); + transactionDeclaration = modelManager.getType('org.acme.T'); + }); + + describe('#process', () => { + + it('should throw if arguments are specified', () => { + (() => { + new ReadOnlyDecorator(transactionDeclaration, { location: { start: { offset: 0, line: 1, column: 1 }, end: { offset: 22, line: 1, column: 23 } }, name: 'readonly', arguments: { list: [ { value: true }, { value: false } ] } }); + }).should.throw(/@readonly decorator expects 0 arguments, but 2 arguments were specified. Line 1 column 1, to line 1 column 23./); + }); + }); + + +}); diff --git a/packages/composer-common/test/readonlydecoratorfactory.js b/packages/composer-common/test/readonlydecoratorfactory.js new file mode 100644 index 0000000000..06d723cd29 --- /dev/null +++ b/packages/composer-common/test/readonlydecoratorfactory.js @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const ReadOnlyDecorator = require('../lib/readonlydecorator'); +const ReadOnlyDecoratorDecoratorFactory = require('../lib/readonlydecoratorfactory'); +const ModelManager = require('../lib/modelmanager'); + +const should = require('chai').should(); + +describe('ReadOnlyDecoratorDecoratorFactory', () => { + + let modelManager; + let transactionDeclaration; + let factory; + + beforeEach(() => { + modelManager = new ModelManager(); + modelManager.addModelFile(` + namespace org.acme + transaction T { } + `); + transactionDeclaration = modelManager.getType('org.acme.T'); + factory = new ReadOnlyDecoratorDecoratorFactory(); + }); + + describe('#process', () => { + + it('should return null for a @foobar decorator', () => { + should.equal(factory.newDecorator(transactionDeclaration, { name: 'foobar' }), null); + }); + + it('should return a readonly decorator instance for a @readonly decorator', () => { + const decorator = factory.newDecorator(transactionDeclaration, { name: 'readonly' }); + decorator.should.be.an.instanceOf(ReadOnlyDecorator); + }); + + }); + +}); diff --git a/packages/composer-common/test/serializer.js b/packages/composer-common/test/serializer.js index 18c07363e3..a0b82665c9 100644 --- a/packages/composer-common/test/serializer.js +++ b/packages/composer-common/test/serializer.js @@ -49,6 +49,8 @@ describe('Serializer', () => { o String participantId o String firstName o String lastName + o ConceptArray[] conceptArray optional + o DateTime theDate optional } transaction SampleTransaction { @@ -66,6 +68,21 @@ describe('Serializer', () => { o String newValue } + enum SampleEnum { + o EMPEROR + o KING + o CHINSTRAP + o GENTOO + } + + concept ConceptArray { + o NestedConcept nestedConcept + } + + concept NestedConcept { + o String value + } + `); factory = new Factory(modelManager); serializer = new Serializer(factory, modelManager); @@ -195,6 +212,17 @@ describe('Serializer', () => { stringValue: '' }); }); + + it('should shortcut the retun if $original is present and useOriginal flag is passed', () => { + const resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); + resource.owner = factory.newRelationship('org.acme.sample', 'SampleParticipant', 'alice@email.com'); + resource.stringValue = ''; + resource.$original = 'penguin'; + + const json = serializer.toJSON(resource, {useOriginal : true}); + json.should.equal('penguin'); + + }); }); describe('#fromJSON', () => { @@ -215,6 +243,15 @@ describe('Serializer', () => { }).should.throw(TypeNotFoundException, /NoSuchAsset/); }); + it('should throw if the class declaration is an instance of Enum', () => { + let mockResource = sinon.createStubInstance(Resource); + mockResource.$class = 'org.acme.sample.SampleEnum'; + let serializer = new Serializer(factory, modelManager); + (() => { + serializer.fromJSON(mockResource); + }).should.throw(Error, /Attempting to create an ENUM declaration is not supported/); + }); + it('should deserialize a valid asset', () => { let json = { $class: 'org.acme.sample.SampleAsset', diff --git a/packages/composer-connector-embedded/test/embeddedconnection.js b/packages/composer-connector-embedded/test/embeddedconnection.js index f2d4751d1b..adfa2aa43c 100644 --- a/packages/composer-connector-embedded/test/embeddedconnection.js +++ b/packages/composer-connector-embedded/test/embeddedconnection.js @@ -14,7 +14,7 @@ 'use strict'; -const { BusinessNetworkDefinition, Certificate, Connection, ConnectionManager } = require('composer-common'); +const { BusinessNetworkDefinition, BusinessNetworkMetadata, Certificate, Connection, ConnectionManager } = require('composer-common'); const { Context, DataCollection, DataService, Engine, LoggingService, InstalledBusinessNetwork } = require('composer-runtime'); const EmbeddedContainer = require('composer-runtime-embedded').EmbeddedContainer; const EmbeddedConnection = require('../lib/embeddedconnection'); @@ -260,6 +260,11 @@ describe('EmbeddedConnection', () => { const mockInstalledBusinessNetwork = sinon.createStubInstance(InstalledBusinessNetwork); sandbox.stub(InstalledBusinessNetwork, 'newInstance').resolves(mockInstalledBusinessNetwork); + const mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); + mockInstalledBusinessNetwork.getDefinition.returns(mockBusinessNetworkDefinition); + const mockBusinessNetworkMetadata = sinon.createStubInstance(BusinessNetworkMetadata); + mockBusinessNetworkDefinition.getMetadata.returns(mockBusinessNetworkMetadata); + mockBusinessNetworkMetadata.getPackageJson.returns({}); // put something weird for installedBusinessNetwork so we can detect it has changed. EmbeddedConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine, 'orgInstalledBusinessNetwork'); diff --git a/packages/composer-runtime-embedded/test/embeddedcontext.js b/packages/composer-runtime-embedded/test/embeddedcontext.js index 94c3c797c6..c43cae8713 100644 --- a/packages/composer-runtime-embedded/test/embeddedcontext.js +++ b/packages/composer-runtime-embedded/test/embeddedcontext.js @@ -16,6 +16,8 @@ const Serializer = require('composer-common').Serializer; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; +const BusinessNetworkMetadata = require('composer-common').BusinessNetworkMetadata; +const InstalledBusinessNetwork = require('composer-runtime').InstalledBusinessNetwork; const Context = require('composer-runtime').Context; const Engine = require('composer-runtime').Engine; const EmbeddedContainer = require('..').EmbeddedContainer; @@ -43,17 +45,22 @@ describe('EmbeddedContext', () => { let mockSerializer; let mockEngine; let mockEventSink; - let mockInstalledBusinessNetwork; let context; - beforeEach(() => { + beforeEach(async () => { mockEmbeddedContainer = sinon.createStubInstance(EmbeddedContainer); mockEmbeddedContainer.getUUID.returns('d8f08eba-2746-4801-8318-3a7611aed45e'); mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockEmbeddedContainer); mockSerializer = sinon.createStubInstance(Serializer); mockEventSink = {}; - mockInstalledBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + + const mockInstalledBusinessNetwork = sinon.createStubInstance(InstalledBusinessNetwork); + const mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); + mockInstalledBusinessNetwork.getDefinition.returns(mockBusinessNetworkDefinition); + const mockBusinessNetworkMetadata = sinon.createStubInstance(BusinessNetworkMetadata); + mockBusinessNetworkDefinition.getMetadata.returns(mockBusinessNetworkMetadata); + mockBusinessNetworkMetadata.getPackageJson.returns({}); context = new EmbeddedContext(mockEngine, identity, mockEventSink, mockInstalledBusinessNetwork); }); diff --git a/packages/composer-runtime-web/test/webcontext.js b/packages/composer-runtime-web/test/webcontext.js index b2ad2cd64c..97d8ca88f9 100644 --- a/packages/composer-runtime-web/test/webcontext.js +++ b/packages/composer-runtime-web/test/webcontext.js @@ -16,6 +16,8 @@ const Context = require('composer-runtime').Context; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; +const BusinessNetworkMetadata = require('composer-common').BusinessNetworkMetadata; +const InstalledBusinessNetwork = require('composer-runtime').InstalledBusinessNetwork; const Engine = require('composer-runtime').Engine; const Serializer = require('composer-common').Serializer; const WebContainer = require('..').WebContainer; @@ -42,7 +44,6 @@ describe('WebContext', () => { let mockSerializer; let mockEngine; let context; - let mockInstalledBusinessNetwork; beforeEach(() => { mockWebContainer = sinon.createStubInstance(WebContainer); @@ -50,7 +51,13 @@ describe('WebContext', () => { mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockWebContainer); mockSerializer = sinon.createStubInstance(Serializer); - mockInstalledBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + + const mockInstalledBusinessNetwork = sinon.createStubInstance(InstalledBusinessNetwork); + const mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); + mockInstalledBusinessNetwork.getDefinition.returns(mockBusinessNetworkDefinition); + const mockBusinessNetworkMetadata = sinon.createStubInstance(BusinessNetworkMetadata); + mockBusinessNetworkDefinition.getMetadata.returns(mockBusinessNetworkMetadata); + mockBusinessNetworkMetadata.getPackageJson.returns({}); context = new WebContext(mockEngine, mockInstalledBusinessNetwork, identity); }); diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js index 32fa1d1834..b2d01fe68a 100644 --- a/packages/composer-runtime/lib/context.js +++ b/packages/composer-runtime/lib/context.js @@ -49,6 +49,7 @@ class Context { this.installedBusinessNetwork = installedBusinessNetwork; this.eventNumber = 0; this.contextId = uuid.v4(); + this.historianEnabled = installedBusinessNetwork.historianEnabled; } /** diff --git a/packages/composer-runtime/lib/engine.js b/packages/composer-runtime/lib/engine.js index 4929b3e8c0..5de640a25c 100644 --- a/packages/composer-runtime/lib/engine.js +++ b/packages/composer-runtime/lib/engine.js @@ -173,7 +173,7 @@ class Engine { } // This step executes the start business network transaction. This is a no-op, but records - // the event into the transaction registry and historian. + // the event into the transaction registry and optionally historian. LOG.debug(method, 'Executing start business network transaction'); await this.submitTransaction(context, [JSON.stringify(transactionData)]); diff --git a/packages/composer-runtime/lib/engine.transactions.js b/packages/composer-runtime/lib/engine.transactions.js index aa88a1023a..e8858c08bf 100644 --- a/packages/composer-runtime/lib/engine.transactions.js +++ b/packages/composer-runtime/lib/engine.transactions.js @@ -64,16 +64,21 @@ class EngineTransactions { LOG.debug(method, 'Getting default transaction registry for ' + transactionFQT); const txRegistry = await registryManager.get('Transaction', transactionFQT); - LOG.debug(method, 'Getting historian registry'); - const historian = await registryManager.get('Asset', 'org.hyperledger.composer.system.HistorianRecord'); - - // Form the historian record - const record = this._createHistorianRecord(context, transaction); + let historian; + let historianRecord; + let canHistorianAdd = null; // this means it's good otherwise it will contain an error object + if (context.historianEnabled === undefined || context.historianEnabled) { + LOG.debug(method, 'Getting historian registry'); + historian = await registryManager.get('Asset', 'org.hyperledger.composer.system.HistorianRecord'); + + // Form the historian record + historianRecord = this._createHistorianRecord(context, transaction); + canHistorianAdd = await historian.testAdd(historianRecord); + } - // check that we can add to both these registries ahead of time - LOG.debug(method, 'Validating ability to create in Transaction and Historian registries'); + // check that we can add to both these registries ahead of time + LOG.debug(method, 'Validating ability to create in Transaction and optionally Historian registries'); let canTxAdd = await txRegistry.testAdd(transaction); - let canHistorianAdd = await historian.testAdd(record); if (canTxAdd || canHistorianAdd){ throw canTxAdd ? canTxAdd : canHistorianAdd; @@ -86,12 +91,14 @@ class EngineTransactions { LOG.debug(method, 'Storing executed transaction in Transaction registry'); await txRegistry.add(transaction, {noTest: true}); - // Update the historian record before we store it. - this._updateHistorianRecord(context, record); + if (context.historianEnabled === undefined || context.historianEnabled) { + // Update the historian record before we store it. + this._updateHistorianRecord(context, historianRecord); - // Store the historian record in the historian registry. - LOG.debug(method, 'Storing Historian record in Historian registry'); - await historian.add(record, {noTest: true}); + // Store the historian record in the historian registry. + LOG.debug(method, 'Storing Historian record in Historian registry'); + await historian.add(historianRecord, {noTest: true, validate: false}); + } context.clearTransaction(); LOG.exit(method, returnValue); @@ -206,7 +213,7 @@ class EngineTransactions { const eventService = context.getEventService(); record.eventsEmitted = []; eventService.getEvents().forEach((element) => { - const r = context.getSerializer().fromJSON(element); + const r = context.getSerializer().fromJSON(element, {validate: false}); record.eventsEmitted.push(r); } ); @@ -224,8 +231,7 @@ class EngineTransactions { LOG.entry(method, context, transaction, returnValues); // Determine whether or not a result was expected. - const transactionDeclaration = transaction.getClassDeclaration(); - const returnsDecorator = transactionDeclaration.getDecorator('returns'); + const returnsDecorator = transaction.getClassDeclaration().getDecorator('returns'); if (!returnsDecorator) { LOG.exit(method, undefined); return undefined; @@ -280,6 +286,7 @@ class EngineTransactions { // Get the type and resolved type. const transactionDeclaration = transaction.getClassDeclaration(); const returnsDecorator = transactionDeclaration.getDecorator('returns'); + const readOnly = transactionDeclaration.getDecorator('readonly') ? true : false; const returnValueType = returnsDecorator.getType(); const returnValueResolvedType = returnsDecorator.getResolvedType(); const isArray = returnsDecorator.isArray(); @@ -297,7 +304,7 @@ class EngineTransactions { LOG.error(method, error); throw error; } - return context.getSerializer().toJSON(actualReturnValue, { convertResourcesToRelationships: true, permitResourcesForRelationships: false }); + return context.getSerializer().toJSON(actualReturnValue, { convertResourcesToRelationships: true, permitResourcesForRelationships: false, useOriginal: readOnly }); }; // Handle the non-array case - a single return value. diff --git a/packages/composer-runtime/lib/installedbusinessnetwork.js b/packages/composer-runtime/lib/installedbusinessnetwork.js index 358cc4b6c4..7b876ae9cf 100644 --- a/packages/composer-runtime/lib/installedbusinessnetwork.js +++ b/packages/composer-runtime/lib/installedbusinessnetwork.js @@ -17,6 +17,9 @@ const AclCompiler = require('./aclcompiler'); const QueryCompiler = require('./querycompiler'); const ScriptCompiler = require('./scriptcompiler'); +const Logger = require('composer-common').Logger; + +const LOG = Logger.getLog('InstalledBusinessNetwork'); /** * Data associated with the currently installed business network, used by Context. @@ -53,11 +56,17 @@ class InstalledBusinessNetwork { * @private */ constructor(networkInfo) { + const method = 'constructor'; this.definition = networkInfo.definition; this.compiledScriptBundle = networkInfo.compiledScriptBundle; this.compiledQueryBundle = networkInfo.compiledQueryBundle; this.compiledAclBundle = networkInfo.compiledAclBundle; this.archive = networkInfo.archive; + this.historianEnabled = true; + if (this.definition.getMetadata().getPackageJson().disableHistorian === true) { + LOG.debug(method, 'Historian disabled'); + this.historianEnabled = false; + } } /** diff --git a/packages/composer-runtime/lib/registry.js b/packages/composer-runtime/lib/registry.js index 4f085af5ae..94ffa61190 100644 --- a/packages/composer-runtime/lib/registry.js +++ b/packages/composer-runtime/lib/registry.js @@ -131,7 +131,7 @@ class Registry extends EventEmitter { let object = await this.dataCollection.get(id); object = Registry.removeInternalProperties(object); try { - const resource = this.serializer.fromJSON(object); + const resource = this.serializer.fromJSON(object, {validate: false}); await this.accessController.check(resource, 'READ'); this.objectMap.set(id, object); return true; diff --git a/packages/composer-runtime/lib/registrymanager.js b/packages/composer-runtime/lib/registrymanager.js index 5b973dc5d2..bece88bea1 100644 --- a/packages/composer-runtime/lib/registrymanager.js +++ b/packages/composer-runtime/lib/registrymanager.js @@ -293,7 +293,7 @@ class RegistryManager extends EventEmitter { // go to the sysregistries datacollection and get the 'resource' for the registry we are interested in const json = await this.sysregistries.get(collectionID); // do we have permission to be looking at this?? - const resource = this.serializer.fromJSON(json); + const resource = this.serializer.fromJSON(json, {validate: false}); await this.accessController.check(resource, 'READ'); // if we got here then, we the accessController.check was OK, get the dataCollection with the actual information // for the require registry @@ -330,7 +330,7 @@ class RegistryManager extends EventEmitter { return this.sysregistries.get(collectionID) .then((result) => { // do we REALLY have permission to be looking at this?? - resource = this.serializer.fromJSON(result); + resource = this.serializer.fromJSON(result, {validate: false}); return this.accessController.check(resource, 'READ'); }) .then(() => { diff --git a/packages/composer-runtime/lib/transactionlogger.js b/packages/composer-runtime/lib/transactionlogger.js index c5d72360b5..d8a65806a0 100644 --- a/packages/composer-runtime/lib/transactionlogger.js +++ b/packages/composer-runtime/lib/transactionlogger.js @@ -66,11 +66,13 @@ class TransactionLogger { // Serialize both the old and new resources. let oldJSON = this.serializer.toJSON(event.oldResource, { - convertResourcesToRelationships: true + convertResourcesToRelationships: true, + validate: false }); LOG.debug(method, 'Serialized old resource'); let newJSON = this.serializer.toJSON(event.newResource, { - convertResourcesToRelationships: true + convertResourcesToRelationships: true, + validate: false }); LOG.debug(method, 'Serialized new resource'); diff --git a/packages/composer-runtime/test/api.js b/packages/composer-runtime/test/api.js index 7e9ce36e4b..83dfcc519d 100644 --- a/packages/composer-runtime/test/api.js +++ b/packages/composer-runtime/test/api.js @@ -296,46 +296,38 @@ describe('Api', () => { }); }); - it('should perform a query using a named query', () => { - return api.query(queryID) - .should.eventually.be.deep.equal(resources) - .then(() => { - sinon.assert.calledOnce(mockCompiledQueryBundle.execute); - sinon.assert.calledWith(mockCompiledQueryBundle.execute, mockDataService, queryID); - sinon.assert.callCount(mockAccessController.check, 5); - }); + it('should perform a query using a named query', async () => { + const result = await api.query(queryID); + result.should.deep.equal(resources); + sinon.assert.calledOnce(mockCompiledQueryBundle.execute); + sinon.assert.calledWith(mockCompiledQueryBundle.execute, mockDataService, queryID); + sinon.assert.callCount(mockAccessController.check, 5); }); - it('should perform a query using a named query and parameters', () => { - return api.query(queryID, queryParams) - .should.eventually.be.deep.equal(resources) - .then(() => { - sinon.assert.calledOnce(mockCompiledQueryBundle.execute); - sinon.assert.calledWith(mockCompiledQueryBundle.execute, mockDataService, queryID, queryParams); - sinon.assert.callCount(mockAccessController.check, 5); - }); + it('should perform a query using a named query and parameters', async () => { + const result = await api.query(queryID, queryParams); + result.should.deep.equal(resources); + sinon.assert.calledOnce(mockCompiledQueryBundle.execute); + sinon.assert.calledWith(mockCompiledQueryBundle.execute, mockDataService, queryID, queryParams); + sinon.assert.callCount(mockAccessController.check, 5); }); - it('should perform a query using a built query', () => { + it('should perform a query using a built query', async () => { const query = new Query(queryHash); - return api.query(query) - .should.eventually.be.deep.equal(resources) - .then(() => { - sinon.assert.calledOnce(mockCompiledQueryBundle.execute); - sinon.assert.calledWith(mockCompiledQueryBundle.execute, mockDataService, queryHash); - sinon.assert.callCount(mockAccessController.check, 5); - }); + const result = await api.query(query); + result.should.deep.equal(resources); + sinon.assert.calledOnce(mockCompiledQueryBundle.execute); + sinon.assert.calledWith(mockCompiledQueryBundle.execute, mockDataService, queryHash); + sinon.assert.callCount(mockAccessController.check, 5); }); - it('should perform a query using a built query and parameters', () => { + it('should perform a query using a built query and parameters', async () => { const query = new Query(queryHash); - return api.query(query, queryParams) - .should.eventually.be.deep.equal(resources) - .then(() => { - sinon.assert.calledOnce(mockCompiledQueryBundle.execute); - sinon.assert.calledWith(mockCompiledQueryBundle.execute, mockDataService, queryHash, queryParams); - sinon.assert.callCount(mockAccessController.check, 5); - }); + const result = await api.query(query, queryParams); + result.should.deep.equal(resources); + sinon.assert.calledOnce(mockCompiledQueryBundle.execute); + sinon.assert.calledWith(mockCompiledQueryBundle.execute, mockDataService, queryHash, queryParams); + sinon.assert.callCount(mockAccessController.check, 5); }); }); diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js index c5a9217d70..c543579821 100644 --- a/packages/composer-runtime/test/context.js +++ b/packages/composer-runtime/test/context.js @@ -75,6 +75,20 @@ describe('Context', () => { new Context(mockEngine); }).should.throw(/No business network/i); }); + + it('should disable historian if InstalledBusinessNetwork says disabled', () => { + const mockInstalledBusinessNetwork = sinon.createStubInstance(InstalledBusinessNetwork); + mockInstalledBusinessNetwork.historianEnabled = false; + context = new Context(mockEngine, mockInstalledBusinessNetwork); + context.historianEnabled.should.be.false; + }); + + it('should enable historian if InstalledBusinessNetwork says enabled', () => { + const mockInstalledBusinessNetwork = sinon.createStubInstance(InstalledBusinessNetwork); + mockInstalledBusinessNetwork.historianEnabled = true; + context = new Context(mockEngine, mockInstalledBusinessNetwork); + context.historianEnabled.should.be.true; + }); }); describe('#getFunction', () => { diff --git a/packages/composer-runtime/test/engine.transactions.js b/packages/composer-runtime/test/engine.transactions.js index c4df02f8d9..d7e1c26b37 100644 --- a/packages/composer-runtime/test/engine.transactions.js +++ b/packages/composer-runtime/test/engine.transactions.js @@ -20,7 +20,7 @@ const Context = require('../lib/context'); const Engine = require('../lib/engine'); const LoggingService = require('../lib/loggingservice'); const EventService = require('../lib/eventservice'); -const { ModelManager, ReturnsDecoratorFactory, ScriptManager } = require('composer-common'); +const { ModelManager, ReturnsDecoratorFactory, ReadOnlyDecoratorFactory, ScriptManager } = require('composer-common'); const Registry = require('../lib/registry'); const RegistryManager = require('../lib/registrymanager'); const Resolver = require('../lib/resolver'); @@ -54,6 +54,7 @@ describe('EngineTransactions', () => { beforeEach(() => { modelManager = new ModelManager(); modelManager.addDecoratorFactory(new ReturnsDecoratorFactory()); + modelManager.addDecoratorFactory(new ReadOnlyDecoratorFactory()); modelManager.addModelFile(` namespace org.acme asset MyAsset identified by assetId { @@ -82,6 +83,16 @@ describe('EngineTransactions', () => { } event MyEvent { + } + @returns(MyAsset) + @readonly + transaction MyTransactionThatReturnsAsset { + o String value + } + @returns(MyAsset[]) + @readonly + transaction MyTransactionThatReturnsAssetArray { + o String value } @returns(MyConcept) transaction MyTransactionThatReturnsConcept { @@ -225,7 +236,29 @@ describe('EngineTransactions', () => { .should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "submitTransaction", expecting "\["serializedResource"\]"/); }); - it('should execute a transaction that does not return a value', async () => { + it('should execute a transaction that does not return a value and write to historian if flag undefined', async () => { + mockRegistryManager.get.withArgs('Transaction', 'org.acme.MyTransaction').resolves(mockTransactionRegistry); + const resolvedTransaction = factory.newTransaction('org.acme', 'MyTransaction'); + mockResolver.resolve.resolves(resolvedTransaction); + const result = await engine.invoke(mockContext, 'submitTransaction', [JSON.stringify({ + $class: 'org.acme.MyTransaction', + transactionId: 'TX_1', + timestamp: new Date(0).toISOString(), + value: 'hello world' + })]); + should.equal(result, undefined); + sinon.assert.calledOnce(mockTransactionRegistry.testAdd); + sinon.assert.calledWith(mockTransactionRegistry.testAdd, sinon.match(transaction => transaction.getFullyQualifiedIdentifier() === 'org.acme.MyTransaction#TX_1')); + sinon.assert.calledOnce(mockHistorian.testAdd); + sinon.assert.calledWith(mockHistorian.testAdd, sinon.match(historianRecord => historianRecord.getFullyQualifiedIdentifier() === 'org.hyperledger.composer.system.HistorianRecord#TX_1')); + sinon.assert.calledOnce(mockTransactionRegistry.add); + sinon.assert.calledWith(mockTransactionRegistry.add, sinon.match(transaction => transaction.getFullyQualifiedIdentifier() === 'org.acme.MyTransaction#TX_1'), { noTest: true }); + sinon.assert.calledOnce(mockHistorian.add); + sinon.assert.calledWith(mockHistorian.add, sinon.match(historianRecord => historianRecord.getFullyQualifiedIdentifier() === 'org.hyperledger.composer.system.HistorianRecord#TX_1'), { noTest: true, validate: false }); + }); + + it('should execute a transaction that does not return a value and write to historian if explicitly requested', async () => { + mockContext.historianEnabled = true; mockRegistryManager.get.withArgs('Transaction', 'org.acme.MyTransaction').resolves(mockTransactionRegistry); const resolvedTransaction = factory.newTransaction('org.acme', 'MyTransaction'); mockResolver.resolve.resolves(resolvedTransaction); @@ -243,7 +276,27 @@ describe('EngineTransactions', () => { sinon.assert.calledOnce(mockTransactionRegistry.add); sinon.assert.calledWith(mockTransactionRegistry.add, sinon.match(transaction => transaction.getFullyQualifiedIdentifier() === 'org.acme.MyTransaction#TX_1'), { noTest: true }); sinon.assert.calledOnce(mockHistorian.add); - sinon.assert.calledWith(mockHistorian.add, sinon.match(historianRecord => historianRecord.getFullyQualifiedIdentifier() === 'org.hyperledger.composer.system.HistorianRecord#TX_1'), { noTest: true }); + sinon.assert.calledWith(mockHistorian.add, sinon.match(historianRecord => historianRecord.getFullyQualifiedIdentifier() === 'org.hyperledger.composer.system.HistorianRecord#TX_1'), { noTest: true, validate: false }); + }); + + it('should not write to historian if disabled', async () => { + mockContext.historianEnabled = false; + mockRegistryManager.get.withArgs('Transaction', 'org.acme.MyTransaction').resolves(mockTransactionRegistry); + const resolvedTransaction = factory.newTransaction('org.acme', 'MyTransaction'); + mockResolver.resolve.resolves(resolvedTransaction); + const result = await engine.invoke(mockContext, 'submitTransaction', [JSON.stringify({ + $class: 'org.acme.MyTransaction', + transactionId: 'TX_1', + timestamp: new Date(0).toISOString(), + value: 'hello world' + })]); + should.equal(result, undefined); + sinon.assert.calledOnce(mockTransactionRegistry.testAdd); + sinon.assert.calledWith(mockTransactionRegistry.testAdd, sinon.match(transaction => transaction.getFullyQualifiedIdentifier() === 'org.acme.MyTransaction#TX_1')); + sinon.assert.notCalled(mockHistorian.testAdd); + sinon.assert.calledOnce(mockTransactionRegistry.add); + sinon.assert.calledWith(mockTransactionRegistry.add, sinon.match(transaction => transaction.getFullyQualifiedIdentifier() === 'org.acme.MyTransaction#TX_1'), { noTest: true }); + sinon.assert.notCalled(mockHistorian.add); }); it('should execute a transaction that returns a value', async () => { @@ -264,7 +317,7 @@ describe('EngineTransactions', () => { sinon.assert.calledOnce(mockTransactionRegistry.add); sinon.assert.calledWith(mockTransactionRegistry.add, sinon.match(transaction => transaction.getFullyQualifiedIdentifier() === 'org.acme.MyTransactionThatReturnsString#TX_1'), { noTest: true }); sinon.assert.calledOnce(mockHistorian.add); - sinon.assert.calledWith(mockHistorian.add, sinon.match(historianRecord => historianRecord.getFullyQualifiedIdentifier() === 'org.hyperledger.composer.system.HistorianRecord#TX_1'), { noTest: true }); + sinon.assert.calledWith(mockHistorian.add, sinon.match(historianRecord => historianRecord.getFullyQualifiedIdentifier() === 'org.hyperledger.composer.system.HistorianRecord#TX_1'), { noTest: true, validate: false }); }); it('should throw if the transaction cannot be added to the transaction registry', async () => { @@ -289,6 +342,17 @@ describe('EngineTransactions', () => { })]).should.be.rejectedWith(/such error/); }); + it('should correctly identify a readonly decorator', () => { + let transaction = factory.newTransaction('org.acme', 'MyTransactionThatReturnsConcept'); + let result = transaction.getClassDeclaration().getDecorator('readonly'); + should.not.exist(result); + + transaction = factory.newTransaction('org.acme', 'MyTransactionThatReturnsAsset'); + result = transaction.getClassDeclaration().getDecorator('readonly'); + should.exist(result); + result.name.should.equal('readonly'); + }); + }); describe('#_executeTransaction', () => { @@ -615,6 +679,36 @@ describe('EngineTransactions', () => { }]); }); + it('should handle a concept return value that is not readonly but contains a $original component', () => { + const transaction = factory.newTransaction('org.acme', 'MyTransactionThatReturnsConcept'); + const concept = factory.newConcept('org.acme', 'MyConcept'); + concept.value = 'hello world'; + concept.$original = 'not to be seen'; + engine._processComplexReturnValue(mockContext, transaction, concept).should.deep.equal({ + $class: 'org.acme.MyConcept', + value: 'hello world' + }); + }); + + it('should handle a readonly Asset return value', () => { + const transaction = factory.newTransaction('org.acme', 'MyTransactionThatReturnsAsset'); + const asset = factory.newResource('org.acme', 'MyAsset','001'); + asset.value = 'hello world'; + asset.$original = 'penguin'; + engine._processComplexReturnValue(mockContext, transaction, asset).should.equal('penguin'); + }); + + it('should handle a readonly Asset[] return value', () => { + const transaction = factory.newTransaction('org.acme', 'MyTransactionThatReturnsAssetArray'); + const asset1 = factory.newResource('org.acme', 'MyAsset','001'); + asset1.value = 'hello world'; + asset1.$original = 'penguin'; + const asset2 = factory.newResource('org.acme', 'MyAsset','002'); + asset2.value = 'hello again world'; + asset2.$original = 'power'; + engine._processComplexReturnValue(mockContext, transaction, [asset1, asset2]).should.deep.equal(['penguin', 'power']); + }); + it('should throw for an invalid (wrong type) concept return value', () => { const transaction = factory.newTransaction('org.acme', 'MyTransactionThatReturnsConcept'); (() => { diff --git a/packages/composer-runtime/test/installedbusinessnetwork.js b/packages/composer-runtime/test/installedbusinessnetwork.js new file mode 100644 index 0000000000..503df96298 --- /dev/null +++ b/packages/composer-runtime/test/installedbusinessnetwork.js @@ -0,0 +1,69 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const InstalledBusinessNetwork = require('../lib/installedbusinessnetwork'); +const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; +const BusinessNetworkMetadata = require('composer-common').BusinessNetworkMetadata; + +const chai = require('chai'); +chai.use(require('chai-as-promised')); +const sinon = require('sinon'); + + +describe('InstalledBusinessNetwork', () => { + describe('#constructor', () => { + + let mockBusinessNetworkDefinition; + let mockBusinessNetworkMetadata; + let mockNetworkInfo; + beforeEach(() => { + mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); + mockBusinessNetworkMetadata = sinon.createStubInstance(BusinessNetworkMetadata); + mockBusinessNetworkDefinition.getMetadata.returns(mockBusinessNetworkMetadata); + mockNetworkInfo = { + definition: mockBusinessNetworkDefinition, + compiledScriptBundle: 'scriptBundle', + compiledQueryBundle: 'queryBundle', + compiledAclBundle: 'aclBundle', + archive: 'archive' + }; + }); + + it('should enable historian if no request to disable is present', () => { + mockBusinessNetworkMetadata.getPackageJson.returns({'something': 'text'}); + const installedBusinessNetwork = new InstalledBusinessNetwork(mockNetworkInfo); + installedBusinessNetwork.historianEnabled.should.be.true; + }); + + it('should disable historian if disableHistorian set to true', () => { + mockBusinessNetworkMetadata.getPackageJson.returns({'disableHistorian': true}); + const installedBusinessNetwork = new InstalledBusinessNetwork(mockNetworkInfo); + installedBusinessNetwork.historianEnabled.should.be.false; + }); + + it('should enable historian if disableHistorian set to false', () => { + mockBusinessNetworkMetadata.getPackageJson.returns({'disableHistorian': false}); + const installedBusinessNetwork = new InstalledBusinessNetwork(mockNetworkInfo); + installedBusinessNetwork.historianEnabled.should.be.true; + }); + + it('should disable historian if disableHistorian set to a non boolean', () => { + mockBusinessNetworkMetadata.getPackageJson.returns({'disableHistorian': 1}); + const installedBusinessNetwork = new InstalledBusinessNetwork(mockNetworkInfo); + installedBusinessNetwork.historianEnabled.should.be.true; + }); + }); +}); diff --git a/packages/composer-tests-functional/systest/accesscontrols.js b/packages/composer-tests-functional/systest/accesscontrols.js index f26bd6069c..fffca23fd0 100644 --- a/packages/composer-tests-functional/systest/accesscontrols.js +++ b/packages/composer-tests-functional/systest/accesscontrols.js @@ -173,18 +173,11 @@ describe('Access control system tests', function() { }); - it('should be able to enforce read access permissions on an asset registry via client getAll', () => { - return Promise.resolve() - .then(() => { - // Alice should only be able to read Alice's car. - return aliceAssetRegistry.getAll() - .should.eventually.be.deep.equal([aliceCar]); - }) - .then(() => { - // Bob should only be able to read Bob's car. - return bobAssetRegistry.getAll() - .should.eventually.be.deep.equal([bobCar]); - }); + it('should be able to enforce read access permissions on an asset registry via client getAll', async () => { + const aliceCars = await aliceAssetRegistry.getAll(); + aliceCars.should.deep.equal([aliceCar]); + const bobCars = await bobAssetRegistry.getAll(); + bobCars.should.deep.equal([bobCar]); }); it('should be able to enforce read access permissions on an asset registry via client get', () => { diff --git a/packages/composer-tests-functional/systest/nohistorian.js b/packages/composer-tests-functional/systest/nohistorian.js new file mode 100644 index 0000000000..f67b4b4e2f --- /dev/null +++ b/packages/composer-tests-functional/systest/nohistorian.js @@ -0,0 +1,422 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +'use strict'; + +const BusinessNetworkDefinition = require('composer-admin').BusinessNetworkDefinition; +const fs = require('fs'); +const path = require('path'); +const TestUtil = require('./testutil'); + +const chai = require('chai'); +chai.use(require('chai-as-promised')); +chai.use(require('chai-subset')); +chai.should(); + +if (process.setMaxListeners) { + process.setMaxListeners(Infinity); +} + +let client; +let cardStore; +let bnID; +let businessNetworkDefinition; + +let createAsset = (assetId) => { + let factory = client.getBusinessNetwork().getFactory(); + let asset = factory.newResource('systest.assets', 'SimpleAsset', assetId); + asset.stringValue = 'hello world'; + asset.stringValues = ['hello', 'world']; + asset.doubleValue = 3.142; + asset.doubleValues = [4.567, 8.901]; + asset.integerValue = 1024; + asset.integerValues = [32768, -4096]; + asset.longValue = 131072; + asset.longValues = [999999999, -1234567890]; + asset.dateTimeValue = new Date('1994-11-05T08:15:30-05:00'); + asset.dateTimeValues = [new Date('2016-11-05T13:15:30Z'), new Date('2063-11-05T13:15:30Z')]; + asset.booleanValue = true; + asset.booleanValues = [false, true]; + asset.enumValue = 'WOW'; + asset.enumValues = ['SUCH', 'MANY', 'MUCH']; + return asset; +}; + +let createParticipant = (participantId) => { + let factory = client.getBusinessNetwork().getFactory(); + let participant = factory.newResource('systest.participants', 'SimpleParticipant', participantId); + participant.stringValue = 'hello world'; + participant.stringValues = ['hello', 'world']; + participant.doubleValue = 3.142; + participant.doubleValues = [4.567, 8.901]; + participant.integerValue = 1024; + participant.integerValues = [32768, -4096]; + participant.longValue = 131072; + participant.longValues = [999999999, -1234567890]; + participant.dateTimeValue = new Date('1994-11-05T08:15:30-05:00'); + participant.dateTimeValues = [new Date('2016-11-05T13:15:30Z'), new Date('2063-11-05T13:15:30Z')]; + participant.booleanValue = true; + participant.booleanValues = [false, true]; + participant.enumValue = 'WOW'; + participant.enumValues = ['SUCH', 'MANY', 'MUCH']; + return participant; +}; + +describe('No Historian Information recorded', function() { + + this.retries(TestUtil.retries()); + + before(async () => { + await TestUtil.setUp(); + const modelFiles = [ + { fileName: 'models/accesscontrols.cto', contents: fs.readFileSync(path.resolve(__dirname, 'data/common-network/accesscontrols.cto'), 'utf8')}, + { fileName: 'models/participants.cto', contents: fs.readFileSync(path.resolve(__dirname, 'data/common-network/participants.cto'), 'utf8')}, + { fileName: 'models/assets.cto', contents: fs.readFileSync(path.resolve(__dirname, 'data/common-network/assets.cto'), 'utf8')}, + { fileName: 'models/transactions.cto', contents: fs.readFileSync(path.resolve(__dirname, 'data/common-network/transactions.cto'), 'utf8')}, + { fileName: 'models/events.cto', contents: fs.readFileSync(path.resolve(__dirname, 'data/events.cto'), 'utf8') } + ]; + const scriptFiles = [ + { identifier: 'transactions.js', contents: fs.readFileSync(path.resolve(__dirname, 'data/common-network/transactions.js'), 'utf8') }, + { identifier: 'events.js', contents: fs.readFileSync(path.resolve(__dirname, 'data/events.js'), 'utf8') } + ]; + businessNetworkDefinition = new BusinessNetworkDefinition('systest-no-historian@0.0.1', 'The network for the access controls system tests'); + businessNetworkDefinition.getMetadata().getPackageJson().disableHistorian = true; + modelFiles.forEach((modelFile) => { + businessNetworkDefinition.getModelManager().addModelFile(modelFile.contents, modelFile.fileName); + }); + scriptFiles.forEach((scriptFile) => { + let scriptManager = businessNetworkDefinition.getScriptManager(); + scriptManager.addScript(scriptManager.createScript(scriptFile.identifier, 'JS', scriptFile.contents)); + }); + let aclFile = businessNetworkDefinition.getAclManager().createAclFile('permissions.acl', fs.readFileSync(path.resolve(__dirname, 'data/common-network/permissions.acl'), 'utf8')); + businessNetworkDefinition.getAclManager().setAclFile(aclFile); + + bnID = businessNetworkDefinition.getName(); + cardStore = await TestUtil.deploy(businessNetworkDefinition); + client = await TestUtil.getClient(cardStore,'systest-no-historian'); + }); + + after(async () => { + await TestUtil.undeploy(businessNetworkDefinition); + await TestUtil.tearDown(); + }); + + beforeEach(async () => { + await TestUtil.resetBusinessNetwork(cardStore,bnID, 0); + }); + + afterEach(async () => { + client = await TestUtil.getClient(cardStore,'systest-historian'); + }); + + describe('CRUD Asset', () => { + it('should track updates for CREATE asset calls ', () => { + let assetRegistry; + let historian; + let hrecords; + return client + .getAssetRegistry('systest.assets.SimpleAsset') + .then(function (result) { + assetRegistry = result; + }) + .then(() => { + let asset = createAsset('dogeAsset1'); + return assetRegistry.add(asset); + }) + .then(function () { + let asset = createAsset('dogeAsset2'); + return assetRegistry.add(asset); + }) + .then(function () { + let asset = createAsset('dogeAsset3'); + return assetRegistry.add(asset); + }) + .then(() => { + return client.getHistorian(); + }).then((result) => { + historian = result; + return client.getTransactionRegistry('org.hyperledger.composer.system.AddAsset'); + }).then((result) => { + return historian.getAll(); + }).then((result) => { + + // there should be a create asset record for the 3 assets + hrecords = result; + hrecords.length.should.equal(0); + }); + + + }); + + + it('should track updates for UPDATE asset calls ', () => { + + let assetRegistry; + let historian; + let hrecords; + return client + .getAssetRegistry('systest.assets.SimpleAsset') + .then(function (result) { + assetRegistry = result; + }) + .then(() => { + let asset = createAsset('dogeAsset1'); + return assetRegistry.add(asset); + }) + .then(() => { + return client.getHistorian(); + }).then((result) => { + historian = result; + return assetRegistry.get('dogeAsset1'); + }).then((asset) => { + + asset.stringValue = 'ciao mondo'; + asset.stringValues = ['ciao', 'mondo']; + return assetRegistry.update(asset); + }).then(() => { + + return assetRegistry.get('dogeAsset1'); + }) + .then((result) => { + + return client.getTransactionRegistry('org.hyperledger.composer.system.UpdateAsset'); + }).then((result) => { + return historian.getAll(); + }).then((result) => { + hrecords = result; + hrecords.length.should.equal(0); + }); + + + + + }); + it('should track updates for DELETE asset calls ', () => { + + let assetRegistry; + let historian; + let hrecords; + return client + .getAssetRegistry('systest.assets.SimpleAsset') + .then(function (result) { + assetRegistry = result; + }) + .then(() => { + let asset = createAsset('dogeAsset1'); + return assetRegistry.add(asset); + }) + .then(() => { + return client.getHistorian(); + }).then((result) => { + historian = result; + return assetRegistry.get('dogeAsset1'); + }) + .then(() => { + return assetRegistry.remove('dogeAsset1'); + }) + .then((result) => { + + return client.getTransactionRegistry('org.hyperledger.composer.system.RemoveAsset'); + }).then((result) => { + return historian.getAll(); + }).then((result) => { + hrecords = result; + hrecords.length.should.equal(0); + }); + }); + + }); + + describe('CRUD Participant', () => { + it('should track updates for CREATE Participant calls ', () => { + let participantRegistry; + let historian; + let hrecords; + return client.getHistorian() + .then((result) => { + historian = result; + return historian.getAll(); + }) + .then((historianRecords) => { + historianRecords.length.should.equal(0); + return client.getParticipantRegistry('systest.participants.SimpleParticipant'); + }) + .then(function (result) { + participantRegistry = result; + }) + .then(() => { + let participant = createParticipant('dogeParticipant1'); + return participantRegistry.add(participant); + }) + .then(function () { + let participant = createParticipant('dogeParticipant2'); + return participantRegistry.add(participant); + }) + .then(function () { + let participant = createParticipant('dogeParticipant3'); + return participantRegistry.add(participant); + }) + .then(() => { + return client.getTransactionRegistry('org.hyperledger.composer.system.AddParticipant'); + }).then((result) => { + return historian.getAll(); + }).then((result) => { + hrecords = result; + hrecords.length.should.equal(0); + }); + }); + + + it('should track updates for UPDATE Participant calls ', () => { + let participantRegistry; + let historian; + let hrecords; + return client + .getParticipantRegistry('systest.participants.SimpleParticipant') + .then(function (result) { + participantRegistry = result; + }) + .then(() => { + let participant = createParticipant('dogeParticipant1'); + return participantRegistry.add(participant); + }) + .then(() => { + return client.getHistorian(); + }).then((result) => { + historian = result; + return participantRegistry.get('dogeParticipant1'); + }).then((participant) => { + + participant.stringValue = 'ciao mondo'; + participant.stringValues = ['ciao', 'mondo']; + return participantRegistry.update(participant); + }).then(() => { + + return participantRegistry.get('dogeParticipant1'); + }) + .then((result) => { + + return client.getTransactionRegistry('org.hyperledger.composer.system.UpdateParticipant'); + }).then((result) => { + return historian.getAll(); + }).then((result) => { + hrecords = result; + hrecords.length.should.equal(0); + }); + }); + + it('should track updates for DELETE Participant calls ', () => { + let participantRegistry; + let historian; + let hrecords; + return client + .getParticipantRegistry('systest.participants.SimpleParticipant') + .then(function (result) { + participantRegistry = result; + }) + .then(() => { + let participant = createParticipant('dogeParticipant1'); + return participantRegistry.add(participant); + }) + .then(() => { + return client.getHistorian(); + }).then((result) => { + historian = result; + return participantRegistry.get('dogeParticipant1'); + }) + .then(() => { + return participantRegistry.remove('dogeParticipant1'); + }) + .then((result) => { + + return client.getTransactionRegistry('org.hyperledger.composer.system.RemoveParticipant'); + }).then((result) => { + return historian.getAll(); + }).then((result) => { + hrecords = result; + hrecords.length.should.equal(0); + }); + }); + }); + + describe('Transaction invocations', () => { + it('Successful transaction should have contents recorded', () => { + let factory = client.getBusinessNetwork().getFactory(); + let transaction = factory.newTransaction('systest.transactions', 'SimpleTransactionWithPrimitiveTypes'); + let historian; + transaction.stringValue = 'what a transaction'; + transaction.doubleValue = 3.142; + transaction.integerValue = 2000000000; + transaction.longValue = 16000000000000; + transaction.dateTimeValue = new Date('2016-10-14T18:30:30+00:00'); + transaction.booleanValue = true; + transaction.enumValue = 'SUCH'; + return client.submitTransaction(transaction) + .then(() => { + // get the historian + return client.getHistorian(); + }).then((result) => { + historian = result; + return client.getTransactionRegistry('systest.transactions.SimpleTransactionWithPrimitiveTypes'); + }).then((result) => { + return historian.get(transaction.getIdentifier()); + }).should.be.rejectedWith(/does not exist/); + }); + + it('Successful transaction should have events recorded',async () => { + let factory = client.getBusinessNetwork().getFactory(); + let transaction = factory.newTransaction('systest.events', 'EmitComplexEvent'); + let historian; + this.timeout(1000); // Delay to prevent transaction failing + + // Listen for the event + const promise = new Promise((resolve, reject) => { + client.on('event', (ev) => { + resolve(); + }); + }); + + await client.submitTransaction(transaction); + await promise; + historian = await client.getHistorian(); + await historian.get(transaction.getIdentifier()).should.be.rejectedWith(/does not exist/); + }); + + it('Unsuccessful transaction should not cause issues', () => { + + let factory = client.getBusinessNetwork().getFactory(); + let transaction = factory.newTransaction('systest.transactions', 'SimpleTransactionWithPrimitiveTypes'); + let historian; + + transaction.stringValue = 'the wrong string value'; + transaction.doubleValue = 3.142; + transaction.integerValue = 2000000000; + transaction.longValue = 16000000000000; + transaction.dateTimeValue = new Date('2016-10-14T18:30:30+00:00'); + transaction.booleanValue = true; + transaction.enumValue = 'SUCH'; + return client.submitTransaction(transaction) + .catch(() => { return; }) + .then(() => { + // get the historian + return client.getHistorian(); + }).then((result) => { + historian = result; + return historian.get(transaction.getIdentifier()); + }).should.be.rejectedWith(/does not exist/); + }); + }); + +});