From 8da28cb51718b5bf8c35111d5126f7f6dafcafaa Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Mon, 20 Mar 2017 17:02:57 +0000 Subject: [PATCH 01/41] Added unit tests for add file component --- packages/composer-playground/package.json | 1 + .../app/add-file/add-file.component.spec.ts | 172 ++++++++++++++---- .../src/app/add-file/add-file.component.ts | 9 +- .../src/app/app.component.ts | 58 +++--- .../src/app/services/about.service.spec.ts | 117 ++++++------ .../src/app/services/admin.service.ts | 10 +- packages/composer-playground/tslint.json | 20 +- 7 files changed, 247 insertions(+), 140 deletions(-) diff --git a/packages/composer-playground/package.json b/packages/composer-playground/package.json index 4a379f876a..36c6666db3 100644 --- a/packages/composer-playground/package.json +++ b/packages/composer-playground/package.json @@ -74,6 +74,7 @@ "dependencies": { "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.20", "cheerio": "^0.22.0", + "codelyzer": "^2.0.1", "composer-common": "^0.5.6", "composer-playground-api": "^0.5.6", "express": "^4.15.2", diff --git a/packages/composer-playground/src/app/add-file/add-file.component.spec.ts b/packages/composer-playground/src/app/add-file/add-file.component.spec.ts index bcb6e396b7..8fc8cc9b11 100644 --- a/packages/composer-playground/src/app/add-file/add-file.component.spec.ts +++ b/packages/composer-playground/src/app/add-file/add-file.component.spec.ts @@ -1,26 +1,27 @@ -import {ComponentFixture, TestBed, async} from '@angular/core/testing'; -import {By} from '@angular/platform-browser'; -import {DebugElement} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import {BehaviorSubject, Subject} from 'rxjs/Rx'; +import { BehaviorSubject, Subject } from 'rxjs/Rx'; -import {BusinessNetworkDefinition, AdminConnection} from 'composer-admin'; -import {ModelFile} from 'composer-common'; +import { BusinessNetworkDefinition, AdminConnection } from 'composer-admin'; +import { ModelFile, ModelManager, ScriptManager, Script } from 'composer-common'; -import {AddFileComponent} from './add-file.component'; -import {FileImporterComponent} from './../file-importer'; -import {FileDragDropDirective} from './../directives/file-drag-drop'; +import { AddFileComponent } from './add-file.component'; +import { FileImporterComponent } from './../file-importer'; +import { FileDragDropDirective } from './../directives/file-drag-drop'; -import {AdminService} from '../services/admin.service'; -import {ClientService} from '../services/client.service'; -import {AlertService} from '../services/alert.service'; +import { AdminService } from '../services/admin.service'; +import { ClientService } from '../services/client.service'; +import { AlertService } from '../services/alert.service'; import * as sinon from 'sinon'; -const fs = require('fs'); +import { expect } from 'chai'; +const fs = require('fs'); class MockAdminService { constructor() { @@ -62,10 +63,14 @@ class MockAlertService { public busyStatus$: Subject = new BehaviorSubject(null); } -describe('AddFileComponent', () => { +describe('AddFileComponent', () => { + let sandbox; let component: AddFileComponent; let fixture: ComponentFixture; + let mockBusinessNetwork; + let mockModelManager; + let mockScriptManager; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -78,17 +83,29 @@ describe('AddFileComponent', () => { FormsModule ], providers: [ - {provide: AdminService, useClass: MockAdminService}, - {provide: AlertService, useClass: MockAlertService}, + { provide: AdminService, useClass: MockAdminService }, + { provide: AlertService, useClass: MockAlertService }, NgbActiveModal ] }); + sandbox = sinon.sandbox.create(); + fixture = TestBed.createComponent(AddFileComponent); component = fixture.componentInstance; + + mockModelManager = sinon.createStubInstance(ModelManager); + mockScriptManager = sinon.createStubInstance(ScriptManager); + mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + mockBusinessNetwork.getModelManager.returns(mockModelManager); + mockBusinessNetwork.getScriptManager.returns(mockScriptManager); })); + afterEach(async(() => { + sandbox.restore(); + })); + describe('#fileDetected', () => { it('should change this.expandInput to true', () => { component.fileDetected(); @@ -104,42 +121,119 @@ describe('AddFileComponent', () => { }); describe('#fileAccepted', () => { - it('should set this.currentFile to a ModelFile', async(() => { - + it('should call this.createModel', async(() => { let b = new Blob(['/**CTO File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.cto'); let createMock = sinon.stub(component, 'createModel'); - let dataBufferMock = sinon.stub(component, 'getDataBuffer').returns(Promise.resolve('some data')); + let dataBufferMock = sinon.stub(component, 'getDataBuffer') + .returns(Promise.resolve('some data')); - component.fileAccepted(file) - .then(() => { - component.createModel.should.have.been.called; - }); + component.fileAccepted(file); + createMock.called; })); - // it('should set this.currentFile to a ScriptFile', () => { - // spyOn(component, 'createScript'); - // // component.fileRejected = jasmine.createSpy('fileRejected spy') - // let file = new File(['/**CTO File*/'], 'newfile.cto'); - // component.fileAccepted(file); - // expect(component.createScript).toHaveBeenCalled(); - // }); + it('should call this.createScript', async(() => { - it('should set currentFile name to the name of the imported file', () => { + let b = new Blob(['/**JS File*/'], {type: 'text/plain'}); + let file = new File([b], 'newfile.js'); - }); + let createMock = sinon.stub(component, 'createScript'); + let dataBufferMock = sinon.stub(component, 'getDataBuffer') + .returns(Promise.resolve('some data')); + + component.fileAccepted(file); + createMock.called; + })); + + it('should call this.fileRejected when there is an error reading the file', async(() => { + + let b = new Blob(['/**CTO File*/'], {type: 'text/plain'}); + let file = new File([b], 'newfile.cto'); + + let createMock = sinon.stub(component, 'fileRejected'); + let dataBufferMock = sinon.stub(component, 'getDataBuffer') + .returns(Promise.reject('some data')); + + component.fileAccepted(file); + createMock.called; + })); + + it('should throw when given incorrect file type', async(() => { + + let b = new Blob(['/**PNG File*/'], {type: 'text/plain'}); + let file = new File([b], 'newfile.png'); + + let createMock = sinon.stub(component, 'fileRejected'); + let dataBufferMock = sinon.stub(component, 'getDataBuffer') + .returns(Promise.resolve('some data')); + + component.fileAccepted(file); + createMock.called; + })); }); describe('#fileRejected', () => { - it('should return an error status from the admin', () => { + it('should return an error status', async(() => { + component.fileRejected('long reason to reject file'); + + component['alertService'].errorStatus$.subscribe( + message => { + expect(message).to.be.equal('long reason to reject file'); + } + ); + })); + }); - }); + describe('#createScript', () => { + it('should create a new script file', async(() => { + component.businessNetwork = mockBusinessNetwork; + let mockScript = sinon.createStubInstance(Script); + mockScript.getIdentifier.returns('newfile.js'); + mockScriptManager.createScript.returns(mockScript); + + let b = new Blob(['/**JS File*/'], {type: 'text/plain'}); + let file = new File([b], 'newfile.js'); + + component.createScript(file, file); + component.fileType.should.equal('js'); + component.currentFile.should.deep.equal(mockScript); + component.currentFileName = mockScript.getIdentifier(); + })); }); - describe('#changeCurrentFileType', () => { - it('should change this.currentFileType', () => { + // describe('#createModel', () => { + // it('should create a new model file', async(() => { + // component.businessNetwork = mockBusinessNetwork; + // let mockModel = sinon.createStubInstance(ModelFile); + // mockModel.getFileName.returns('newfile.cto'); + // let b = new Blob([ + // `/**CTO File**/ + // namespace test` + // ], {type: 'text/plain'}); + // let file = new File([b], 'newfile.cto'); + + // component.createModel(file, file); + // component.fileType.should.equal('cto'); + // component.currentFile.should.deep.equal(mockModel); + // component.currentFileName = mockModel.getIdentifier(); + // })); + // }); - }); + describe('#changeCurrentFileType', () => { + it('should change this.currentFileType to a js file', async(() => { + let mockScript = sinon.createStubInstance(Script); + mockScript.getIdentifier.returns('script.js'); + mockScriptManager.getScripts.returns([]); + mockScriptManager.createScript.returns(mockScript); + + component.fileType = 'js'; + component.addScriptFileExtension = 'js'; + component.businessNetwork = mockBusinessNetwork; + + component.changeCurrentFileType(); + component.currentFileName.should.equal('script.js'); + component.currentFile.should.deep.equal(mockScript); + })); }); }); diff --git a/packages/composer-playground/src/app/add-file/add-file.component.ts b/packages/composer-playground/src/app/add-file/add-file.component.ts index d6f0459421..be7f4d71b3 100644 --- a/packages/composer-playground/src/app/add-file/add-file.component.ts +++ b/packages/composer-playground/src/app/add-file/add-file.component.ts @@ -53,21 +53,22 @@ export class AddFileComponent implements OnInit { this.expandInput = false; } - fileAccepted(file: File): Promise { + fileAccepted(file: File) { let type = file.name.substr(file.name.lastIndexOf('.') + 1); - return this.getDataBuffer(file) + this.getDataBuffer(file) .then((data) => { switch (type) { case 'js': + this.expandInput = true; this.createScript(file, data); break; case 'cto': + this.expandInput = true; this.createModel(file, data); break; default: throw new Error('Unexpected File Type'); } - this.expandInput = true; }) .catch((err) => { this.fileRejected(err); @@ -98,7 +99,7 @@ export class AddFileComponent implements OnInit { createModel(file: File, dataBuffer) { this.fileType = 'cto'; - let modelManager = this.businessNetwork.getScriptManager(); + let modelManager = this.businessNetwork.getModelManager(); this.currentFile = new ModelFile(modelManager, dataBuffer.toString(), file.name || this.addModelFileName); this.currentFileName = this.currentFile.getFileName(); } diff --git a/packages/composer-playground/src/app/app.component.ts b/packages/composer-playground/src/app/app.component.ts index 0c1ebea6e4..fb28a56a71 100644 --- a/packages/composer-playground/src/app/app.component.ts +++ b/packages/composer-playground/src/app/app.component.ts @@ -75,35 +75,35 @@ export class AppComponent { console.log('Initial App State', this.appState.state); this.subs = [ - this.alertService.busyStatus$.subscribe((busyStatus) => { - this.onBusyStatus(busyStatus); - }), - this.alertService.errorStatus$.subscribe((errorStatus) => { - this.onErrorStatus(errorStatus); - }), - this.alertService.successStatus$.subscribe((successStatus) => { - this.onSuccessStatus(successStatus); - }), - this.adminService.connectionProfileChanged$.subscribe(() => { - this.updateConnectionData(); - }), - this.route.queryParams.subscribe((queryParams) => { - this.queryParamsUpdated(queryParams); - }), - this.router.events.filter(e => e instanceof NavigationEnd).subscribe((e) => { - if(e.url === '/') { - this.openWelcomeModal(); - } - else{ - return this.checkVersion().then((success)=>{ - if(!success){ - this.openVersionModal(); - } - }); - } - - }) - ]; + this.alertService.busyStatus$.subscribe((busyStatus) => { + this.onBusyStatus(busyStatus); + }), + this.alertService.errorStatus$.subscribe((errorStatus) => { + this.onErrorStatus(errorStatus); + }), + this.alertService.successStatus$.subscribe((successStatus) => { + this.onSuccessStatus(successStatus); + }), + this.adminService.connectionProfileChanged$.subscribe(() => { + this.updateConnectionData(); + }), + this.route.queryParams.subscribe((queryParams) => { + this.queryParamsUpdated(queryParams); + }), + this.router.events.filter(e => e instanceof NavigationEnd).subscribe((e) => { + if(e.url === '/') { + this.openWelcomeModal(); + } + else{ + return this.checkVersion().then((success)=>{ + if(!success){ + this.openVersionModal(); + } + }); + } + + }) + ]; } diff --git a/packages/composer-playground/src/app/services/about.service.spec.ts b/packages/composer-playground/src/app/services/about.service.spec.ts index 907acee8cf..cf2d6ff7d5 100644 --- a/packages/composer-playground/src/app/services/about.service.spec.ts +++ b/packages/composer-playground/src/app/services/about.service.spec.ts @@ -10,73 +10,72 @@ import { import { MockBackend } from '@angular/http/testing'; const mockResponse = { - 'name': 'composer-playground', - 'version': '1', - 'dependencies': { - 'composer-admin': { - 'version': '2' - }, - 'composer-client': { - 'version': '3' - }, - 'composer-common': { - 'version': '4' - } + 'name': 'composer-playground', + 'version': '1', + 'dependencies': { + 'composer-admin': { + 'version': '2' + }, + 'composer-client': { + 'version': '3' + }, + 'composer-common': { + 'version': '4' } + } }; const expectedResponse = { - 'playground': { - name: 'playground', - version: '1' - }, - 'common': { - name: 'composer-common', - version: '2' - }, - 'client': { - name: 'composer-client', - version: '3' - }, - 'admin': { - name: 'composer-admin', - version: '4' - } + 'playground': { + name: 'playground', + version: '1' + }, + 'common': { + name: 'composer-common', + version: '2' + }, + 'client': { + name: 'composer-client', + version: '3' + }, + 'admin': { + name: 'composer-admin', + version: '4' + } }; describe('AboutService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpModule], + providers: [ + AboutService, + { provide: XHRBackend, useClass: MockBackend } + ] + }); + }); - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpModule], - providers: [ - AboutService, - { provide: XHRBackend, useClass: MockBackend } - ] - }); + it ('it should make an http call to retrieve the json that shows the versions', + async(inject([AboutService, XHRBackend], (aboutService, mockBackend) => { + // setup a mocked response + mockBackend.connections.subscribe((connection) => { + connection.mockRespond(new Response(new ResponseOptions({ + body: mockResponse + }))); }); - it ('it should make an http call to retrieve the json that shows the versions', - async(inject([AboutService, XHRBackend], (aboutService, mockBackend) => { - // setup a mocked response - mockBackend.connections.subscribe((connection) => { - connection.mockRespond(new Response(new ResponseOptions({ - body: mockResponse - }))); - }); - - // make the call to the service which was injected - let result = aboutService.getVersions() - .then((versions) => { - versions.playground.name.should.equal('playground'); - versions.playground.version.should.equal('1'); - versions.admin.name.should.equal('composer-admin'); - versions.admin.version.should.equal('2'); - versions.client.name.should.equal('composer-client'); - versions.client.version.should.equal('3'); - versions.common.name.should.equal('composer-common'); - versions.common.version.should.equal('4'); - }); - return result; - }))); + // make the call to the service which was injected + let result = aboutService.getVersions() + .then((versions) => { + versions.playground.name.should.equal('playground'); + versions.playground.version.should.equal('1'); + versions.admin.name.should.equal('composer-admin'); + versions.admin.version.should.equal('2'); + versions.client.name.should.equal('composer-client'); + versions.client.version.should.equal('3'); + versions.common.name.should.equal('composer-common'); + versions.common.version.should.equal('4'); + }); + return result; + }))); }); diff --git a/packages/composer-playground/src/app/services/admin.service.ts b/packages/composer-playground/src/app/services/admin.service.ts index 6b253e25d5..132ede57f3 100644 --- a/packages/composer-playground/src/app/services/admin.service.ts +++ b/packages/composer-playground/src/app/services/admin.service.ts @@ -15,15 +15,19 @@ import WebConnectionManager = require('composer-connector-web'); @Injectable() export class AdminService { + public connectionProfileChanged$: Subject = new BehaviorSubject(null); + private adminConnection: AdminConnection = null; private isConnected: boolean = false; private connectingPromise: Promise = null; private initialDeploy: boolean = false; private config: any = null; - public connectionProfileChanged$: Subject = new BehaviorSubject(null); - - constructor(private connectionProfileService: ConnectionProfileService, private walletService: WalletService, private identityService: IdentityService, private http: Http, private alertService: AlertService) { + constructor(private connectionProfileService: ConnectionProfileService, + private walletService: WalletService, + private identityService: IdentityService, + private http: Http, + private alertService: AlertService) { Logger.setFunctionalLogger({ log: () => { } diff --git a/packages/composer-playground/tslint.json b/packages/composer-playground/tslint.json index 4cbb76a4f7..5685d102f3 100644 --- a/packages/composer-playground/tslint.json +++ b/packages/composer-playground/tslint.json @@ -11,10 +11,10 @@ "variables-before-functions" ], "no-any": false, - "no-inferrable-types": false, + "no-inferrable-types": [ false ], "no-internal-module": true, "no-var-requires": false, - "typedef": false, + "typedef": [ false ], "typedef-whitespace": [ true, { @@ -57,7 +57,7 @@ "no-shadowed-variable": true, "no-string-literal": false, "no-switch-case-fall-through": true, - "no-unreachable": true, + // "no-unreachable": true, "no-unused-expression": true, "no-unused-variable": false, "no-use-before-declare": true, @@ -88,15 +88,23 @@ } ], - "align": false, + "align": [ + true, + "parameters", + "statements" + ], "class-name": true, "comment-format": [ true, "check-space" ], - "interface-name": false, + "interface-name": [ + false + ], "jsdoc-format": true, - "no-consecutive-blank-lines": false, + "no-consecutive-blank-lines": [ + false + ], "one-line": [ false, "check-open-brace", From 838b047f4ede88b5c107b1b9b7eb6c89e76a5348 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Tue, 21 Mar 2017 17:35:34 +0000 Subject: [PATCH 02/41] Finished add-file component test and edit import to clear stubs --- .../app/add-file/add-file.component.spec.ts | 168 ++++++++++++++---- .../src/app/add-file/add-file.component.ts | 5 +- .../src/app/import/import.component.spec.ts | 20 ++- 3 files changed, 147 insertions(+), 46 deletions(-) diff --git a/packages/composer-playground/src/app/add-file/add-file.component.spec.ts b/packages/composer-playground/src/app/add-file/add-file.component.spec.ts index 8fc8cc9b11..a68e494722 100644 --- a/packages/composer-playground/src/app/add-file/add-file.component.spec.ts +++ b/packages/composer-playground/src/app/add-file/add-file.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed, async, fakeAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { FormsModule } from '@angular/forms'; @@ -72,7 +72,7 @@ describe('AddFileComponent', () => { let mockModelManager; let mockScriptManager; - beforeEach(async(() => { + beforeEach(() => { TestBed.configureTestingModule({ declarations: [ FileImporterComponent, @@ -99,12 +99,12 @@ describe('AddFileComponent', () => { mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); mockBusinessNetwork.getModelManager.returns(mockModelManager); mockBusinessNetwork.getScriptManager.returns(mockScriptManager); - })); + }); - afterEach(async(() => { + afterEach(() => { sandbox.restore(); - })); + }); describe('#fileDetected', () => { it('should change this.expandInput to true', () => { @@ -116,7 +116,7 @@ describe('AddFileComponent', () => { describe('#fileLeft', () => { it('should change this.expectedInput to false', () => { component.fileLeft(); - component.expandInput.should.equal(false) + component.expandInput.should.equal(false); }); }); @@ -125,52 +125,52 @@ describe('AddFileComponent', () => { let b = new Blob(['/**CTO File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.cto'); - let createMock = sinon.stub(component, 'createModel'); - let dataBufferMock = sinon.stub(component, 'getDataBuffer') + let createMock = sandbox.stub(component, 'createModel'); + let dataBufferMock = sandbox.stub(component, 'getDataBuffer') .returns(Promise.resolve('some data')); component.fileAccepted(file); createMock.called; })); - it('should call this.createScript', async(() => { + it('should call this.createScript', () => { let b = new Blob(['/**JS File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.js'); - let createMock = sinon.stub(component, 'createScript'); - let dataBufferMock = sinon.stub(component, 'getDataBuffer') + let createMock = sandbox.stub(component, 'createScript'); + let dataBufferMock = sandbox.stub(component, 'getDataBuffer') .returns(Promise.resolve('some data')); component.fileAccepted(file); createMock.called; - })); + }); - it('should call this.fileRejected when there is an error reading the file', async(() => { + it('should call this.fileRejected when there is an error reading the file', () => { let b = new Blob(['/**CTO File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.cto'); - let createMock = sinon.stub(component, 'fileRejected'); - let dataBufferMock = sinon.stub(component, 'getDataBuffer') + let createMock = sandbox.stub(component, 'fileRejected'); + let dataBufferMock = sandbox.stub(component, 'getDataBuffer') .returns(Promise.reject('some data')); component.fileAccepted(file); createMock.called; - })); + }); - it('should throw when given incorrect file type', async(() => { + it('should throw when given incorrect file type', () => { let b = new Blob(['/**PNG File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.png'); - let createMock = sinon.stub(component, 'fileRejected'); - let dataBufferMock = sinon.stub(component, 'getDataBuffer') + let createMock = sandbox.stub(component, 'fileRejected'); + let dataBufferMock = sandbox.stub(component, 'getDataBuffer') .returns(Promise.resolve('some data')); component.fileAccepted(file); createMock.called; - })); + }); }); describe('#fileRejected', () => { @@ -202,23 +202,22 @@ describe('AddFileComponent', () => { })); }); - // describe('#createModel', () => { - // it('should create a new model file', async(() => { - // component.businessNetwork = mockBusinessNetwork; - // let mockModel = sinon.createStubInstance(ModelFile); - // mockModel.getFileName.returns('newfile.cto'); - // let b = new Blob([ - // `/**CTO File**/ - // namespace test` - // ], {type: 'text/plain'}); - // let file = new File([b], 'newfile.cto'); - - // component.createModel(file, file); - // component.fileType.should.equal('cto'); - // component.currentFile.should.deep.equal(mockModel); - // component.currentFileName = mockModel.getIdentifier(); - // })); - // }); + describe('#createModel', () => { + it('should create a new model file', async(() => { + component.businessNetwork = mockBusinessNetwork; + let b = new Blob( + [ `/**CTO File**/ namespace test` ], + { type: 'text/plain' } + ); + let file = new File([b], 'newfile.cto'); + let dataBuffer = new Buffer('/**CTO File**/ namespace test'); + let mockModel = new ModelFile(mockModelManager, dataBuffer.toString(), file.name); + component.createModel(file, dataBuffer); + component.fileType.should.equal('cto'); + component.currentFile.should.deep.equal(mockModel); + component.currentFileName.should.equal(mockModel.getFileName()); + })); + }); describe('#changeCurrentFileType', () => { it('should change this.currentFileType to a js file', async(() => { @@ -228,12 +227,105 @@ describe('AddFileComponent', () => { mockScriptManager.createScript.returns(mockScript); component.fileType = 'js'; - component.addScriptFileExtension = 'js'; + component.addScriptFileExtension = '.js'; component.businessNetwork = mockBusinessNetwork; component.changeCurrentFileType(); component.currentFileName.should.equal('script.js'); component.currentFile.should.deep.equal(mockScript); })); + + it('should change this.currentFileType to a cto file', async(() => { + mockModelManager.getModelFiles.returns([]); + let b = new Blob( + [ `/** + * New model file + */ + +namespace org.acme.model` ], + { type: 'text/plain' } + ); + let file = new File([b], 'lib/org.acme.model.cto'); + let dataBuffer = new Buffer(`/** + * New model file + */ + +namespace org.acme.model`); + let mockModel = new ModelFile(mockModelManager, dataBuffer.toString(), file.name); + + component.fileType = 'cto'; + component.businessNetwork = mockBusinessNetwork; + + component.changeCurrentFileType(); + component.currentFileName.should.equal('lib/org.acme.model.cto'); + component.currentFile.should.deep.equal(mockModel); + + })); + }); + + describe('#removeFile', () => { + it('should reset back to default values', async(() => { + component.expandInput = true; + component.currentFile = true; + component.currentFileName = true; + component.fileType = 'js'; + + component.removeFile(); + component.expandInput.should.not.be.true; + expect(component.currentFile).to.be.null; + expect(component.currentFileName).to.be.null; + component.fileType.should.equal(''); + })); + }); + + describe('#getDataBuffer', () => { + let file; + let mockFileReadObj; + let mockBuffer; + let mockFileRead; + let content; + + beforeEach(() => { + content = 'hello world'; + let data = new Blob([content], {type: 'text/plain'}); + file = new File([data], 'mock.bna'); + + mockFileReadObj = { + readAsArrayBuffer: sandbox.stub(), + result: content, + onload: () => { + }, + onerror: () => { + } + }; + + mockFileRead = sinon.stub(window, 'FileReader'); + mockFileRead.returns(mockFileReadObj); + }); + + afterEach(() => { + mockFileRead.restore(); + }); + + it('should return data from a file', () => { + let promise = component.getDataBuffer(file); + mockFileReadObj.onload(); + return promise + .then(data => { + data.toString().should.equal(content); + }); + }); + + it('should give error in promise chain', () => { + let promise = component.getDataBuffer(file); + mockFileReadObj.onerror('error'); + return promise + .then(data => { + data.should.be.null; + }) + .catch(err => { + err.should.equal('error'); + }); + }); }); }); diff --git a/packages/composer-playground/src/app/add-file/add-file.component.ts b/packages/composer-playground/src/app/add-file/add-file.component.ts index be7f4d71b3..9b2df45ec9 100644 --- a/packages/composer-playground/src/app/add-file/add-file.component.ts +++ b/packages/composer-playground/src/app/add-file/add-file.component.ts @@ -9,7 +9,7 @@ import {AlertService} from '../services/alert.service'; templateUrl: './add-file.component.html', styleUrls: ['./add-file.component.scss'.toString()] }) -export class AddFileComponent implements OnInit { +export class AddFileComponent { @Input() businessNetwork: BusinessNetworkDefinition; @@ -35,9 +35,6 @@ export class AddFileComponent implements OnInit { public activeModal: NgbActiveModal) { } - ngOnInit() { - } - removeFile() { this.expandInput = false; this.currentFile = null; diff --git a/packages/composer-playground/src/app/import/import.component.spec.ts b/packages/composer-playground/src/app/import/import.component.spec.ts index 2a230a9581..4969cd14aa 100644 --- a/packages/composer-playground/src/app/import/import.component.spec.ts +++ b/packages/composer-playground/src/app/import/import.component.spec.ts @@ -52,6 +52,7 @@ class MockFileImporterDirective { } describe('ImportComponent', () => { + let sandbox; let component: ImportComponent; let fixture: ComponentFixture; @@ -79,6 +80,8 @@ describe('ImportComponent', () => { {provide: AlertService, useValue: mockAlertService}] }); + sandbox = sinon.sandbox.create(); + fixture = TestBed.createComponent(ImportComponent); component = fixture.componentInstance; @@ -86,6 +89,10 @@ describe('ImportComponent', () => { mockDragDropComponent = mockDragDropElement.injector.get(MockDragDropDirective) as MockDragDropDirective; }); + afterAll(() => { + sandbox.restore(); + }); + describe('ngInit', () => { let onShowMock; @@ -209,13 +216,13 @@ describe('ImportComponent', () => { }); describe('fileAccepted', () => { - let file; let mockFileReadObj; - - let mockFileRead = sinon.stub(window, 'FileReader'); + let mockFileRead; beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockFileRead = sandbox.stub(window, 'FileReader'); let content = "Hello World"; let data = new Blob([content], {type: 'text/plain'}); let arrayOfBlob = new Array(); @@ -225,13 +232,18 @@ describe('ImportComponent', () => { mockFileReadObj = { onload: () => { }, - readAsArrayBuffer: sinon.stub(), + readAsArrayBuffer: sandbox.stub(), result: 'my file' }; mockFileRead.returns(mockFileReadObj); }); + afterEach(() => { + mockFileRead.restore(); + sandbox.restore(); + }); + it('should read a file', fakeAsync(() => { mockBusinessNetworkService.getBusinessNetworkFromArchive.returns(Promise.resolve({network: 'mockNetwork'})); From eb68ef730cffb04c22920aa7b44115cec9bb3bc7 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 22 Mar 2017 10:03:50 +0000 Subject: [PATCH 03/41] Bumped up about.service code coverage --- .../src/app/services/about.service.spec.ts | 32 ++++++++++++++----- .../src/app/services/about.service.ts | 8 ++--- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/composer-playground/src/app/services/about.service.spec.ts b/packages/composer-playground/src/app/services/about.service.spec.ts index cf2d6ff7d5..665a524d84 100644 --- a/packages/composer-playground/src/app/services/about.service.spec.ts +++ b/packages/composer-playground/src/app/services/about.service.spec.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-unused-variable */ -import { TestBed, async, inject } from '@angular/core/testing'; +import { TestBed, async, inject, fakeAsync, tick } from '@angular/core/testing'; import { AboutService } from './about.service'; import { HttpModule, @@ -44,14 +44,14 @@ const expectedResponse = { } }; -describe('AboutService', () => { +fdescribe('AboutService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpModule], providers: [ - AboutService, - { provide: XHRBackend, useClass: MockBackend } - ] + AboutService, + { provide: XHRBackend, useClass: MockBackend } + ] }); }); @@ -60,12 +60,12 @@ describe('AboutService', () => { // setup a mocked response mockBackend.connections.subscribe((connection) => { connection.mockRespond(new Response(new ResponseOptions({ - body: mockResponse + body: JSON.stringify(mockResponse) }))); }); // make the call to the service which was injected - let result = aboutService.getVersions() + return aboutService.getVersions() .then((versions) => { versions.playground.name.should.equal('playground'); versions.playground.version.should.equal('1'); @@ -76,6 +76,22 @@ describe('AboutService', () => { versions.common.name.should.equal('composer-common'); versions.common.version.should.equal('4'); }); - return result; + }))); + + it('should enter catch block', + async(inject([AboutService, XHRBackend], (aboutService, mockBackend) => { + mockBackend.connections.subscribe( + (connection) => { + connection.mockError(new Error('error')); + } + ); + + return aboutService.getVersions() + .then(() => { + // Ignore this + }) + .catch((error) => { + error.message.should.equal('error'); + }); }))); }); diff --git a/packages/composer-playground/src/app/services/about.service.ts b/packages/composer-playground/src/app/services/about.service.ts index b8d64f7d1a..cf62b202b7 100644 --- a/packages/composer-playground/src/app/services/about.service.ts +++ b/packages/composer-playground/src/app/services/about.service.ts @@ -26,15 +26,15 @@ export class AboutService { version: modules.dependencies['composer-admin'].version } }; + }) + .catch((e) => { + console.log(e); }); } private getModules(): Promise { return this.http.get('assets/npmlist.json') .map(res => res.json()) - .toPromise() - .catch((e) => { - console.log(e); - }); + .toPromise(); } } From 307abce323f69a44ceb574eeaa065467a33374d2 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 22 Mar 2017 10:09:18 +0000 Subject: [PATCH 04/41] Fixed minor linting issues --- .../src/app/registry/registry.component.ts | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/composer-playground/src/app/registry/registry.component.ts b/packages/composer-playground/src/app/registry/registry.component.ts index e7567d3a72..446301c178 100644 --- a/packages/composer-playground/src/app/registry/registry.component.ts +++ b/packages/composer-playground/src/app/registry/registry.component.ts @@ -36,7 +36,7 @@ export class RegistryComponent { @Input() set reload(reload) { - if (this._reload!==null) { + if (this._reload !== null) { this.loadResources(); } this._reload = reload; @@ -65,10 +65,6 @@ export class RegistryComponent { }); } - private isTransactionRegistry(): boolean { - return this.registryType === "Transaction"; - } - serialize(resource: any): string { let serializer = this.clientService.getBusinessNetwork().getSerializer(); return JSON.stringify(serializer.toJSON(resource), null, 2); @@ -76,7 +72,7 @@ export class RegistryComponent { expandResource(resourceToExpand) { if (this.expandedResource === resourceToExpand.getIdentifier()) { - this.expandedResource = null + this.expandedResource = null; } else { this.expandedResource = resourceToExpand.getIdentifier(); } @@ -85,7 +81,7 @@ export class RegistryComponent { openNewResourceModal() { const modalRef = this.modalService.open(ResourceComponent); modalRef.componentInstance.registryID = this._registry.id; - modalRef.result.then(()=>{ + modalRef.result.then(() => { // refresh current resource list this.loadResources(); }); @@ -99,32 +95,38 @@ export class RegistryComponent { const editModalRef = this.modalService.open(ResourceComponent); editModalRef.componentInstance.registryID = this._registry.id; editModalRef.componentInstance.resource = resource; - editModalRef.result.then(()=>{ + editModalRef.result.then(() => { // refresh current resource list this.loadResources(); }); } - + openDeleteResourceModal(resource: any) { const confirmModalRef = this.modalService.open(ConfirmComponent); - confirmModalRef.componentInstance.confirmMessage='Please confirm that you want to delete Asset: '+resource.getIdentifier(); - confirmModalRef.result.then((result)=>{ + confirmModalRef.componentInstance.confirmMessage = 'Please confirm that you want to delete Asset: ' + resource.getIdentifier(); + + confirmModalRef.result.then((result) => { if(result) { this._registry.remove(resource) .then(() => { this.loadResources(); }) .catch((error) => { - console.log('ERR: '+error); - this.alertService.errorStatus$.next('Removing the selected item from the registry failed:'+ error); + console.log('ERR: ', error); + this.alertService.errorStatus$.next( + 'Removing the selected item from the registry failed:' + error + ); }); } else { - //todo - some error handling - we should always get called with a code for this usage of the + // TODO: we should always get called with a code for this usage of the // modal but will that always be true - } + } }); } + private isTransactionRegistry(): boolean { + return this.registryType === 'Transaction'; + } } From b6995d81d8714d0d83a05158d2663575cd314e08 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 22 Mar 2017 10:25:35 +0000 Subject: [PATCH 05/41] Removed fdescribe --- .../composer-playground/src/app/services/about.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/composer-playground/src/app/services/about.service.spec.ts b/packages/composer-playground/src/app/services/about.service.spec.ts index 665a524d84..325aeec024 100644 --- a/packages/composer-playground/src/app/services/about.service.spec.ts +++ b/packages/composer-playground/src/app/services/about.service.spec.ts @@ -44,7 +44,7 @@ const expectedResponse = { } }; -fdescribe('AboutService', () => { +describe('AboutService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpModule], From 69dc287b073723c824da9f2a3237be71f7ca039a Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Thu, 23 Mar 2017 10:39:23 +0000 Subject: [PATCH 06/41] Increase code coverage of add-file to 100% --- .../app/add-file/add-file.component.spec.ts | 100 ++++++++++++++++-- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/packages/composer-playground/src/app/add-file/add-file.component.spec.ts b/packages/composer-playground/src/app/add-file/add-file.component.spec.ts index a68e494722..5df438643b 100644 --- a/packages/composer-playground/src/app/add-file/add-file.component.spec.ts +++ b/packages/composer-playground/src/app/add-file/add-file.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, async, fakeAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { FormsModule } from '@angular/forms'; @@ -121,7 +121,7 @@ describe('AddFileComponent', () => { }); describe('#fileAccepted', () => { - it('should call this.createModel', async(() => { + it('should call this.createModel', fakeAsync(() => { let b = new Blob(['/**CTO File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.cto'); @@ -130,10 +130,11 @@ describe('AddFileComponent', () => { .returns(Promise.resolve('some data')); component.fileAccepted(file); + tick(); createMock.called; })); - it('should call this.createScript', () => { + it('should call this.createScript', fakeAsync(() => { let b = new Blob(['/**JS File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.js'); @@ -143,10 +144,11 @@ describe('AddFileComponent', () => { .returns(Promise.resolve('some data')); component.fileAccepted(file); + tick(); createMock.called; - }); + })); - it('should call this.fileRejected when there is an error reading the file', () => { + it('should call this.fileRejected when there is an error reading the file', fakeAsync(() => { let b = new Blob(['/**CTO File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.cto'); @@ -156,10 +158,11 @@ describe('AddFileComponent', () => { .returns(Promise.reject('some data')); component.fileAccepted(file); + tick(); createMock.called; - }); + })); - it('should throw when given incorrect file type', () => { + it('should throw when given incorrect file type', fakeAsync(() => { let b = new Blob(['/**PNG File*/'], {type: 'text/plain'}); let file = new File([b], 'newfile.png'); @@ -169,8 +172,9 @@ describe('AddFileComponent', () => { .returns(Promise.resolve('some data')); component.fileAccepted(file); + tick(); createMock.called; - }); + })); }); describe('#fileRejected', () => { @@ -198,8 +202,26 @@ describe('AddFileComponent', () => { component.createScript(file, file); component.fileType.should.equal('js'); component.currentFile.should.deep.equal(mockScript); - component.currentFileName = mockScript.getIdentifier(); + component.currentFileName.should.equal(mockScript.getIdentifier()); })); + + it('should use the addScriptFileName variable as the file name', () => { + let fileName = 'testFileName.js'; + component.addScriptFileName = fileName; + component.businessNetwork = mockBusinessNetwork; + let mockScript = sinon.createStubInstance(Script); + mockScript.getIdentifier.returns(fileName); + mockScriptManager.createScript.returns(mockScript); + + let b = new Blob(['/**JS File*/'], {type: 'text/plain'}); + let file = new File([b], ''); + + component.createScript(file, file); + component.fileType.should.equal('js'); + component.currentFile.should.deep.equal(mockScript); + component.currentFileName.should.equal(mockScript.getIdentifier()); + component.currentFileName.should.equal(fileName); + }); }); describe('#createModel', () => { @@ -217,6 +239,24 @@ describe('AddFileComponent', () => { component.currentFile.should.deep.equal(mockModel); component.currentFileName.should.equal(mockModel.getFileName()); })); + + it('should use the addModelFileName variable as the file name', async(() => { + let fileName = 'testFileName.cto'; + component.addModelFileName = fileName; + component.businessNetwork = mockBusinessNetwork; + let b = new Blob( + [ `/**CTO File**/ namespace test` ], + { type: 'text/plain' } + ); + let file = new File([b], ''); + let dataBuffer = new Buffer('/**CTO File**/ namespace test'); + let mockModel = new ModelFile(mockModelManager, dataBuffer.toString(), fileName); + component.createModel(file, dataBuffer); + component.fileType.should.equal('cto'); + component.currentFile.should.deep.equal(mockModel); + component.currentFileName.should.equal(mockModel.getFileName()); + component.currentFileName.should.equal(fileName); + })); }); describe('#changeCurrentFileType', () => { @@ -235,6 +275,20 @@ describe('AddFileComponent', () => { component.currentFile.should.deep.equal(mockScript); })); + it('should append the file number to the js file name', async(() => { + let mockScript = sinon.createStubInstance(Script); + mockScript.getIdentifier.returns('lib/script1.js'); + mockScriptManager.getScripts.returns([mockScript]); + mockScriptManager.createScript.returns(mockScript); + + component.fileType = 'js'; + component.addScriptFileExtension = '.js'; + component.businessNetwork = mockBusinessNetwork; + + component.changeCurrentFileType(); + component.currentFileName.should.equal('lib/script1.js'); + })); + it('should change this.currentFileType to a cto file', async(() => { mockModelManager.getModelFiles.returns([]); let b = new Blob( @@ -261,6 +315,34 @@ namespace org.acme.model`); component.currentFile.should.deep.equal(mockModel); })); + + it('should append the file number to the cto file name', () => { + + let b = new Blob( + [ `/** + * New model file + */ + +namespace org.acme.model` ], + { type: 'text/plain' } + ); + let file = new File([b], 'lib/org.acme.model.cto'); + let dataBuffer = new Buffer(`/** + * New model file + */ + +namespace org.acme.model`); + let mockModel = new ModelFile(mockModelManager, dataBuffer.toString(), file.name); + + // One element, so the number 1 should be appended + mockModelManager.getModelFiles.returns([mockModel]); + + component.fileType = 'cto'; + component.businessNetwork = mockBusinessNetwork; + + component.changeCurrentFileType(); + component.currentFileName.should.equal('lib/org.acme.model1.cto'); + }); }); describe('#removeFile', () => { From 859f0fc7c8cdfe4e2fcf5aa130156abac2c59292 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Thu, 23 Mar 2017 14:00:16 +0000 Subject: [PATCH 07/41] Export AssetRegistry and ParticipantRegistry --- packages/composer-client/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/composer-client/index.js b/packages/composer-client/index.js index 70cfda7e25..7c307947d4 100644 --- a/packages/composer-client/index.js +++ b/packages/composer-client/index.js @@ -39,6 +39,8 @@ require('composer-common').ConnectionProfileManager.registerConnectionManagerLoa */ module.exports.BusinessNetworkConnection = require('./lib/businessnetworkconnection'); +module.exports.AssetRegistry = require('./lib/assetregistry'); +module.exports.ParticipantRegisty = require('./lib/participantregistry'); module.exports.TransactionRegistry = require('./lib/transactionregistry'); /** From 764717485b2e72c6edeca92ad32932025926614e Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Fri, 24 Mar 2017 16:12:43 +0000 Subject: [PATCH 08/41] Registry unit tests --- .../app/registry/registry.component.spec.ts | 364 ++++++++++++++++-- .../src/app/registry/registry.component.ts | 6 +- 2 files changed, 328 insertions(+), 42 deletions(-) diff --git a/packages/composer-playground/src/app/registry/registry.component.spec.ts b/packages/composer-playground/src/app/registry/registry.component.spec.ts index e531e3f5b4..9d2d519573 100644 --- a/packages/composer-playground/src/app/registry/registry.component.spec.ts +++ b/packages/composer-playground/src/app/registry/registry.component.spec.ts @@ -1,57 +1,345 @@ -import {NO_ERRORS_SCHEMA} from '@angular/core'; import { inject, async, TestBed, - ComponentFixture + ComponentFixture, + fakeAsync, + tick } from '@angular/core/testing'; -import {Component} from '@angular/core'; +import { Input, Output, EventEmitter, Directive } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { BehaviorSubject, Subject } from 'rxjs/Rx'; + +import { BusinessNetworkConnection, AssetRegistry, TransactionRegistry } from 'composer-client'; +import { BusinessNetworkDefinition, Util, Serializer, Resource } from 'composer-common'; // Load the implementations that should be tested -import {RegistryComponent} from './registry.component'; -import {ClientService} from '../services/client.service'; +import { RegistryComponent } from './registry.component'; +import { ResourceComponent } from './../resource/resource.component'; -describe(`Registry`, () => { - /** let comp: RegistryComponent; - let fixture: ComponentFixture; +import { ClientService } from '../services/client.service'; +import { AlertService } from '../services/alert.service'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; + +import * as chai from 'chai'; + +import * as sinon from 'sinon'; + +let should = chai.should(); +@Directive({ + selector: '[checkOverFlow]' +}) +class MockCheckOverFlowDirective { + @Output() public hasOverFlow: EventEmitter = new EventEmitter(); + @Input() public changed: boolean; +} + +describe(`RegistryComponent`, () => { + let component: RegistryComponent; + let fixture: ComponentFixture; + let mockBusinessNetwork; + let mockSerializer; + let mockNgbModal; + let mockAlertService; let mockClientService; - let clientServiceStub; + let mockBehaviourSubject; + let mockResourceComponent; + + let mockAssetRegistry; + let assetRegistryContents; + let mockTransactionRegistry; + let transactionRegistryContents; + + let mockCheckOverFlowDirective; + + let sandbox; - // async beforeEach beforeEach(() => { - mockClientService = { - getCaz: () => { - return 'BOB' - } - // getBusinessNetwork : () => { - // return {}; - // } - // user: { name: 'Test User'} - }; - - //clientServiceStub = sinon.createStubInstance(ClientService); - //console.log(clientServiceStub); - // stub UserService for test purposes - /** let mockServiceStub = { - isLoggedIn: true, - user: { name: 'Test User'} - };**/ - - /** TestBed.configureTestingModule({ - declarations: [RegistryComponent], - providers: [{provide: ClientService, useValue: mockClientService}] + mockNgbModal = sinon.createStubInstance(NgbModal); + mockAlertService = sinon.createStubInstance(AlertService); + mockClientService = sinon.createStubInstance(ClientService); + mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + mockSerializer = sinon.createStubInstance(Serializer); + mockBehaviourSubject = sinon.createStubInstance(BehaviorSubject); + mockResourceComponent = sinon.createStubInstance(ResourceComponent); + + mockClientService.getBusinessNetwork.returns(mockBusinessNetwork); + mockBusinessNetwork.getSerializer.returns(mockSerializer); + mockSerializer.toJSON.returns({'$class': 'mock.class'}); + mockBehaviourSubject.next = sinon.stub(); + + mockAlertService.errorStatus$ = mockBehaviourSubject; + mockAlertService.busyStatus$ = mockBehaviourSubject; + mockAlertService.successStatus$ = mockBehaviourSubject; + + TestBed.configureTestingModule({ + imports: [ + FormsModule + ], + declarations: [ + MockCheckOverFlowDirective, + RegistryComponent, + ], + providers: [ + { provide: ClientService, useValue: mockClientService }, + { provide: AlertService, useValue: mockAlertService }, + { provide: NgbModal, useValue: mockNgbModal }, + ] }); fixture = TestBed.createComponent(RegistryComponent); - comp = fixture.componentInstance; + component = fixture.componentInstance; + sandbox = sinon.sandbox.create(); + + mockAssetRegistry = sinon.createStubInstance(AssetRegistry); + mockAssetRegistry.registryType = 'Asset'; + + mockTransactionRegistry = sinon.createStubInstance(TransactionRegistry); + mockTransactionRegistry.registryType = 'Transaction'; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#loadResources', () => { + + beforeEach(() => { + let asset1 = sinon.createStubInstance(Resource); + asset1.getIdentifier.returns('1'); + let asset2 = sinon.createStubInstance(Resource); + asset2.getIdentifier.returns('2'); + let asset3 = sinon.createStubInstance(Resource); + asset3.getIdentifier.returns('3'); + assetRegistryContents = [asset2, asset3, asset1]; + + let trans1 = sinon.createStubInstance(Resource); + trans1.timestamp = '1'; + let trans2 = sinon.createStubInstance(Resource); + trans2.timestamp = '2'; + let trans3 = sinon.createStubInstance(Resource); + trans3.timestamp = '3'; + + transactionRegistryContents = [trans1, trans2, trans3]; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should call loadResources when registry is set', () => { + sandbox.stub(component, 'loadResources'); + mockAssetRegistry.registryType = 'transaction'; + component.registry = mockAssetRegistry; + component.loadResources.should.be.called; + component['_registry'].should.equal(mockAssetRegistry); + component['registryType'].should.equal('transaction'); + }); + + it('should not call loadResources if null registry is given', () => { + sandbox.stub(component, 'loadResources'); + component.registry = null; + component.loadResources.should.not.be.called; + }); + + it('should call loadResources when reload is set', () => { + sandbox.stub(component, 'loadResources'); + component['_reload'] = true; + component.reload = true; + component.loadResources.should.be.called; + component['_reload'].should.equal(true); + }); + + it('should not call loadResources if null reload is given', () => { + sandbox.stub(component, 'loadResources'); + component['_reload'] = null; + component.reload = true; + component.loadResources.should.not.to.be.called; + }); + + it('should sort a list of assets by identifier', fakeAsync(() => { + mockAssetRegistry.getAll.returns(Promise.resolve(assetRegistryContents)); + component['_registry'] = mockAssetRegistry; + component['registryType'] = mockAssetRegistry.registryType; + component.loadResources(); + tick(); + component['resources'][0].getIdentifier().should.equal('1'); + component['resources'][1].getIdentifier().should.equal('2'); + component['resources'][2].getIdentifier().should.equal('3'); + })); + + it('should sort a list of transactions by timestamp', fakeAsync(() => { + mockTransactionRegistry.getAll.returns(Promise.resolve(transactionRegistryContents)); + component['_registry'] = mockTransactionRegistry; + component['registryType'] = mockTransactionRegistry.registryType; + component.loadResources(); + tick(); + component['resources'][0].timestamp.should.equal('3'); + component['resources'][1].timestamp.should.equal('2'); + component['resources'][2].timestamp.should.equal('1'); + })); + + it('should call AlertService.errorstatus$.next() on error', fakeAsync(() => { + const error = 'error'; + mockAssetRegistry.getAll.returns(Promise.reject(error)); + component['_registry'] = mockAssetRegistry; + component.loadResources(); + tick(); + component['alertService'].errorStatus$.next.should.be.called; + component['alertService'].errorStatus$.next.should.be.calledWith(error); + })); + }); + describe('#serialize', () => { + it('should return the stringified version of an object', () => { + mockSerializer.toJSON.returns({'$class': 'mock.class'}); + // Resource class is unimportant.. + let result = component.serialize({}); + result.should.equal('{\n "$class": "mock.class"\n}'); + }); + }); + + describe('#expandResource', () => { + let mockResource; + beforeEach(() => { + mockResource = sinon.createStubInstance(Resource); + }); + + it('should set expandResource to null', () => { + mockResource.getIdentifier.returns('1'); + component['expandedResource'] = '1'; + + component.expandResource(mockResource); + should.not.exist(component['expandedResource']); + }); + + it('should set expandResource to the chosen resource', () => { + mockResource.getIdentifier.returns('1'); + component['expandedResource'] = '2'; + + component.expandResource(mockResource); + component['expandedResource'].should.equal('1'); + }); }); - it('should have default data', () => { - // let result = comp.loadResources(); - console.log('CAZ BANANA CAKE'); - //expect(result).toBe('BOB'); - // result.should.equal('BOB'); - });**/ + describe('#openNewResourceModal', () => { + it('should call loadResources', fakeAsync(() => { + mockNgbModal.open = sandbox.stub().returns({ + componentInstance: sandbox.stub(), + result: Promise.resolve() + }); + mockAssetRegistry.id = 'registry_id'; + component['_registry'] = mockAssetRegistry; + + sinon.stub(component, 'loadResources'); + + component.openNewResourceModal(); + tick(); + component.loadResources.should.be.called; + })); + }); + + describe('#hasOverflow', () => { + it('should take the value of the prameter passed in', () => { + component.hasOverFlow(true); + component['showExpand'].should.be.true; + component.hasOverFlow(false); + component['showExpand'].should.be.false; + }); + }); + + describe('#editResource', () => { + let mockAsset; + beforeEach(() => { + mockAsset = sinon.createStubInstance(Resource); + mockAsset.getIdentifier.returns('1'); + }); + + it('should call loadResources and set the correct values', fakeAsync(() => { + let mockNgbModalRef = sandbox.stub(); + mockNgbModalRef.resource = null; + + mockNgbModal.open = sandbox.stub().returns({ + componentInstance: mockNgbModalRef, + result: Promise.resolve() + }); + mockAssetRegistry.id = 'registry_id'; + component['_registry'] = mockAssetRegistry; + + sinon.stub(component, 'loadResources'); + + component.editResource(mockAsset); + tick(); + mockNgbModalRef.resource.should.equal(mockAsset); + mockNgbModalRef.registryID.should.equal('registry_id'); + component.loadResources.should.be.called; + })); + }); + + describe('#openDeleteResourceModal', () => { + let mockNgbModalRef; + let mockResource; + + beforeEach(() => { + mockResource = sinon.createStubInstance(Resource); + mockResource.getIdentifier.returns('1'); + mockNgbModalRef = { + result: Promise.resolve(true), + componentInstance: { + confirmMessage: '', + } + }; + mockNgbModal.open = sandbox.stub().returns(mockNgbModalRef); + }); + + it('should run loadResources', fakeAsync(() => { + sandbox.stub(component, 'loadResources'); + mockAssetRegistry.remove.returns(Promise.resolve()); + component['_registry'] = mockAssetRegistry; + component.openDeleteResourceModal(mockResource); + tick(); + tick(); + component.loadResources.should.be.called; + mockNgbModalRef.componentInstance.confirmMessage.should. + equal('Please confirm that you want to delete Asset: ' + mockResource.getIdentifier()); + })); + + it('should create a new error with the alert service', fakeAsync(() => { + mockAssetRegistry.remove.returns(Promise.reject('error message')); + component['_registry'] = mockAssetRegistry; + component.openDeleteResourceModal(mockResource); + tick(); + tick(); + mockNgbModalRef.componentInstance.confirmMessage.should. + equal('Please confirm that you want to delete Asset: ' + mockResource.getIdentifier()); + mockAlertService.errorStatus$.next.should.be.called; + mockAlertService.errorStatus$.next.should.be + .calledWith('Removing the selected item from the registry failed:error message'); + })); + + it('should do nothing', fakeAsync(() => { + sandbox.stub(component, 'loadResources'); + mockNgbModalRef.result = Promise.resolve(false); + component['_registry'] = mockAssetRegistry; + component.openDeleteResourceModal(mockResource); + mockAlertService.errorStatus$.next.should.not.be.called; + component.loadResources.should.not.be.called; + })); + }); + + describe('#isTransactionRegistry', () => { + it('should return true if reigstry type is transaction', () => { + component['registryType'] = 'Transaction'; + + component['isTransactionRegistry']().should.be.true; + }); + + it('should return false if registry type is not transaction', () => { + component['registryType'] = 'NotTransaction'; + + component['isTransactionRegistry']().should.be.false; + }); + }); }); diff --git a/packages/composer-playground/src/app/registry/registry.component.ts b/packages/composer-playground/src/app/registry/registry.component.ts index 446301c178..c3e7ce1414 100644 --- a/packages/composer-playground/src/app/registry/registry.component.ts +++ b/packages/composer-playground/src/app/registry/registry.component.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ClientService } from '../services/client.service'; -import { AlertService } from '../services/alert.service' +import { AlertService } from '../services/alert.service'; import { ResourceComponent } from '../resource/resource.component'; import { ConfirmComponent } from '../confirm/confirm.component'; import { TransactionRegistry } from 'composer-client'; @@ -106,13 +106,12 @@ export class RegistryComponent { confirmModalRef.componentInstance.confirmMessage = 'Please confirm that you want to delete Asset: ' + resource.getIdentifier(); confirmModalRef.result.then((result) => { - if(result) { + if (result) { this._registry.remove(resource) .then(() => { this.loadResources(); }) .catch((error) => { - console.log('ERR: ', error); this.alertService.errorStatus$.next( 'Removing the selected item from the registry failed:' + error ); @@ -124,7 +123,6 @@ export class RegistryComponent { }); } - private isTransactionRegistry(): boolean { return this.registryType === 'Transaction'; } From 0780bc0d6d7d9fd1dfc4a9dfeb525d3b5b05645e Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Mon, 27 Mar 2017 13:29:29 +0100 Subject: [PATCH 09/41] First trasaction component test --- .../transaction/transaction.component.spec.ts | 123 +++++++++++++++--- 1 file changed, 102 insertions(+), 21 deletions(-) diff --git a/packages/composer-playground/src/app/transaction/transaction.component.spec.ts b/packages/composer-playground/src/app/transaction/transaction.component.spec.ts index 2e5603499c..93eb97e99e 100644 --- a/packages/composer-playground/src/app/transaction/transaction.component.spec.ts +++ b/packages/composer-playground/src/app/transaction/transaction.component.spec.ts @@ -1,28 +1,109 @@ /* tslint:disable:no-unused-variable */ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { DebugElement, Component, Input } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { TransactionComponent } from './transaction.component'; +import { CodemirrorComponent } from 'ng2-codemirror'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ClientService } from './../services/client.service'; +import { InitializationService } from './../initialization.service'; -describe('TransactionComponent', () => { - // let component: TransactionComponent; - // let fixture: ComponentFixture; - - // beforeEach(async(() => { - // TestBed.configureTestingModule({ - // declarations: [ TransactionComponent ] - // }) - // .compileComponents(); - // })); - - // beforeEach(() => { - // fixture = TestBed.createComponent(TransactionComponent); - // component = fixture.componentInstance; - // fixture.detectChanges(); - // }); - - // it('should create', () => { - // expect(component).toBeTruthy(); - // }); +import { TransactionDeclaration, BusinessNetworkDefinition, Serializer, Factory, Resource } from 'composer-common'; +import { BusinessNetworkConnection, AssetRegistry, TransactionRegistry } from 'composer-client'; + +import * as chai from 'chai'; +import * as sinon from 'sinon'; +let should = chai.should(); + +@Component({ + selector: 'codemirror', + template: '' +}) +class MockCodeMirrorComponent { + @Input() config: any; +} + +fdescribe('TransactionComponent', () => { + let component: TransactionComponent; + let fixture: ComponentFixture; + let mockNgbActiveModal; + let mockClientService; + let mockInitializationService; + let mockTransaction; + let mockBusinessNetwork; + let mockSerializer; + let mockFactory; + let mockResource; + + let sandbox; + + beforeEach(() => { + mockNgbActiveModal = sinon.createStubInstance(NgbActiveModal); + mockClientService = sinon.createStubInstance(ClientService); + mockInitializationService = sinon.createStubInstance(InitializationService); + mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + mockSerializer = sinon.createStubInstance(Serializer); + mockFactory = sinon.createStubInstance(Factory); + mockResource = sinon.createStubInstance(Resource); + + mockClientService.getBusinessNetwork.returns(mockBusinessNetwork); + mockBusinessNetwork.getSerializer.returns(mockSerializer); + mockSerializer.toJSON.returns({'$class': 'mock.class'}); + + mockTransaction = sinon.createStubInstance(TransactionDeclaration); + + TestBed.configureTestingModule({ + imports: [ + FormsModule + ], + declarations: [ + TransactionComponent, + MockCodeMirrorComponent + ], + providers: [ + { provide: NgbActiveModal, useValue: mockNgbActiveModal }, + { provide: ClientService, useValue: mockClientService }, + { provide: InitializationService, useValue: mockInitializationService }, + ] + }); + fixture = TestBed.createComponent(TransactionComponent); + component = fixture.componentInstance; + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#ngOnInit', () => { + + }); + + describe('#onTransactionSelect', () => { + it('should call generateTransactionDeclaration', () => { + sandbox.stub(component, 'generateTransactionDeclaration'); + let transactionName = 'mockTransaction'; + let transactionType = 'Transaction'; + mockTransaction.getName.returns(transactionName); + + component.onTransactionSelect(mockTransaction); + component['selectedTransaction'].should.equal(mockTransaction); + component['selectedTransactionName'].should.equal(transactionName); + component['generateTransactionDeclaration'].should.be.called; + }); + }); + + describe('#generateTransactionDeclaration', () => { + + }); + + describe('#onDefinitionChanged', () => { + + }); + + describe('#submitTransaction', () => { + + }); }); From bf41d2ea3eec42decc86e405e973241914e901c5 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Tue, 28 Mar 2017 10:39:33 +0100 Subject: [PATCH 10/41] Transaction component unit tests --- .../transaction/transaction.component.spec.ts | 183 +++++++++++++++++- .../app/transaction/transaction.component.ts | 28 +-- 2 files changed, 190 insertions(+), 21 deletions(-) diff --git a/packages/composer-playground/src/app/transaction/transaction.component.spec.ts b/packages/composer-playground/src/app/transaction/transaction.component.spec.ts index 93eb97e99e..e78756c157 100644 --- a/packages/composer-playground/src/app/transaction/transaction.component.spec.ts +++ b/packages/composer-playground/src/app/transaction/transaction.component.spec.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-unused-variable */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement, Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; @@ -10,7 +10,15 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ClientService } from './../services/client.service'; import { InitializationService } from './../initialization.service'; -import { TransactionDeclaration, BusinessNetworkDefinition, Serializer, Factory, Resource } from 'composer-common'; +import { + TransactionDeclaration, + BusinessNetworkDefinition, + Serializer, + Factory, + Resource, + ModelFile, + Introspector +} from 'composer-common'; import { BusinessNetworkConnection, AssetRegistry, TransactionRegistry } from 'composer-client'; import * as chai from 'chai'; @@ -33,26 +41,39 @@ fdescribe('TransactionComponent', () => { let mockInitializationService; let mockTransaction; let mockBusinessNetwork; + let mockBusinessNetworkConnection; let mockSerializer; + let mockIntrospector; let mockFactory; let mockResource; let sandbox; beforeEach(() => { + sandbox = sinon.sandbox.create(); mockNgbActiveModal = sinon.createStubInstance(NgbActiveModal); mockClientService = sinon.createStubInstance(ClientService); mockInitializationService = sinon.createStubInstance(InitializationService); mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + mockBusinessNetworkConnection = sinon.createStubInstance(BusinessNetworkConnection); mockSerializer = sinon.createStubInstance(Serializer); + mockIntrospector = sinon.createStubInstance(Introspector); mockFactory = sinon.createStubInstance(Factory); mockResource = sinon.createStubInstance(Resource); - + mockClientService.getBusinessNetworkConnection.returns(mockBusinessNetworkConnection); mockClientService.getBusinessNetwork.returns(mockBusinessNetwork); mockBusinessNetwork.getSerializer.returns(mockSerializer); - mockSerializer.toJSON.returns({'$class': 'mock.class'}); - + mockBusinessNetwork.getFactory.returns(mockFactory); + mockBusinessNetwork.getIntrospector.returns(mockIntrospector); + mockSerializer.toJSON.returns({ + '$class': 'mock.class', + 'timestamp': 'now', + 'transactionId': 'A' + }); + mockBusinessNetworkConnection.submitTransaction = sandbox.stub(); mockTransaction = sinon.createStubInstance(TransactionDeclaration); + mockNgbActiveModal.close = sandbox.stub(); + mockInitializationService.initialize.returns(Promise.resolve()); TestBed.configureTestingModule({ imports: [ @@ -70,15 +91,57 @@ fdescribe('TransactionComponent', () => { }); fixture = TestBed.createComponent(TransactionComponent); component = fixture.componentInstance; - sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); + describe('#CodeMirror', () => { + it('should call the correct functions', () => { + let cm = { + foldCode: sandbox.stub(), + getCursor: sandbox.stub() + }; + + component['codeConfig'].extraKeys['Ctrl-Q'](cm); + cm.foldCode.should.be.called; + cm.getCursor.should.be.called; + }); + }); + describe('#ngOnInit', () => { + it('should set transactionTypes, selectedTransaction and hiddenTransactionItems', + fakeAsync(() => { + sandbox.stub(component, 'generateTransactionDeclaration'); + mockTransaction.isAbstract.returns(false); + mockIntrospector.getClassDeclarations.returns([mockTransaction]); + component.ngOnInit(); + tick(); + mockBusinessNetwork.getIntrospector.should.be.called; + component['transactionTypes'].length.should.equal(1); + component['generateTransactionDeclaration'].should.be.called; + })); + + it('should not set transactionTypes, selectedTransaction and hiddenTransactionItems', + fakeAsync(() => { + mockIntrospector.getClassDeclarations.returns([]); + component.ngOnInit(); + tick(); + mockBusinessNetwork.getIntrospector.should.be.called; + component['transactionTypes'].length.should.equal(0); + })); + + it('should not set transactionTypes when abstract class', fakeAsync(() => { + sandbox.stub(component, 'generateTransactionDeclaration'); + mockTransaction.isAbstract.returns(true); + mockIntrospector.getClassDeclarations.returns([mockTransaction]); + component.ngOnInit(); + tick(); + mockBusinessNetwork.getIntrospector.should.be.called; + component['transactionTypes'].length.should.equal(0); + })); }); describe('#onTransactionSelect', () => { @@ -96,14 +159,120 @@ fdescribe('TransactionComponent', () => { }); describe('#generateTransactionDeclaration', () => { + let mockModelFile; + beforeEach(() => { + mockModelFile = sinon.createStubInstance(ModelFile); + mockModelFile.getNamespace.returns('com.test'); + }); + + it('should generate valid transaction definition', () => { + sandbox.stub(JSON, 'stringify'); + component['selectedTransaction'] = mockTransaction; + mockTransaction.getIdentifierFieldName.returns('transactionId'); + mockTransaction.getModelFile.returns(mockModelFile); + mockResource = sinon.createStubInstance(Resource); + component['generateTransactionDeclaration'](); + mockSerializer.toJSON.should.be.called; + JSON.stringify.should.be.called; + }); + + it('should remove hidden transactions', () => { + component['selectedTransaction'] = mockTransaction; + mockTransaction.getIdentifierFieldName.returns('transactionId'); + mockTransaction.getModelFile.returns(mockModelFile); + mockResource = sinon.createStubInstance(Resource); + component['hiddenTransactionItems'].set('transactionId', 'transactionId'); + component['hiddenTransactionItems'].set('timestamp', 'transactionId'); + + component['generateTransactionDeclaration'](); + + let resourceDefenition = component['resourceDefinition']; + resourceDefenition = JSON.parse(resourceDefenition); + + mockSerializer.toJSON.should.be.called; + should.not.exist(component['definitionError']); + should.not.exist(resourceDefenition['timestamp']); + should.not.exist(resourceDefenition['transactionId']); + }); + + it('should set definitionError', () => { + component['selectedTransaction'] = mockTransaction; + mockTransaction.getIdentifierFieldName.returns('transactionId'); + mockTransaction.getModelFile.returns(mockModelFile); + mockResource = sinon.createStubInstance(Resource); + mockSerializer.toJSON = () => { + throw new Error(); + }; + + component['generateTransactionDeclaration'](); + component['definitionError'].should.not.be.null; + + }); }); describe('#onDefinitionChanged', () => { + it('should validate a resource', () => { + mockResource.validate = sandbox.stub(); + mockSerializer.fromJSON.returns(mockResource); + component['resourceDefinition'] = JSON.stringify({ + '$class': 'mock.class', + 'timestamp': + 'now', + 'transactionId': 'A' + }); + component['hiddenTransactionItems'].set('transactionId', 'transactionId'); + component['hiddenTransactionItems'].set('timestamp', 'transactionId'); + + component['onDefinitionChanged'](); + should.not.exist(component['definitionError']); + mockResource.validate.should.be.called; + }); + it('should set definitionError', () => { + mockSerializer.fromJSON = () => { + throw new Error(); + }; + component['resourceDefinition'] = JSON.stringify({ + '$class': 'mock.class', + 'timestamp': + 'now', + 'transactionId': 'A' + }); + component['hiddenTransactionItems'].set('transactionId', 'transactionId'); + component['hiddenTransactionItems'].set('timestamp', 'transactionId'); + + component['onDefinitionChanged'](); + should.exist(component['definitionError']); + }); }); describe('#submitTransaction', () => { - + it('should submit a transaction', fakeAsync(() => { + mockSerializer.fromJSON.returns(mockResource); + component['resourceDefinition'] = JSON.stringify({ + '$class': 'mock.class', + 'timestamp': + 'now', + 'transactionId': 'A' + }); + component['selectedTransaction'] = mockTransaction; + component['submitTransaction'](); + component['submitInProgress'].should.be.true; + tick(); + tick(); + component['submitInProgress'].should.be.false; + should.not.exist(component['definitionError']); + mockNgbActiveModal.close.should.be.called; + })); }); + + it('should give set definitionError', fakeAsync(() => { + component['resourceDefinition'] = 'error'; + component['submitTransaction'](); + tick(); + tick(); + should.exist(component['definitionError']); + component['submitInProgress'].should.be.false; + })); }); diff --git a/packages/composer-playground/src/app/transaction/transaction.component.ts b/packages/composer-playground/src/app/transaction/transaction.component.ts index 893e23e893..1447c5b16c 100644 --- a/packages/composer-playground/src/app/transaction/transaction.component.ts +++ b/packages/composer-playground/src/app/transaction/transaction.component.ts @@ -34,7 +34,7 @@ export class TransactionComponent implements OnInit { private resourceDefinition: string = null; private submitInProgress: boolean = false; - private defitionError: string = null; + private definitionError: string = null; private codeConfig = { lineNumbers: true, @@ -73,7 +73,7 @@ export class TransactionComponent implements OnInit { }); // Set first in list as selectedTransaction - if (this.transactionTypes && this.transactionTypes.length>0) { + if (this.transactionTypes && this.transactionTypes.length > 0) { this.selectedTransaction = this.transactionTypes[0]; this.selectedTransactionName = this.selectedTransaction.getName(); @@ -105,38 +105,38 @@ export class TransactionComponent implements OnInit { let businessNetworkDefinition = this.clientService.getBusinessNetwork(); let factory = businessNetworkDefinition.getFactory(); let id = this.hiddenTransactionItems.get(this.selectedTransaction.getIdentifierFieldName()); - const generateParameters = { generate: true, "withSampleData": withSampleData }; + const generateParameters = { generate: true, 'withSampleData': withSampleData }; let resource = factory.newResource(this.selectedTransaction.getModelFile().getNamespace(), this.selectedTransaction.getName(), id, generateParameters); let serializer = this.clientService.getBusinessNetwork().getSerializer(); try { let json = serializer.toJSON(resource); // remove hidden items from json - this.hiddenTransactionItems.forEach((value,key) => { + this.hiddenTransactionItems.forEach((value, key) => { delete json[key]; }); this.resourceDefinition = JSON.stringify(json, null, 2); } catch (error) { // We can't generate a sample instance for some reason. - this.defitionError = error.toString(); + this.definitionError = error.toString(); } } /** - * Validate the defition of the TransactionDeclaration, accounting for hidden fields. + * Validate the definition of the TransactionDeclaration, accounting for hidden fields. */ private onDefinitionChanged() { try { let json = JSON.parse(this.resourceDefinition); // Add required items that are hidden from user - this.hiddenTransactionItems.forEach((value,key) => { - json[key]=value; + this.hiddenTransactionItems.forEach((value, key) => { + json[key] = value; }); let serializer = this.clientService.getBusinessNetwork().getSerializer(); let resource = serializer.fromJSON(json); resource.validate(); - this.defitionError = null; + this.definitionError = null; } catch (error) { - this.defitionError = error.toString(); + this.definitionError = error.toString(); } } @@ -154,16 +154,16 @@ export class TransactionComponent implements OnInit { let serializer = this.clientService.getBusinessNetwork().getSerializer(); let resource = serializer.fromJSON(json); return this.clientService.getBusinessNetworkConnection().submitTransaction(resource); - }) + }) .then(() => { this.submitInProgress = false; - this.defitionError = null; + this.definitionError = null; this.activeModal.close(); }) .catch((error) => { - this.defitionError = error.toString(); + this.definitionError = error.toString(); this.submitInProgress = false; - }) + }); } } From c3ab845312170571cd3df05326b5e8dbd060bc36 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Tue, 28 Mar 2017 10:54:00 +0100 Subject: [PATCH 11/41] Remove fdiscribe --- .../src/app/transaction/transaction.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/composer-playground/src/app/transaction/transaction.component.spec.ts b/packages/composer-playground/src/app/transaction/transaction.component.spec.ts index e78756c157..0dfd1f9e9a 100644 --- a/packages/composer-playground/src/app/transaction/transaction.component.spec.ts +++ b/packages/composer-playground/src/app/transaction/transaction.component.spec.ts @@ -33,7 +33,7 @@ class MockCodeMirrorComponent { @Input() config: any; } -fdescribe('TransactionComponent', () => { +describe('TransactionComponent', () => { let component: TransactionComponent; let fixture: ComponentFixture; let mockNgbActiveModal; From 008d3867dcffc8d5a02fd78c6f5eaf8ba65c5a97 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Tue, 28 Mar 2017 16:47:52 +0100 Subject: [PATCH 12/41] Added resource unit tests --- .../app/resource/resource.component.spec.ts | 408 +++++++++++++++++- .../src/app/resource/resource.component.ts | 39 +- 2 files changed, 408 insertions(+), 39 deletions(-) diff --git a/packages/composer-playground/src/app/resource/resource.component.spec.ts b/packages/composer-playground/src/app/resource/resource.component.spec.ts index df95afcccc..b6f461be65 100644 --- a/packages/composer-playground/src/app/resource/resource.component.spec.ts +++ b/packages/composer-playground/src/app/resource/resource.component.spec.ts @@ -1,28 +1,394 @@ /* tslint:disable:no-unused-variable */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { DebugElement, Component, Input } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TransactionComponent } from './transaction.component'; +import { CodemirrorComponent } from 'ng2-codemirror'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ClientService } from './../services/client.service'; +import { InitializationService } from './../initialization.service'; + +import { + Resource, + BusinessNetworkDefinition, + Serializer, + Introspector, + AssetDeclaration, + ParticipantDeclaration, + TransactionDeclaration, + ClassDeclaration, + Property, + Factory, + ModelFile + } from 'composer-common'; + + import { BusinessNetworkConnection, AssetRegistry } from 'composer-client'; import { ResourceComponent } from './resource.component'; +import * as sinon from 'sinon'; +let should = chai.should(); + +@Component({ + selector: 'codemirror', + template: '' +}) +class MockCodeMirrorComponent { + @Input() config: any; +} + describe('ResourceComponent', () => { - // let component: ResourceComponent; - // let fixture: ComponentFixture; - - // beforeEach(async(() => { - // TestBed.configureTestingModule({ - // declarations: [ ResourceComponent ] - // }) - // .compileComponents(); - // })); - - // beforeEach(() => { - // fixture = TestBed.createComponent(ResourceComponent); - // component = fixture.componentInstance; - // fixture.detectChanges(); - // }); - - // it('should create', () => { - // expect(component).toBeTruthy(); - // }); + let component: ResourceComponent; + let fixture: ComponentFixture; + + let mockNgbActiveModal; + let mockClientService; + let mockInitializationService; + let mockActivatedRoute; + + let mockBusinessNetworkConnection; + let mockBusinessNetwork; + let mockSerializer; + let mockFactory; + let mockIntrospector; + let mockResource; + + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + mockNgbActiveModal = sinon.createStubInstance(NgbActiveModal); + mockClientService = sinon.createStubInstance(ClientService); + mockInitializationService = sinon.createStubInstance(InitializationService); + + mockNgbActiveModal.open = sandbox.stub(); + mockNgbActiveModal.close = sandbox.stub(); + mockInitializationService.initialize.returns(Promise.resolve()); + + mockResource = sinon.createStubInstance(Resource); + mockBusinessNetworkConnection = sinon.createStubInstance(BusinessNetworkConnection); + mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + mockSerializer = sinon.createStubInstance(Serializer); + mockFactory = sinon.createStubInstance(Factory); + mockIntrospector = sinon.createStubInstance(Introspector); + + mockClientService.getBusinessNetwork.returns(mockBusinessNetwork); + mockClientService.getBusinessNetworkConnection.returns(mockBusinessNetworkConnection); + mockBusinessNetwork.getSerializer.returns(mockSerializer); + mockBusinessNetwork.getFactory.returns(mockFactory); + mockBusinessNetwork.getIntrospector.returns(mockIntrospector); + + TestBed.configureTestingModule({ + imports: [ + FormsModule + ], + declarations: [ + ResourceComponent, + MockCodeMirrorComponent + ], + providers: [ + { provide: NgbActiveModal, useValue: mockNgbActiveModal }, + { provide: ClientService, useValue: mockClientService }, + { provide: InitializationService, useValue: mockInitializationService } + ] + }); + fixture = TestBed.createComponent(ResourceComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#CodeMirror', () => { + it('should call the correct functions', () => { + let cm = { + foldCode: sandbox.stub(), + getCursor: sandbox.stub() + }; + + component['codeConfig'].extraKeys['Ctrl-Q'](cm); + cm.foldCode.should.be.called; + cm.getCursor.should.be.called; + }); + }); + + describe('#ngOnInit', () => { + let mockClassDeclaration; + beforeEach(() => { + component['registryID'] = 'org.acme.fqn'; + component['retrieveResourceType'] = sandbox.stub(); + component['generateResource'] = sandbox.stub(); + component['onDefinitionChanged'] = sandbox.stub(); + component['getResourceJSON'] = sandbox.stub(); + mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); + mockClassDeclaration.getFullyQualifiedName.returns('org.acme.fqn'); + mockIntrospector.getClassDeclarations.returns([mockClassDeclaration]); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should prepare to create a resource', fakeAsync(() => { + component['editMode'] = sandbox.stub().returns(false); + component.ngOnInit(); + tick(); + component['retrieveResourceType'].should.be.called; + component['editMode'].should.be.called; + component['generateResource'].should.be.called; + component['onDefinitionChanged'].should.be.called; + component['getResourceJSON'].should.be.called; + })); + + it('should prepare to edit a resource', fakeAsync(() => { + component['editMode'] = sandbox.stub().returns(true); + component.ngOnInit(); + tick(); + component['retrieveResourceType'].should.be.called; + component['editMode'].should.be.called; + component['onDefinitionChanged'].should.be.called; + component['getResourceJSON'].should.be.called; + component['generateResource'].should.not.be.called; + })); + + it('should skip resources in the incorrect registry', fakeAsync(() => { + mockClassDeclaration.getFullyQualifiedName.returns('org.acme.fqn.incorrect'); + component['editMode'] = sandbox.stub().returns(true); + component.ngOnInit(); + tick(); + component['retrieveResourceType'].should.not.be.called; + component['editMode'].should.not.be.called; + component['onDefinitionChanged'].should.not.be.called; + component['getResourceJSON'].should.not.be.called; + component['generateResource'].should.not.be.called; + })); + }); + + describe('#editMode', () => { + it('should return false', () => { + const result = component['editMode'](); + result.should.be.false; + }); + + it('should return true', () => { + component['resource'] = mockResource; + const result = component['editMode'](); + result.should.be.true; + }); + }); + + describe('#generateSampleData', () => { + it('should call generateResource and onDefinitionChanged', () => { + sandbox.stub(component, 'generateResource'); + sandbox.stub(component, 'onDefinitionChanged'); + component['generateSampleData'](); + component['generateResource'].should.be.called; + component['generateResource'].should.be.calledWith(true); + component['onDefinitionChanged'].should.be.called; + }); + }); + + describe('#generateResource', () => { + let mockClassDeclaration; + let mockModelFile; + beforeEach(() => { + mockModelFile = sinon.createStubInstance(ModelFile); + mockModelFile.getName.returns('model.cto'); + mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); + mockClassDeclaration.getModelFile.returns(mockModelFile); + mockClassDeclaration.getName.returns('class.declaration'); + mockSerializer.toJSON.returns({'$class': 'com.org'}); + }); + + it('should generate a valid resource', () => { + component['resourceDeclaration'] = mockClassDeclaration; + component['generateResource'](); + component['resourceDefinition'].should.equal('{\n "$class": "com.org"\n}'); + }); + + it('should set definitionError', () => { + mockSerializer.toJSON = () => { + throw new Error('error'); + }; + component['resourceDeclaration'] = mockClassDeclaration; + component['generateResource'](); + should.exist(component['defitionError']); + }); + }); + + describe('#getResourceJSON', () => { + it('should return stringified json', () => { + let json = { + '$class': 'com.acme', + }; + component['resource'] = mockResource; + mockSerializer.toJSON.returns(json); + const result = component['getResourceJSON'](); + result.should.equal(JSON.stringify(json, null, 2)); + }); + }); + + describe('#addOrUpdateResource', () => { + let mockRegistry; + beforeEach(() => { + mockRegistry = sinon.createStubInstance(AssetRegistry); + component['retrieveResourceRegistry'] = () => { + return Promise.resolve(mockRegistry); + }; + mockResource.validate = sandbox.stub(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should add a resource', fakeAsync(() => { + component['editMode'] = sandbox.stub().returns(false); + component['resourceDefinition'] = '{"class": "org.acme"}'; + mockSerializer.fromJSON.returns(mockResource); + + component['addOrUpdateResource'](); + + component['actionInProgress'].should.be.true; + tick(); + mockResource.validate.should.be.called; + component['actionInProgress'].should.be.false; + component['editMode'].should.be.called; + mockRegistry.add.should.be.called; + tick(); + component['actionInProgress'].should.be.false; + mockNgbActiveModal.close.should.be.called; + })); + + it('should update a resource', fakeAsync(() => { + component['editMode'] = sandbox.stub().returns(true); + component['resourceDefinition'] = '{"class": "org.acme"}'; + mockSerializer.fromJSON.returns(mockResource); + + component['addOrUpdateResource'](); + + component['actionInProgress'].should.be.true; + tick(); + mockResource.validate.should.be.called; + component['actionInProgress'].should.be.false; + component['editMode'].should.be.called; + mockRegistry.update.should.be.called; + tick(); + component['actionInProgress'].should.be.false; + mockNgbActiveModal.close.should.be.called; + })); + + it('should set definitionError', fakeAsync(() => { + component['resourceDefinition'] = 'will error'; + + component['addOrUpdateResource'](); + tick(); + should.exist(component['defitionError']); + component['actionInProgress'].should.be.false; + })); + }); + + describe('#onDefinitionChanged', () => { + it('should call validate()', () => { + mockResource.validate = sandbox.stub(); + component['resourceDefinition'] = '{"$class": "org.acme"}'; + mockSerializer.fromJSON.returns(mockResource); + + component['onDefinitionChanged'](); + + mockSerializer.fromJSON.should.be.called; + mockSerializer.fromJSON.should.be.calledWith({'$class': 'org.acme'}); + mockResource.validate.should.be.called; + should.not.exist(component['defitionError']); + }); + + it('should set definitionError', () => { + component['resourceDefinition'] = 'will error'; + component['onDefinitionChanged'](); + should.exist(component['defitionError']); + }); + }); + + describe('#retrieveResourceType', () => { + it('should return transaction', () => { + let mockTransaction = sinon.createStubInstance(TransactionDeclaration); + let result = component['retrieveResourceType'](mockTransaction); + result.should.equal('Transaction'); + }); + + it('should return asset', () => { + let mockAsset = sinon.createStubInstance(AssetDeclaration); + let result = component['retrieveResourceType'](mockAsset); + result.should.equal('Asset'); + }); + + it('should return participant', () => { + let mockParticipant = sinon.createStubInstance(ParticipantDeclaration); + let result = component['retrieveResourceType'](mockParticipant); + result.should.equal('Participant'); + }); + + it('should return nothing', () => { + let mockParticipant = sinon.createStubInstance(Object); + let result = component['retrieveResourceType']({}); + should.not.exist(result); + }); + }); + + describe('#generateDefinitionStub', () => { + it('should return a json schema stub', () => { + let registryId = 'com.acme.registry'; + let mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); + let mockProperty1 = sinon.createStubInstance(Property); + mockProperty1.getName.returns('prop1'); + let mockProperty2 = sinon.createStubInstance(Property); + mockProperty2.getName.returns('prop2'); + mockClassDeclaration.getProperties.returns([ + mockProperty1, + mockProperty2 + ]); + + let result = component['generateDefinitionStub'](registryId, mockClassDeclaration); + result.should.equal('{\n "$class": "com.acme.registry",\n "prop1": "",\n "prop2": ""\n}'); + }); + }); + + describe('#retrieveResourceRegistry', () => { + it('should return an AssetRegistry', () => { + let registryId = 'registryId'; + component['registryID'] = registryId; + mockBusinessNetworkConnection.getAssetRegistry.returns('testing'); + + let result = component['retrieveResourceRegistry']('Asset'); + mockBusinessNetworkConnection.getAssetRegistry.should.be.called; + mockBusinessNetworkConnection.getAssetRegistry.should.be.calledWith(registryId); + result.should.equal('testing'); + }); + + it('should return an PerticipantRegistry', () => { + let registryId = 'registryId'; + component['registryID'] = registryId; + mockBusinessNetworkConnection.getParticipantRegistry.returns('testing'); + + let result = component['retrieveResourceRegistry']('Participant'); + mockBusinessNetworkConnection.getParticipantRegistry.should.be.called; + mockBusinessNetworkConnection.getParticipantRegistry.should.be.calledWith(registryId); + result.should.equal('testing'); + }); + + it('should return a TransactionRegistry', () => { + let registryId = 'registryId'; + component['registryID'] = registryId; + mockBusinessNetworkConnection.getTransactionRegistry.returns('testing'); + + let result = component['retrieveResourceRegistry']('Transaction'); + mockBusinessNetworkConnection.getTransactionRegistry.should.be.called; + result.should.equal('testing'); + }); + }); }); diff --git a/packages/composer-playground/src/app/resource/resource.component.ts b/packages/composer-playground/src/app/resource/resource.component.ts index 155792d093..a7a8bc4187 100644 --- a/packages/composer-playground/src/app/resource/resource.component.ts +++ b/packages/composer-playground/src/app/resource/resource.component.ts @@ -3,7 +3,11 @@ import { ActivatedRoute } from '@angular/router'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ClientService } from '../services/client.service'; import { InitializationService } from '../initialization.service'; -import { ClassDeclaration, AssetDeclaration, ParticipantDeclaration, TransactionDeclaration } from 'composer-common'; +import { + ClassDeclaration, + AssetDeclaration, + ParticipantDeclaration, + TransactionDeclaration } from 'composer-common'; import leftPad = require('left-pad'); import 'codemirror/mode/javascript/javascript'; @@ -54,15 +58,10 @@ export class ResourceComponent implements OnInit { constructor( public activeModal: NgbActiveModal, - private route: ActivatedRoute, private clientService: ClientService, private initializationService: InitializationService) { } - private editMode(): boolean { - return (this.resource ? true : false); - } - ngOnInit(): Promise { return this.initializationService.initialize() .then(() => { @@ -76,7 +75,7 @@ export class ResourceComponent implements OnInit { // Set resource declaration this.resourceDeclaration = modelClassDeclaration; - this.resourceType = this.retrieveResourceType(modelClassDeclaration) + this.resourceType = this.retrieveResourceType(modelClassDeclaration); if (this.editMode()) { this.resourceAction = 'Update'; @@ -95,11 +94,15 @@ export class ResourceComponent implements OnInit { }); } + private editMode(): boolean { + return (this.resource ? true : false); + } + private generateSampleData(): void { this.generateResource(true); this.onDefinitionChanged(); } - + /** * Generate the json description of a resource */ @@ -113,7 +116,7 @@ export class ResourceComponent implements OnInit { this.resourceDeclaration.getModelFile().getNamespace(), this.resourceDeclaration.getName(), id, - { generate: true, "withSampleData": withSampleData }); + { generate: true, 'withSampleData': withSampleData }); let serializer = this.clientService.getBusinessNetwork().getSerializer(); try { let json = serializer.toJSON(resource); @@ -121,7 +124,7 @@ export class ResourceComponent implements OnInit { } catch (error) { // We can't generate a sample instance for some reason. this.defitionError = error.toString(); - this.resourceDefinition = ""; + this.resourceDefinition = ''; } } @@ -141,7 +144,7 @@ export class ResourceComponent implements OnInit { let serializer = this.clientService.getBusinessNetwork().getSerializer(); let resource = serializer.fromJSON(json); resource.validate(); - if(this.editMode()) { + if (this.editMode()) { return registry.update(resource); } else { return registry.add(resource); @@ -154,7 +157,7 @@ export class ResourceComponent implements OnInit { .catch((error) => { this.defitionError = error.toString(); this.actionInProgress = false; - }) + }); } @@ -178,18 +181,18 @@ export class ResourceComponent implements OnInit { */ private retrieveResourceType(modelClassDeclaration): string { if (modelClassDeclaration instanceof TransactionDeclaration) { - return "Transaction"; + return 'Transaction'; } else if (modelClassDeclaration instanceof AssetDeclaration) { - return "Asset"; + return 'Asset'; } else if (modelClassDeclaration instanceof ParticipantDeclaration) { - return "Participant"; + return 'Participant'; } } /** * Generate a stub resource definition */ - private generateDefinitionStub(registryID, modelClassDeclaration) : string { + private generateDefinitionStub(registryID, modelClassDeclaration): string { let stub = ''; stub = '{\n "$class": "' + registryID + '"'; let resourceProperties = modelClassDeclaration.getProperties(); @@ -205,7 +208,7 @@ export class ResourceComponent implements OnInit { */ private retrieveResourceRegistry(type) { - let client =this.clientService; + let client = this.clientService; let id = this.registryID; function isAsset() { @@ -220,7 +223,7 @@ export class ResourceComponent implements OnInit { return client.getBusinessNetworkConnection().getParticipantRegistry(id); } - var types = { + let types = { 'Asset': isAsset, 'Participant': isParticipant, 'Transaction': isTransaction From 351670c1e8cbc9645a94c1bcfe148fc8820743ba Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 29 Mar 2017 17:25:27 +0100 Subject: [PATCH 13/41] Github components unit tests --- .../src/app/github/github.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/composer-playground/src/app/github/github.component.ts b/packages/composer-playground/src/app/github/github.component.ts index b903885bb8..25acdc5e2d 100644 --- a/packages/composer-playground/src/app/github/github.component.ts +++ b/packages/composer-playground/src/app/github/github.component.ts @@ -1,8 +1,8 @@ -import {Component, OnInit} from '@angular/core'; -import {ActivatedRoute, Router} from '@angular/router'; -import {Http} from '@angular/http'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Http } from '@angular/http'; -import {SampleBusinessNetworkService} from '../services/samplebusinessnetwork.service'; +import { SampleBusinessNetworkService } from '../services/samplebusinessnetwork.service'; @Component({ selector: 'github', @@ -29,7 +29,7 @@ export class GithubComponent implements OnInit { this.sampleBusinessNetworkService.setUpGithub(token.access_token); this.sampleBusinessNetworkService.OPEN_SAMPLE = true; return this.router.navigate(['/editor']); - }) + }); }); } From 4c8c021c54454795367ff8c0d0ae5da77a28b3da Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 29 Mar 2017 17:28:44 +0100 Subject: [PATCH 14/41] Added test file --- .../src/app/github/github.component.spec.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 packages/composer-playground/src/app/github/github.component.spec.ts diff --git a/packages/composer-playground/src/app/github/github.component.spec.ts b/packages/composer-playground/src/app/github/github.component.spec.ts new file mode 100644 index 0000000000..4365adbe3b --- /dev/null +++ b/packages/composer-playground/src/app/github/github.component.spec.ts @@ -0,0 +1,109 @@ +import { TestBed, async, inject, fakeAsync, tick } from '@angular/core/testing'; +import { + HttpModule, + Http, + Response, + ResponseOptions, + XHRBackend, + ConnectionBackend +} from '@angular/http'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MockBackend } from '@angular/http/testing'; +import { SampleBusinessNetworkService } from './../services/samplebusinessnetwork.service'; +import { Observable } from 'rxjs/Observable'; + +import { GithubComponent } from './github.component'; + +import * as sinon from 'sinon'; +let should = chai.should(); + +class MockActivatedRoute { + queryParams = Observable.of({code: 'code'}); +} + +class MockRouter { + navigate = sinon.stub(); +} + +describe('GithubComponent', () => { + let sandbox; + let fixture; + let component; + let mockSampleBusinessNetwork; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + mockSampleBusinessNetwork = sinon.createStubInstance(SampleBusinessNetworkService); + mockSampleBusinessNetwork.setUpGithub = sandbox.stub(); + + TestBed.configureTestingModule({ + imports: [HttpModule], + declarations: [ + GithubComponent + ], + providers: [ + { provide: ActivatedRoute, useClass: MockActivatedRoute }, + { provide: Router, useClass: MockRouter}, + { provide: ConnectionBackend, useClass: MockBackend }, + { provide: SampleBusinessNetworkService, useValue: mockSampleBusinessNetwork }, + Http + ] + }); + + fixture = TestBed.createComponent(GithubComponent); + component = fixture.componentInstance; + }); + + describe('#ngOnInit', () => { + it('should setup the component', fakeAsync(() => { + sandbox.stub(component, 'exchangeCodeForAccessToken') + .returns(Promise.resolve({access_token: 'token'})); + + component.ngOnInit(); + tick(); + mockSampleBusinessNetwork.setUpGithub.should.be.called; + mockSampleBusinessNetwork.setUpGithub.should.be.calledWith('token'); + component.router.navigate.should.be.calledWith(['/editor']); + })); + }); + + describe('#exchangeCodeForAccessToken', () => { + + it('should get a token back from the http request', + async(inject([ConnectionBackend], (mockBackend) => { + + const mockResponse = { + access_token: 'token' + }; + + mockBackend.connections.subscribe((connection) => { + connection.mockRespond(new Response(new ResponseOptions({ + body: JSON.stringify(mockResponse) + }))); + }); + + component.exchangeCodeForAccessToken('code') + .then((response) => { + response.should.deep.equal(mockResponse); + }); + }))); + + it('should give an error back for a http request', + async(inject([ConnectionBackend], (mockBackend) => { + mockBackend.connections.subscribe( + (connection) => { + connection.mockError(new Error('error')); + } + ); + + component.exchangeCodeForAccessToken('code') + .then((response) => { + should.not.exist(response); + }) + .catch(error => { + should.exist(error); + }); + }))); + }); +}); From 6c49e4fa133b8a2fc8ee0b9d64dd5263248ec7ff Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 5 Apr 2017 13:51:36 +0100 Subject: [PATCH 15/41] Business network yeoman generator --- .../generators/angular/index.js | 1 - .../generators/angular/templates/package.json | 2 +- .../generators/app/index.js | 6 + .../generators/businessnetwork/index.js | 113 ++++++++++++++++++ .../businessnetwork/templates/README.md | 1 + .../templates/_dot_eslintrc.yml | 48 ++++++++ .../businessnetwork/templates/index.js | 13 ++ .../businessnetwork/templates/lib/logic.js | 24 ++++ .../templates/models/namespace.cto | 20 ++++ .../businessnetwork/templates/package.json | 22 ++++ .../businessnetwork/templates/test/logic.js | 104 ++++++++++++++++ 11 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 packages/generator-fabric-composer/generators/businessnetwork/index.js create mode 100644 packages/generator-fabric-composer/generators/businessnetwork/templates/README.md create mode 100755 packages/generator-fabric-composer/generators/businessnetwork/templates/_dot_eslintrc.yml create mode 100644 packages/generator-fabric-composer/generators/businessnetwork/templates/index.js create mode 100644 packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js create mode 100644 packages/generator-fabric-composer/generators/businessnetwork/templates/models/namespace.cto create mode 100644 packages/generator-fabric-composer/generators/businessnetwork/templates/package.json create mode 100644 packages/generator-fabric-composer/generators/businessnetwork/templates/test/logic.js diff --git a/packages/generator-fabric-composer/generators/angular/index.js b/packages/generator-fabric-composer/generators/angular/index.js index 641e1f2313..467ff43a69 100755 --- a/packages/generator-fabric-composer/generators/angular/index.js +++ b/packages/generator-fabric-composer/generators/angular/index.js @@ -1,7 +1,6 @@ 'use strict'; let yeoman = require('yeoman-generator'); let fs = require('fs'); -// let fs = require('fs'); let shell = require('shelljs'); const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; diff --git a/packages/generator-fabric-composer/generators/angular/templates/package.json b/packages/generator-fabric-composer/generators/angular/templates/package.json index d64bf05762..91ba3f0bb5 100644 --- a/packages/generator-fabric-composer/generators/angular/templates/package.json +++ b/packages/generator-fabric-composer/generators/angular/templates/package.json @@ -21,7 +21,7 @@ "@angular/platform-browser": "^2.4.0", "@angular/platform-browser-dynamic": "^2.4.0", "@angular/router": "^3.4.0", - "composer-client": "^0.5.0", + "composer-client": "^0.5.5", "composer-rest-server": "^0.5.5", "bootstrap": "^3.3.7", "concurrently": "^3.1.0", diff --git a/packages/generator-fabric-composer/generators/app/index.js b/packages/generator-fabric-composer/generators/app/index.js index ae61ec62d6..8228722cac 100755 --- a/packages/generator-fabric-composer/generators/app/index.js +++ b/packages/generator-fabric-composer/generators/app/index.js @@ -38,6 +38,9 @@ module.exports = generators.Base.extend({ },{ name: 'Angular2 Application', value: 'Angular2' + }, { + name: 'Skeleton Business Network', + value: 'businessnetwork' } ], store: true, @@ -66,6 +69,9 @@ module.exports = generators.Base.extend({ else if(this.generatorType === 'Angular2'){ console.log('You can run this generator using: \'yo fabric-composer:angular\''); this.composeWith(require.resolve('../angular')); + } else if (this.generatorType === 'businessnetwork') { + console.log('You can run this generator using: \'yo fabric-composer:businessnetwork\''); + this.composeWith(require.resolve('../businessnetwork')); } else{ console.log('Generator type not recognised'); diff --git a/packages/generator-fabric-composer/generators/businessnetwork/index.js b/packages/generator-fabric-composer/generators/businessnetwork/index.js new file mode 100644 index 0000000000..a20da0a78c --- /dev/null +++ b/packages/generator-fabric-composer/generators/businessnetwork/index.js @@ -0,0 +1,113 @@ +'use strict'; + +let yeoman = require('yeoman-generator'); + +module.exports = yeoman.Base.extend({ + constructor: function() { + yeoman.Base.apply(this, arguments); + this.options = this.env.options; + }, + + prompting: function() { + console.log('Welcome to the business network skeleton generator'); + + let questions = [ + { + type: 'input', + name: 'appname', + message: 'What is the business network\'s identifier?', + store: false, + validate: function(input) { + if(input !== null && input !== undefined && input !== '' && input.indexOf(' ') === -1) { + return true; + } else { + return 'Name cannot be null, empty or contain a space.'; + } + } + }, + { + type: 'input', + name: 'namespace', + message: 'What is the business network\'s namespace?', + default: 'org.acme.biznet', + store: false, + validate: function(input) { + if(input !== null && input !== undefined && input.match(/^(?:[a-z]\d*(?:\.[a-z])?)+$/)) { + return true; + } else { + return 'Name must mactch: ^(?:[a-z]\d*(?:\.[a-z])?)+$'; + } + } + }, + { + type: 'input', + name: 'appdescription', + message: 'Describe the business network', + store: false, + validate: function(input) { + if(input !== null && input !== undefined && input !== '') { + return true; + } else { + return 'Description cannot be null or empty.'; + } + } + }, + { + type: 'input', + name: 'appauthor', + message: 'Who is the author?', + store: false, + validate: function(input) { + if(input !== null && input !== undefined && input !== '') { + return true; + } else { + return 'Author cannot be null or empty.'; + } + } + }, + { + type: 'input', + name: 'applicense', + message: 'Which license do you want to use?', + default: 'Apache-2', + store: false, + validate: function(input) { + if(input !== null && input !== undefined && input !== '') { + return true; + } else { + return 'Licence cannot be null or empty.'; + } + } + } + ]; + + return this.prompt(questions) + .then((answers) => { + this.appname = answers.appname; + this.namespace = answers.namespace; + this.appdescription = answers.appdescription; + this.appauthor = answers.appauthor; + this.applicense = answers.applicense; + }); + }, + + configuring: function() { + this.destinationRoot(this.appname); + }, + + writing: function() { + let model = this._generateTemplateModel(); + this.fs.copyTpl(this.templatePath('**/*'), this.destinationPath(), model); + this.fs.move(this.destinationPath('_dot_eslintrc.yml'), this.destinationPath('.eslintrc.yml')); + }, + + _generateTemplateModel: function() { + return { + appname: this.appname, + namespace: this.namespace, + appdescription: this.appdescription, + appauthor: this.appauthor, + applicense: this.applicense + }; + } +}); \ No newline at end of file diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/README.md b/packages/generator-fabric-composer/generators/businessnetwork/templates/README.md new file mode 100644 index 0000000000..eed546d9c0 --- /dev/null +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/README.md @@ -0,0 +1 @@ +# <%= namespace%> diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/_dot_eslintrc.yml b/packages/generator-fabric-composer/generators/businessnetwork/templates/_dot_eslintrc.yml new file mode 100755 index 0000000000..6405ad7c8d --- /dev/null +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/_dot_eslintrc.yml @@ -0,0 +1,48 @@ +env: + node: true + mocha: true +extends: 'eslint:recommended' +parserOptions: + sourceType: + - script +globals: + getAssetRegistry: true + getFactory: true + getParticipantRegistry: true + getCurrentParticipant: true +rules: + indent: + - error + - 4 + linebreak-style: + - error + - unix + quotes: + - error + - single + semi: + - error + - always + no-unused-vars: + - 0 + - args: none + no-console: off + curly: error + eqeqeq: error + no-throw-literal: error + strict: error + dot-notation: error + no-tabs: error + no-trailing-spaces: error + no-useless-call: error + no-with: error + operator-linebreak: error + require-jsdoc: + - error + - require: + ClassDeclaration: true + MethodDefinition: true + FunctionDeclaration: true + yoda: error + no-confusing-arrow: 2 + no-constant-condition: 2 diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/index.js b/packages/generator-fabric-composer/generators/businessnetwork/templates/index.js new file mode 100644 index 0000000000..e1971c44ef --- /dev/null +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/index.js @@ -0,0 +1,13 @@ +/* + * 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. + */ diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js b/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js new file mode 100644 index 0000000000..13a7594c4a --- /dev/null +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js @@ -0,0 +1,24 @@ +'use strict'; +/** + * Write your transction processor functions here + */ + +/** + * Sample transaction + * @param {<%= namespace%>.ChangeAssetValue} changeAssetValue + * @transaction + */ +function onChangeAssetValue(changeAssetValue) { + var assetRegistry; + var id = changeAssetValue.relatedAsset.assetId; + var value = changeAssetValue.relatedAsset.newValue; + return getAssetRegistry('<%= namespace%>.Asset') + .then(function(ar) { + assetRegistry = ar; + return assetRegistry.get(id) + }) + .then(function(asset) { + asset.value = changeAssetValue.newValue + return assetRegistry.update(asset); + }); +} \ No newline at end of file diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/models/namespace.cto b/packages/generator-fabric-composer/generators/businessnetwork/templates/models/namespace.cto new file mode 100644 index 0000000000..13b441749b --- /dev/null +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/models/namespace.cto @@ -0,0 +1,20 @@ +/** + * Write your model definitions here + */ + +namespace <%= namespace %> + +participant User identified by email { + o String email +} + +asset Asset identified by assetId { + o String assetId + o String value +} + +transaction ChangeAssetValue identified by transactionId { + o String transactionId + o String newValue + --> Asset relatedAsset +} \ No newline at end of file diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/package.json b/packages/generator-fabric-composer/generators/businessnetwork/templates/package.json new file mode 100644 index 0000000000..29eca99494 --- /dev/null +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/package.json @@ -0,0 +1,22 @@ +{ + "name": "<%= appname %>", + "version": "0.0.1", + "description": "<%= appdescription %>", + "scripts": { + "test": "mocha --recursive" + }, + "author": "<%= appauthor %>", + "license": "<%= applicense %>", + "devDependencies": { + "composer-admin": "latest", + "composer-cli": "latest", + "composer-client": "latest", + "composer-connector-embedded": "latest", + "browserfs": "latest", + "chai": "latest", + "eslint": "latest", + "istanbul": "latest", + "mkdirp": "latest", + "mocha": "latest" + } +} diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/test/logic.js b/packages/generator-fabric-composer/generators/businessnetwork/templates/test/logic.js new file mode 100644 index 0000000000..510ee4254c --- /dev/null +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/test/logic.js @@ -0,0 +1,104 @@ + +/* + * 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'; +/** + * Write the unit tests for your transction processor functions here + */ + +var AdminConnection = require('composer-admin').AdminConnection; +var BrowserFS = require('browserfs/dist/node/index'); +var BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; +var BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; +var path = require('path'); +var fs = require('fs'); + +require('chai').should(); + +var bfs_fs = BrowserFS.BFSRequire('fs'); +var NS = '<%= namespace%>'; + +describe('#'+NS, function() { + + var businessNetworkConnection; + + before(function() { + BrowserFS.initialize(new BrowserFS.FileSystem.InMemory()); + var adminConnection = new AdminConnection({ fs: bfs_fs }); + return adminConnection.createProfile('defaultProfile', { + type: 'embedded' + }) + .then(function() { + return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); + }) + .then(function() { + return BusinessNetworkDefinition.fromDirectory(path.resolve(__dirname, '..')); + }) + .then(function(businessNetworkDefinition) { + return adminConnection.deploy(businessNetworkDefinition); + }) + .then(function() { + businessNetworkConnection = new BusinessNetworkConnection({ fs: bfs_fs }); + return businessNetworkConnection.connect('defaultProfile', '<%= appname%>', 'admin', 'Xurw3yU9zI0l'); + }); + }); + + describe('ChangeAssetValue()', function() { + + it('should change the value property of Asset to newValue', () => { + + var factory = businessNetworkConnection.getBusinessNetwork().getFactory(); + + // create a user + var user = factory.newResource(NS, 'User', '<%= appauthor%>'); + + // create the asset + var asset = factory.newResource(NS, 'Asset', 'ASSET_001'); + asset.value = 'old-value'; + + var changeAssetValue = factory.newTransaction(NS, 'ChangeAssetValue'); + changeAssetValue.relatedAsset = factory.newRelationship(NS, 'Asset', asset.$identifier); + changeAssetValue.newValue = 'new-value'; + + // Get the asset registry. + return businessNetworkConnection.getAssetRegistry(NS + '.Asset') + .then(function(registry) { + + // Add the Asset to the asset registry. + return registry.add(asset) + .then(function() { + return businessNetworkConnection.getParticipantRegistry(NS + '.User'); + }) + .then(function(userRegistry) { + return userRegistry.add(user); + }) + .then(function() { + // submit the transaction + return businessNetworkConnection.submitTransaction(changeAssetValue); + }) + .then(function() { + return businessNetworkConnection.getAssetRegistry(NS + '.Asset'); + }) + .then(function(registry) { + // get the listing + return registry.get(asset.$identifier); + }) + .then(function(newAsset) { + newAsset.value.should.equal('new-value'); + }); + }); + }); + }); +}); \ No newline at end of file From 690f68fe4624708f416098fca8d7bf0d2b46f411 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 5 Apr 2017 15:20:34 +0100 Subject: [PATCH 16/41] Clean up template and give transaction template the correct name --- .../generators/businessnetwork/index.js | 3 ++- .../generators/businessnetwork/templates/lib/logic.js | 2 +- .../generators/businessnetwork/templates/package.json | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/generator-fabric-composer/generators/businessnetwork/index.js b/packages/generator-fabric-composer/generators/businessnetwork/index.js index a20da0a78c..bfe4fadd25 100644 --- a/packages/generator-fabric-composer/generators/businessnetwork/index.js +++ b/packages/generator-fabric-composer/generators/businessnetwork/index.js @@ -97,8 +97,9 @@ module.exports = yeoman.Base.extend({ writing: function() { let model = this._generateTemplateModel(); - this.fs.copyTpl(this.templatePath('**/*'), this.destinationPath(), model); + this.fs.copyTpl(this.templatePath('**/!(models|node_modules)*'), this.destinationPath(), model); this.fs.move(this.destinationPath('_dot_eslintrc.yml'), this.destinationPath('.eslintrc.yml')); + this.fs.move(this.destinationPath('./models/namespace.cto'), this.destinationPath('./models/'+this.namespace+'.cto')); }, _generateTemplateModel: function() { diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js b/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js index 13a7594c4a..8d5258402a 100644 --- a/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js @@ -4,7 +4,7 @@ */ /** - * Sample transaction + * Sample transaction * @param {<%= namespace%>.ChangeAssetValue} changeAssetValue * @transaction */ diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/package.json b/packages/generator-fabric-composer/generators/businessnetwork/templates/package.json index 29eca99494..b12538a6cf 100644 --- a/packages/generator-fabric-composer/generators/businessnetwork/templates/package.json +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/package.json @@ -9,7 +9,6 @@ "license": "<%= applicense %>", "devDependencies": { "composer-admin": "latest", - "composer-cli": "latest", "composer-client": "latest", "composer-connector-embedded": "latest", "browserfs": "latest", From ea7659409eccc3ed489008e4a6e624cc53266fb3 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 5 Apr 2017 15:28:28 +0100 Subject: [PATCH 17/41] Added missing semi colon --- .../generators/businessnetwork/templates/lib/logic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js b/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js index 8d5258402a..3264a07324 100644 --- a/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/lib/logic.js @@ -18,7 +18,7 @@ function onChangeAssetValue(changeAssetValue) { return assetRegistry.get(id) }) .then(function(asset) { - asset.value = changeAssetValue.newValue + asset.value = changeAssetValue.newValue; return assetRegistry.update(asset); }); } \ No newline at end of file From ff9fa14a244666f55b89c02825206d0a698a84c5 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Thu, 6 Apr 2017 10:18:09 +0100 Subject: [PATCH 18/41] Added docs about skeleton business network generator --- .../jekylldocs/business-network/bnd-define.md | 42 ++++++++++++++++++ .../generators/businessnetwork/index.js | 2 +- .../businessnetwork/templates/index.js | 13 ------ .../businessnetwork/templates/test/logic.js | 43 ++++++------------- 4 files changed, 57 insertions(+), 43 deletions(-) diff --git a/packages/composer-website/jekylldocs/business-network/bnd-define.md b/packages/composer-website/jekylldocs/business-network/bnd-define.md index 27baa18864..28ebd3b72a 100644 --- a/packages/composer-website/jekylldocs/business-network/bnd-define.md +++ b/packages/composer-website/jekylldocs/business-network/bnd-define.md @@ -15,6 +15,48 @@ A business network definition is composed of three major items: * a set of domain models that define the structure of the participants, assets and transactions within the network * a set of scripts that define business logic +## Generating Skeleton Business Network + +### Installing Yeoman Generator +Instructions on installing `yo` and Fabric Composer's Yeoman generator are found [here](../applications/genapp.md) + +### Generation +1. `yo fabric-composer` + +```bash +Welcome to the Fabric Composer Skeleton Application Generator? +Please select the type of Application: + CLI Application + Angular2 Application +❯ Skeleton Business Network +``` +And select `Skeleton Business Netork` + +2. Answer all of the questions +```bash +Welcome to the Fabric Composer Skeleton Application Generator +? Please select the type of Application: Skeleton Business Network +You can run this generator using: 'yo fabric-composer:businessnetwork' +Welcome to the business network skeleton generator +? What is the business network's name? sample-network +? What is the business network's namespace? org.acme.biznet +? Describe the business network Sample Business Network +? Who is the author? Joe Bloggs +? Which license do you want to use? Apache-2 + create index.js + create lib/logic.js + create package.json + create README.md + create test/logic.js + create .eslintrc.yml + create models/org.acme.biznet.cto + +``` + +This generates a skeleton business network with an `asset`, `participant` and `transaction` defined, as well as a `mocha` unit test. + +Also included, is a 'best practices' eslint config file + ## Metadata A Business Network Definition has a name (limited to basic ASCII alphanumeric characters and `-`), a human-readable description and a version number. The version number for the network should take the form Major.Minor.Micro and diff --git a/packages/generator-fabric-composer/generators/businessnetwork/index.js b/packages/generator-fabric-composer/generators/businessnetwork/index.js index bfe4fadd25..a49a37f23e 100644 --- a/packages/generator-fabric-composer/generators/businessnetwork/index.js +++ b/packages/generator-fabric-composer/generators/businessnetwork/index.js @@ -15,7 +15,7 @@ module.exports = yeoman.Base.extend({ { type: 'input', name: 'appname', - message: 'What is the business network\'s identifier?', + message: 'What is the business network\'s name?', store: false, validate: function(input) { if(input !== null && input !== undefined && input !== '' && input.indexOf(' ') === -1) { diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/index.js b/packages/generator-fabric-composer/generators/businessnetwork/templates/index.js index e1971c44ef..e69de29bb2 100644 --- a/packages/generator-fabric-composer/generators/businessnetwork/templates/index.js +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/index.js @@ -1,13 +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. - */ diff --git a/packages/generator-fabric-composer/generators/businessnetwork/templates/test/logic.js b/packages/generator-fabric-composer/generators/businessnetwork/templates/test/logic.js index 510ee4254c..8eef5d1a65 100644 --- a/packages/generator-fabric-composer/generators/businessnetwork/templates/test/logic.js +++ b/packages/generator-fabric-composer/generators/businessnetwork/templates/test/logic.js @@ -1,42 +1,27 @@ - -/* - * 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'; /** * Write the unit tests for your transction processor functions here */ -var AdminConnection = require('composer-admin').AdminConnection; -var BrowserFS = require('browserfs/dist/node/index'); -var BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; -var BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; -var path = require('path'); -var fs = require('fs'); +let AdminConnection = require('composer-admin').AdminConnection; +let BrowserFS = require('browserfs/dist/node/index'); +let BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; +let BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; +let path = require('path'); +let fs = require('fs'); require('chai').should(); -var bfs_fs = BrowserFS.BFSRequire('fs'); -var NS = '<%= namespace%>'; +let bfs_fs = BrowserFS.BFSRequire('fs'); +let NS = '<%= namespace%>'; describe('#'+NS, function() { - var businessNetworkConnection; + let businessNetworkConnection; before(function() { BrowserFS.initialize(new BrowserFS.FileSystem.InMemory()); - var adminConnection = new AdminConnection({ fs: bfs_fs }); + let adminConnection = new AdminConnection({ fs: bfs_fs }); return adminConnection.createProfile('defaultProfile', { type: 'embedded' }) @@ -59,16 +44,16 @@ describe('#'+NS, function() { it('should change the value property of Asset to newValue', () => { - var factory = businessNetworkConnection.getBusinessNetwork().getFactory(); + let factory = businessNetworkConnection.getBusinessNetwork().getFactory(); // create a user - var user = factory.newResource(NS, 'User', '<%= appauthor%>'); + let user = factory.newResource(NS, 'User', '<%= appauthor%>'); // create the asset - var asset = factory.newResource(NS, 'Asset', 'ASSET_001'); + let asset = factory.newResource(NS, 'Asset', 'ASSET_001'); asset.value = 'old-value'; - var changeAssetValue = factory.newTransaction(NS, 'ChangeAssetValue'); + let changeAssetValue = factory.newTransaction(NS, 'ChangeAssetValue'); changeAssetValue.relatedAsset = factory.newRelationship(NS, 'Asset', asset.$identifier); changeAssetValue.newValue = 'new-value'; From 02bad519652fd7a7a65519d122fb9aa2b5ebfac3 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Thu, 6 Apr 2017 10:52:44 +0100 Subject: [PATCH 19/41] Added newlines to docs to stop md parse issues --- .../jekylldocs/business-network/bnd-define.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/composer-website/jekylldocs/business-network/bnd-define.md b/packages/composer-website/jekylldocs/business-network/bnd-define.md index 28ebd3b72a..add7d43a1c 100644 --- a/packages/composer-website/jekylldocs/business-network/bnd-define.md +++ b/packages/composer-website/jekylldocs/business-network/bnd-define.md @@ -10,7 +10,8 @@ excerpt: How to create a business network definition --- -A business network definition is composed of three major items: +A business network definition is composed of three major items: + * basic metadata for the business network definition (name, version and description) * a set of domain models that define the structure of the participants, assets and transactions within the network * a set of scripts that define business logic @@ -23,7 +24,7 @@ Instructions on installing `yo` and Fabric Composer's Yeoman generator are found ### Generation 1. `yo fabric-composer` -```bash +``` Welcome to the Fabric Composer Skeleton Application Generator? Please select the type of Application: CLI Application @@ -33,7 +34,8 @@ Please select the type of Application: And select `Skeleton Business Netork` 2. Answer all of the questions -```bash + +``` Welcome to the Fabric Composer Skeleton Application Generator ? Please select the type of Application: Skeleton Business Network You can run this generator using: 'yo fabric-composer:businessnetwork' @@ -50,7 +52,6 @@ Welcome to the business network skeleton generator create test/logic.js create .eslintrc.yml create models/org.acme.biznet.cto - ``` This generates a skeleton business network with an `asset`, `participant` and `transaction` defined, as well as a `mocha` unit test. From 9d0dac384a719229087ee858001ca3575da4491f Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Fri, 7 Apr 2017 13:31:48 +0100 Subject: [PATCH 20/41] Stop parser accepting floats as string defaults and ranges, and vice versa for ranges --- .../lib/introspect/parser.pegjs | 56 ++++++++++++--- .../test/data/parser/types.cto | 8 +-- .../composer-common/test/introspect/types.js | 70 +++++++++++++++++++ 3 files changed, 121 insertions(+), 13 deletions(-) diff --git a/packages/composer-common/lib/introspect/parser.pegjs b/packages/composer-common/lib/introspect/parser.pegjs index c27e4b48e3..ed8f572aa4 100644 --- a/packages/composer-common/lib/introspect/parser.pegjs +++ b/packages/composer-common/lib/introspect/parser.pegjs @@ -280,6 +280,10 @@ DecimalLiteral / DecimalIntegerLiteral ExponentPart? { return { type: "Literal", value: parseFloat(text()) }; } + +SignedRealLiteral= [+-]? DecimalIntegerLiteral "." DecimalDigit* ExponentPart? { + return { type: "Literal", value: parseFloat(text()) }; + } DecimalIntegerLiteral = "0" @@ -1293,6 +1297,12 @@ BooleanType = "Boolean" !IdentifierPart { NumberType = IntegerType / DoubleType / LongType + +RealNumberType + = DoubleType + +WholeNumberType + = IntegerType / LongType PrimitiveType = StringType / @@ -1396,23 +1406,29 @@ StringDefault return def.value; } -NumberDefault - = "default" __ "=" __ def:$SignedNumber { +BooleanDefault + = "default" __ "=" __ def:$BooleanLiteral { return def; } + +IntegerDefault + = "default" __ "=" __ def:$SignedInteger { + return def; + } -BooleanDefault - = "default" __ "=" __ def:$BooleanLiteral { +RealDefault + = "default" __ "=" __ def:SignedRealLiteral{ return def; } FieldDeclarations = StringFieldDeclaration - / NumberFieldDeclaration + / RealFieldDeclaration / BooleanFieldDeclaration / DateTimeFieldDeclaration / RelationshipDeclaration / ObjectFieldDeclaration + / IntegerFieldDeclaration ClassDeclarationBody = decls:FieldDeclarations* { @@ -1481,16 +1497,38 @@ StringRegexValidator return regex } -NumericDomainValidator - = "range" __ "=" __ "[" __ lower:SignedNumber? __ "," __ upper:SignedNumber? __ "]" { +RealDomainValidator + = "range" __ "=" __ "[" __ lower:SignedRealLiteral? __ "," __ upper:SignedRealLiteral? __ "]" { + return { + lower: lower, + upper: upper + } + } + +IntegerDomainValidator + = "range" __ "=" __ "[" __ lower:SignedInteger? __ "," __ upper:SignedInteger? __ "]" { return { lower: lower, upper: upper } } -NumberFieldDeclaration - = "o" __ propertyType:NumberType __ array:"[]"? __ id:Identifier __ d:NumberDefault? __ range:NumericDomainValidator? __ optional:Optional? __ { +RealFieldDeclaration + = "o" __ propertyType:RealNumberType __ array:"[]"? __ id:Identifier __ d:RealDefault? __ range:RealDomainValidator? __ optional:Optional? __ { + return { + type: "FieldDeclaration", + id: id, + propertyType: {name:propertyType}, + array: array, + range: range, + default: d, + optional: optional, + location: location() + } + } + +IntegerFieldDeclaration + = "o" __ propertyType:WholeNumberType __ array:"[]"? __ id:Identifier __ d:IntegerDefault? __ range:IntegerDomainValidator? __ optional:Optional? __ { return { type: "FieldDeclaration", id: id, diff --git a/packages/composer-common/test/data/parser/types.cto b/packages/composer-common/test/data/parser/types.cto index f1d94ca0e0..2434efaad1 100644 --- a/packages/composer-common/test/data/parser/types.cto +++ b/packages/composer-common/test/data/parser/types.cto @@ -20,7 +20,7 @@ concept BooleanValue { concept TestConcept { o String s default="test" regex=/test/ - o Double d default = 1 range=[0,10] optional + o Double d default = 1.0 range=[0.0,10.0] optional o Long l default = 1 range=[0,10] o Integer i default = 1 range=[0,10] optional o DateTime date default = "" optional @@ -62,7 +62,7 @@ concept Defaults { } concept Ranges { - o Double d range=[-10,10] - o Double d2 range=[,10] - o Double d3 range=[-10,] + o Double d range=[-10.0,10.0] + o Double d2 range=[,10.0] + o Double d3 range=[-10.0,] } \ No newline at end of file diff --git a/packages/composer-common/test/introspect/types.js b/packages/composer-common/test/introspect/types.js index 0ad7e811ed..e8981171bb 100644 --- a/packages/composer-common/test/introspect/types.js +++ b/packages/composer-common/test/introspect/types.js @@ -44,5 +44,75 @@ describe('ModelFile type parsing', () => { const mf = new ModelFile(mockModelManager,invalidModel, 'types.cto'); mf.validate(); }); + + it('should be invalid due to decimal default', () => { + const model = ` + namespace org.acme + + concept Address { + o Integer foo default=104.0 + } + `; + (() => { + const mf = new ModelFile(mockModelManager, model, 'test.cto'); + mf.validate(); + }).should.throw(); + }); + + it('should be invalid due to no decimal default', () => { + const model = ` + namespace org.acme + + concept Address { + o Double foo default=104 + } + `; + (() => { + const mf = new ModelFile(mockModelManager, model, 'test.cto'); + mf.validate(); + }).should.throw(); + }); + + it('should be invalid due to decimal range', () => { + const model = ` + namespace org.acme + + concept Address { + o Integer foo range=[0.1, 0.2] + } + `; + (() => { + const mf = new ModelFile(mockModelManager, model, 'test.cto'); + mf.validate(); + }).should.throw(); + }); + + it('should be invalid due to decimal default', () => { + const model = ` + namespace org.acme + + concept Address { + o Integer foo default=104.0 + } + `; + (() => { + const mf = new ModelFile(mockModelManager, model, 'test.cto'); + mf.validate(); + }).should.throw(); + }); + + it('should be invalid due to no decimal range', () => { + const model = ` + namespace org.acme + + concept Address { + o Double foo range=[0, 1] + } + `; + (() => { + const mf = new ModelFile(mockModelManager, model, 'test.cto'); + mf.validate(); + }).should.throw(); + }); }); }); From 53f25783cc53575c2bc6be37d14b6fbabe6f2165 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Fri, 7 Apr 2017 14:13:34 +0100 Subject: [PATCH 21/41] Added option to allow create of models only --- .../generators/businessnetwork/index.js | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/generator-fabric-composer/generators/businessnetwork/index.js b/packages/generator-fabric-composer/generators/businessnetwork/index.js index a49a37f23e..5d05b3c613 100644 --- a/packages/generator-fabric-composer/generators/businessnetwork/index.js +++ b/packages/generator-fabric-composer/generators/businessnetwork/index.js @@ -1,6 +1,7 @@ 'use strict'; let yeoman = require('yeoman-generator'); +let mkdirp = require('mkdirp'); module.exports = yeoman.Base.extend({ constructor: function() { @@ -12,11 +13,18 @@ module.exports = yeoman.Base.extend({ console.log('Welcome to the business network skeleton generator'); let questions = [ + { + type: 'confirm', + name: 'ismodel', + message: 'Do you only want to generate a model?', + store: true, + default: false + }, { type: 'input', name: 'appname', message: 'What is the business network\'s name?', - store: false, + store: true, validate: function(input) { if(input !== null && input !== undefined && input !== '' && input.indexOf(' ') === -1) { return true; @@ -30,7 +38,7 @@ module.exports = yeoman.Base.extend({ name: 'namespace', message: 'What is the business network\'s namespace?', default: 'org.acme.biznet', - store: false, + store: true, validate: function(input) { if(input !== null && input !== undefined && input.match(/^(?:[a-z]\d*(?:\.[a-z])?)+$/)) { return true; @@ -43,7 +51,7 @@ module.exports = yeoman.Base.extend({ type: 'input', name: 'appdescription', message: 'Describe the business network', - store: false, + store: true, validate: function(input) { if(input !== null && input !== undefined && input !== '') { return true; @@ -56,7 +64,7 @@ module.exports = yeoman.Base.extend({ type: 'input', name: 'appauthor', message: 'Who is the author?', - store: false, + store: true, validate: function(input) { if(input !== null && input !== undefined && input !== '') { return true; @@ -70,7 +78,7 @@ module.exports = yeoman.Base.extend({ name: 'applicense', message: 'Which license do you want to use?', default: 'Apache-2', - store: false, + store: true, validate: function(input) { if(input !== null && input !== undefined && input !== '') { return true; @@ -88,6 +96,7 @@ module.exports = yeoman.Base.extend({ this.appdescription = answers.appdescription; this.appauthor = answers.appauthor; this.applicense = answers.applicense; + this.ismodel = answers.ismodel; }); }, @@ -97,9 +106,15 @@ module.exports = yeoman.Base.extend({ writing: function() { let model = this._generateTemplateModel(); - this.fs.copyTpl(this.templatePath('**/!(models|node_modules)*'), this.destinationPath(), model); - this.fs.move(this.destinationPath('_dot_eslintrc.yml'), this.destinationPath('.eslintrc.yml')); - this.fs.move(this.destinationPath('./models/namespace.cto'), this.destinationPath('./models/'+this.namespace+'.cto')); + this.fs.copyTpl(this.templatePath('**!(models|lib|test)*'), this.destinationPath(), model); + this.fs.copyTpl(this.templatePath('models/namespace.cto'), this.destinationPath('models/'+this.namespace+'.cto'), model); + this.fs.move(this.destinationPath('_dot_eslintrc.yml'), this.destinationPath('.eslintrc.yml'), model); + if (!this.ismodel) { + this.fs.copyTpl(this.templatePath('./test'), this.destinationPath('./test'), model); + this.fs.copyTpl(this.templatePath('./lib'), this.destinationPath('./lib'), model); + } else { + mkdirp.sync(this.destinationPath('test')); + } }, _generateTemplateModel: function() { From da8db71c0ef8e07f3df243e0f64c18ddd11fe279 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Tue, 11 Apr 2017 15:27:22 +0100 Subject: [PATCH 22/41] Fixed truncation of large data when pressing 'Show All' --- .../src/app/registry/registry.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/composer-playground/src/app/registry/registry.component.scss b/packages/composer-playground/src/app/registry/registry.component.scss index ed68e258dd..7cfda73463 100644 --- a/packages/composer-playground/src/app/registry/registry.component.scss +++ b/packages/composer-playground/src/app/registry/registry.component.scss @@ -68,7 +68,7 @@ registry { &.expanded { pre { - max-height: 600px; + max-height: 100%; overflow: hidden; @include transition(all); From abfb3077425d28e4ecb59646219839ef99c06af7 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Tue, 18 Apr 2017 10:40:12 +0100 Subject: [PATCH 23/41] Fixed business network generator issue #668 --- packages/generator-fabric-composer/package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/generator-fabric-composer/package.json b/packages/generator-fabric-composer/package.json index 0c5f3293dc..9285b65bb6 100755 --- a/packages/generator-fabric-composer/package.json +++ b/packages/generator-fabric-composer/package.json @@ -6,11 +6,6 @@ "node": ">=6", "npm": ">=3" }, - "files": [ - "generators/app", - "generators/cli", - "generators/angular" - ], "keywords": [ "yeoman-generator" ], From 508678e3467daeacfbc03d5dcc386c30b3fcf4b4 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Tue, 18 Apr 2017 10:53:41 +0100 Subject: [PATCH 24/41] Slight imporovement to deploy error message --- packages/composer-cli/lib/cmds/network/lib/deploy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/composer-cli/lib/cmds/network/lib/deploy.js b/packages/composer-cli/lib/cmds/network/lib/deploy.js index 2c47159829..44466bba2a 100644 --- a/packages/composer-cli/lib/cmds/network/lib/deploy.js +++ b/packages/composer-cli/lib/cmds/network/lib/deploy.js @@ -117,7 +117,9 @@ class Deploy { return result; }).catch((error) => { - spinner.fail(); + if (spinner) { + spinner.fail(); + } console.log(); throw error; From 3a449d6beffc1667be7e68c610ce21d7965b805b Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Fri, 21 Apr 2017 14:40:07 +0100 Subject: [PATCH 25/41] Added checks and tests to solve issue #240 --- .../lib/introspect/classdeclaration.js | 66 +++++++++++++++++++ .../parser/classdeclaration.dupeassetname.cto | 11 ++++ .../classdeclaration.dupeconceptname.cto | 11 ++++ .../parser/classdeclaration.dupeenumname.cto | 11 ++++ .../classdeclaration.dupeparticipantname.cto | 11 ++++ .../classdeclaration.dupetransactionname.cto | 11 ++++ .../test/introspect/assetdeclaration.js | 1 - .../test/introspect/classdeclaration.js | 53 +++++++++++++++ .../test/serializer/jsongenerator.js | 2 +- .../test/serializer/jsonpopulator.js | 2 +- 10 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 packages/composer-common/test/data/parser/classdeclaration.dupeassetname.cto create mode 100644 packages/composer-common/test/data/parser/classdeclaration.dupeconceptname.cto create mode 100644 packages/composer-common/test/data/parser/classdeclaration.dupeenumname.cto create mode 100644 packages/composer-common/test/data/parser/classdeclaration.dupeparticipantname.cto create mode 100644 packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto diff --git a/packages/composer-common/lib/introspect/classdeclaration.js b/packages/composer-common/lib/introspect/classdeclaration.js index b85f1dd16e..0d2cdc5084 100644 --- a/packages/composer-common/lib/introspect/classdeclaration.js +++ b/packages/composer-common/lib/introspect/classdeclaration.js @@ -129,6 +129,72 @@ class ClassDeclaration { * @private */ validate() { + + const assetDeclarations = this.getModelFile().getAssetDeclarations(); + for(let n=0; n < assetDeclarations.length; n++) { + let declaration = assetDeclarations[n]; + + // check we don't have an asset with the same name + for(let i=n+1; i < assetDeclarations.length; i++) { + let otherDeclaration = assetDeclarations[i]; + if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { + throw new IllegalModelException(`Duplicate asset name ${declaration.getName()}`); + } + } + } + + const participantDeclarations = this.getModelFile().getParticipantDeclarations(); + for(let n=0; n < participantDeclarations.length; n++) { + let declaration = participantDeclarations[n]; + + // check we don't have an asset with the same name + for(let i=n+1; i < participantDeclarations.length; i++) { + let otherDeclaration = participantDeclarations[i]; + if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { + throw new IllegalModelException(`Duplicate participant name ${declaration.getName()}`); + } + } + } + + const transactionDeclarations = this.getModelFile().getTransactionDeclarations(); + for(let n=0; n < transactionDeclarations.length; n++) { + let declaration = transactionDeclarations[n]; + + // check we don't have an asset with the same name + for(let i=n+1; i < transactionDeclarations.length; i++) { + let otherDeclaration = transactionDeclarations[i]; + if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { + throw new IllegalModelException(`Duplicate transaction name ${declaration.getName()}`); + } + } + } + + const conceptDeclarations = this.getModelFile().getConceptDeclarations(); + for(let n=0; n < conceptDeclarations.length; n++) { + let declaration = conceptDeclarations[n]; + + // check we don't have an asset with the same name + for(let i=n+1; i < conceptDeclarations.length; i++) { + let otherDeclaration = conceptDeclarations[i]; + if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { + throw new IllegalModelException(`Duplicate concept name ${declaration.getName()}`); + } + } + } + + const enumDeclarations = this.getModelFile().getEnumDeclarations(); + for(let n=0; n < enumDeclarations.length; n++) { + let declaration = enumDeclarations[n]; + + // check we don't have an asset with the same name + for(let i=n+1; i < enumDeclarations.length; i++) { + let otherDeclaration = enumDeclarations[i]; + if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { + throw new IllegalModelException(`Duplicate enum name ${declaration.getName()}`); + } + } + } + // TODO (LG) check that all imported classes exist, rather than just // used imports diff --git a/packages/composer-common/test/data/parser/classdeclaration.dupeassetname.cto b/packages/composer-common/test/data/parser/classdeclaration.dupeassetname.cto new file mode 100644 index 0000000000..03b9ac26da --- /dev/null +++ b/packages/composer-common/test/data/parser/classdeclaration.dupeassetname.cto @@ -0,0 +1,11 @@ +namespace com.testing + +asset Asset identified by id { + o String id + o String newValue +} + +asset Asset identified by id { + o String id + o String newValue +} \ No newline at end of file diff --git a/packages/composer-common/test/data/parser/classdeclaration.dupeconceptname.cto b/packages/composer-common/test/data/parser/classdeclaration.dupeconceptname.cto new file mode 100644 index 0000000000..2ec96b1d56 --- /dev/null +++ b/packages/composer-common/test/data/parser/classdeclaration.dupeconceptname.cto @@ -0,0 +1,11 @@ +namespace com.testing + +concept Concept{ + o String id + o String newValue +} + +concept Concept{ + o String id + o String newValue +} \ No newline at end of file diff --git a/packages/composer-common/test/data/parser/classdeclaration.dupeenumname.cto b/packages/composer-common/test/data/parser/classdeclaration.dupeenumname.cto new file mode 100644 index 0000000000..be35dd1afd --- /dev/null +++ b/packages/composer-common/test/data/parser/classdeclaration.dupeenumname.cto @@ -0,0 +1,11 @@ +namespace com.testing + +enum list { + o ENTRY1 + o ENTRY2 +} + +enum list { + o ENTRY1 + o ENTRY2 +} \ No newline at end of file diff --git a/packages/composer-common/test/data/parser/classdeclaration.dupeparticipantname.cto b/packages/composer-common/test/data/parser/classdeclaration.dupeparticipantname.cto new file mode 100644 index 0000000000..e02531cc4f --- /dev/null +++ b/packages/composer-common/test/data/parser/classdeclaration.dupeparticipantname.cto @@ -0,0 +1,11 @@ +namespace com.testing + +participant Participant identified by participantKey { + o String participantKey + o String newValue +} + +participant Participant identified by participantKey { + o String participantKey + o String newValue +} \ No newline at end of file diff --git a/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto b/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto new file mode 100644 index 0000000000..901e107513 --- /dev/null +++ b/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto @@ -0,0 +1,11 @@ +namespace com.testing + +transaction AssetTransaction identified by id { + o String id + o String newValue +} + +transaction AssetTransaction identified by id { + o String id + o String newValue +} \ No newline at end of file diff --git a/packages/composer-common/test/introspect/assetdeclaration.js b/packages/composer-common/test/introspect/assetdeclaration.js index 67308a87bc..6700723b68 100644 --- a/packages/composer-common/test/introspect/assetdeclaration.js +++ b/packages/composer-common/test/introspect/assetdeclaration.js @@ -118,7 +118,6 @@ describe('AssetDeclaration', () => { asset.validate(); }).should.throw(/more than one field named/); }); - }); describe('#getSuperType', () => { diff --git a/packages/composer-common/test/introspect/classdeclaration.js b/packages/composer-common/test/introspect/classdeclaration.js index 2b0e9f88db..d8188e118d 100644 --- a/packages/composer-common/test/introspect/classdeclaration.js +++ b/packages/composer-common/test/introspect/classdeclaration.js @@ -15,8 +15,14 @@ 'use strict'; const ClassDeclaration = require('../../lib/introspect/classdeclaration'); +const AssetDeclaration = require('../../lib/introspect/assetdeclaration'); +const EnumDeclaration = require('../../lib/introspect/enumdeclaration'); +const ConceptDeclaration = require('../../lib/introspect/conceptdeclaration'); +const ParticipantDeclaration = require('../../lib/introspect/participantdeclaration'); +const TransactionDeclaration = require('../../lib/introspect/transactiondeclaration'); const ModelFile = require('../../lib/introspect/modelfile'); const ModelManager = require('../../lib/modelmanager'); +const fs = require('fs'); require('chai').should(); const sinon = require('sinon'); @@ -32,6 +38,13 @@ describe('ClassDeclaration', () => { mockModelFile.getModelManager.returns(mockModelManager); }); + let loadLastDeclaration = (modelFileName, type) => { + let modelDefinitions = fs.readFileSync(modelFileName, 'utf8'); + let modelFile = new ModelFile(mockModelManager, modelDefinitions); + let assets = modelFile.getDeclarations(type); + return assets[assets.length - 1]; + }; + describe('#constructor', () => { it('should throw if modelFile not specified', () => { @@ -65,6 +78,46 @@ describe('ClassDeclaration', () => { }); + describe('#validate', () => { + + + it('should throw when asset name is duplicted in a modelfile', () => { + let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupeassetname.cto', AssetDeclaration); + (() => { + asset.validate(); + }).should.throw(/Duplicate asset/); + }); + + it('should throw when transaction name is duplicted in a modelfile', () => { + let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupetransactionname.cto', TransactionDeclaration); + (() => { + asset.validate(); + }).should.throw(/Duplicate transaction/); + }); + + it('should throw when participant name is duplicted in a modelfile', () => { + let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupeparticipantname.cto', ParticipantDeclaration); + (() => { + asset.validate(); + }).should.throw(/Duplicate participant/); + }); + + it('should throw when concept name is duplicted in a modelfile', () => { + let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupeconceptname.cto', ConceptDeclaration); + (() => { + asset.validate(); + }).should.throw(/Duplicate concept/); + }); + + it('should throw when enum name is duplicted in a modelfile', () => { + let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupeenumname.cto', EnumDeclaration); + (() => { + asset.validate(); + }).should.throw(/Duplicate enum/); + }); + + }); + describe('#accept', () => { it('should call the visitor', () => { diff --git a/packages/composer-common/test/serializer/jsongenerator.js b/packages/composer-common/test/serializer/jsongenerator.js index 532e403773..a3e06b94aa 100644 --- a/packages/composer-common/test/serializer/jsongenerator.js +++ b/packages/composer-common/test/serializer/jsongenerator.js @@ -54,7 +54,7 @@ describe('JSONGenerator', () => { o String assetId o MyAsset1 myAsset } - asset MyContainerAsset1 identified by assetId { + asset MyContainerAsset2 identified by assetId { o String assetId o MyAsset1[] myAssets } diff --git a/packages/composer-common/test/serializer/jsonpopulator.js b/packages/composer-common/test/serializer/jsonpopulator.js index 2c331a5b2f..2a3f843264 100644 --- a/packages/composer-common/test/serializer/jsonpopulator.js +++ b/packages/composer-common/test/serializer/jsonpopulator.js @@ -50,7 +50,7 @@ describe('JSONPopulator', () => { o String assetId o MyAsset1 myAsset } - asset MyContainerAsset1 identified by assetId { + asset MyContainerAsset2 identified by assetId { o String assetId o MyAsset1[] myAssets } From a44d490b74cdd23beef431b5423271b26e58c633 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Fri, 21 Apr 2017 15:09:39 +0100 Subject: [PATCH 26/41] Improved code efficiency and robustness --- .../lib/introspect/classdeclaration.js | 64 ++----------------- .../test/introspect/classdeclaration.js | 10 +-- 2 files changed, 11 insertions(+), 63 deletions(-) diff --git a/packages/composer-common/lib/introspect/classdeclaration.js b/packages/composer-common/lib/introspect/classdeclaration.js index 0d2cdc5084..6246bc089d 100644 --- a/packages/composer-common/lib/introspect/classdeclaration.js +++ b/packages/composer-common/lib/introspect/classdeclaration.js @@ -130,67 +130,15 @@ class ClassDeclaration { */ validate() { - const assetDeclarations = this.getModelFile().getAssetDeclarations(); - for(let n=0; n < assetDeclarations.length; n++) { - let declaration = assetDeclarations[n]; + const declarations = this.getModelFile().getAllDeclarations(); + for(let n=0; n < declarations.length; n++) { + let declaration = declarations[n]; // check we don't have an asset with the same name - for(let i=n+1; i < assetDeclarations.length; i++) { - let otherDeclaration = assetDeclarations[i]; + for(let i=n+1; i < declarations.length; i++) { + let otherDeclaration = declarations[i]; if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { - throw new IllegalModelException(`Duplicate asset name ${declaration.getName()}`); - } - } - } - - const participantDeclarations = this.getModelFile().getParticipantDeclarations(); - for(let n=0; n < participantDeclarations.length; n++) { - let declaration = participantDeclarations[n]; - - // check we don't have an asset with the same name - for(let i=n+1; i < participantDeclarations.length; i++) { - let otherDeclaration = participantDeclarations[i]; - if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { - throw new IllegalModelException(`Duplicate participant name ${declaration.getName()}`); - } - } - } - - const transactionDeclarations = this.getModelFile().getTransactionDeclarations(); - for(let n=0; n < transactionDeclarations.length; n++) { - let declaration = transactionDeclarations[n]; - - // check we don't have an asset with the same name - for(let i=n+1; i < transactionDeclarations.length; i++) { - let otherDeclaration = transactionDeclarations[i]; - if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { - throw new IllegalModelException(`Duplicate transaction name ${declaration.getName()}`); - } - } - } - - const conceptDeclarations = this.getModelFile().getConceptDeclarations(); - for(let n=0; n < conceptDeclarations.length; n++) { - let declaration = conceptDeclarations[n]; - - // check we don't have an asset with the same name - for(let i=n+1; i < conceptDeclarations.length; i++) { - let otherDeclaration = conceptDeclarations[i]; - if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { - throw new IllegalModelException(`Duplicate concept name ${declaration.getName()}`); - } - } - } - - const enumDeclarations = this.getModelFile().getEnumDeclarations(); - for(let n=0; n < enumDeclarations.length; n++) { - let declaration = enumDeclarations[n]; - - // check we don't have an asset with the same name - for(let i=n+1; i < enumDeclarations.length; i++) { - let otherDeclaration = enumDeclarations[i]; - if(declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) { - throw new IllegalModelException(`Duplicate enum name ${declaration.getName()}`); + throw new IllegalModelException(`Duplicate class name ${declaration.getName()}`); } } } diff --git a/packages/composer-common/test/introspect/classdeclaration.js b/packages/composer-common/test/introspect/classdeclaration.js index d8188e118d..cf16b92d93 100644 --- a/packages/composer-common/test/introspect/classdeclaration.js +++ b/packages/composer-common/test/introspect/classdeclaration.js @@ -85,35 +85,35 @@ describe('ClassDeclaration', () => { let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupeassetname.cto', AssetDeclaration); (() => { asset.validate(); - }).should.throw(/Duplicate asset/); + }).should.throw(/Duplicate class/); }); it('should throw when transaction name is duplicted in a modelfile', () => { let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupetransactionname.cto', TransactionDeclaration); (() => { asset.validate(); - }).should.throw(/Duplicate transaction/); + }).should.throw(/Duplicate class/); }); it('should throw when participant name is duplicted in a modelfile', () => { let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupeparticipantname.cto', ParticipantDeclaration); (() => { asset.validate(); - }).should.throw(/Duplicate participant/); + }).should.throw(/Duplicate class/); }); it('should throw when concept name is duplicted in a modelfile', () => { let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupeconceptname.cto', ConceptDeclaration); (() => { asset.validate(); - }).should.throw(/Duplicate concept/); + }).should.throw(/Duplicate class/); }); it('should throw when enum name is duplicted in a modelfile', () => { let asset = loadLastDeclaration('test/data/parser/classdeclaration.dupeenumname.cto', EnumDeclaration); (() => { asset.validate(); - }).should.throw(/Duplicate enum/); + }).should.throw(/Duplicate class/); }); }); From b057b2784e3e505220d75b41dea2e5163489afb6 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Mon, 24 Apr 2017 10:42:11 +0100 Subject: [PATCH 27/41] Change to open error modal if import fails --- .../src/app/import/import.component.spec.ts | 12 ++++++++---- .../src/app/import/import.component.ts | 13 ++++++++++--- packages/composer-playground/tslint.json | 1 - 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/composer-playground/src/app/import/import.component.spec.ts b/packages/composer-playground/src/app/import/import.component.spec.ts index 671fe9aa5f..b951d159f7 100644 --- a/packages/composer-playground/src/app/import/import.component.spec.ts +++ b/packages/composer-playground/src/app/import/import.component.spec.ts @@ -9,7 +9,7 @@ import {ImportComponent} from './import.component'; import {AdminService} from '../services/admin.service'; import {ClientService} from '../services/client.service'; import {SampleBusinessNetworkService} from '../services/samplebusinessnetwork.service'; -import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {AlertService} from '../services/alert.service'; import * as sinon from 'sinon'; @@ -62,6 +62,7 @@ describe('ImportComponent', () => { let mockAlertService = sinon.createStubInstance(AlertService); let mockClientService = sinon.createStubInstance(ClientService); let mockActiveModal = sinon.createStubInstance(NgbActiveModal); + let mockNgbModal = sinon.createStubInstance(NgbModal); mockAlertService.errorStatus$ = { next: sinon.stub() @@ -76,11 +77,14 @@ describe('ImportComponent', () => { {provide: AdminService, useValue: mockAdminService}, {provide: ClientService, useValue: mockClientService}, {provide: NgbActiveModal, useValue: mockActiveModal}, - {provide: AlertService, useValue: mockAlertService}] + {provide: AlertService, useValue: mockAlertService}, + {provide: NgbModal, useValue: mockNgbModal}] }); sandbox = sinon.sandbox.create(); + mockNgbModal.open.returns({componentInstance: {}}); + fixture = TestBed.createComponent(ImportComponent); component = fixture.componentInstance; @@ -332,7 +336,7 @@ describe('ImportComponent', () => { tick(); component['deployInProgress'].should.equal(false); - mockActiveModal.dismiss.should.have.been.called; + component.modalService.open.should.have.been.called; })); it('should handle error', fakeAsync(() => { @@ -348,7 +352,7 @@ describe('ImportComponent', () => { tick(); component['deployInProgress'].should.equal(false); - mockActiveModal.dismiss.should.have.been.called; + component.modalService.open.should.have.been.called; })); }); diff --git a/packages/composer-playground/src/app/import/import.component.ts b/packages/composer-playground/src/app/import/import.component.ts index de38b53af7..a4f4a7ddf1 100644 --- a/packages/composer-playground/src/app/import/import.component.ts +++ b/packages/composer-playground/src/app/import/import.component.ts @@ -1,11 +1,13 @@ import {Component, OnInit} from '@angular/core'; -import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {AdminService} from '../services/admin.service'; import {ClientService} from '../services/client.service'; import {SampleBusinessNetworkService} from '../services/samplebusinessnetwork.service'; import {AlertService} from '../services/alert.service'; +import { ErrorComponent } from '../error'; + const fabricComposerOwner = 'hyperledger'; const fabricComposerRepository = 'composer-sample-networks'; @@ -36,6 +38,7 @@ export class ImportComponent implements OnInit { constructor(private adminService: AdminService, private clientService: ClientService, public activeModal: NgbActiveModal, + public modalService: NgbModal, private sampleBusinessNetworkService: SampleBusinessNetworkService, private alertService: AlertService) { @@ -91,7 +94,9 @@ export class ImportComponent implements OnInit { } this.gitHubInProgress = false; - this.activeModal.dismiss(error); + + let modalRef = this.modalService.open(ErrorComponent); + modalRef.componentInstance.error = error; }); } } @@ -176,7 +181,9 @@ export class ImportComponent implements OnInit { } this.deployInProgress = false; - this.activeModal.dismiss(error); + + let modalRef = this.modalService.open(ErrorComponent); + modalRef.componentInstance.error = error; }); diff --git a/packages/composer-playground/tslint.json b/packages/composer-playground/tslint.json index 5685d102f3..4ff3d08fdc 100644 --- a/packages/composer-playground/tslint.json +++ b/packages/composer-playground/tslint.json @@ -57,7 +57,6 @@ "no-shadowed-variable": true, "no-string-literal": false, "no-switch-case-fall-through": true, - // "no-unreachable": true, "no-unused-expression": true, "no-unused-variable": false, "no-use-before-declare": true, From 4477eda5bc0f7fc36b6200e81f8ede43ea2e16f9 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Tue, 2 May 2017 09:46:18 +0100 Subject: [PATCH 28/41] Fixed issue #792 docs problem --- .../composer-runtime-hlf/vendor/github.com/hyperledger/fabric | 2 +- .../composer-runtime-hlf/vendor/github.com/robertkrimen/otto | 2 +- packages/composer-runtime-hlf/vendor/gopkg.in/sourcemap.v1 | 2 +- .../vendor/gopkg.in/olebedev/go-duktape.v3 | 2 +- packages/composer-runtime/lib/api/factory.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/composer-runtime-hlf/vendor/github.com/hyperledger/fabric b/packages/composer-runtime-hlf/vendor/github.com/hyperledger/fabric index 1f146986c4..16debfe244 160000 --- a/packages/composer-runtime-hlf/vendor/github.com/hyperledger/fabric +++ b/packages/composer-runtime-hlf/vendor/github.com/hyperledger/fabric @@ -1 +1 @@ -Subproject commit 1f146986c4c06b6b9e21fff052b4dd38e8dd73cc +Subproject commit 16debfe244b5659d001f59cc7b947726fb099ec5 diff --git a/packages/composer-runtime-hlf/vendor/github.com/robertkrimen/otto b/packages/composer-runtime-hlf/vendor/github.com/robertkrimen/otto index 21ec96599b..7a965dd45d 160000 --- a/packages/composer-runtime-hlf/vendor/github.com/robertkrimen/otto +++ b/packages/composer-runtime-hlf/vendor/github.com/robertkrimen/otto @@ -1 +1 @@ -Subproject commit 21ec96599b1279b5673e4df0097dd56bb8360068 +Subproject commit 7a965dd45d832989a1a40fdb9c3f373e7a2db6d2 diff --git a/packages/composer-runtime-hlf/vendor/gopkg.in/sourcemap.v1 b/packages/composer-runtime-hlf/vendor/gopkg.in/sourcemap.v1 index 6e83acea00..eef8f47ab6 160000 --- a/packages/composer-runtime-hlf/vendor/gopkg.in/sourcemap.v1 +++ b/packages/composer-runtime-hlf/vendor/gopkg.in/sourcemap.v1 @@ -1 +1 @@ -Subproject commit 6e83acea0053641eff084973fee085f0c193c61a +Subproject commit eef8f47ab679652a7d3a4ee34c34314d255d2536 diff --git a/packages/composer-runtime-hlfv1/vendor/gopkg.in/olebedev/go-duktape.v3 b/packages/composer-runtime-hlfv1/vendor/gopkg.in/olebedev/go-duktape.v3 index bb87dea2db..cd62510143 160000 --- a/packages/composer-runtime-hlfv1/vendor/gopkg.in/olebedev/go-duktape.v3 +++ b/packages/composer-runtime-hlfv1/vendor/gopkg.in/olebedev/go-duktape.v3 @@ -1 +1 @@ -Subproject commit bb87dea2db4a1f68f67b5c7a664ff5df9f279e49 +Subproject commit cd62510143a3b9bd61c7268ecd037c5ca6d9ae0c diff --git a/packages/composer-runtime/lib/api/factory.js b/packages/composer-runtime/lib/api/factory.js index 744bb4b704..5ff1e4cbac 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 From 04db8427bbaf7b5a517eaeefc6c42ab80019a52c Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Thu, 4 May 2017 16:53:30 +0100 Subject: [PATCH 29/41] Added event support to base and web runtime --- packages/composer-common/lib/factory.js | 31 ++++++ packages/composer-runtime-web/index.js | 1 + .../composer-runtime-web/lib/webcontainer.js | 2 + .../composer-runtime-web/lib/webcontext.js | 12 ++ .../lib/webeventservice.js | 53 +++++++++ .../composer-runtime-web/test/webcontainer.js | 1 + .../composer-runtime-web/test/webcontext.js | 10 ++ .../test/webeventservice.js | 61 ++++++++++ packages/composer-runtime/index.js | 1 + packages/composer-runtime/lib/api.js | 18 ++- packages/composer-runtime/lib/api/factory.js | 14 +++ packages/composer-runtime/lib/context.js | 11 +- .../lib/engine.transactions.js | 4 + packages/composer-runtime/lib/eventservice.js | 92 ++++++++++++++++ packages/composer-runtime/test/api/factory.js | 9 ++ packages/composer-runtime/test/context.js | 13 +++ .../test/engine.transactions.js | 6 + .../composer-runtime/test/eventservice.js | 104 ++++++++++++++++++ 18 files changed, 440 insertions(+), 3 deletions(-) create mode 100644 packages/composer-runtime-web/lib/webeventservice.js create mode 100644 packages/composer-runtime-web/test/webeventservice.js create mode 100644 packages/composer-runtime/lib/eventservice.js create mode 100644 packages/composer-runtime/test/eventservice.js diff --git a/packages/composer-common/lib/factory.js b/packages/composer-common/lib/factory.js index f224214f5c..7766ee0560 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 = uuid.v4(); + 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-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..cc3b1ede24 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. @@ -50,6 +51,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.getSerializer()); + } + 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..dc24ff1532 --- /dev/null +++ b/packages/composer-runtime-web/lib/webeventservice.js @@ -0,0 +1,53 @@ +/* + * 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 EventEmitter = require('events').EventEmitter; + +const LOG = Logger.getLog('WebDataService'); + +/** + * Base class representing the event service provided by a {@link Container}. + * @protected + */ +class WebEventService extends EventService { + + /** + * Constructor. + * @param {Serializer} serializer Serializer instance. + */ + constructor(serializer) { + super(serializer); + const method = 'constructor'; + + this.emitter = new EventEmitter(); + this.emitter.addListener('composer', (data) => { + console.log('triggered', data); + }); + + LOG.exit(method); + } + + /** + * Emit the events stored in eventBuffer + */ + commit() { + this.emitter.emit('composer', this.serializeBuffer()); + } +} + +module.exports = WebEventService; diff --git a/packages/composer-runtime-web/test/webcontainer.js b/packages/composer-runtime-web/test/webcontainer.js index 24e86ba256..e5c6753e21 100644 --- a/packages/composer-runtime-web/test/webcontainer.js +++ b/packages/composer-runtime-web/test/webcontainer.js @@ -17,6 +17,7 @@ const Container = require('composer-runtime').Container; const DataService = require('composer-runtime').DataService; const LoggingService = require('composer-runtime').LoggingService; +const EventService = require('composer-runtime').EventService; const WebContainer = require('..').WebContainer; const uuid = require('uuid'); const version = require('../package.json').version; diff --git a/packages/composer-runtime-web/test/webcontext.js b/packages/composer-runtime-web/test/webcontext.js index 235482ce12..4c3b961233 100644 --- a/packages/composer-runtime-web/test/webcontext.js +++ b/packages/composer-runtime-web/test/webcontext.js @@ -18,6 +18,7 @@ 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; @@ -66,4 +67,13 @@ describe('WebContext', () => { }); + describe('#getEventService', () => { + + it('should return the container event service', () => { + let context = new WebContext(mockEngine, 'bob1'); + context.getEventService().should.be.an.instanceOf(EventService); + }); + + }); + }); diff --git a/packages/composer-runtime-web/test/webeventservice.js b/packages/composer-runtime-web/test/webeventservice.js new file mode 100644 index 0000000000..21fc29c171 --- /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 Serializer = require('composer-common').Serializer; + +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 mockSerializer; + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockSerializer = sinon.createStubInstance(Serializer); + eventService = new WebEventService(mockSerializer); + sinon.stub(EventEmitter); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#constructor', () => { + it('should assign a default event emitter', () => { + eventService = new WebEventService(mockSerializer); + (eventService.emitter instanceof EventEmitter).should.be.true; + }); + }); + + describe('#commit', () => { + it ('should emit a list of events', () => { + eventService.serializeBuffer = sinon.stub(); + eventService.serializeBuffer.returns(['serialized JS']); + eventService.commit(); + sinon.assert.calledOnce(eventService.serializeBuffer); + sinon.assert.calledOnce(EventEmitter.emit); + sinon.assert.calledWith(EventEmitter.emit, 'composer', ['serialized JS']); + }); + }); +}); 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..530d23e6ff 100644 --- a/packages/composer-runtime/lib/api.js +++ b/packages/composer-runtime/lib/api.js @@ -37,11 +37,12 @@ class Api { * @param {Factory} factory The factory to use. * @param {Resource} participant The current participant. * @param {RegistryManager} registryManager The registry manager to use. + * @param {EventService} eventService The event service to use. * @private */ - constructor(factory, participant, registryManager) { + constructor(factory, participant, registryManager, eventService) { const method = 'constructor'; - LOG.entry(method, factory, participant, registryManager); + LOG.entry(method, factory, participant, registryManager, eventService); /** * Get the factory. The factory can be used to create new instances of @@ -161,6 +162,19 @@ 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); + eventService.emit(event); + 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 5ff1e4cbac..be00e2570e 100644 --- a/packages/composer-runtime/lib/api/factory.js +++ b/packages/composer-runtime/lib/api/factory.js @@ -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 257f76bc1f..3f3c391fe0 100644 --- a/packages/composer-runtime/lib/context.js +++ b/packages/composer-runtime/lib/context.js @@ -237,6 +237,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 +340,7 @@ class Context { */ getApi() { if (!this.api) { - this.api = new Api(this.getFactory(), this.getParticipant(), this.getRegistryManager()); + this.api = new Api(this.getFactory(), this.getParticipant(), this.getRegistryManager(), this.getEventService()); } return this.api; } diff --git a/packages/composer-runtime/lib/engine.transactions.js b/packages/composer-runtime/lib/engine.transactions.js index e3e3ab2f42..3605a9c95c 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 + 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..fe630602a6 --- /dev/null +++ b/packages/composer-runtime/lib/eventservice.js @@ -0,0 +1,92 @@ +/* + * 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'; + +/** + * Base class representing the event service provided by a {@link Container}. + * @protected + * @abstract + * @memberof module:composer-runtime + */ +class EventService { + + /** + * Constructor. + * @param {Serializer} serializer A serializer instance. + */ + constructor(serializer) { + this.serializer = serializer; + this.eventBuffer = []; + } + + /** + * Add an event to the buffer + * @abstract + * @param {Resource} event The event to be emitted + * when complete, or rejected with an error. + */ + emit(event) { + this.eventBuffer.push(event); + } + + /** + * 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 serialized events + * @return {Object[]} - An array of serialized events + */ + serializeBuffer() { + let eb = []; + this.eventBuffer.forEach((event) => { + eb.push(this.serializer.toJSON(event)); + }); + return eb; + } + + /** + * Stop serialization of this object. + * @return {Object} An empty object. + */ + toJSON() { + return {}; + } + +} + +module.exports = EventService; 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 8766380905..96c5065d66 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'); @@ -266,6 +267,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 +430,8 @@ 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.getApi().should.be.an.instanceOf(Api); }); 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..7a9b8f1021 --- /dev/null +++ b/packages/composer-runtime/test/eventservice.js @@ -0,0 +1,104 @@ +/* + * 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 Resource = require('composer-common').Resource; +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 serializer property', () => { + should.exist(eventService.serializer); + }); + + 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 serialize an array of events into json', () => { + let mockResource = sinon.createStubInstance(Resource); + mockSerializer.toJSON.returns({'$class': 'much.wow'}); + eventService.eventBuffer = [ mockResource ]; + + eventService.serializeBuffer().should.deep.equal([{'$class': 'much.wow'}]); + }); + }); + + describe('#toJSON', () => { + + it('should return an empty object', () => { + eventService.toJSON().should.deep.equal({}); + }); + + }); + +}); From 31a12dff73121f9f9607d970ab55dc5939267db9 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Thu, 4 May 2017 17:00:49 +0100 Subject: [PATCH 30/41] Rebrand cli readme --- packages/composer-cli/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/composer-cli/README.md b/packages/composer-cli/README.md index b5b3445c04..7464af3840 100644 --- a/packages/composer-cli/README.md +++ b/packages/composer-cli/README.md @@ -1,4 +1,4 @@ -# Fabric Composer CLI +# Hyperledger Composer CLI Set up the Composer command with @@ -7,7 +7,7 @@ npm install composer-cli ``` ## Overview -Contains the Fabric composer CLIs for administering business networks. +Contains the Hyperledger Composer CLIs for administering business networks. Type `composer --help` to list the available commands. From a4cf5bed9065e74c8c275559a7a06e7129ea5812 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Fri, 5 May 2017 10:52:44 +0100 Subject: [PATCH 31/41] Web runtime events --- packages/composer-common/api.txt | 1 + packages/composer-common/changelog.txt | 3 ++ packages/composer-common/test/factory.js | 37 +++++++++++++++++++ .../lib/webeventservice.js | 18 ++++++--- .../composer-runtime-web/test/webcontainer.js | 1 - .../composer-runtime-web/test/webcontext.js | 9 +++++ .../test/webeventservice.js | 22 +++++++++-- packages/composer-runtime/test/api.js | 13 ++++++- 8 files changed, 94 insertions(+), 10 deletions(-) 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/test/factory.js b/packages/composer-common/test/factory.js index a0cabed8e5..a1433dd49b 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('5604bdfe-7b96-45d0-9883-9c05c18fe638'); + 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', '5604bdfe-7b96-45d0-9883-9c05c18fe638', { hello: 'world' }); + }); + }); + describe('#toJSON', () => { it('should return an empty object', () => { diff --git a/packages/composer-runtime-web/lib/webeventservice.js b/packages/composer-runtime-web/lib/webeventservice.js index dc24ff1532..9d43688a55 100644 --- a/packages/composer-runtime-web/lib/webeventservice.js +++ b/packages/composer-runtime-web/lib/webeventservice.js @@ -34,10 +34,7 @@ class WebEventService extends EventService { super(serializer); const method = 'constructor'; - this.emitter = new EventEmitter(); - this.emitter.addListener('composer', (data) => { - console.log('triggered', data); - }); + this.emitter = this.getEventEmitter(); LOG.exit(method); } @@ -46,7 +43,18 @@ class WebEventService extends EventService { * Emit the events stored in eventBuffer */ commit() { - this.emitter.emit('composer', this.serializeBuffer()); + this.getEventEmitter().emit('composer', this.serializeBuffer()); + } + + /** + * Gets an eventEmitter instance + * @return {EventEmitter} EventEmitter instance + */ + getEventEmitter() { + if (!this.emitter) { + return new EventEmitter(); + } + return this.emitter; } } diff --git a/packages/composer-runtime-web/test/webcontainer.js b/packages/composer-runtime-web/test/webcontainer.js index e5c6753e21..24e86ba256 100644 --- a/packages/composer-runtime-web/test/webcontainer.js +++ b/packages/composer-runtime-web/test/webcontainer.js @@ -17,7 +17,6 @@ const Container = require('composer-runtime').Container; const DataService = require('composer-runtime').DataService; const LoggingService = require('composer-runtime').LoggingService; -const EventService = require('composer-runtime').EventService; const WebContainer = require('..').WebContainer; const uuid = require('uuid'); const version = require('../package.json').version; diff --git a/packages/composer-runtime-web/test/webcontext.js b/packages/composer-runtime-web/test/webcontext.js index 4c3b961233..7f82c3736a 100644 --- a/packages/composer-runtime-web/test/webcontext.js +++ b/packages/composer-runtime-web/test/webcontext.js @@ -14,6 +14,7 @@ '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; @@ -29,6 +30,7 @@ describe('WebContext', () => { let mockWebContainer; let mockDataService; + let mockSerializer; let mockEngine; beforeEach(() => { @@ -37,6 +39,7 @@ describe('WebContext', () => { mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockWebContainer); mockWebContainer.getDataService.returns(mockDataService); + mockSerializer = sinon.createStubInstance(Serializer); }); describe('#constructor', () => { @@ -71,9 +74,15 @@ describe('WebContext', () => { 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 index 21fc29c171..52aa1782b1 100644 --- a/packages/composer-runtime-web/test/webeventservice.js +++ b/packages/composer-runtime-web/test/webeventservice.js @@ -28,13 +28,16 @@ describe('WebEventService', () => { let eventService; let mockSerializer; + let mockEventEmitter; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); mockSerializer = sinon.createStubInstance(Serializer); + mockEventEmitter = sinon.createStubInstance(EventEmitter); eventService = new WebEventService(mockSerializer); - sinon.stub(EventEmitter); + eventService.getEventEmitter = sinon.stub(); + eventService.getEventEmitter.returns(mockEventEmitter); }); afterEach(() => { @@ -54,8 +57,21 @@ describe('WebEventService', () => { eventService.serializeBuffer.returns(['serialized JS']); eventService.commit(); sinon.assert.calledOnce(eventService.serializeBuffer); - sinon.assert.calledOnce(EventEmitter.emit); - sinon.assert.calledWith(EventEmitter.emit, 'composer', ['serialized JS']); + sinon.assert.calledOnce(mockEventEmitter.emit); + sinon.assert.calledWith(mockEventEmitter.emit, 'composer', ['serialized JS']); }); }); + + describe('#getEventEmitter', () => { + it('should return an EventEmitter', () => { + eventService.getEventEmitter().should.be.instanceOf(EventEmitter); + }); + + it('should return emiiter if it is set', () => { + let eventService = new WebEventService(mockSerializer); + eventService.emitter = {}; + eventService.getEventEmitter().should.deep.equal({}); + }); + + }); }); diff --git a/packages/composer-runtime/test/api.js b/packages/composer-runtime/test/api.js index 9cd7cd4706..e4857ce83c 100644 --- a/packages/composer-runtime/test/api.js +++ b/packages/composer-runtime/test/api.js @@ -22,6 +22,7 @@ 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 chai = require('chai'); chai.should(); @@ -35,13 +36,15 @@ describe('Api', () => { let mockFactory; let mockParticipant; let mockRegistryManager; + let mockEventService; let api; beforeEach(() => { mockFactory = sinon.createStubInstance(realFactory); mockParticipant = sinon.createStubInstance(Resource); mockRegistryManager = sinon.createStubInstance(RegistryManager); - api = new Api(mockFactory, mockParticipant, mockRegistryManager); + mockEventService = sinon.createStubInstance(EventService); + api = new Api(mockFactory, mockParticipant, mockRegistryManager, mockEventService); }); describe('#constructor', () => { @@ -106,4 +109,12 @@ describe('Api', () => { }); + describe('#getCurrentParticipant', () => { + it('should call eventService.emit', () => { + api.emit({}); + sinon.assert.calledOnce(mockEventService.emit); + sinon.assert.calledWith(mockEventService.emit, {}); + }); + }); + }); From bc404c7dfaa6ef1d5458c5fccfd40042041817d4 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Mon, 8 May 2017 12:56:47 +0100 Subject: [PATCH 32/41] HLFv1 can now emit events --- packages/composer-runtime-embedded/index.js | 1 + .../lib/embeddedcontext.js | 12 +++ .../lib/embeddedeventservice.js | 61 +++++++++++++ .../test/embeddedcontext.js | 19 +++++ .../test/embeddedeventservice.js | 77 +++++++++++++++++ packages/composer-runtime-hlfv1/context.go | 15 ++++ .../composer-runtime-hlfv1/eventservice.go | 85 +++++++++++++++++++ 7 files changed, 270 insertions(+) create mode 100644 packages/composer-runtime-embedded/lib/embeddedeventservice.js create mode 100644 packages/composer-runtime-embedded/test/embeddedeventservice.js create mode 100644 packages/composer-runtime-hlfv1/eventservice.go 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..fc4ecb9088 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. @@ -50,6 +51,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.getSerializer()); + } + 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..feac683910 --- /dev/null +++ b/packages/composer-runtime-embedded/lib/embeddedeventservice.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 EventService = require('composer-runtime').EventService; +const Logger = require('composer-common').Logger; +const EventEmitter = require('events').EventEmitter; + +const LOG = Logger.getLog('WebDataService'); + +/** + * Base class representing the event service provided by a {@link Container}. + * @protected + */ +class EmbeddedEventService extends EventService { + + /** + * Constructor. + * @param {Serializer} serializer Serializer instance. + */ + constructor(serializer) { + super(serializer); + const method = 'constructor'; + + this.emitter = this.getEventEmitter(); + + LOG.exit(method); + } + + /** + * Emit the events stored in eventBuffer + */ + commit() { + this.getEventEmitter().emit('composer', this.serializeBuffer()); + } + + /** + * Gets an eventEmitter instance + * @return {EventEmitter} EventEmitter instance + */ + getEventEmitter() { + if (!this.emitter) { + return new EventEmitter(); + } + return this.emitter; + } +} + +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..c1f5bdeed2 --- /dev/null +++ b/packages/composer-runtime-embedded/test/embeddedeventservice.js @@ -0,0 +1,77 @@ +/* + * 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 Serializer = require('composer-common').Serializer; + +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 mockSerializer; + let mockEventEmitter; + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockSerializer = sinon.createStubInstance(Serializer); + mockEventEmitter = sinon.createStubInstance(EventEmitter); + eventService = new EmbeddedEventService(mockSerializer); + eventService.getEventEmitter = sinon.stub(); + eventService.getEventEmitter.returns(mockEventEmitter); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#constructor', () => { + it('should assign a default event emitter', () => { + eventService = new EmbeddedEventService(mockSerializer); + (eventService.emitter instanceof EventEmitter).should.be.true; + }); + }); + + describe('#commit', () => { + it ('should emit a list of events', () => { + eventService.serializeBuffer = sinon.stub(); + eventService.serializeBuffer.returns(['serialized JS']); + eventService.commit(); + sinon.assert.calledOnce(eventService.serializeBuffer); + sinon.assert.calledOnce(mockEventEmitter.emit); + sinon.assert.calledWith(mockEventEmitter.emit, 'composer', ['serialized JS']); + }); + }); + + describe('#getEventEmitter', () => { + it('should return an EventEmitter', () => { + eventService.getEventEmitter().should.be.instanceOf(EventEmitter); + }); + + it('should return emiiter if it is set', () => { + let eventService = new EmbeddedEventService(mockSerializer); + eventService.emitter = {}; + eventService.getEventEmitter().should.deep.equal({}); + }); + + }); +}); 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..ae3e766763 --- /dev/null +++ b/packages/composer-runtime-hlfv1/eventservice.go @@ -0,0 +1,85 @@ +/* + * 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.PutPropString(-2, "_commit") // [ global composer theEventService ] + + // 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.PcallMethod(0) // [ theEventService, returnValue ] + vm.RequireObjectCoercible(-1) // [ theEventService, returnValue ] + vm.JsonEncode(-1) + value := vm.RequireString(-1) + + if len(value) > 0 { + eventService.Stub.SetEvent("composer", []byte(value)) + } + + return 0 +} From 2abf60c8b5ff745d192be2b1f66f6b5632b849e0 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Mon, 8 May 2017 17:02:27 +0100 Subject: [PATCH 33/41] GO event hub changes --- .../lib/businessnetworkconnection.js | 7 +++++++ packages/composer-common/lib/connection.js | 4 +++- .../lib/embeddedconnection.js | 6 +++--- .../lib/hlfconnection.js | 14 +++++++++++++ .../lib/webconnection.js | 6 +++--- .../lib/embeddedcontext.js | 6 ++++-- .../lib/embeddedeventservice.js | 19 ++++-------------- .../composer-runtime-hlfv1/eventservice.go | 11 ++++++++-- .../composer-runtime-web/lib/webcontext.js | 6 ++++-- .../lib/webeventservice.js | 20 ++++--------------- .../lib/engine.transactions.js | 2 +- packages/composer-runtime/lib/eventservice.js | 3 +++ 12 files changed, 59 insertions(+), 45 deletions(-) diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js index 0f32e425ce..6c9c2d254c 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -284,6 +284,13 @@ class BusinessNetworkConnection extends EventEmitter { connect(connectionProfile, businessNetwork, enrollmentID, enrollmentSecret, additionalConnectOptions) { return this.connectionProfileManager.connect(connectionProfile, businessNetwork, additionalConnectOptions) .then((connection) => { + connection.on('event', (events) => { + let serializer = this.getBusinessNetwork().getSerializer(); + events.forEach((event) => { + event = serializer.fromJSON(event); + this.emit('event', event); + }); + }); this.connection = connection; return connection.login(enrollmentID, enrollmentSecret); }) 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-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-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js index ec1ccc2e00..8f960438a8 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -83,6 +83,20 @@ class HLFConnection extends Connection { this.client = client; this.chain = chain; this.eventHubs = eventHubs; + + if (businessNetworkIdentifier) { + eventHubs.forEach((eventHub) => { + LOG.entry('registerChaincodeEvent', businessNetworkIdentifier, 'composer'); + eventHub.registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { + this.emit('event', event.payload.buffer.toString('utf8')); + }); + this.on('event', (event) => { + console.log('hlf-event', event); + console.log('end'); + }); + }); + } + this.caClient = caClient; // We create promisified versions of these APIs. diff --git a/packages/composer-connector-web/lib/webconnection.js b/packages/composer-connector-web/lib/webconnection.js index 747661ec5f..aacfb2dd5c 100644 --- a/packages/composer-connector-web/lib/webconnection.js +++ b/packages/composer-connector-web/lib/webconnection.js @@ -192,7 +192,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 +259,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 +278,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-runtime-embedded/lib/embeddedcontext.js b/packages/composer-runtime-embedded/lib/embeddedcontext.js index fc4ecb9088..b8df71596f 100644 --- a/packages/composer-runtime-embedded/lib/embeddedcontext.js +++ b/packages/composer-runtime-embedded/lib/embeddedcontext.js @@ -28,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; } /** @@ -58,7 +60,7 @@ class EmbeddedContext extends Context { */ getEventService() { if (!this.eventService) { - this.eventService = new EmbeddedEventService(this.getSerializer()); + this.eventService = new EmbeddedEventService(this.getSerializer(), this.eventSink); } return this.eventService; } diff --git a/packages/composer-runtime-embedded/lib/embeddedeventservice.js b/packages/composer-runtime-embedded/lib/embeddedeventservice.js index feac683910..87f0be07d0 100644 --- a/packages/composer-runtime-embedded/lib/embeddedeventservice.js +++ b/packages/composer-runtime-embedded/lib/embeddedeventservice.js @@ -16,7 +16,6 @@ const EventService = require('composer-runtime').EventService; const Logger = require('composer-common').Logger; -const EventEmitter = require('events').EventEmitter; const LOG = Logger.getLog('WebDataService'); @@ -29,12 +28,13 @@ class EmbeddedEventService extends EventService { /** * Constructor. * @param {Serializer} serializer Serializer instance. + * @param {EventEmitter} eventSink the event emitter */ - constructor(serializer) { + constructor(serializer, eventSink) { super(serializer); const method = 'constructor'; - this.emitter = this.getEventEmitter(); + this.emitter = eventSink; LOG.exit(method); } @@ -43,18 +43,7 @@ class EmbeddedEventService extends EventService { * Emit the events stored in eventBuffer */ commit() { - this.getEventEmitter().emit('composer', this.serializeBuffer()); - } - - /** - * Gets an eventEmitter instance - * @return {EventEmitter} EventEmitter instance - */ - getEventEmitter() { - if (!this.emitter) { - return new EventEmitter(); - } - return this.emitter; + this.emitter.emit('composer', this.serializeBuffer()); } } diff --git a/packages/composer-runtime-hlfv1/eventservice.go b/packages/composer-runtime-hlfv1/eventservice.go index ae3e766763..5c2baf12af 100644 --- a/packages/composer-runtime-hlfv1/eventservice.go +++ b/packages/composer-runtime-hlfv1/eventservice.go @@ -74,12 +74,19 @@ func (eventService *EventService) commit(vm *duktape.Context) (result int) { vm.Dup(-2) // [ theEventService, serializeBuffer, theEventService ] vm.PcallMethod(0) // [ theEventService, returnValue ] vm.RequireObjectCoercible(-1) // [ theEventService, returnValue ] - vm.JsonEncode(-1) - value := vm.RequireString(-1) + vm.JsonEncode(-1) // [ theEventService, returnValue ] + value := vm.RequireString(-1) // [ theEventService, returnValue ] if len(value) > 0 { + logger.Debug("setting event", 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/lib/webcontext.js b/packages/composer-runtime-web/lib/webcontext.js index cc3b1ede24..fa3c11b2a0 100644 --- a/packages/composer-runtime-web/lib/webcontext.js +++ b/packages/composer-runtime-web/lib/webcontext.js @@ -28,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; } /** @@ -57,7 +59,7 @@ class WebContext extends Context { */ getEventService() { if (!this.eventService) { - this.eventService = new WebEventService(this.getSerializer()); + this.eventService = new WebEventService(this.getSerializer(), this.eventSink); } return this.eventService; } diff --git a/packages/composer-runtime-web/lib/webeventservice.js b/packages/composer-runtime-web/lib/webeventservice.js index 9d43688a55..5acb1359e1 100644 --- a/packages/composer-runtime-web/lib/webeventservice.js +++ b/packages/composer-runtime-web/lib/webeventservice.js @@ -16,7 +16,6 @@ const EventService = require('composer-runtime').EventService; const Logger = require('composer-common').Logger; -const EventEmitter = require('events').EventEmitter; const LOG = Logger.getLog('WebDataService'); @@ -29,12 +28,12 @@ class WebEventService extends EventService { /** * Constructor. * @param {Serializer} serializer Serializer instance. + * @param {EventEmitter} eventSink the event emitter */ - constructor(serializer) { + constructor(serializer, eventSink) { super(serializer); const method = 'constructor'; - - this.emitter = this.getEventEmitter(); + this.eventSink = eventSink; LOG.exit(method); } @@ -43,18 +42,7 @@ class WebEventService extends EventService { * Emit the events stored in eventBuffer */ commit() { - this.getEventEmitter().emit('composer', this.serializeBuffer()); - } - - /** - * Gets an eventEmitter instance - * @return {EventEmitter} EventEmitter instance - */ - getEventEmitter() { - if (!this.emitter) { - return new EventEmitter(); - } - return this.emitter; + this.eventSink.emit('event', this.serializeBuffer()); } } diff --git a/packages/composer-runtime/lib/engine.transactions.js b/packages/composer-runtime/lib/engine.transactions.js index 3605a9c95c..397021dd55 100644 --- a/packages/composer-runtime/lib/engine.transactions.js +++ b/packages/composer-runtime/lib/engine.transactions.js @@ -96,7 +96,7 @@ class EngineTransactions { }) .then(() => { // Commit all transactions - context.getEventService().commit(); + return context.getEventService().commit(); }); } diff --git a/packages/composer-runtime/lib/eventservice.js b/packages/composer-runtime/lib/eventservice.js index fe630602a6..51c1262b9b 100644 --- a/packages/composer-runtime/lib/eventservice.js +++ b/packages/composer-runtime/lib/eventservice.js @@ -72,10 +72,13 @@ class EventService { * @return {Object[]} - An array of serialized events */ serializeBuffer() { + console.log('serializeBuffer ENTER', this.eventBuffer); let eb = []; this.eventBuffer.forEach((event) => { + console.log('Event', event); eb.push(this.serializer.toJSON(event)); }); + console.log('serializeBuffer EXIT', eb); return eb; } From 9359529a9cdee93c59f5283910469048a0d5ac0b Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 10 May 2017 14:47:01 +0100 Subject: [PATCH 34/41] merge commit --- .gitignore | 3 ++ .../lib/businessnetworkconnection.js | 7 ++-- packages/composer-client/scripts/tsgen.js | 38 +++++++++++-------- packages/composer-common/lib/factory.js | 2 +- .../lib/hlfconnection.js | 13 ++----- .../lib/proxyconnectionmanager.js | 6 +++ .../lib/connectorserver.js | 5 +++ .../lib/webconnection.js | 1 + .../src/app/test/test.component.ts | 3 ++ .../lib/embeddedcontext.js | 2 +- .../lib/embeddedeventservice.js | 5 +-- .../composer-runtime-hlfv1/eventservice.go | 9 +++-- .../composer-runtime-web/lib/webcontext.js | 2 +- .../lib/webeventservice.js | 7 ++-- packages/composer-runtime/lib/api.js | 12 ++++-- packages/composer-runtime/lib/context.js | 19 +++++++++- packages/composer-runtime/lib/eventservice.js | 20 ++++++---- 17 files changed, 100 insertions(+), 54 deletions(-) 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-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js index 6c9c2d254c..8302ab6be4 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -284,11 +284,10 @@ class BusinessNetworkConnection extends EventEmitter { connect(connectionProfile, businessNetwork, enrollmentID, enrollmentSecret, additionalConnectOptions) { return this.connectionProfileManager.connect(connectionProfile, businessNetwork, additionalConnectOptions) .then((connection) => { - connection.on('event', (events) => { - let serializer = this.getBusinessNetwork().getSerializer(); + connection.on('events', (events) => { events.forEach((event) => { - event = serializer.fromJSON(event); - this.emit('event', event); + let serializedEvent = this.getBusinessNetwork().getSerializer().fromJSON(event); + this.emit('event', serializedEvent); }); }); this.connection = connection; 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-common/lib/factory.js b/packages/composer-common/lib/factory.js index 2f685ef7c4..7e26f44187 100644 --- a/packages/composer-common/lib/factory.js +++ b/packages/composer-common/lib/factory.js @@ -322,7 +322,7 @@ class Factory { } else if (!type) { throw new Error('type not specified'); } - const id = uuid.v4(); + const id = 'valid'; let event = this.newResource(ns, type, id, options); const classDeclaration = event.getClassDeclaration(); if (!(classDeclaration instanceof EventDeclaration)) { diff --git a/packages/composer-connector-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js index 8f960438a8..5108c827b7 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -64,7 +64,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'); @@ -85,15 +84,9 @@ class HLFConnection extends Connection { this.eventHubs = eventHubs; if (businessNetworkIdentifier) { - eventHubs.forEach((eventHub) => { - LOG.entry('registerChaincodeEvent', businessNetworkIdentifier, 'composer'); - eventHub.registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { - this.emit('event', event.payload.buffer.toString('utf8')); - }); - this.on('event', (event) => { - console.log('hlf-event', event); - console.log('end'); - }); + LOG.entry('registerChaincodeEvent', businessNetworkIdentifier, 'composer'); + eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { + this.emit('events', event.payload.toString('utf8')); }); } diff --git a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js index 5ee0a5be47..463d092802 100644 --- a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js +++ b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js @@ -88,6 +88,12 @@ class ProxyConnectionManager extends ConnectionManager { return reject(ProxyUtil.inflaterr(error)); } let connection = new ProxyConnection(this, connectionProfile, businessNetworkIdentifier, this.socket, connectionID); + // Only emit when client + this.socket.on('events', (myConnectionID, events) => { + if (myConnectionID === connectionID) { + connection.emit('events', JSON.parse(events)); + } + }); resolve(connection); }); }); diff --git a/packages/composer-connector-server/lib/connectorserver.js b/packages/composer-connector-server/lib/connectorserver.js index a5d1867482..61dd980991 100644 --- a/packages/composer-connector-server/lib/connectorserver.js +++ b/packages/composer-connector-server/lib/connectorserver.js @@ -161,6 +161,11 @@ class ConnectorServer { callback(null, securityContextID); LOG.exit(method, securityContextID); }) + .then(() => { + connection.on('events', (events) => { + this.socket.emit('events', connectionID, events); + }); + }) .catch((error) => { LOG.error(error); callback(ConnectorServer.serializerr(error)); diff --git a/packages/composer-connector-web/lib/webconnection.js b/packages/composer-connector-web/lib/webconnection.js index aacfb2dd5c..11687157ac 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'); diff --git a/packages/composer-playground/src/app/test/test.component.ts b/packages/composer-playground/src/app/test/test.component.ts index 31a6c12199..a9df222de2 100644 --- a/packages/composer-playground/src/app/test/test.component.ts +++ b/packages/composer-playground/src/app/test/test.component.ts @@ -29,6 +29,9 @@ export class TestComponent implements OnInit { } ngOnInit(): Promise { + this.clientService.getBusinessNetworkConnection().on('event', (event) => { + console.log(event); + }); return this.initializationService.initialize() .then(() => { return this.clientService.getBusinessNetworkConnection().getAllAssetRegistries() diff --git a/packages/composer-runtime-embedded/lib/embeddedcontext.js b/packages/composer-runtime-embedded/lib/embeddedcontext.js index b8df71596f..02f03fa4d8 100644 --- a/packages/composer-runtime-embedded/lib/embeddedcontext.js +++ b/packages/composer-runtime-embedded/lib/embeddedcontext.js @@ -60,7 +60,7 @@ class EmbeddedContext extends Context { */ getEventService() { if (!this.eventService) { - this.eventService = new EmbeddedEventService(this.getSerializer(), this.eventSink); + this.eventService = new EmbeddedEventService(this.eventSink); } return this.eventService; } diff --git a/packages/composer-runtime-embedded/lib/embeddedeventservice.js b/packages/composer-runtime-embedded/lib/embeddedeventservice.js index 87f0be07d0..2cda0b30f7 100644 --- a/packages/composer-runtime-embedded/lib/embeddedeventservice.js +++ b/packages/composer-runtime-embedded/lib/embeddedeventservice.js @@ -27,11 +27,10 @@ class EmbeddedEventService extends EventService { /** * Constructor. - * @param {Serializer} serializer Serializer instance. * @param {EventEmitter} eventSink the event emitter */ - constructor(serializer, eventSink) { - super(serializer); + constructor(eventSink) { + super(); const method = 'constructor'; this.emitter = eventSink; diff --git a/packages/composer-runtime-hlfv1/eventservice.go b/packages/composer-runtime-hlfv1/eventservice.go index 5c2baf12af..3dc2e15b96 100644 --- a/packages/composer-runtime-hlfv1/eventservice.go +++ b/packages/composer-runtime-hlfv1/eventservice.go @@ -54,7 +54,10 @@ func NewEventService(vm *duktape.Context, context *Context, stub shim.ChaincodeS // Bind the methods into the JavaScript object. vm.PushGoFunction(result.commit) // [ global composer theEventService commit ] - vm.PutPropString(-2, "_commit") // [ global composer theEventService ] + 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 @@ -72,13 +75,13 @@ func (eventService *EventService) commit(vm *duktape.Context) (result int) { vm.GetPropString(-1, "serializeBuffer") // [ theEventService, serializeBuffer ] vm.RequireFunction(-1) // [ theEventService, serializeBuffer ] vm.Dup(-2) // [ theEventService, serializeBuffer, theEventService ] - vm.PcallMethod(0) // [ theEventService, returnValue ] + 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("setting event", value) + logger.Debug("Emitting event from EventService.commit", value) eventService.Stub.SetEvent("composer", []byte(value)) } diff --git a/packages/composer-runtime-web/lib/webcontext.js b/packages/composer-runtime-web/lib/webcontext.js index fa3c11b2a0..fa5b527fc2 100644 --- a/packages/composer-runtime-web/lib/webcontext.js +++ b/packages/composer-runtime-web/lib/webcontext.js @@ -59,7 +59,7 @@ class WebContext extends Context { */ getEventService() { if (!this.eventService) { - this.eventService = new WebEventService(this.getSerializer(), this.eventSink); + this.eventService = new WebEventService(this.eventSink); } return this.eventService; } diff --git a/packages/composer-runtime-web/lib/webeventservice.js b/packages/composer-runtime-web/lib/webeventservice.js index 5acb1359e1..b66e1b0553 100644 --- a/packages/composer-runtime-web/lib/webeventservice.js +++ b/packages/composer-runtime-web/lib/webeventservice.js @@ -27,11 +27,10 @@ class WebEventService extends EventService { /** * Constructor. - * @param {Serializer} serializer Serializer instance. * @param {EventEmitter} eventSink the event emitter */ - constructor(serializer, eventSink) { - super(serializer); + constructor(eventSink) { + super(); const method = 'constructor'; this.eventSink = eventSink; @@ -42,7 +41,7 @@ class WebEventService extends EventService { * Emit the events stored in eventBuffer */ commit() { - this.eventSink.emit('event', this.serializeBuffer()); + this.eventSink.emit('events', this.serializeBuffer()); } } diff --git a/packages/composer-runtime/lib/api.js b/packages/composer-runtime/lib/api.js index 530d23e6ff..66178bd376 100644 --- a/packages/composer-runtime/lib/api.js +++ b/packages/composer-runtime/lib/api.js @@ -35,14 +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, eventService) { + constructor(factory, serializer, participant, registryManager, eventService, context) { const method = 'constructor'; - LOG.entry(method, factory, participant, registryManager, eventService); + LOG.entry(method, factory, serializer, participant, registryManager, eventService, context); /** * Get the factory. The factory can be used to create new instances of @@ -171,7 +173,11 @@ class Api { this.emit = function emit(event) { const method = 'emit'; LOG.entry(method); - eventService.emit(event); + // event.setIdentifier(`${context.getIdentifier()}#${context.getEventNumber()}`); + let serializedEvent = serializer.toJSON(event); + context.incrementEventNumber(); + LOG.debug(method, event.getFullyQualifiedIdentifier(), serializedEvent); + eventService.emit(serializedEvent); LOG.exit(method); }; diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js index 3f3c391fe0..c64f568c5c 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; } /** @@ -340,7 +341,7 @@ class Context { */ getApi() { if (!this.api) { - this.api = new Api(this.getFactory(), this.getParticipant(), this.getRegistryManager(), this.getEventService()); + this.api = new Api(this.getFactory(), this.getSerializer(), this.getParticipant(), this.getRegistryManager(), this.getEventService(), this); } return this.api; } @@ -471,6 +472,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/eventservice.js b/packages/composer-runtime/lib/eventservice.js index 51c1262b9b..54f4f172b8 100644 --- a/packages/composer-runtime/lib/eventservice.js +++ b/packages/composer-runtime/lib/eventservice.js @@ -14,6 +14,9 @@ '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 @@ -24,21 +27,22 @@ class EventService { /** * Constructor. - * @param {Serializer} serializer A serializer instance. */ - constructor(serializer) { - this.serializer = serializer; + constructor() { this.eventBuffer = []; } /** * Add an event to the buffer - * @abstract * @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); } /** @@ -72,13 +76,13 @@ class EventService { * @return {Object[]} - An array of serialized events */ serializeBuffer() { - console.log('serializeBuffer ENTER', this.eventBuffer); + const method = 'serializeBuffer'; + LOG.entry(method); let eb = []; this.eventBuffer.forEach((event) => { - console.log('Event', event); - eb.push(this.serializer.toJSON(event)); + eb.push(event); }); - console.log('serializeBuffer EXIT', eb); + LOG.exit(method, eb); return eb; } From 86602cce5ff88d8ee04bed9a10af6fa47389613f Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 10 May 2017 17:11:02 +0100 Subject: [PATCH 35/41] Events given numbers --- packages/composer-runtime/lib/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/composer-runtime/lib/api.js b/packages/composer-runtime/lib/api.js index 66178bd376..f7fa5001b7 100644 --- a/packages/composer-runtime/lib/api.js +++ b/packages/composer-runtime/lib/api.js @@ -173,7 +173,7 @@ class Api { this.emit = function emit(event) { const method = 'emit'; LOG.entry(method); - // event.setIdentifier(`${context.getIdentifier()}#${context.getEventNumber()}`); + event.setIdentifier(context.transaction.$identifier + '#' + context.getEventNumber()); let serializedEvent = serializer.toJSON(event); context.incrementEventNumber(); LOG.debug(method, event.getFullyQualifiedIdentifier(), serializedEvent); From 91b8403c352db6594cf2de1ab872e3c0aee2c6ed Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Fri, 12 May 2017 14:01:04 +0100 Subject: [PATCH 36/41] Stop subscribing to events multiple times --- .../composer-admin/lib/adminconnection.js | 3 --- .../lib/businessnetworkconnection.js | 13 ++++++++++ packages/composer-common/test/factory.js | 4 +-- .../lib/hlfconnection.js | 8 +++++- .../lib/proxyconnection.js | 10 ++++++++ .../lib/proxyconnectionmanager.js | 8 ++++++ .../lib/connectorserver.js | 7 ++++++ .../connection-profile-data.component.ts | 1 + .../src/app/test/test.component.ts | 7 ++++++ .../composer-runtime-hlfv1/chaincode_test.go | 15 ----------- packages/composer-runtime/lib/api.js | 2 +- packages/composer-runtime/lib/eventservice.js | 8 ++---- packages/composer-runtime/test/api.js | 25 ++++++++++++++++--- packages/composer-runtime/test/context.js | 3 +++ .../composer-runtime/test/eventservice.js | 12 +++------ 15 files changed, 85 insertions(+), 41 deletions(-) delete mode 100644 packages/composer-runtime-hlfv1/chaincode_test.go 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 8302ab6be4..032d6f04db 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -282,11 +282,15 @@ 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) => { + LOG.debug('@14gracel', 'on events connection'); connection.on('events', (events) => { events.forEach((event) => { let serializedEvent = this.getBusinessNetwork().getSerializer().fromJSON(event); + LOG.debug('@14gracel', 'emit event connection'); this.emit('event', serializedEvent); }); }); @@ -307,6 +311,7 @@ class BusinessNetworkConnection extends EventEmitter { }) .then((businessNetwork) => { this.businessNetwork = businessNetwork; + LOG.exit(method); return this.businessNetwork; }); } @@ -327,14 +332,22 @@ 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('@14gracel', 'remove events connection'); + }); + }) .then(() => { this.connection = null; this.securityContext = null; this.businessNetwork = null; + LOG.exit(method); }); } diff --git a/packages/composer-common/test/factory.js b/packages/composer-common/test/factory.js index a1433dd49b..e33029fa32 100644 --- a/packages/composer-common/test/factory.js +++ b/packages/composer-common/test/factory.js @@ -242,7 +242,7 @@ describe('Factory', () => { it('should create a new instance with a generated ID', () => { let resource = factory.newEvent('org.acme.test', 'MyEvent'); - resource.eventId.should.equal('5604bdfe-7b96-45d0-9883-9c05c18fe638'); + resource.eventId.should.equal('valid'); resource.timestamp.should.be.an.instanceOf(Date); }); @@ -250,7 +250,7 @@ describe('Factory', () => { 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', '5604bdfe-7b96-45d0-9883-9c05c18fe638', { hello: 'world' }); + sinon.assert.calledWith(spy, 'org.acme.test', 'MyEvent', 'valid', { hello: 'world' }); }); }); diff --git a/packages/composer-connector-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js index 11188d788a..b8269e40f2 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -99,11 +99,15 @@ class HLFConnection extends Connection { this.connectOptions = connectOptions; this.client = client; this.chain = chain; + this.businessNetworkIdentifier = businessNetworkIdentifier; + this.eventHubs = eventHubs; if (businessNetworkIdentifier) { - LOG.entry('registerChaincodeEvent', businessNetworkIdentifier, 'composer'); + LOG.entry('@14gracel', 'registerChaincodeEvent', businessNetworkIdentifier, 'composer'); eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { + + LOG.entry('@14gracel', 'emit events connection'); this.emit('events', event.payload.toString('utf8')); }); } @@ -141,6 +145,8 @@ class HLFConnection extends Connection { if (eventHub.isconnected()) { eventHub.disconnect(); } + LOG.debug('@14gracel', 'Unregister from the chaincode events'); + this.eventHubs[0].unregisterChaincodeEvent(this.businessNetworkIdentifier); }); LOG.exit(method); }) diff --git a/packages/composer-connector-proxy/lib/proxyconnection.js b/packages/composer-connector-proxy/lib/proxyconnection.js index 4c0afde38a..d84e2572a4 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,12 @@ class ProxyConnection extends Connection { } resolve(); }); + }) + .then(() => { + this.socket.removeListener('events', () => { + LOG.debug('@14gracel', 'removed events'); + }); + LOG.exit(method); }); } diff --git a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js index 463d092802..82d53293a9 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'; @@ -80,6 +83,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) => { @@ -89,11 +94,14 @@ class ProxyConnectionManager extends ConnectionManager { } let connection = new ProxyConnection(this, connectionProfile, businessNetworkIdentifier, this.socket, connectionID); // Only emit when client + LOG.debug('@14gracel', 'socket on events'); this.socket.on('events', (myConnectionID, events) => { if (myConnectionID === connectionID) { + LOG.debug('@14gracel', 'connection emit events'); connection.emit('events', JSON.parse(events)); } }); + LOG.exit(method); resolve(connection); }); }); diff --git a/packages/composer-connector-server/lib/connectorserver.js b/packages/composer-connector-server/lib/connectorserver.js index 61dd980991..cd570a206b 100644 --- a/packages/composer-connector-server/lib/connectorserver.js +++ b/packages/composer-connector-server/lib/connectorserver.js @@ -123,6 +123,11 @@ class ConnectorServer { return Promise.resolve(); } delete this.connections[connectionID]; + + connection.removeListener('events', () => { + LOG.debug('@14gracel', 'removed events'); + }); + return connection.disconnect() .then(() => { callback(null); @@ -162,7 +167,9 @@ class ConnectorServer { LOG.exit(method, securityContextID); }) .then(() => { + LOG.debug('@14gracel', 'on events connection'); connection.on('events', (events) => { + LOG.debug('@14gracel', 'emit events socket'); this.socket.emit('events', connectionID, events); }); }) 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..549e76329f 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'; @@ -28,6 +29,12 @@ export class TestComponent implements OnInit { } ngOnInit(): Promise { + if (this.clientService.getBusinessNetworkConnection().listenerCount('event') === 0) { + this.clientService.getBusinessNetworkConnection().on('event' , (event) => { + console.log('event', event); + }); + } + return this.initializationService.initialize() .then(() => { return this.clientService.getBusinessNetworkConnection().getAllAssetRegistries() 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/lib/api.js b/packages/composer-runtime/lib/api.js index f7fa5001b7..da9386eff5 100644 --- a/packages/composer-runtime/lib/api.js +++ b/packages/composer-runtime/lib/api.js @@ -173,7 +173,7 @@ class Api { this.emit = function emit(event) { const method = 'emit'; LOG.entry(method); - event.setIdentifier(context.transaction.$identifier + '#' + context.getEventNumber()); + event.setIdentifier(context.getTransaction().getIdentifier() + '#' + context.getEventNumber()); let serializedEvent = serializer.toJSON(event); context.incrementEventNumber(); LOG.debug(method, event.getFullyQualifiedIdentifier(), serializedEvent); diff --git a/packages/composer-runtime/lib/eventservice.js b/packages/composer-runtime/lib/eventservice.js index 54f4f172b8..997d9aae73 100644 --- a/packages/composer-runtime/lib/eventservice.js +++ b/packages/composer-runtime/lib/eventservice.js @@ -78,12 +78,8 @@ class EventService { serializeBuffer() { const method = 'serializeBuffer'; LOG.entry(method); - let eb = []; - this.eventBuffer.forEach((event) => { - eb.push(event); - }); - LOG.exit(method, eb); - return eb; + LOG.exit(method, this.eventBuffer); + return this.eventBuffer; } /** diff --git a/packages/composer-runtime/test/api.js b/packages/composer-runtime/test/api.js index e4857ce83c..069da4db67 100644 --- a/packages/composer-runtime/test/api.js +++ b/packages/composer-runtime/test/api.js @@ -23,6 +23,8 @@ 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(); @@ -34,17 +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); mockEventService = sinon.createStubInstance(EventService); - api = new Api(mockFactory, mockParticipant, mockRegistryManager, mockEventService); + mockContext = sinon.createStubInstance(Context); + api = new Api(mockFactory, mockSerializer, mockParticipant, mockRegistryManager, mockEventService, mockContext); }); describe('#constructor', () => { @@ -109,11 +115,22 @@ describe('Api', () => { }); - describe('#getCurrentParticipant', () => { + 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({}); + api.emit(mockEvent); sinon.assert.calledOnce(mockEventService.emit); - sinon.assert.calledWith(mockEventService.emit, {}); + // sinon.assert.calledWith(mockEventService.emit, mockEvent); }); }); diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js index 96c5065d66..985505582e 100644 --- a/packages/composer-runtime/test/context.js +++ b/packages/composer-runtime/test/context.js @@ -46,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(); }); @@ -432,6 +434,7 @@ describe('Context', () => { 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); }); diff --git a/packages/composer-runtime/test/eventservice.js b/packages/composer-runtime/test/eventservice.js index 7a9b8f1021..1281d5e083 100644 --- a/packages/composer-runtime/test/eventservice.js +++ b/packages/composer-runtime/test/eventservice.js @@ -14,7 +14,6 @@ 'use strict'; const EventService = require('../lib/eventservice'); -const Resource = require('composer-common').Resource; const Serializer = require('composer-common').Serializer; const should = require('chai').should(); @@ -32,10 +31,6 @@ describe('EventService', () => { }); describe('#constructor', () => { - it('should have a serializer property', () => { - should.exist(eventService.serializer); - }); - it('should have a property for buffering events', () => { should.exist(eventService.eventBuffer); }); @@ -84,10 +79,9 @@ describe('EventService', () => { }); describe('#serializeBuffer', () => { - it('should serialize an array of events into json', () => { - let mockResource = sinon.createStubInstance(Resource); - mockSerializer.toJSON.returns({'$class': 'much.wow'}); - eventService.eventBuffer = [ mockResource ]; + it('should return the list of events that are to be comitted', () => { + let event = {'$class': 'much.wow'}; + eventService.eventBuffer = [ event ]; eventService.serializeBuffer().should.deep.equal([{'$class': 'much.wow'}]); }); From 956fd309dc5823556cb8cc5379fd965379714690 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Mon, 15 May 2017 14:53:55 +0100 Subject: [PATCH 37/41] Latest events --- .../test/businessnetworkconnection.js | 21 +++++++++++++++++++ packages/composer-common/test/factory.js | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js index afa1c79b2a..e53499ee8f 100644 --- a/packages/composer-client/test/businessnetworkconnection.js +++ b/packages/composer-client/test/businessnetworkconnection.js @@ -155,6 +155,27 @@ 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); + // sandbox.spy(businessNetworkConnection, 'emit'); + const cb = sinon.stub(); + businessNetworkConnection.on('event', cb); + mockConnection.on.withArgs('events', sinon.match.func).yields(['event1', 'event2']); + + return businessNetworkConnection.connect('testprofile', 'testnetwork', 'enrollmentID', 'enrollmentSecret', { some: 'other', options: true }) + .then((result) => { + sinon.assert.calledTwice(cb); // two events + sinon.assert.calledWith(cb, 'event1'); + sinon.assert.calledWith(cb, 'event2'); + }); + }); }); describe('#disconnect', () => { diff --git a/packages/composer-common/test/factory.js b/packages/composer-common/test/factory.js index a1433dd49b..e33029fa32 100644 --- a/packages/composer-common/test/factory.js +++ b/packages/composer-common/test/factory.js @@ -242,7 +242,7 @@ describe('Factory', () => { it('should create a new instance with a generated ID', () => { let resource = factory.newEvent('org.acme.test', 'MyEvent'); - resource.eventId.should.equal('5604bdfe-7b96-45d0-9883-9c05c18fe638'); + resource.eventId.should.equal('valid'); resource.timestamp.should.be.an.instanceOf(Date); }); @@ -250,7 +250,7 @@ describe('Factory', () => { 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', '5604bdfe-7b96-45d0-9883-9c05c18fe638', { hello: 'world' }); + sinon.assert.calledWith(spy, 'org.acme.test', 'MyEvent', 'valid', { hello: 'world' }); }); }); From b7225541fcfa346e3ed91348dd0b5a830615f761 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 17 May 2017 09:32:01 +0100 Subject: [PATCH 38/41] Unit tests for events --- .../lib/businessnetworkconnection.js | 2 - .../test/businessnetworkconnection.js | 9 ++-- .../lib/hlfconnection.js | 13 +++--- .../test/hlfconnection.js | 13 ++++++ .../lib/proxyconnection.js | 5 +-- .../lib/proxyconnectionmanager.js | 16 ++++++- .../test/proxyconnection.js | 3 +- .../test/proxyconnectionmanager.js | 43 ++++++++++++++++--- .../test/connectorserver.js | 8 +++- .../lib/webconnection.js | 2 +- .../src/app/test/test.component.ts | 6 --- .../test/embeddedeventservice.js | 24 ++--------- .../test/webeventservice.js | 24 ++--------- packages/composer-runtime/test/context.js | 13 ++++++ .../test/jstransactionexecutor.js | 5 ++- 15 files changed, 115 insertions(+), 71 deletions(-) diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js index 032d6f04db..806bc732d4 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -342,8 +342,6 @@ class BusinessNetworkConnection extends EventEmitter { this.connection.removeListener('events', () => { LOG.debug('@14gracel', 'remove events connection'); }); - }) - .then(() => { this.connection = null; this.securityContext = null; this.businessNetwork = null; diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js index e53499ee8f..a00df3c9c7 100644 --- a/packages/composer-client/test/businessnetworkconnection.js +++ b/packages/composer-client/test/businessnetworkconnection.js @@ -164,16 +164,17 @@ describe('BusinessNetworkConnection', () => { })); sandbox.stub(Util, 'queryChainCode').withArgs(mockSecurityContext, 'getBusinessNetwork', []).resolves(buffer); sandbox.stub(BusinessNetworkDefinition, 'fromArchive').resolves(mockBusinessNetworkDefinition); - // sandbox.spy(businessNetworkConnection, 'emit'); 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'); - sinon.assert.calledWith(cb, 'event2'); + sinon.assert.calledWith(cb, 'event1#serialized'); + sinon.assert.calledWith(cb, 'event2#serialized'); }); }); }); @@ -190,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-connector-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js index b8269e40f2..11f1b5bd73 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -104,12 +104,13 @@ class HLFConnection extends Connection { this.eventHubs = eventHubs; if (businessNetworkIdentifier) { - LOG.entry('@14gracel', 'registerChaincodeEvent', businessNetworkIdentifier, 'composer'); - eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { - - LOG.entry('@14gracel', 'emit events connection'); - this.emit('events', event.payload.toString('utf8')); - }); + if (eventHubs.length > 0) { + LOG.entry('@14gracel', 'registerChaincodeEvent', businessNetworkIdentifier, 'composer'); + eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { + LOG.entry('@14gracel', 'emit events connection'); + this.emit('events', event.payload.toString('utf8')); + }); + } } this.caClient = caClient; diff --git a/packages/composer-connector-hlfv1/test/hlfconnection.js b/packages/composer-connector-hlfv1/test/hlfconnection.js index 1d8adc0d70..642dbb6fc6 100644 --- a/packages/composer-connector-hlfv1/test/hlfconnection.js +++ b/packages/composer-connector-hlfv1/test/hlfconnection.js @@ -92,6 +92,19 @@ describe('HLFConnection', () => { describe('#constructor', () => { + it('should subscribe to the eventHub and emit events', () => { + const payload = { + toString: () => { + return 'event'; + } + }; + connection.emit = sandbox.stub(); + mockEventHub.registerChaincodeEvent.withArgs('org.acme.biznet', 'composer', sinon.match.func).yield({payload: payload}); + 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); diff --git a/packages/composer-connector-proxy/lib/proxyconnection.js b/packages/composer-connector-proxy/lib/proxyconnection.js index d84e2572a4..1dee5c36b5 100644 --- a/packages/composer-connector-proxy/lib/proxyconnection.js +++ b/packages/composer-connector-proxy/lib/proxyconnection.js @@ -58,9 +58,8 @@ class ProxyConnection extends Connection { }); }) .then(() => { - this.socket.removeListener('events', () => { - LOG.debug('@14gracel', 'removed events'); - }); + LOG.debug('@14gracel', 'removed events'); + 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 82d53293a9..4b4f604fc8 100644 --- a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js +++ b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js @@ -40,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 @@ -92,7 +106,7 @@ 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 LOG.debug('@14gracel', 'socket on events'); this.socket.on('events', (myConnectionID, events) => { diff --git a/packages/composer-connector-proxy/test/proxyconnection.js b/packages/composer-connector-proxy/test/proxyconnection.js index 4661f3751a..ce0f5da2ec 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); diff --git a/packages/composer-connector-proxy/test/proxyconnectionmanager.js b/packages/composer-connector-proxy/test/proxyconnectionmanager.js index 742a94bd2a..83bbda88c3 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/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 11687157ac..1658787b9f 100644 --- a/packages/composer-connector-web/lib/webconnection.js +++ b/packages/composer-connector-web/lib/webconnection.js @@ -14,7 +14,7 @@ 'use strict'; -const Resource = require('composer-common').Resource; +// const Resource = require('composer-common').Resource; const Connection = require('composer-common').Connection; const Engine = require('composer-runtime').Engine; const uuid = require('uuid'); diff --git a/packages/composer-playground/src/app/test/test.component.ts b/packages/composer-playground/src/app/test/test.component.ts index 549e76329f..ae4fba4cd2 100644 --- a/packages/composer-playground/src/app/test/test.component.ts +++ b/packages/composer-playground/src/app/test/test.component.ts @@ -29,12 +29,6 @@ export class TestComponent implements OnInit { } ngOnInit(): Promise { - if (this.clientService.getBusinessNetworkConnection().listenerCount('event') === 0) { - this.clientService.getBusinessNetworkConnection().on('event' , (event) => { - console.log('event', event); - }); - } - return this.initializationService.initialize() .then(() => { return this.clientService.getBusinessNetworkConnection().getAllAssetRegistries() diff --git a/packages/composer-runtime-embedded/test/embeddedeventservice.js b/packages/composer-runtime-embedded/test/embeddedeventservice.js index c1f5bdeed2..1f1f247fc3 100644 --- a/packages/composer-runtime-embedded/test/embeddedeventservice.js +++ b/packages/composer-runtime-embedded/test/embeddedeventservice.js @@ -16,7 +16,6 @@ const EmbeddedEventService = require('..').EmbeddedEventService; const EventEmitter = require('events').EventEmitter; -const Serializer = require('composer-common').Serializer; const chai = require('chai'); chai.should(); @@ -27,17 +26,13 @@ require('sinon-as-promised'); describe('EmbeddedEventService', () => { let eventService; - let mockSerializer; let mockEventEmitter; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); - mockSerializer = sinon.createStubInstance(Serializer); mockEventEmitter = sinon.createStubInstance(EventEmitter); - eventService = new EmbeddedEventService(mockSerializer); - eventService.getEventEmitter = sinon.stub(); - eventService.getEventEmitter.returns(mockEventEmitter); + eventService = new EmbeddedEventService(mockEventEmitter); }); afterEach(() => { @@ -46,8 +41,8 @@ describe('EmbeddedEventService', () => { describe('#constructor', () => { it('should assign a default event emitter', () => { - eventService = new EmbeddedEventService(mockSerializer); - (eventService.emitter instanceof EventEmitter).should.be.true; + eventService = new EmbeddedEventService(mockEventEmitter); + eventService.emitter.should.be.an.instanceOf(EventEmitter); }); }); @@ -61,17 +56,4 @@ describe('EmbeddedEventService', () => { sinon.assert.calledWith(mockEventEmitter.emit, 'composer', ['serialized JS']); }); }); - - describe('#getEventEmitter', () => { - it('should return an EventEmitter', () => { - eventService.getEventEmitter().should.be.instanceOf(EventEmitter); - }); - - it('should return emiiter if it is set', () => { - let eventService = new EmbeddedEventService(mockSerializer); - eventService.emitter = {}; - eventService.getEventEmitter().should.deep.equal({}); - }); - - }); }); diff --git a/packages/composer-runtime-web/test/webeventservice.js b/packages/composer-runtime-web/test/webeventservice.js index 52aa1782b1..94c623fa4e 100644 --- a/packages/composer-runtime-web/test/webeventservice.js +++ b/packages/composer-runtime-web/test/webeventservice.js @@ -16,7 +16,6 @@ const WebEventService = require('..').WebEventService; const EventEmitter = require('events').EventEmitter; -const Serializer = require('composer-common').Serializer; const chai = require('chai'); chai.should(); @@ -27,15 +26,13 @@ require('sinon-as-promised'); describe('WebEventService', () => { let eventService; - let mockSerializer; let mockEventEmitter; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); - mockSerializer = sinon.createStubInstance(Serializer); mockEventEmitter = sinon.createStubInstance(EventEmitter); - eventService = new WebEventService(mockSerializer); + eventService = new WebEventService(mockEventEmitter); eventService.getEventEmitter = sinon.stub(); eventService.getEventEmitter.returns(mockEventEmitter); }); @@ -46,8 +43,8 @@ describe('WebEventService', () => { describe('#constructor', () => { it('should assign a default event emitter', () => { - eventService = new WebEventService(mockSerializer); - (eventService.emitter instanceof EventEmitter).should.be.true; + eventService = new WebEventService(mockEventEmitter); + (eventService.eventSink instanceof EventEmitter).should.be.true; }); }); @@ -58,20 +55,7 @@ describe('WebEventService', () => { eventService.commit(); sinon.assert.calledOnce(eventService.serializeBuffer); sinon.assert.calledOnce(mockEventEmitter.emit); - sinon.assert.calledWith(mockEventEmitter.emit, 'composer', ['serialized JS']); + sinon.assert.calledWith(mockEventEmitter.emit, 'events', ['serialized JS']); }); }); - - describe('#getEventEmitter', () => { - it('should return an EventEmitter', () => { - eventService.getEventEmitter().should.be.instanceOf(EventEmitter); - }); - - it('should return emiiter if it is set', () => { - let eventService = new WebEventService(mockSerializer); - eventService.emitter = {}; - eventService.getEventEmitter().should.deep.equal({}); - }); - - }); }); diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js index 985505582e..1e37b9d8e5 100644 --- a/packages/composer-runtime/test/context.js +++ b/packages/composer-runtime/test/context.js @@ -658,6 +658,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/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(() => { From 83795369419f0ea08167b3d237c6d04fac906ac9 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Wed, 17 May 2017 09:37:32 +0100 Subject: [PATCH 39/41] Duktape version fix --- .../vendor/gopkg.in/olebedev/go-duktape.v3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/composer-runtime-hlfv1/vendor/gopkg.in/olebedev/go-duktape.v3 b/packages/composer-runtime-hlfv1/vendor/gopkg.in/olebedev/go-duktape.v3 index cd62510143..bb87dea2db 160000 --- a/packages/composer-runtime-hlfv1/vendor/gopkg.in/olebedev/go-duktape.v3 +++ b/packages/composer-runtime-hlfv1/vendor/gopkg.in/olebedev/go-duktape.v3 @@ -1 +1 @@ -Subproject commit cd62510143a3b9bd61c7268ecd037c5ca6d9ae0c +Subproject commit bb87dea2db4a1f68f67b5c7a664ff5df9f279e49 From cbabd0adc84a63cdb210207f74e6abe3ed06864b Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Thu, 18 May 2017 10:13:48 +0100 Subject: [PATCH 40/41] hlf0.6 event support --- .../lib/businessnetworkconnection.js | 4 +--- .../lib/hfcconnection.js | 19 +++++++++++++++++++ .../test/hfcconnection.js | 13 ++++++++++++- .../lib/hlfconnection.js | 12 ++++-------- .../test/hlfconnection.js | 11 ++++++----- .../lib/proxyconnection.js | 3 +-- .../lib/proxyconnectionmanager.js | 4 +--- .../test/proxyconnection.js | 1 + .../test/proxyconnectionmanager.js | 2 +- .../lib/connectorserver.js | 7 ++----- .../lib/webconnectionmanager.js | 3 ++- .../lib/embeddedeventservice.js | 5 +++-- .../test/embeddedeventservice.js | 6 +++--- packages/composer-runtime-hlf/context.go | 11 +++++++++++ .../lib/webeventservice.js | 3 ++- .../test/webeventservice.js | 4 ++-- packages/composer-runtime/lib/eventservice.js | 6 +++--- .../composer-runtime/test/eventservice.js | 2 +- 18 files changed, 75 insertions(+), 41 deletions(-) diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js index 806bc732d4..1f4a97c215 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -286,11 +286,9 @@ class BusinessNetworkConnection extends EventEmitter { LOG.entry(method, connectionProfile, businessNetwork, enrollmentID, enrollmentSecret, additionalConnectOptions); return this.connectionProfileManager.connect(connectionProfile, businessNetwork, additionalConnectOptions) .then((connection) => { - LOG.debug('@14gracel', 'on events connection'); connection.on('events', (events) => { events.forEach((event) => { let serializedEvent = this.getBusinessNetwork().getSerializer().fromJSON(event); - LOG.debug('@14gracel', 'emit event connection'); this.emit('event', serializedEvent); }); }); @@ -340,7 +338,7 @@ class BusinessNetworkConnection extends EventEmitter { return this.connection.disconnect() .then(() => { this.connection.removeListener('events', () => { - LOG.debug('@14gracel', 'remove events connection'); + LOG.debug(method, 'removeLisener'); }); this.connection = null; this.securityContext = null; 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 11f1b5bd73..9d1edadecc 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -104,13 +104,10 @@ class HLFConnection extends Connection { this.eventHubs = eventHubs; if (businessNetworkIdentifier) { - if (eventHubs.length > 0) { - LOG.entry('@14gracel', 'registerChaincodeEvent', businessNetworkIdentifier, 'composer'); - eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { - LOG.entry('@14gracel', 'emit events connection'); - this.emit('events', event.payload.toString('utf8')); - }); - } + LOG.entry(method, 'registerChaincodeEvent', businessNetworkIdentifier, 'composer'); + eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { + this.emit('events', JSON.parse(event.payload.toString('utf8'))); + }); } this.caClient = caClient; @@ -146,7 +143,6 @@ class HLFConnection extends Connection { if (eventHub.isconnected()) { eventHub.disconnect(); } - LOG.debug('@14gracel', 'Unregister from the chaincode events'); 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 642dbb6fc6..385d4a9fd1 100644 --- a/packages/composer-connector-hlfv1/test/hlfconnection.js +++ b/packages/composer-connector-hlfv1/test/hlfconnection.js @@ -93,13 +93,15 @@ describe('HLFConnection', () => { describe('#constructor', () => { it('should subscribe to the eventHub and emit events', () => { - const payload = { - toString: () => { - return 'event'; + const events = { + payload: { + toString: () => { + return '{"event":"event"}'; + } } }; connection.emit = sandbox.stub(); - mockEventHub.registerChaincodeEvent.withArgs('org.acme.biznet', 'composer', sinon.match.func).yield({payload: payload}); + 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); @@ -141,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 1dee5c36b5..3cc045d52c 100644 --- a/packages/composer-connector-proxy/lib/proxyconnection.js +++ b/packages/composer-connector-proxy/lib/proxyconnection.js @@ -58,8 +58,7 @@ class ProxyConnection extends Connection { }); }) .then(() => { - LOG.debug('@14gracel', 'removed events'); - this.socket.removeListener('events'); + 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 4b4f604fc8..2754af09ee 100644 --- a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js +++ b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js @@ -108,11 +108,9 @@ class ProxyConnectionManager extends ConnectionManager { } let connection = ProxyConnectionManager.createConnection(this, connectionProfile, businessNetworkIdentifier, this.socket, connectionID); // Only emit when client - LOG.debug('@14gracel', 'socket on events'); this.socket.on('events', (myConnectionID, events) => { if (myConnectionID === connectionID) { - LOG.debug('@14gracel', 'connection emit events'); - connection.emit('events', JSON.parse(events)); + connection.emit('events', events); } }); LOG.exit(method); diff --git a/packages/composer-connector-proxy/test/proxyconnection.js b/packages/composer-connector-proxy/test/proxyconnection.js index ce0f5da2ec..e10c1725f7 100644 --- a/packages/composer-connector-proxy/test/proxyconnection.js +++ b/packages/composer-connector-proxy/test/proxyconnection.js @@ -60,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 83bbda88c3..c7d2a7c2db 100644 --- a/packages/composer-connector-proxy/test/proxyconnectionmanager.js +++ b/packages/composer-connector-proxy/test/proxyconnectionmanager.js @@ -144,7 +144,7 @@ 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"}]'); + 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); diff --git a/packages/composer-connector-server/lib/connectorserver.js b/packages/composer-connector-server/lib/connectorserver.js index cd570a206b..29e18613d8 100644 --- a/packages/composer-connector-server/lib/connectorserver.js +++ b/packages/composer-connector-server/lib/connectorserver.js @@ -124,9 +124,7 @@ class ConnectorServer { } delete this.connections[connectionID]; - connection.removeListener('events', () => { - LOG.debug('@14gracel', 'removed events'); - }); + connection.removeListener('events', () => {}); return connection.disconnect() .then(() => { @@ -167,9 +165,8 @@ class ConnectorServer { LOG.exit(method, securityContextID); }) .then(() => { - LOG.debug('@14gracel', 'on events connection'); connection.on('events', (events) => { - LOG.debug('@14gracel', 'emit events socket'); + LOG.debug('@14gracel', events); this.socket.emit('events', connectionID, events); }); }) 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-runtime-embedded/lib/embeddedeventservice.js b/packages/composer-runtime-embedded/lib/embeddedeventservice.js index 2cda0b30f7..f0de0c4b49 100644 --- a/packages/composer-runtime-embedded/lib/embeddedeventservice.js +++ b/packages/composer-runtime-embedded/lib/embeddedeventservice.js @@ -33,7 +33,7 @@ class EmbeddedEventService extends EventService { super(); const method = 'constructor'; - this.emitter = eventSink; + this.eventSink = eventSink; LOG.exit(method); } @@ -42,7 +42,8 @@ class EmbeddedEventService extends EventService { * Emit the events stored in eventBuffer */ commit() { - this.emitter.emit('composer', this.serializeBuffer()); + const jsonEvent = JSON.parse(this.serializeBuffer()); + this.eventSink.emit('events', jsonEvent); } } diff --git a/packages/composer-runtime-embedded/test/embeddedeventservice.js b/packages/composer-runtime-embedded/test/embeddedeventservice.js index 1f1f247fc3..ef6c6a0f2d 100644 --- a/packages/composer-runtime-embedded/test/embeddedeventservice.js +++ b/packages/composer-runtime-embedded/test/embeddedeventservice.js @@ -42,18 +42,18 @@ describe('EmbeddedEventService', () => { describe('#constructor', () => { it('should assign a default event emitter', () => { eventService = new EmbeddedEventService(mockEventEmitter); - eventService.emitter.should.be.an.instanceOf(EventEmitter); + eventService.eventSink.should.be.an.instanceOf(EventEmitter); }); }); describe('#commit', () => { it ('should emit a list of events', () => { eventService.serializeBuffer = sinon.stub(); - eventService.serializeBuffer.returns(['serialized JS']); + eventService.serializeBuffer.returns('[{"event":"event"}]'); eventService.commit(); sinon.assert.calledOnce(eventService.serializeBuffer); sinon.assert.calledOnce(mockEventEmitter.emit); - sinon.assert.calledWith(mockEventEmitter.emit, 'composer', ['serialized JS']); + 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-web/lib/webeventservice.js b/packages/composer-runtime-web/lib/webeventservice.js index b66e1b0553..28ab7e94d6 100644 --- a/packages/composer-runtime-web/lib/webeventservice.js +++ b/packages/composer-runtime-web/lib/webeventservice.js @@ -41,7 +41,8 @@ class WebEventService extends EventService { * Emit the events stored in eventBuffer */ commit() { - this.eventSink.emit('events', this.serializeBuffer()); + const jsonEvent = JSON.parse(this.serializeBuffer()); + this.eventSink.emit('events', jsonEvent); } } diff --git a/packages/composer-runtime-web/test/webeventservice.js b/packages/composer-runtime-web/test/webeventservice.js index 94c623fa4e..c85f397f68 100644 --- a/packages/composer-runtime-web/test/webeventservice.js +++ b/packages/composer-runtime-web/test/webeventservice.js @@ -51,11 +51,11 @@ describe('WebEventService', () => { describe('#commit', () => { it ('should emit a list of events', () => { eventService.serializeBuffer = sinon.stub(); - eventService.serializeBuffer.returns(['serialized JS']); + eventService.serializeBuffer.returns('[{"event": "event"}]'); eventService.commit(); sinon.assert.calledOnce(eventService.serializeBuffer); sinon.assert.calledOnce(mockEventEmitter.emit); - sinon.assert.calledWith(mockEventEmitter.emit, 'events', ['serialized JS']); + sinon.assert.calledWith(mockEventEmitter.emit, 'events', [{'event': 'event'}]); }); }); }); diff --git a/packages/composer-runtime/lib/eventservice.js b/packages/composer-runtime/lib/eventservice.js index 997d9aae73..b3bf518443 100644 --- a/packages/composer-runtime/lib/eventservice.js +++ b/packages/composer-runtime/lib/eventservice.js @@ -72,14 +72,14 @@ class EventService { } /** - * Get an array of serialized events - * @return {Object[]} - An array of serialized events + * 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 this.eventBuffer; + return JSON.stringify(this.eventBuffer); } /** diff --git a/packages/composer-runtime/test/eventservice.js b/packages/composer-runtime/test/eventservice.js index 1281d5e083..f2a8d437f1 100644 --- a/packages/composer-runtime/test/eventservice.js +++ b/packages/composer-runtime/test/eventservice.js @@ -83,7 +83,7 @@ describe('EventService', () => { let event = {'$class': 'much.wow'}; eventService.eventBuffer = [ event ]; - eventService.serializeBuffer().should.deep.equal([{'$class': 'much.wow'}]); + eventService.serializeBuffer().should.equal('[{"$class":"much.wow"}]'); }); }); From 11b9ef2051a5b533116679b8550a0eecff328e07 Mon Sep 17 00:00:00 2001 From: Liam Grace Date: Thu, 18 May 2017 10:48:36 +0100 Subject: [PATCH 41/41] Logging updates --- .../lib/proxyconnectionmanager.js | 1 + .../lib/connectorserver.js | 2 +- packages/composer-runtime-hlf/eventservice.go | 80 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/composer-runtime-hlf/eventservice.go diff --git a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js index 2754af09ee..9f3df3db9a 100644 --- a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js +++ b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js @@ -109,6 +109,7 @@ class ProxyConnectionManager extends ConnectionManager { 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); } diff --git a/packages/composer-connector-server/lib/connectorserver.js b/packages/composer-connector-server/lib/connectorserver.js index 29e18613d8..972f0f8fc2 100644 --- a/packages/composer-connector-server/lib/connectorserver.js +++ b/packages/composer-connector-server/lib/connectorserver.js @@ -166,7 +166,7 @@ class ConnectorServer { }) .then(() => { connection.on('events', (events) => { - LOG.debug('@14gracel', events); + LOG.debug(method, events); this.socket.emit('events', connectionID, events); }); }) 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() +}