diff --git a/.gitignore b/.gitignore index 7a1360f9a9..1b492eabc7 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ packages/composer-website/jekylldocs/jsdoc/ packages/composer-playground/src/assets/npmlist.json packages/composer-systests/systestv1/tls/ca/fabric-ca-server.db +packages/composer-runtime-hlfv1/vendor/github.com/ +packages/composer-runtime-hlfv1/vendor/gopkg.in/sourcemap.v1/ + diff --git a/packages/composer-admin/lib/adminconnection.js b/packages/composer-admin/lib/adminconnection.js index aa7d81525e..a1a40dd459 100644 --- a/packages/composer-admin/lib/adminconnection.js +++ b/packages/composer-admin/lib/adminconnection.js @@ -91,9 +91,6 @@ class AdminConnection { if (businessNetworkIdentifier) { return this.connection.ping(this.securityContext); } - }) - .then(() => { - }); } diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js index 0f32e425ce..1f4a97c215 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -282,8 +282,16 @@ class BusinessNetworkConnection extends EventEmitter { * @return {Promise} A promise to a BusinessNetworkDefinition that indicates the connection is complete */ connect(connectionProfile, businessNetwork, enrollmentID, enrollmentSecret, additionalConnectOptions) { + const method = 'connect'; + LOG.entry(method, connectionProfile, businessNetwork, enrollmentID, enrollmentSecret, additionalConnectOptions); return this.connectionProfileManager.connect(connectionProfile, businessNetwork, additionalConnectOptions) .then((connection) => { + connection.on('events', (events) => { + events.forEach((event) => { + let serializedEvent = this.getBusinessNetwork().getSerializer().fromJSON(event); + this.emit('event', serializedEvent); + }); + }); this.connection = connection; return connection.login(enrollmentID, enrollmentSecret); }) @@ -301,6 +309,7 @@ class BusinessNetworkConnection extends EventEmitter { }) .then((businessNetwork) => { this.businessNetwork = businessNetwork; + LOG.exit(method); return this.businessNetwork; }); } @@ -321,14 +330,20 @@ class BusinessNetworkConnection extends EventEmitter { * terminated. */ disconnect() { + const method = 'disconnect'; + LOG.entry(method); if (!this.connection) { return Promise.resolve(); } return this.connection.disconnect() .then(() => { + this.connection.removeListener('events', () => { + LOG.debug(method, 'removeLisener'); + }); this.connection = null; this.securityContext = null; this.businessNetwork = null; + LOG.exit(method); }); } diff --git a/packages/composer-client/scripts/tsgen.js b/packages/composer-client/scripts/tsgen.js index c8f0d933be..50b2b942c7 100755 --- a/packages/composer-client/scripts/tsgen.js +++ b/packages/composer-client/scripts/tsgen.js @@ -41,22 +41,30 @@ function renderClass(key, clazz) { fileContents += ` static ${statick}(${insert}): any;\n`; } }); - let members = Object.getOwnPropertyNames(clazz.prototype); - members.forEach((member) => { - let func = clazz.prototype[member]; - if (typeof func === 'function') { - const args = new Array(func.length).fill('temp'); - args.forEach((value, index, array) => { - args[index] = `arg${index}?: any`; - }); - const insert = args.join(', '); - if (member === 'constructor') { - fileContents += ` ${member}(${insert});\n`; - } else { - fileContents += ` ${member}(${insert}): any;\n`; + let foundConstructor = false; + let prototype = clazz.prototype; + while(prototype) { + let members = Object.getOwnPropertyNames(prototype); + members.forEach((member) => { + let func = prototype[member]; + if (typeof func === 'function') { + const args = new Array(func.length).fill('temp'); + args.forEach((value, index, array) => { + args[index] = `arg${index}?: any`; + }); + const insert = args.join(', '); + if (member === 'constructor') { + if (!foundConstructor) { + foundConstructor = true; + fileContents += ` ${member}(${insert});\n`; + } + } else { + fileContents += ` ${member}(${insert}): any;\n`; + } } - } - }); + }); + prototype = Object.getPrototypeOf(prototype); + } fileContents += '}\n'; } diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js index afa1c79b2a..a00df3c9c7 100644 --- a/packages/composer-client/test/businessnetworkconnection.js +++ b/packages/composer-client/test/businessnetworkconnection.js @@ -155,6 +155,28 @@ describe('BusinessNetworkConnection', () => { }); }); + it('should create a connection, listen for events, and emit the events it detects individually', () => { + sandbox.stub(businessNetworkConnection.connectionProfileManager, 'connect').resolves(mockConnection); + mockConnection.login.resolves(mockSecurityContext); + mockConnection.ping.resolves(); + const buffer = Buffer.from(JSON.stringify({ + data: 'aGVsbG8=' + })); + sandbox.stub(Util, 'queryChainCode').withArgs(mockSecurityContext, 'getBusinessNetwork', []).resolves(buffer); + sandbox.stub(BusinessNetworkDefinition, 'fromArchive').resolves(mockBusinessNetworkDefinition); + const cb = sinon.stub(); + businessNetworkConnection.on('event', cb); + mockConnection.on.withArgs('events', sinon.match.func).yields(['event1', 'event2']); + mockSerializer.fromJSON.onCall(0).returns('event1#serialized'); + mockSerializer.fromJSON.onCall(1).returns('event2#serialized'); + + return businessNetworkConnection.connect('testprofile', 'testnetwork', 'enrollmentID', 'enrollmentSecret', { some: 'other', options: true }) + .then((result) => { + sinon.assert.calledTwice(cb); // two events + sinon.assert.calledWith(cb, 'event1#serialized'); + sinon.assert.calledWith(cb, 'event2#serialized'); + }); + }); }); describe('#disconnect', () => { @@ -169,9 +191,11 @@ describe('BusinessNetworkConnection', () => { return businessNetworkConnection.disconnect() .then(() => { sinon.assert.calledOnce(mockConnection.disconnect); + sinon.assert.calledOnce(mockConnection.removeListener); return businessNetworkConnection.disconnect(); }) .then(() => { + mockConnection.removeListener.withArgs('events', sinon.match.func).yield(['event1', 'event2']); should.equal(businessNetworkConnection.connection, null); sinon.assert.calledOnce(mockConnection.disconnect); }); diff --git a/packages/composer-common/api.txt b/packages/composer-common/api.txt index 9fc8952c97..e10ea85c7a 100644 --- a/packages/composer-common/api.txt +++ b/packages/composer-common/api.txt @@ -31,6 +31,7 @@ class Factory { + Resource newConcept(string,string,Object,boolean,string) throws ModelException + Relationship newRelationship(string,string,string) throws ModelException + Resource newTransaction(string,string,string,Object,string) + + Resource newEvent(string,string,Object,string) + Object toJSON() } class FileWallet extends Wallet { diff --git a/packages/composer-common/changelog.txt b/packages/composer-common/changelog.txt index dde1baff07..1d025c8b0b 100644 --- a/packages/composer-common/changelog.txt +++ b/packages/composer-common/changelog.txt @@ -12,6 +12,9 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 0.7.1 {90a630e8b408292357ee801e5b79512c} 2017-05-04 +- Added Factory.newEvent + Version 0.7.0 {632a80837e835bbe0343d4b37ce12742} 2017-05-01 - Added Typed.instanceOf diff --git a/packages/composer-common/lib/connection.js b/packages/composer-common/lib/connection.js index 4b9c4bc4a9..f28884f67f 100644 --- a/packages/composer-common/lib/connection.js +++ b/packages/composer-common/lib/connection.js @@ -15,6 +15,7 @@ 'use strict'; const ConnectionManager = require('./connectionmanager'); +const EventEmitter = require('events'); /** * Base class representing a connection to a business network. @@ -23,7 +24,7 @@ const ConnectionManager = require('./connectionmanager'); * @class * @memberof module:composer-common */ -class Connection { +class Connection extends EventEmitter { /** * Constructor. @@ -32,6 +33,7 @@ class Connection { * @param {string} businessNetworkIdentifier The identifier of the business network for this connection, or null if an admin connection */ constructor(connectionManager, connectionProfile, businessNetworkIdentifier) { + super(); if (!(connectionManager instanceof ConnectionManager)) { throw new Error('connectionManager not specified'); } else if (!connectionProfile) { diff --git a/packages/composer-common/lib/factory.js b/packages/composer-common/lib/factory.js index b464dc64f5..7e26f44187 100644 --- a/packages/composer-common/lib/factory.js +++ b/packages/composer-common/lib/factory.js @@ -29,6 +29,7 @@ const Concept = require('./model/concept'); const ValidatedConcept = require('./model/validatedconcept'); const TransactionDeclaration = require('./introspect/transactiondeclaration'); +const EventDeclaration = require('./introspect/eventdeclaration'); const uuid = require('uuid'); @@ -304,6 +305,36 @@ class Factory { return transaction; } + /** + * Create a new event object. The identifier of the event is + * set to a UUID. + * @param {string} ns - the namespace of the event. + * @param {string} type - the type of the event. + * @param {Object} [options] - an optional set of options + * @param {string} [options.generate] - Pass one of:
+ *
sample
return a resource instance with generated sample data.
+ *
empty
return a resource instance with empty property values.
+ * @return {Resource} A resource for the new event. + */ + newEvent(ns, type, options) { + if (!ns) { + throw new Error('ns not specified'); + } else if (!type) { + throw new Error('type not specified'); + } + const id = 'valid'; + let event = this.newResource(ns, type, id, options); + const classDeclaration = event.getClassDeclaration(); + if (!(classDeclaration instanceof EventDeclaration)) { + throw new Error(event.getClassDeclaration().getFullyQualifiedName() + ' is not an event'); + } + + // set the timestamp + event.timestamp = new Date(); + + return event; + } + /** * Stop serialization of this object. * @return {Object} An empty object. diff --git a/packages/composer-common/test/factory.js b/packages/composer-common/test/factory.js index a0cabed8e5..e33029fa32 100644 --- a/packages/composer-common/test/factory.js +++ b/packages/composer-common/test/factory.js @@ -44,6 +44,10 @@ describe('Factory', () => { transaction MyTransaction identified by transactionId { o String transactionId o String newValue + } + event MyEvent identified by eventId { + o String eventId + o String value }`); factory = new Factory(modelManager); sandbox = sinon.sandbox.create(); @@ -217,6 +221,39 @@ describe('Factory', () => { }); + describe('#newEvent', () => { + it('should throw if ns not specified', () => { + (() => { + factory.newEvent(null, 'MyEvent'); + }).should.throw(/ns not specified/); + }); + + it('should throw if type not specified', () => { + (() => { + factory.newEvent('org.acme.test', null); + }).should.throw(/type not specified/); + }); + + it('should throw if a non event type was specified', () => { + (() => { + factory.newEvent('org.acme.test', 'MyTransaction'); + }).should.throw(/not an event/); + }); + + it('should create a new instance with a generated ID', () => { + let resource = factory.newEvent('org.acme.test', 'MyEvent'); + resource.eventId.should.equal('valid'); + resource.timestamp.should.be.an.instanceOf(Date); + }); + + it('should pass options onto newEvent', () => { + let spy = sandbox.spy(factory, 'newResource'); + factory.newEvent('org.acme.test', 'MyEvent', { hello: 'world' }); + sinon.assert.calledOnce(spy); + sinon.assert.calledWith(spy, 'org.acme.test', 'MyEvent', 'valid', { hello: 'world' }); + }); + }); + describe('#toJSON', () => { it('should return an empty object', () => { diff --git a/packages/composer-connector-embedded/lib/embeddedconnection.js b/packages/composer-connector-embedded/lib/embeddedconnection.js index 540b737b63..fdd08697e9 100644 --- a/packages/composer-connector-embedded/lib/embeddedconnection.js +++ b/packages/composer-connector-embedded/lib/embeddedconnection.js @@ -182,7 +182,7 @@ class EmbeddedConnection extends Connection { let engine = EmbeddedConnection.createEngine(container); EmbeddedConnection.addBusinessNetwork(businessNetwork.getName(), this.connectionProfile, chaincodeUUID); EmbeddedConnection.addChaincode(chaincodeUUID, container, engine); - let context = new EmbeddedContext(engine, userID); + let context = new EmbeddedContext(engine, userID, this); return businessNetwork.toArchive() .then((businessNetworkArchive) => { return engine.init(context, 'init', [businessNetworkArchive.toString('base64')]); @@ -246,7 +246,7 @@ class EmbeddedConnection extends Connection { let userID = securityContext.getUserID(); let chaincodeUUID = securityContext.getChaincodeID(); let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID); - let context = new EmbeddedContext(chaincode.engine, userID); + let context = new EmbeddedContext(chaincode.engine, userID, this); return chaincode.engine.query(context, functionName, args) .then((data) => { return Buffer.from(JSON.stringify(data)); @@ -265,7 +265,7 @@ class EmbeddedConnection extends Connection { let userID = securityContext.getUserID(); let chaincodeUUID = securityContext.getChaincodeID(); let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID); - let context = new EmbeddedContext(chaincode.engine, userID); + let context = new EmbeddedContext(chaincode.engine, userID, this); return chaincode.engine.invoke(context, functionName, args) .then((data) => { return undefined; diff --git a/packages/composer-connector-hlf/lib/hfcconnection.js b/packages/composer-connector-hlf/lib/hfcconnection.js index ffcbcd677f..53be1b04aa 100644 --- a/packages/composer-connector-hlf/lib/hfcconnection.js +++ b/packages/composer-connector-hlf/lib/hfcconnection.js @@ -50,6 +50,8 @@ class HFCConnection extends Connection { LOG.info('constructor', 'Creating connection', this.getIdentifier()); this.chain = chain; this.connectOptions = connectOptions; + + this.composerEventId = null; } /** @@ -66,6 +68,7 @@ class HFCConnection extends Connection { * terminated, or rejected with an error. */ disconnect() { + // this.chain.getEventHub().unregisterChaincodeEvent(this.composerEventId); this.chain.eventHubDisconnect(); this.businessNetworkIdentifier = null; this.connectionProfile = null; @@ -99,6 +102,7 @@ class HFCConnection extends Connection { result.setUser(enrollmentID); result.setEnrolledMember(enrolledMember); result.setEventHub(self.chain.getEventHub()); + LOG.info('login', 'Successful login', self.getIdentifier()); resolve(result); }); @@ -126,6 +130,7 @@ class HFCConnection extends Connection { } }) .then(() => { + this.subscribeToEvents(securityContext.getChaincodeID()); return securityContext; }); }); @@ -336,6 +341,20 @@ class HFCConnection extends Connection { }); } + /** + * Subscribe to events emitted by transactions + * @param {String} chaincodeID The chaincode ID + */ + subscribeToEvents(chaincodeID) { + if (this.chain.getEventHub() && chaincodeID) { + LOG.entry('registerChaincodeEvent', chaincodeID, 'composer'); + this.composerEventId = this.chain.getEventHub().registerChaincodeEvent(chaincodeID, 'composer', (event) => { + const jsonEvent = JSON.parse(event.payload.toString('utf8')); + this.emit('events', jsonEvent); + }); + } + } + } module.exports = HFCConnection; diff --git a/packages/composer-connector-hlf/test/hfcconnection.js b/packages/composer-connector-hlf/test/hfcconnection.js index 282dcdf3d7..957da1433e 100644 --- a/packages/composer-connector-hlf/test/hfcconnection.js +++ b/packages/composer-connector-hlf/test/hfcconnection.js @@ -130,17 +130,28 @@ describe('HFCConnection', () => { }); it('should enroll against the Hyperledger Fabric', function() { - + const events = { + payload: { + toString: () => { + return '{"events": "events"}'; + } + } + }; + connection.emit = sinon.stub(); // Login to the Hyperledger Fabric using the mock hfc. let enrollmentID = 'doge'; let enrollmentSecret = 'suchsecret'; return connection .login('doge', 'suchsecret') .then(function(securityContext) { + mockEventHub.registerChaincodeEvent.withArgs('123', 'composer', sinon.match.func).yield(events); sinon.assert.calledOnce(mockChain.enroll); sinon.assert.calledWith(mockChain.enroll, enrollmentID, enrollmentSecret); sinon.assert.calledOnce(mockChain.setRegistrar); sinon.assert.calledWith(mockChain.setRegistrar, mockMember); + sinon.assert.calledOnce(mockEventHub.registerChaincodeEvent); + sinon.assert.calledOnce(connection.emit); + sinon.assert.calledWith(connection.emit, 'events', {'events':'events'}); securityContext.should.be.a.instanceOf(HFCSecurityContext); securityContext.getEnrolledMember().should.equal(mockMember); securityContext.getEventHub().should.equal(mockEventHub); diff --git a/packages/composer-connector-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js index 2dd94ca9b5..9d1edadecc 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -82,7 +82,6 @@ class HLFConnection extends Connection { super(connectionManager, connectionProfile, businessNetworkIdentifier); const method = 'constructor'; LOG.entry(method, connectionManager, connectionProfile, businessNetworkIdentifier, connectOptions, client, chain, eventHubs, caClient); - // Validate all the arguments. if (!connectOptions) { throw new Error('connectOptions not specified'); @@ -100,7 +99,17 @@ class HLFConnection extends Connection { this.connectOptions = connectOptions; this.client = client; this.chain = chain; + this.businessNetworkIdentifier = businessNetworkIdentifier; + this.eventHubs = eventHubs; + + if (businessNetworkIdentifier) { + LOG.entry(method, 'registerChaincodeEvent', businessNetworkIdentifier, 'composer'); + eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { + this.emit('events', JSON.parse(event.payload.toString('utf8'))); + }); + } + this.caClient = caClient; // We create promisified versions of these APIs. @@ -134,6 +143,7 @@ class HLFConnection extends Connection { if (eventHub.isconnected()) { eventHub.disconnect(); } + this.eventHubs[0].unregisterChaincodeEvent(this.businessNetworkIdentifier); }); LOG.exit(method); }) diff --git a/packages/composer-connector-hlfv1/test/hlfconnection.js b/packages/composer-connector-hlfv1/test/hlfconnection.js index 1d8adc0d70..385d4a9fd1 100644 --- a/packages/composer-connector-hlfv1/test/hlfconnection.js +++ b/packages/composer-connector-hlfv1/test/hlfconnection.js @@ -92,6 +92,21 @@ describe('HLFConnection', () => { describe('#constructor', () => { + it('should subscribe to the eventHub and emit events', () => { + const events = { + payload: { + toString: () => { + return '{"event":"event"}'; + } + } + }; + connection.emit = sandbox.stub(); + mockEventHub.registerChaincodeEvent.withArgs('org.acme.biznet', 'composer', sinon.match.func).yield(events); + sinon.assert.calledOnce(mockEventHub.registerChaincodeEvent); + sinon.assert.calledWith(mockEventHub.registerChaincodeEvent, 'org.acme.biznet', 'composer', sinon.match.func); + sinon.assert.calledOnce(connection.emit); + }); + it('should throw if connectOptions not specified', () => { (() => { new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', null, mockClient, mockChain, mockEventHub, mockCAClient); @@ -128,7 +143,6 @@ describe('HLFConnection', () => { new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, mockChain, [mockEventHub], null); }).should.throw(/caClient not specified/); }); - }); describe('#getConnectionOptions', () => { diff --git a/packages/composer-connector-proxy/lib/proxyconnection.js b/packages/composer-connector-proxy/lib/proxyconnection.js index 4c0afde38a..3cc045d52c 100644 --- a/packages/composer-connector-proxy/lib/proxyconnection.js +++ b/packages/composer-connector-proxy/lib/proxyconnection.js @@ -17,7 +17,9 @@ const Connection = require('composer-common').Connection; const ProxyUtil = require('./proxyutil'); const ProxySecurityContext = require('./proxysecuritycontext'); +const Logger = require('composer-common').Logger; +const LOG = Logger.getLog('ProxyConnection'); /** * Base class representing a connection to a business network. * @protected @@ -45,6 +47,8 @@ class ProxyConnection extends Connection { * terminated, or rejected with an error. */ disconnect() { + const method = 'disconnect'; + LOG.entry(method); return new Promise((resolve, reject) => { this.socket.emit('/api/connectionDisconnect', this.connectionID, (error) => { if (error) { @@ -52,6 +56,10 @@ class ProxyConnection extends Connection { } resolve(); }); + }) + .then(() => { + this.socket.removeListener('events', () => {}); + LOG.exit(method); }); } diff --git a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js index 5ee0a5be47..9f3df3db9a 100644 --- a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js +++ b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js @@ -18,6 +18,9 @@ const ConnectionManager = require('composer-common').ConnectionManager; const ProxyConnection = require('./proxyconnection'); const ProxyUtil = require('./proxyutil'); const socketIOClient = require('socket.io-client'); +const Logger = require('composer-common').Logger; + +const LOG = Logger.getLog('ProxyConnectionManager'); let connectorServerURL = 'http://localhost:15699'; @@ -37,6 +40,20 @@ class ProxyConnectionManager extends ConnectionManager { connectorServerURL = url; } + /** + * Create a connection for ease of unit testing + * @param {ProxyConnectionManager} _this The ConnectionManaget + * @param {String} connectionProfile The connection profile to use + * @param {String} businessNetworkIdentifier The network identifier to use + * @param {Socket.io} socket The socket to use + * @param {String} connectionID The connection ID to use + * @returns {ProxyConnection} The connection + */ + static createConnection(_this, connectionProfile, businessNetworkIdentifier, socket, connectionID) { + return new ProxyConnection(_this, connectionProfile, businessNetworkIdentifier, socket, connectionID); + + } + /** * Creates a new ProxyConnectionManager * @param {ConnectionProfileManager} connectionProfileManager @@ -80,6 +97,8 @@ class ProxyConnectionManager extends ConnectionManager { * object once the connection is established, or rejected with a connection error. */ connect(connectionProfile, businessNetworkIdentifier, connectionOptions) { + const method = 'connect'; + LOG.entry(method, connectionProfile, businessNetworkIdentifier, connectionOptions); return this.ensureConnected() .then(() => { return new Promise((resolve, reject) => { @@ -87,7 +106,15 @@ class ProxyConnectionManager extends ConnectionManager { if (error) { return reject(ProxyUtil.inflaterr(error)); } - let connection = new ProxyConnection(this, connectionProfile, businessNetworkIdentifier, this.socket, connectionID); + let connection = ProxyConnectionManager.createConnection(this, connectionProfile, businessNetworkIdentifier, this.socket, connectionID); + // Only emit when client + this.socket.on('events', (myConnectionID, events) => { + LOG.debug(method, events); + if (myConnectionID === connectionID) { + connection.emit('events', events); + } + }); + LOG.exit(method); resolve(connection); }); }); diff --git a/packages/composer-connector-proxy/test/proxyconnection.js b/packages/composer-connector-proxy/test/proxyconnection.js index 4661f3751a..e10c1725f7 100644 --- a/packages/composer-connector-proxy/test/proxyconnection.js +++ b/packages/composer-connector-proxy/test/proxyconnection.js @@ -44,7 +44,8 @@ describe('ProxyConnection', () => { beforeEach(() => { mockConnectionManager = sinon.createStubInstance(ConnectionManager); mockSocket = { - emit: sinon.stub() + emit: sinon.stub(), + removeListener: sinon.stub() }; mockSocket.emit.throws(new Error('unexpected call')); connection = new ProxyConnection(mockConnectionManager, connectionProfile, businessNetworkIdentifier, mockSocket, connectionID); @@ -59,6 +60,7 @@ describe('ProxyConnection', () => { .then(() => { sinon.assert.calledOnce(mockSocket.emit); sinon.assert.calledWith(mockSocket.emit, '/api/connectionDisconnect', connectionID, sinon.match.func); + mockSocket.removeListener.withArgs('events', sinon.match.func).yield(); }); }); diff --git a/packages/composer-connector-proxy/test/proxyconnectionmanager.js b/packages/composer-connector-proxy/test/proxyconnectionmanager.js index 742a94bd2a..c7d2a7c2db 100644 --- a/packages/composer-connector-proxy/test/proxyconnectionmanager.js +++ b/packages/composer-connector-proxy/test/proxyconnectionmanager.js @@ -38,6 +38,9 @@ describe('ProxyConnectionManager', () => { let mockConnectionProfileManager; let ProxyConnectionManager; + let connectionManager; + let mockConnection; + beforeEach(() => { mockConnectionProfileManager = sinon.createStubInstance(ConnectionProfileManager); mockSocket = { @@ -45,9 +48,9 @@ describe('ProxyConnectionManager', () => { once: sinon.stub(), on: sinon.stub() }; - mockSocket.emit.throws(new Error('unexpected call')); - mockSocket.once.throws(new Error('unexpected call')); - mockSocket.on.throws(new Error('unexpected call')); + // mockSocket.emit.throws(new Error('unexpected call')); + // mockSocket.once.throws(new Error('unexpected call')); + // mockSocket.on.throws(new Error('unexpected call')); mockSocketFactory = sinon.stub().returns(mockSocket); ProxyConnectionManager = proxyquire('../lib/proxyconnectionmanager', { 'socket.io-client': mockSocketFactory @@ -128,9 +131,10 @@ describe('ProxyConnectionManager', () => { describe('#connect', () => { - let connectionManager; - beforeEach(() => { + mockConnection = sinon.createStubInstance(ProxyConnection); + mockConnection.connectionID = connectionID; + mockConnection.socket = mockSocket; mockSocket.on.withArgs('connect').returns(); mockSocket.on.withArgs('disconnect').returns(); connectionManager = new ProxyConnectionManager(mockConnectionProfileManager); @@ -139,6 +143,8 @@ describe('ProxyConnectionManager', () => { it('should send a connect call to the connector server', () => { mockSocket.emit.withArgs('/api/connectionManagerConnect', connectionProfile, businessNetworkIdentifier, connectionOptions, sinon.match.func).yields(null, connectionID); + sinon.stub(ProxyConnectionManager, 'createConnection').returns(mockConnection); + mockSocket.on.withArgs('events', sinon.match.func).yields(connectionID, [{'event': 'event1'}, {'evnet': 'event2'}]); return connectionManager.connect(connectionProfile, businessNetworkIdentifier, connectionOptions) .then((connection) => { sinon.assert.calledOnce(mockSocket.emit); @@ -146,6 +152,26 @@ describe('ProxyConnectionManager', () => { connection.should.be.an.instanceOf(ProxyConnection); connection.socket.should.equal(mockSocket); connection.connectionID.should.equal(connectionID); + sinon.assert.calledThrice(mockSocket.on); + sinon.assert.calledWith(mockSocket.on, 'events', sinon.match.func); + sinon.assert.calledWith(mockConnection.emit, 'events', [{'event': 'event1'}, {'evnet': 'event2'}]); + }); + }); + + it('should not emit events if connectionID and myConnectionID dont match', () => { + mockSocket.emit.withArgs('/api/connectionManagerConnect', connectionProfile, businessNetworkIdentifier, connectionOptions, sinon.match.func).yields(null, connectionID); + sinon.stub(ProxyConnectionManager, 'createConnection').returns(mockConnection); + mockSocket.on.withArgs('events', sinon.match.func).yields('myConnectionID', '[{"event": "event1"}, {"evnet": "event2"}]'); + return connectionManager.connect(connectionProfile, businessNetworkIdentifier, connectionOptions) + .then((connection) => { + sinon.assert.calledOnce(mockSocket.emit); + sinon.assert.calledWith(mockSocket.emit, '/api/connectionManagerConnect', connectionProfile, businessNetworkIdentifier, connectionOptions, sinon.match.func); + connection.should.be.an.instanceOf(ProxyConnection); + connection.socket.should.equal(mockSocket); + connection.connectionID.should.equal(connectionID); + sinon.assert.calledThrice(mockSocket.on); + sinon.assert.calledWith(mockSocket.on, 'events', sinon.match.func); + sinon.assert.notCalled(connection.emit); }); }); @@ -157,4 +183,11 @@ describe('ProxyConnectionManager', () => { }); + describe('#createConnection', () => { + it('should create an instance of ProxyConnection', () => { + let cm = ProxyConnectionManager.createConnection(connectionManager, 'profile', 'businessNetworkIdentifier', mockSocket, connectionID); + cm.should.be.an.instanceOf(ProxyConnection); + }); + }); + }); diff --git a/packages/composer-connector-server/lib/connectorserver.js b/packages/composer-connector-server/lib/connectorserver.js index a5d1867482..972f0f8fc2 100644 --- a/packages/composer-connector-server/lib/connectorserver.js +++ b/packages/composer-connector-server/lib/connectorserver.js @@ -123,6 +123,9 @@ class ConnectorServer { return Promise.resolve(); } delete this.connections[connectionID]; + + connection.removeListener('events', () => {}); + return connection.disconnect() .then(() => { callback(null); @@ -161,6 +164,12 @@ class ConnectorServer { callback(null, securityContextID); LOG.exit(method, securityContextID); }) + .then(() => { + connection.on('events', (events) => { + LOG.debug(method, events); + this.socket.emit('events', connectionID, events); + }); + }) .catch((error) => { LOG.error(error); callback(ConnectorServer.serializerr(error)); diff --git a/packages/composer-connector-server/test/connectorserver.js b/packages/composer-connector-server/test/connectorserver.js index 927dbb990b..4ecd700809 100644 --- a/packages/composer-connector-server/test/connectorserver.js +++ b/packages/composer-connector-server/test/connectorserver.js @@ -63,7 +63,8 @@ describe('ConnectorServer', () => { mockConnectionProfileStore.load.throws(new Error('unexpected call')); mockConnectionProfileStore.save.throws(new Error('unexpected call')); mockSocket = { - on: sinon.stub() + on: sinon.stub(), + emit: sinon.stub() }; mockConnection = sinon.createStubInstance(Connection); mockSecurityContext = sinon.createStubInstance(SecurityContext); @@ -200,7 +201,9 @@ describe('ConnectorServer', () => { const cb = sinon.stub(); return connectorServer.connectionDisconnect(connectionID, cb) .then(() => { + mockConnection.removeListener.withArgs('events', sinon.match.func).yield(['event1', 'event2']); should.equal(connectorServer.connections[connectionID], undefined); + sinon.assert.calledOnce(mockConnection.removeListener); sinon.assert.calledOnce(cb); sinon.assert.calledWith(cb, null); }); @@ -249,6 +252,9 @@ describe('ConnectorServer', () => { sinon.assert.calledOnce(cb); sinon.assert.calledWith(cb, null); connectorServer.securityContexts[securityContextID].should.equal(mockSecurityContext); + mockConnection.on.withArgs('events', sinon.match.func).yield(['event1', 'event2']); + sinon.assert.calledOnce(mockSocket.emit); + sinon.assert.calledWith(mockSocket.emit, 'events', connectionID, ['event1', 'event2']); }); }); diff --git a/packages/composer-connector-web/lib/webconnection.js b/packages/composer-connector-web/lib/webconnection.js index 747661ec5f..1658787b9f 100644 --- a/packages/composer-connector-web/lib/webconnection.js +++ b/packages/composer-connector-web/lib/webconnection.js @@ -14,6 +14,7 @@ 'use strict'; +// const Resource = require('composer-common').Resource; const Connection = require('composer-common').Connection; const Engine = require('composer-runtime').Engine; const uuid = require('uuid'); @@ -192,7 +193,7 @@ class WebConnection extends Connection { let engine = WebConnection.createEngine(container); WebConnection.addBusinessNetwork(businessNetwork.getName(), this.connectionProfile, chaincodeID); WebConnection.addChaincode(chaincodeID, container, engine); - let context = new WebContext(engine, userID); + let context = new WebContext(engine, userID, this); return businessNetwork.toArchive() .then((businessNetworkArchive) => { return engine.init(context, 'init', [businessNetworkArchive.toString('base64')]); @@ -259,7 +260,7 @@ class WebConnection extends Connection { let userID = securityContext.getUserID(); let chaincodeID = securityContext.getChaincodeID(); let chaincode = WebConnection.getChaincode(chaincodeID); - let context = new WebContext(chaincode.engine, userID); + let context = new WebContext(chaincode.engine, userID, this); return chaincode.engine.query(context, functionName, args) .then((data) => { return Buffer.from(JSON.stringify(data)); @@ -278,7 +279,7 @@ class WebConnection extends Connection { let userID = securityContext.getUserID(); let chaincodeID = securityContext.getChaincodeID(); let chaincode = WebConnection.getChaincode(chaincodeID); - let context = new WebContext(chaincode.engine, userID); + let context = new WebContext(chaincode.engine, userID, this); return chaincode.engine.invoke(context, functionName, args) .then((data) => { return undefined; diff --git a/packages/composer-connector-web/lib/webconnectionmanager.js b/packages/composer-connector-web/lib/webconnectionmanager.js index 43406c94cc..4300796b53 100644 --- a/packages/composer-connector-web/lib/webconnectionmanager.js +++ b/packages/composer-connector-web/lib/webconnectionmanager.js @@ -43,7 +43,8 @@ class WebConnectionManager extends ConnectionManager { * object once the connection is established, or rejected with a connection error. */ connect(connectionProfile, businessNetworkIdentifier, connectionOptions) { - return Promise.resolve(new WebConnection(this, connectionProfile, businessNetworkIdentifier)); + let connection = new WebConnection(this, connectionProfile, businessNetworkIdentifier); + return Promise.resolve(connection); } } diff --git a/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.ts b/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.ts index 68e83c36dc..2b9e52ccd9 100644 --- a/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.ts +++ b/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.ts @@ -192,6 +192,7 @@ export class ConnectionProfileDataComponent { this.profileUpdated.emit({updated: true}); }, (reason) => { + console.log(reason); if (reason && reason !== 1) { // someone hasn't pressed escape this.alertService.errorStatus$.next(reason); } diff --git a/packages/composer-playground/src/app/test/test.component.ts b/packages/composer-playground/src/app/test/test.component.ts index ab8616f5a8..ae4fba4cd2 100644 --- a/packages/composer-playground/src/app/test/test.component.ts +++ b/packages/composer-playground/src/app/test/test.component.ts @@ -1,3 +1,4 @@ + import { Component, OnInit } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ClientService } from '../services/client.service'; diff --git a/packages/composer-runtime-embedded/index.js b/packages/composer-runtime-embedded/index.js index d833536e23..7771b32bbd 100644 --- a/packages/composer-runtime-embedded/index.js +++ b/packages/composer-runtime-embedded/index.js @@ -18,5 +18,6 @@ module.exports.EmbeddedContainer = require('./lib/embeddedcontainer'); module.exports.EmbeddedContext = require('./lib/embeddedcontext'); module.exports.EmbeddedDataCollection = require('./lib/embeddeddatacollection'); module.exports.EmbeddedDataService = require('./lib/embeddeddataservice'); +module.exports.EmbeddedEventService = require('./lib/embeddedeventservice'); module.exports.EmbeddedIdentityService = require('./lib/embeddedidentityservice'); module.exports.EmbeddedLoggingService = require('./lib/embeddedloggingservice'); diff --git a/packages/composer-runtime-embedded/lib/embeddedcontext.js b/packages/composer-runtime-embedded/lib/embeddedcontext.js index b7433069ce..02f03fa4d8 100644 --- a/packages/composer-runtime-embedded/lib/embeddedcontext.js +++ b/packages/composer-runtime-embedded/lib/embeddedcontext.js @@ -16,6 +16,7 @@ const Context = require('composer-runtime').Context; const EmbeddedIdentityService = require('./embeddedidentityservice'); +const EmbeddedEventService = require('./embeddedeventservice'); /** * A class representing the current request being handled by the JavaScript engine. @@ -27,11 +28,13 @@ class EmbeddedContext extends Context { * Constructor. * @param {Engine} engine The owning engine. * @param {String} userID The current user ID. + * @param {EventEmitter} eventSink The event emitter */ - constructor(engine, userID) { + constructor(engine, userID, eventSink) { super(engine); this.dataService = engine.getContainer().getDataService(); this.identityService = new EmbeddedIdentityService(userID); + this.eventSink = eventSink; } /** @@ -50,6 +53,17 @@ class EmbeddedContext extends Context { return this.identityService; } + + /** + * Get the event service provided by the chaincode container. + * @return {EventService} The event service provided by the chaincode container. + */ + getEventService() { + if (!this.eventService) { + this.eventService = new EmbeddedEventService(this.eventSink); + } + return this.eventService; + } } module.exports = EmbeddedContext; diff --git a/packages/composer-runtime-embedded/lib/embeddedeventservice.js b/packages/composer-runtime-embedded/lib/embeddedeventservice.js new file mode 100644 index 0000000000..f0de0c4b49 --- /dev/null +++ b/packages/composer-runtime-embedded/lib/embeddedeventservice.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 EventService = require('composer-runtime').EventService; +const Logger = require('composer-common').Logger; + +const LOG = Logger.getLog('WebDataService'); + +/** + * Base class representing the event service provided by a {@link Container}. + * @protected + */ +class EmbeddedEventService extends EventService { + + /** + * Constructor. + * @param {EventEmitter} eventSink the event emitter + */ + constructor(eventSink) { + super(); + const method = 'constructor'; + + this.eventSink = eventSink; + + LOG.exit(method); + } + + /** + * Emit the events stored in eventBuffer + */ + commit() { + const jsonEvent = JSON.parse(this.serializeBuffer()); + this.eventSink.emit('events', jsonEvent); + } +} + +module.exports = EmbeddedEventService; diff --git a/packages/composer-runtime-embedded/test/embeddedcontext.js b/packages/composer-runtime-embedded/test/embeddedcontext.js index 2ea470790a..4200d9d81f 100644 --- a/packages/composer-runtime-embedded/test/embeddedcontext.js +++ b/packages/composer-runtime-embedded/test/embeddedcontext.js @@ -14,12 +14,14 @@ 'use strict'; +const Serializer = require('composer-common').Serializer; const Context = require('composer-runtime').Context; const DataService = require('composer-runtime').DataService; const Engine = require('composer-runtime').Engine; const EmbeddedContainer = require('..').EmbeddedContainer; const EmbeddedContext = require('..').EmbeddedContext; const IdentityService = require('composer-runtime').IdentityService; +const EventService = require('composer-runtime').EventService; require('chai').should(); const sinon = require('sinon'); @@ -28,6 +30,7 @@ describe('EmbeddedContext', () => { let mockEmbeddedContainer; let mockDataService; + let mockSerializer; let mockEngine; beforeEach(() => { @@ -36,6 +39,7 @@ describe('EmbeddedContext', () => { mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockEmbeddedContainer); mockEmbeddedContainer.getDataService.returns(mockDataService); + mockSerializer = sinon.createStubInstance(Serializer); }); describe('#constructor', () => { @@ -66,4 +70,19 @@ describe('EmbeddedContext', () => { }); + describe('#getEventService', () => { + + it('should return the container event service', () => { + let context = new EmbeddedContext(mockEngine, 'bob1'); + context.getSerializer = sinon.stub().returns(mockSerializer); + context.getEventService().should.be.an.instanceOf(EventService); + }); + + it('should return this.eventService if it is set', () => { + let context = new EmbeddedContext(mockEngine, 'bob1'); + context.eventService = {}; + context.getEventService().should.deep.equal({}); + }); + }); + }); diff --git a/packages/composer-runtime-embedded/test/embeddedeventservice.js b/packages/composer-runtime-embedded/test/embeddedeventservice.js new file mode 100644 index 0000000000..ef6c6a0f2d --- /dev/null +++ b/packages/composer-runtime-embedded/test/embeddedeventservice.js @@ -0,0 +1,59 @@ +/* + * 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 EmbeddedEventService = require('..').EmbeddedEventService; +const EventEmitter = require('events').EventEmitter; + +const chai = require('chai'); +chai.should(); +chai.use(require('chai-as-promised')); +const sinon = require('sinon'); +require('sinon-as-promised'); + +describe('EmbeddedEventService', () => { + + let eventService; + let mockEventEmitter; + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockEventEmitter = sinon.createStubInstance(EventEmitter); + eventService = new EmbeddedEventService(mockEventEmitter); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#constructor', () => { + it('should assign a default event emitter', () => { + eventService = new EmbeddedEventService(mockEventEmitter); + eventService.eventSink.should.be.an.instanceOf(EventEmitter); + }); + }); + + describe('#commit', () => { + it ('should emit a list of events', () => { + eventService.serializeBuffer = sinon.stub(); + eventService.serializeBuffer.returns('[{"event":"event"}]'); + eventService.commit(); + sinon.assert.calledOnce(eventService.serializeBuffer); + sinon.assert.calledOnce(mockEventEmitter.emit); + sinon.assert.calledWith(mockEventEmitter.emit, 'events', [{'event':'event'}]); + }); + }); +}); diff --git a/packages/composer-runtime-hlf/context.go b/packages/composer-runtime-hlf/context.go index 21629277d4..3e5369363d 100644 --- a/packages/composer-runtime-hlf/context.go +++ b/packages/composer-runtime-hlf/context.go @@ -26,6 +26,7 @@ type Context struct { This *otto.Object DataService *DataService IdentityService *IdentityService + EventService *EventService } // NewContext creates a Go wrapper around a new instance of the Context JavaScript class. @@ -52,10 +53,12 @@ func NewContext(vm *otto.Otto, engine *Engine, stub shim.ChaincodeStubInterface) // Create the services. result.DataService = NewDataService(vm, result, stub) result.IdentityService = NewIdentityService(vm, result, stub) + result.EventService = NewEventService(vm, result, stub) // Bind the methods into the JavaScript object. result.This.Set("getDataService", result.getDataService) result.This.Set("getIdentityService", result.getIdentityService) + result.This.Set("getEventService", result.getEventService) return result } @@ -75,3 +78,11 @@ func (context *Context) getIdentityService(call otto.FunctionCall) (result otto. return context.IdentityService.This.Value() } + +// getEventService ... +func (context *Context) getEventService(call otto.FunctionCall) (result otto.Value) { + logger.Debug("Entering Context.getEventService", call) + defer func() { logger.Debug("Exiting Context.getEventService", result) }() + + return context.EventService.This.Value() +} diff --git a/packages/composer-runtime-hlf/eventservice.go b/packages/composer-runtime-hlf/eventservice.go new file mode 100644 index 0000000000..7b5a278ca9 --- /dev/null +++ b/packages/composer-runtime-hlf/eventservice.go @@ -0,0 +1,80 @@ +/* + * 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. + */ + +package main + +import ( + "fmt" + + "github.com/hyperledger/fabric/core/chaincode/shim" + "github.com/robertkrimen/otto" +) + +// EventService is a go wrapper around the EventService JavaScript class +type EventService struct { + This *otto.Object + Stub shim.ChaincodeStubInterface +} + +// NewEventService creates a Go wrapper around a new instance of the EventService JavaScript class. +func NewEventService(vm *otto.Otto, context *Context, stub shim.ChaincodeStubInterface) (result *EventService) { + logger.Debug("Entering NewEventService", vm, context, &stub) + defer func() { logger.Debug("Exiting NewEventServce", result) }() + + // Create a new instance of the JavaScript chaincode class. + temp, err := vm.Call("new concerto.EventService", nil, context.This) + if err != nil { + panic(fmt.Sprintf("Failed to create new instance of EventService JavaScript class: %v", err)) + } else if !temp.IsObject() { + panic("New instance of EventService JavaScript class is not an object") + } + object := temp.Object() + + // Add a pointer to the Go object into the JavaScript object. + result = &EventService{This: temp.Object(), Stub: stub} + err = object.Set("$this", result) + if err != nil { + panic(fmt.Sprintf("Failed to store Go object in EventService JavaScript object: %v", err)) + } + + // Bind the methods into the JavaScript object. + result.This.Set("_commit", result.commit) + return result +} + +// Serializes the buffered events and emits them +func (eventService *EventService) commit(call otto.FunctionCall) (result otto.Value) { + logger.Debug("Entering EventService.commit", call) + defer func() { logger.Debug("Exiting EventService.commit", result) }() + + callback := call.Argument(0) + + value, err := call.This.Object().Call("serializeBuffer") + + if err != nil { + panic(err) + } + + if len(value.String()) > 0 { + logger.Debug("Emitting event from EventService.commit", value.String()) + eventService.Stub.SetEvent("composer", []byte(value.String())) + } + + _, err = callback.Call(callback, nil, eventService.This) + if err != nil { + panic(err) + } + + return otto.UndefinedValue() +} diff --git a/packages/composer-runtime-hlfv1/chaincode_test.go b/packages/composer-runtime-hlfv1/chaincode_test.go deleted file mode 100644 index 534d0658c9..0000000000 --- a/packages/composer-runtime-hlfv1/chaincode_test.go +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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. - */ - -package main diff --git a/packages/composer-runtime-hlfv1/context.go b/packages/composer-runtime-hlfv1/context.go index eaf4fa5349..a52e197ab0 100644 --- a/packages/composer-runtime-hlfv1/context.go +++ b/packages/composer-runtime-hlfv1/context.go @@ -24,6 +24,7 @@ type Context struct { VM *duktape.Context DataService *DataService IdentityService *IdentityService + EventService *EventService } // NewContext creates a Go wrapper around a new instance of the Context JavaScript class. @@ -40,6 +41,7 @@ func NewContext(vm *duktape.Context, engine *Engine, stub shim.ChaincodeStubInte // Create the services. result.DataService = NewDataService(vm, result, stub) result.IdentityService = NewIdentityService(vm, result, stub) + result.EventService = NewEventService(vm, result, stub) // Find the JavaScript engine object. vm.PushGlobalStash() // [ stash ] @@ -64,6 +66,8 @@ func NewContext(vm *duktape.Context, engine *Engine, stub shim.ChaincodeStubInte vm.PutPropString(-2, "getDataService") // [ stash theEngine global composer theContext ] vm.PushGoFunction(result.getIdentityService) // [ stash theEngine global composer theContext getIdentityService ] vm.PutPropString(-2, "getIdentityService") // [ stash theEngine global composer theContext ] + vm.PushGoFunction(result.getEventService) // [ stash theEngine global composer theContext getEventService ] + vm.PutPropString(-2, "getEventService") // [ stash theEngine global composer theContext ] // Return the new context. return result @@ -90,3 +94,14 @@ func (context *Context) getIdentityService(vm *duktape.Context) (result int) { vm.GetPropString(-1, "identityService") return 1 } + +// getEventService returns the event service to use. +func (context *Context) getEventService(vm *duktape.Context) (result int) { + logger.Debug("Entering Context.getEventService", vm) + defer func() { logger.Debug("Exiting Context.getEventService", result) }() + + // Return the JavaScript object from the global stash. + vm.PushGlobalStash() + vm.GetPropString(-1, "eventService") + return 1 +} diff --git a/packages/composer-runtime-hlfv1/eventservice.go b/packages/composer-runtime-hlfv1/eventservice.go new file mode 100644 index 0000000000..3dc2e15b96 --- /dev/null +++ b/packages/composer-runtime-hlfv1/eventservice.go @@ -0,0 +1,95 @@ +/* + * 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. + */ + +package main + +import ( + duktape "gopkg.in/olebedev/go-duktape.v3" + + "github.com/hyperledger/fabric/core/chaincode/shim" +) + +// EventService is a go wrapper around the EventService JavaScript class +type EventService struct { + VM *duktape.Context + Stub shim.ChaincodeStubInterface +} + +// NewEventService creates a Go wrapper around a new instance of the EventService JavaScript class. +func NewEventService(vm *duktape.Context, context *Context, stub shim.ChaincodeStubInterface) (result *EventService) { + logger.Debug("Entering NewEventService", vm, context, &stub) + defer func() { logger.Debug("Exiting NewEventServce", result) }() + + // Ensure the JavaScript stack is reset. + defer vm.SetTop(vm.GetTop()) + + // Create a new event service + result = &EventService{VM: vm, Stub: stub} + + //Create a new instance of the JavaScript EventService class. + vm.PushGlobalObject() // [ global ] + vm.GetPropString(-1, "composer") // [ global composer ] + vm.GetPropString(-1, "EventService") // [ global composer EventService ] + err := vm.Pnew(0) // [ global composer theEventService ] + if err != nil { + panic(err) + } + + // Store the event service into the global stash. + vm.PushGlobalStash() // [ global composer theEventService stash ] + vm.Dup(-2) // [ global composer theEventService stash theEventService ] + vm.PutPropString(-2, "eventService") // [ global composer theEventService stash ] + vm.Pop() // [ global composer theEventService ] + + // Bind the methods into the JavaScript object. + vm.PushGoFunction(result.commit) // [ global composer theEventService commit ] + vm.PushString("bind") // [ global composer theEventService commit "bind" ] + vm.Dup(-3) // [ global composer theEventService commit "bind" theEventService ] + vm.PcallProp(-3, 1) // [ global composer theEventService commit boundCommit ] + vm.PutPropString(-3, "_commit") // [ global composer theEventService commit ] + + // Return a new event service + return result +} + +// Serializes the buffered events and emits them +func (eventService *EventService) commit(vm *duktape.Context) (result int) { + logger.Debug("Entering EventService.commit", vm) + defer func() { logger.Debug("Exiting EventService.commit", result) }() + + // Validate the arguments from JavaScript. + vm.RequireFunction(0) + + vm.PushThis() // [ theEventService ] + vm.GetPropString(-1, "serializeBuffer") // [ theEventService, serializeBuffer ] + vm.RequireFunction(-1) // [ theEventService, serializeBuffer ] + vm.Dup(-2) // [ theEventService, serializeBuffer, theEventService ] + vm.CallMethod(0) // [ theEventService, returnValue ] + vm.RequireObjectCoercible(-1) // [ theEventService, returnValue ] + vm.JsonEncode(-1) // [ theEventService, returnValue ] + value := vm.RequireString(-1) // [ theEventService, returnValue ] + + if len(value) > 0 { + logger.Debug("Emitting event from EventService.commit", value) + eventService.Stub.SetEvent("composer", []byte(value)) + } + + // Call the callback. + vm.Dup(0) + vm.PushNull() + if vm.Pcall(1) == duktape.ExecError { + panic(vm.ToString(-1)) + } + return 0 +} diff --git a/packages/composer-runtime-web/index.js b/packages/composer-runtime-web/index.js index 1b3191ba95..62e1c82779 100644 --- a/packages/composer-runtime-web/index.js +++ b/packages/composer-runtime-web/index.js @@ -18,5 +18,6 @@ module.exports.WebContainer = require('./lib/webcontainer'); module.exports.WebContext = require('./lib/webcontext'); module.exports.WebDataCollection = require('./lib/webdatacollection'); module.exports.WebDataService = require('./lib/webdataservice'); +module.exports.WebEventService = require('./lib/webeventservice'); module.exports.WebIdentityService = require('./lib/webidentityservice'); module.exports.WebLoggingService = require('./lib/webloggingservice'); diff --git a/packages/composer-runtime-web/lib/webcontainer.js b/packages/composer-runtime-web/lib/webcontainer.js index 074945cb23..fcc13fd75f 100644 --- a/packages/composer-runtime-web/lib/webcontainer.js +++ b/packages/composer-runtime-web/lib/webcontainer.js @@ -19,6 +19,7 @@ const uuidv4 = require('uuid'); const version = require('../package.json').version; const WebDataService = require('./webdataservice'); const WebLoggingService = require('./webloggingservice'); +const WebEventService = require('./webeventservice'); /** * A class representing the chaincode container hosting the JavaScript engine. @@ -35,6 +36,7 @@ class WebContainer extends Container { this.uuid = uuid || uuidv4.v4(); this.dataService = new WebDataService(this.uuid); this.loggingService = new WebLoggingService(); + this.eventService = new WebEventService(); } /** diff --git a/packages/composer-runtime-web/lib/webcontext.js b/packages/composer-runtime-web/lib/webcontext.js index 7e6874996a..fa5b527fc2 100644 --- a/packages/composer-runtime-web/lib/webcontext.js +++ b/packages/composer-runtime-web/lib/webcontext.js @@ -16,6 +16,7 @@ const Context = require('composer-runtime').Context; const WebIdentityService = require('./webidentityservice'); +const WebEventService = require('./webeventservice'); /** * A class representing the current request being handled by the JavaScript engine. @@ -27,11 +28,13 @@ class WebContext extends Context { * Constructor. * @param {Engine} engine The owning engine. * @param {String} userID The current user ID. + * @param {EventEmitter} eventSink The event emitter */ - constructor(engine, userID) { + constructor(engine, userID, eventSink) { super(engine); this.dataService = engine.getContainer().getDataService(); this.identityService = new WebIdentityService(userID); + this.eventSink = eventSink; } /** @@ -50,6 +53,17 @@ class WebContext extends Context { return this.identityService; } + /** + * Get the event service provided by the chaincode container. + * @return {EventService} The event service provided by the chaincode container. + */ + getEventService() { + if (!this.eventService) { + this.eventService = new WebEventService(this.eventSink); + } + return this.eventService; + } + } module.exports = WebContext; diff --git a/packages/composer-runtime-web/lib/webeventservice.js b/packages/composer-runtime-web/lib/webeventservice.js new file mode 100644 index 0000000000..28ab7e94d6 --- /dev/null +++ b/packages/composer-runtime-web/lib/webeventservice.js @@ -0,0 +1,49 @@ +/* + * 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 EventService = require('composer-runtime').EventService; +const Logger = require('composer-common').Logger; + +const LOG = Logger.getLog('WebDataService'); + +/** + * Base class representing the event service provided by a {@link Container}. + * @protected + */ +class WebEventService extends EventService { + + /** + * Constructor. + * @param {EventEmitter} eventSink the event emitter + */ + constructor(eventSink) { + super(); + const method = 'constructor'; + this.eventSink = eventSink; + + LOG.exit(method); + } + + /** + * Emit the events stored in eventBuffer + */ + commit() { + const jsonEvent = JSON.parse(this.serializeBuffer()); + this.eventSink.emit('events', jsonEvent); + } +} + +module.exports = WebEventService; diff --git a/packages/composer-runtime-web/test/webcontext.js b/packages/composer-runtime-web/test/webcontext.js index 235482ce12..7f82c3736a 100644 --- a/packages/composer-runtime-web/test/webcontext.js +++ b/packages/composer-runtime-web/test/webcontext.js @@ -14,10 +14,12 @@ 'use strict'; +const Serializer = require('composer-common').Serializer; const Context = require('composer-runtime').Context; const DataService = require('composer-runtime').DataService; const Engine = require('composer-runtime').Engine; const IdentityService = require('composer-runtime').IdentityService; +const EventService = require('composer-runtime').EventService; const WebContainer = require('..').WebContainer; const WebContext = require('..').WebContext; @@ -28,6 +30,7 @@ describe('WebContext', () => { let mockWebContainer; let mockDataService; + let mockSerializer; let mockEngine; beforeEach(() => { @@ -36,6 +39,7 @@ describe('WebContext', () => { mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockWebContainer); mockWebContainer.getDataService.returns(mockDataService); + mockSerializer = sinon.createStubInstance(Serializer); }); describe('#constructor', () => { @@ -66,4 +70,19 @@ describe('WebContext', () => { }); + describe('#getEventService', () => { + + it('should return the container event service', () => { + let context = new WebContext(mockEngine, 'bob1'); + context.getSerializer = sinon.stub().returns(mockSerializer); + context.getEventService().should.be.an.instanceOf(EventService); + }); + + it('should return this.eventService if it is set', () => { + let context = new WebContext(mockEngine, 'bob1'); + context.eventService = {}; + context.getEventService().should.deep.equal({}); + }); + }); + }); diff --git a/packages/composer-runtime-web/test/webeventservice.js b/packages/composer-runtime-web/test/webeventservice.js new file mode 100644 index 0000000000..c85f397f68 --- /dev/null +++ b/packages/composer-runtime-web/test/webeventservice.js @@ -0,0 +1,61 @@ +/* + * 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 WebEventService = require('..').WebEventService; +const EventEmitter = require('events').EventEmitter; + +const chai = require('chai'); +chai.should(); +chai.use(require('chai-as-promised')); +const sinon = require('sinon'); +require('sinon-as-promised'); + +describe('WebEventService', () => { + + let eventService; + let mockEventEmitter; + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockEventEmitter = sinon.createStubInstance(EventEmitter); + eventService = new WebEventService(mockEventEmitter); + eventService.getEventEmitter = sinon.stub(); + eventService.getEventEmitter.returns(mockEventEmitter); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#constructor', () => { + it('should assign a default event emitter', () => { + eventService = new WebEventService(mockEventEmitter); + (eventService.eventSink instanceof EventEmitter).should.be.true; + }); + }); + + describe('#commit', () => { + it ('should emit a list of events', () => { + eventService.serializeBuffer = sinon.stub(); + eventService.serializeBuffer.returns('[{"event": "event"}]'); + eventService.commit(); + sinon.assert.calledOnce(eventService.serializeBuffer); + sinon.assert.calledOnce(mockEventEmitter.emit); + sinon.assert.calledWith(mockEventEmitter.emit, 'events', [{'event': 'event'}]); + }); + }); +}); diff --git a/packages/composer-runtime/index.js b/packages/composer-runtime/index.js index 999ec33662..08967a5f37 100644 --- a/packages/composer-runtime/index.js +++ b/packages/composer-runtime/index.js @@ -25,6 +25,7 @@ module.exports.Context = require('./lib/context'); module.exports.DataCollection = require('./lib/datacollection'); module.exports.DataService = require('./lib/dataservice'); module.exports.Engine = require('./lib/engine'); +module.exports.EventService = require('./lib/eventservice'); module.exports.IdentityService = require('./lib/identityservice'); module.exports.JSTransactionExecutor = require('./lib/jstransactionexecutor'); module.exports.LoggingService = require('./lib/loggingservice'); diff --git a/packages/composer-runtime/lib/api.js b/packages/composer-runtime/lib/api.js index 534f0af053..da9386eff5 100644 --- a/packages/composer-runtime/lib/api.js +++ b/packages/composer-runtime/lib/api.js @@ -35,13 +35,16 @@ class Api { /** * Constructor. * @param {Factory} factory The factory to use. + * @param {Serializer} serializer The serializer to use. * @param {Resource} participant The current participant. * @param {RegistryManager} registryManager The registry manager to use. + * @param {EventService} eventService The event service to use. + * @param {Context} context The transaction context. * @private */ - constructor(factory, participant, registryManager) { + constructor(factory, serializer, participant, registryManager, eventService, context) { const method = 'constructor'; - LOG.entry(method, factory, participant, registryManager); + LOG.entry(method, factory, serializer, participant, registryManager, eventService, context); /** * Get the factory. The factory can be used to create new instances of @@ -161,6 +164,23 @@ class Api { return result; }; + /** + * Emit an event defined in the transaction + * @method module:composer-runtime#emit + * @param {Resource} event The event to be emitted + * @public + */ + this.emit = function emit(event) { + const method = 'emit'; + LOG.entry(method); + event.setIdentifier(context.getTransaction().getIdentifier() + '#' + context.getEventNumber()); + let serializedEvent = serializer.toJSON(event); + context.incrementEventNumber(); + LOG.debug(method, event.getFullyQualifiedIdentifier(), serializedEvent); + eventService.emit(serializedEvent); + LOG.exit(method); + }; + Object.freeze(this); LOG.exit(method); } diff --git a/packages/composer-runtime/lib/api/factory.js b/packages/composer-runtime/lib/api/factory.js index 744bb4b704..be00e2570e 100644 --- a/packages/composer-runtime/lib/api/factory.js +++ b/packages/composer-runtime/lib/api/factory.js @@ -139,7 +139,7 @@ class Factory { * // Get the factory. * var factory = getFactory(); * // Create a new relationship to the vehicle. - * var record = factory.newConcept('org.acme', 'Record', 'RECORD_1'); + * var record = factory.newConcept('org.acme', 'Record'); * // Add the record to the persons array of records. * person.records.push(record); * @public @@ -154,6 +154,20 @@ class Factory { return factory.newConcept(ns, type); }; + /** + * Create a new type with a given namespace and type + * @public + * @method module:composer-runtime.Factory#newEvent + * @param {string} ns The namespace of the event. + * @param {string} type The type of the event. + * @return {Resource} The new instance of the event. + * @throws {Error} If the specified type (specified by the namespace and + * type) is not defined in the current version of the business network. + */ + this.newEvent = function newEvent(ns, type) { + return factory.newEvent(ns, type); + }; + Object.freeze(this); LOG.exit(method); } diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js index 79c3fa4299..b190ae83e9 100644 --- a/packages/composer-runtime/lib/context.js +++ b/packages/composer-runtime/lib/context.js @@ -68,6 +68,7 @@ class Context { this.accessController = null; this.sysregistries = null; this.sysidentities = null; + this.eventNumber = 0; } /** @@ -237,6 +238,15 @@ class Context { throw new Error('abstract function called'); } + /** + * Get the event service provided by the chaincode container. + * @abstract + * @return {EventService} The event service provided by the chaincode container. + */ + getEventService() { + throw new Error('abstract function called'); + } + /** * Get the model manager. * @return {ModelManager} The model manager. @@ -331,7 +341,7 @@ class Context { */ getApi() { if (!this.api) { - this.api = new Api(this.getFactory(), this.getParticipant(), this.getRegistryManager()); + this.api = new Api(this.getFactory(), this.getSerializer(), this.getParticipant(), this.getRegistryManager(), this.getEventService(), this); } return this.api; } @@ -463,6 +473,22 @@ class Context { return this.sysidentities; } + /** + * Get the next event number + * @return {integer} the event number. + */ + getEventNumber() { + return this.eventNumber; + } + + /** + * Incrememnt the event number by 1 + * @return {integer} the event number. + */ + incrementEventNumber() { + return this.eventNumber++; + } + /** * Stop serialization of this object. * @return {Object} An empty object. diff --git a/packages/composer-runtime/lib/engine.transactions.js b/packages/composer-runtime/lib/engine.transactions.js index e3e3ab2f42..397021dd55 100644 --- a/packages/composer-runtime/lib/engine.transactions.js +++ b/packages/composer-runtime/lib/engine.transactions.js @@ -93,6 +93,10 @@ class EngineTransactions { LOG.debug(method, 'Storing executed transaction in transaction registry'); return transactionRegistry.add(transaction); + }) + .then(() => { + // Commit all transactions + return context.getEventService().commit(); }); } diff --git a/packages/composer-runtime/lib/eventservice.js b/packages/composer-runtime/lib/eventservice.js new file mode 100644 index 0000000000..b3bf518443 --- /dev/null +++ b/packages/composer-runtime/lib/eventservice.js @@ -0,0 +1,95 @@ +/* + * 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 Logger = require('composer-common').Logger; +const LOG = Logger.getLog('EventService'); + +/** + * Base class representing the event service provided by a {@link Container}. + * @protected + * @abstract + * @memberof module:composer-runtime + */ +class EventService { + + /** + * Constructor. + */ + constructor() { + this.eventBuffer = []; + } + + /** + * Add an event to the buffer + * @param {Resource} event The event to be emitted + * when complete, or rejected with an error. + */ + emit(event) { + const method = 'emit'; + LOG.entry(method, event); + this.eventBuffer.push(event); + LOG.debug(method, this.eventBuffer); + LOG.exit(method); + } + + /** + * Emit all buffered events + * @abstract + * @return {Promise} A promise that will be resolved with a {@link DataCollection} + */ + commit() { + return new Promise((resolve, reject) => { + this._commit((error) => { + if (error) { + return reject(error); + } + return resolve(); + }); + }); + } + + /** + * Emit all buffered events + * @abstract + * + * @param {commitCallback} callback The callback function to call when complete. + */ + _commit(callback) { + throw new Error('abstract function called'); + } + + /** + * Get an array of events as a string + * @return {String} - An array of serialized events + */ + serializeBuffer() { + const method = 'serializeBuffer'; + LOG.entry(method); + LOG.exit(method, this.eventBuffer); + return JSON.stringify(this.eventBuffer); + } + + /** + * Stop serialization of this object. + * @return {Object} An empty object. + */ + toJSON() { + return {}; + } + +} + +module.exports = EventService; diff --git a/packages/composer-runtime/test/api.js b/packages/composer-runtime/test/api.js index 9cd7cd4706..069da4db67 100644 --- a/packages/composer-runtime/test/api.js +++ b/packages/composer-runtime/test/api.js @@ -22,6 +22,9 @@ const realFactory = require('composer-common').Factory; const Registry = require('../lib/registry'); const RegistryManager = require('../lib/registrymanager'); const Resource = require('composer-common').Resource; +const EventService = require('../lib/eventservice'); +const Context = require('../lib/context'); +const Serializer = require('composer-common').Serializer; const chai = require('chai'); chai.should(); @@ -33,15 +36,21 @@ require('sinon-as-promised'); describe('Api', () => { let mockFactory; + let mockSerializer; let mockParticipant; let mockRegistryManager; + let mockEventService; + let mockContext; let api; beforeEach(() => { mockFactory = sinon.createStubInstance(realFactory); + mockSerializer = sinon.createStubInstance(Serializer); mockParticipant = sinon.createStubInstance(Resource); mockRegistryManager = sinon.createStubInstance(RegistryManager); - api = new Api(mockFactory, mockParticipant, mockRegistryManager); + mockEventService = sinon.createStubInstance(EventService); + mockContext = sinon.createStubInstance(Context); + api = new Api(mockFactory, mockSerializer, mockParticipant, mockRegistryManager, mockEventService, mockContext); }); describe('#constructor', () => { @@ -106,4 +115,23 @@ describe('Api', () => { }); + describe('#emit', () => { + let mockTransaction; + let mockEvent; + + beforeEach(() => { + mockTransaction = sinon.createStubInstance(Resource); + mockEvent = sinon.createStubInstance(Resource); + mockTransaction.getIdentifier.returns('much.wow'); + mockContext.getTransaction.returns(mockTransaction); + mockContext.getEventNumber.returns(0); + }); + + it('should call eventService.emit', () => { + api.emit(mockEvent); + sinon.assert.calledOnce(mockEventService.emit); + // sinon.assert.calledWith(mockEventService.emit, mockEvent); + }); + }); + }); diff --git a/packages/composer-runtime/test/api/factory.js b/packages/composer-runtime/test/api/factory.js index f413101cdc..fa1ed5e5ff 100644 --- a/packages/composer-runtime/test/api/factory.js +++ b/packages/composer-runtime/test/api/factory.js @@ -91,4 +91,13 @@ describe('AssetRegistry', () => { }); + describe('#newEvent', () => { + + it('should proxy to the registry', () => { + mockFactory.newEvent.withArgs('org.acme', 'Doge').returns(mockResource); + factory.newEvent('org.acme', 'Doge').should.equal(mockResource); + }); + + }); + }); diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js index 88ae5c427a..d0e350b9eb 100644 --- a/packages/composer-runtime/test/context.js +++ b/packages/composer-runtime/test/context.js @@ -22,6 +22,7 @@ const Context = require('../lib/context'); const DataCollection = require('../lib/datacollection'); const DataService = require('../lib/dataservice'); const Engine = require('../lib/engine'); +const EventService = require('../lib/eventservice'); const Factory = require('composer-common').Factory; const IdentityManager = require('../lib/identitymanager'); const IdentityService = require('../lib/identityservice'); @@ -45,12 +46,14 @@ require('sinon-as-promised'); describe('Context', () => { + let mockBusinessNetworkDefinition; let mockEngine; let context; let sandbox; beforeEach(() => { mockEngine = sinon.createStubInstance(Engine); + mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); context = new Context(mockEngine); sandbox = sinon.sandbox.create(); }); @@ -266,6 +269,16 @@ describe('Context', () => { }); + describe('#getEventService', () => { + + it('should throw as abstract method', () => { + (() => { + context.getEventService(); + }).should.throw(/abstract function called/); + }); + + }); + describe('#getModelManager', () => { it('should throw if not initialized', () => { @@ -419,6 +432,9 @@ describe('Context', () => { sinon.stub(context, 'getParticipant').returns(mockParticipant); let mockRegistryManager = sinon.createStubInstance(RegistryManager); sinon.stub(context, 'getRegistryManager').returns(mockRegistryManager); + let mockEventService = sinon.createStubInstance(EventService); + sinon.stub(context, 'getEventService').returns(mockEventService); + context.businessNetworkDefinition = mockBusinessNetworkDefinition; context.getApi().should.be.an.instanceOf(Api); }); @@ -648,6 +664,19 @@ describe('Context', () => { }); + describe('#getEventNumber', () => { + it('should get the current event number', () => { + context.getEventNumber().should.equal(0); + }); + }); + + describe('#incrementEventNumber', () => { + it('should get the incremenet current event number', () => { + context.incrementEventNumber(); + context.getEventNumber().should.equal(1); + }); + }); + describe('#toJSON', () => { it('should return an empty object', () => { diff --git a/packages/composer-runtime/test/engine.transactions.js b/packages/composer-runtime/test/engine.transactions.js index be9ab1c5d7..e76e59d23f 100644 --- a/packages/composer-runtime/test/engine.transactions.js +++ b/packages/composer-runtime/test/engine.transactions.js @@ -18,6 +18,7 @@ const Api = require('../lib/api'); const Container = require('../lib/container'); const Context = require('../lib/context'); const Engine = require('../lib/engine'); +const EventService = require('../lib/eventservice'); const LoggingService = require('../lib/loggingservice'); const Registry = require('../lib/registry'); const RegistryManager = require('../lib/registrymanager'); @@ -38,6 +39,7 @@ describe('EngineTransactions', () => { let mockContainer; let mockLoggingService; + let mockEventService; let mockContext; let engine; let mockRegistryManager; @@ -71,6 +73,8 @@ describe('EngineTransactions', () => { mockContext.getTransactionExecutors.returns([mockTransactionExecutor]); mockRegistry = sinon.createStubInstance(Registry); mockRegistryManager.get.withArgs('Transaction', 'default').resolves(mockRegistry); + mockEventService = sinon.createStubInstance(EventService); + mockContext.getEventService.returns(mockEventService); }); describe('#submitTransaction', () => { @@ -114,6 +118,7 @@ describe('EngineTransactions', () => { should.equal(transaction.$resolved, undefined); return true; })); + sinon.assert.calledOnce(mockEventService.commit); }); }); @@ -160,6 +165,7 @@ describe('EngineTransactions', () => { should.equal(transaction.$resolved, undefined); return true; })); + sinon.assert.calledOnce(mockEventService.commit); }); }); diff --git a/packages/composer-runtime/test/eventservice.js b/packages/composer-runtime/test/eventservice.js new file mode 100644 index 0000000000..f2a8d437f1 --- /dev/null +++ b/packages/composer-runtime/test/eventservice.js @@ -0,0 +1,98 @@ +/* + * 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 EventService = require('../lib/eventservice'); +const Serializer = require('composer-common').Serializer; + +const should = require('chai').should(); +require('chai-as-promised'); +const sinon = require('sinon'); + +describe('EventService', () => { + + let mockSerializer; + let eventService; + + beforeEach(() => { + mockSerializer = sinon.createStubInstance(Serializer); + eventService = new EventService(mockSerializer); + }); + + describe('#constructor', () => { + it('should have a property for buffering events', () => { + should.exist(eventService.eventBuffer); + }); + }); + + describe('#emit', () => { + it('should add the event to the data buffer', () => { + eventService.emit({}); + eventService.eventBuffer[0].should.deep.equal({}); + }); + }); + + describe('#eventService', () => { + + it('should call _commit and handle no error', () => { + sinon.stub(eventService, '_commit').yields(null, {}); + return eventService.commit() + .then(() => { + sinon.assert.calledWith(eventService._commit); + }); + }); + + + it('should call _commit and handle an error', () => { + sinon.stub(eventService, '_commit').yields(new Error('error')); + return eventService.commit() + .then((result) => { + throw new Error('should not get here'); + }) + .catch((error) => { + sinon.assert.calledWith(eventService._commit); + error.should.match(/error/); + }); + }); + + }); + + describe('#_commit', () => { + + it('should throw as abstract method', () => { + (() => { + eventService._commit(); + }).should.throw(/abstract function called/); + }); + + }); + + describe('#serializeBuffer', () => { + it('should return the list of events that are to be comitted', () => { + let event = {'$class': 'much.wow'}; + eventService.eventBuffer = [ event ]; + + eventService.serializeBuffer().should.equal('[{"$class":"much.wow"}]'); + }); + }); + + describe('#toJSON', () => { + + it('should return an empty object', () => { + eventService.toJSON().should.deep.equal({}); + }); + + }); + +}); diff --git a/packages/composer-runtime/test/jstransactionexecutor.js b/packages/composer-runtime/test/jstransactionexecutor.js index 65fd096b79..3e828f2955 100644 --- a/packages/composer-runtime/test/jstransactionexecutor.js +++ b/packages/composer-runtime/test/jstransactionexecutor.js @@ -20,6 +20,7 @@ const JSTransactionExecutor = require('../lib/jstransactionexecutor'); const ModelManager = require('composer-common').ModelManager; const RegistryManager = require('../lib/registrymanager'); const ScriptManager = require('composer-common').ScriptManager; +const Serializer = require('composer-common').Serializer; const chai = require('chai'); chai.should(); @@ -37,6 +38,7 @@ describe('JSTransactionExecutor', () => { let participant; let scriptManager; let mockRegistryManager; + let mockSerializer; let api; beforeEach(() => { @@ -59,7 +61,8 @@ describe('JSTransactionExecutor', () => { participant = factory.newResource('org.acme', 'TestParticipant', '1'); scriptManager = new ScriptManager(modelManager); mockRegistryManager = sinon.createStubInstance(RegistryManager); - api = new Api(factory, participant, mockRegistryManager); + mockSerializer = sinon.createStubInstance(Serializer); + api = new Api(factory, mockSerializer, participant, mockRegistryManager); }); afterEach(() => {