@accordproject/cicero-engine
Version:
Cicero Engine - Node.js VM based implementation of Accord Protcol Template Specification execution
538 lines (457 loc) • 22.6 kB
JavaScript
/*
* 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.
*/
;
const Template = require('@accordproject/cicero-core').Template;
const Clause = require('@accordproject/cicero-core').Clause;
const Engine = require('../lib/engine');
const chai = require('chai');
chai.should();
chai.use(require('chai-things'));
chai.use(require('chai-as-promised'));
const fs = require('fs');
const path = require('path');
const options = { skipUpdateExternalModels: true };
describe('EngineLatePenalty', () => {
let engine;
let clause;
let clause2;
let template;
let template2;
const testLatePenaltyInput = fs.readFileSync(path.resolve(__dirname, 'data/latedeliveryandpenalty', 'text/sample.md'), 'utf8');
const testLatePenaltyPeriodInput = fs.readFileSync(path.resolve(__dirname, 'data/latedeliveryandpenalty-period', 'text/sample.md'), 'utf8');
before(async () => {
template = await Template.fromDirectory('./test/data/latedeliveryandpenalty', options);
template2 = await Template.fromDirectory('./test/data/latedeliveryandpenalty-period', options);
});
beforeEach(async function () {
engine = new Engine();
clause = new Clause(template);
clause.parse(testLatePenaltyInput);
clause2 = new Clause(template2);
clause2.parse(testLatePenaltyPeriodInput);
});
afterEach(() => {});
describe('#triggerergo', function () {
it('should trigger a late delivery and penalty smart clause', async function () {
const request = {};
request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
request.forceMajeure = false;
request.agreedDelivery = '2017-10-07T16:38:01Z';
request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state, '2017-11-12T17:38:01Z');
result.should.not.be.null;
result.response.penalty.should.equal(110);
result.response.buyerMayTerminate.should.equal(true);
});
it('should trigger a late delivery and penalty smart clause in the ET timezone', async function () {
const request = {};
request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
request.forceMajeure = false;
request.agreedDelivery = '2017-11-10T16:38:01-05:00';
request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state, '2017-11-12T17:38:01-05:00');
result.should.not.be.null;
result.response.penalty.should.equal(21);
result.response.buyerMayTerminate.should.equal(false);
});
it('should fail to trigger if the current time is not in the right format', async function () {
const request = {};
request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
request.forceMajeure = false;
request.agreedDelivery = '2017-11-10T16:38:01-05:00';
request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
return engine.trigger(clause, request, state, 'foobar').should.be.rejectedWith('Current time \'foobar\' is not in standard UTC format');
});
it('should trigger a late delivery and penalty smart clause with a time period', async function () {
const request = {};
request.$class = 'org.accordproject.simplelatedeliveryandpenalty.SimpleLateDeliveryAndPenaltyRequest';
request.agreedDelivery = '2017-10-07T16:38:01Z';
request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause2, request, state, '2019-11-12T17:38:01Z');
result.should.not.be.null;
result.response.penalty.should.equal(87.5);
result.response.buyerMayTerminate.should.equal(true);
});
});
describe('#invokeergo', function () {
it('should invoke a late delivery and penalty smart clause', async function () {
const params = { 'request' : {} };
params.request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
params.request.forceMajeure = false;
params.request.agreedDelivery = '2017-10-07T16:38:01Z';
params.request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.invoke(clause, 'latedeliveryandpenalty', params, state, '2017-11-12T17:38:01Z');
result.should.not.be.null;
result.response.penalty.should.equal(110);
result.response.buyerMayTerminate.should.equal(true);
});
it('should invoke a late delivery and penalty smart clause in the ET timezone', async function () {
const params = { 'request' : {} };
params.request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
params.request.forceMajeure = false;
params.request.agreedDelivery = '2017-11-10T16:38:01-05:00';
params.request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.invoke(clause, 'latedeliveryandpenalty', params, state, '2017-11-12T17:38:01-05:00');
result.should.not.be.null;
result.response.penalty.should.equal(21);
result.response.buyerMayTerminate.should.equal(false);
});
it('should fail to invoke if the current time is not in the right format', async function () {
const params = { 'request' : {} };
params.request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
params.request.forceMajeure = false;
params.request.agreedDelivery = '2017-11-10T16:38:01-05:00';
params.request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
return engine.invoke(clause, 'latedeliveryandpenalty', params, state, 'foobar').should.be.rejectedWith('Current time \'foobar\' is not in standard UTC format');
});
it('should invoke a late delivery and penalty smart clause with a time period', async function () {
const params = { 'request' : {} };
params.request.$class = 'org.accordproject.simplelatedeliveryandpenalty.SimpleLateDeliveryAndPenaltyRequest';
params.request.agreedDelivery = '2017-10-07T16:38:01Z';
params.request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.invoke(clause2, 'latedeliveryandpenalty', params, state, '2019-11-12T17:38:01Z');
result.should.not.be.null;
result.response.penalty.should.equal(87.5);
result.response.buyerMayTerminate.should.equal(true);
});
});
});
describe('EngineLatePenalty (JavaScript)', () => {
let engine;
let clause;
const testLatePenaltyInput = fs.readFileSync(path.resolve(__dirname, 'data/latedeliveryandpenalty_js', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/latedeliveryandpenalty_js', options);
clause = new Clause(template);
clause.parse(testLatePenaltyInput);
});
afterEach(() => {});
describe('#triggerjavascript', function () {
it('should trigger a late delivery and penalty smart clause (JavaScript)', async function () {
const request = {};
request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
request.forceMajeure = false;
request.agreedDelivery = '2017-10-07T16:38:01Z';
request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state, '2017-11-12T17:38:01Z');
result.should.not.be.null;
result.response.penalty.should.equal(110);
result.response.buyerMayTerminate.should.equal(true);
});
it('should trigger a late delivery and penalty smart clause in the ET timezone (JavaScript)', async function () {
const request = {};
request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
request.forceMajeure = false;
request.agreedDelivery = '2017-11-10T16:38:01-05:00';
request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state, '2017-11-12T17:38:01-05:00');
result.should.not.be.null;
result.response.penalty.should.equal(21);
result.response.buyerMayTerminate.should.equal(false);
});
it('should fail to trigger if the current time is not in the right format (JavaScript)', async function () {
const request = {};
request.$class = 'org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest';
request.forceMajeure = false;
request.agreedDelivery = '2017-11-10T16:38:01-05:00';
request.goodsValue = 200.00;
const state = {};
state.$class = 'org.accordproject.runtime.State';
return engine.trigger(clause, request, state, 'foobar').should.be.rejectedWith('Current time \'foobar\' is not in standard UTC format');
});
});
});
describe('EngineHelloWorld', () => {
let engine;
let clause;
const helloWorldInput = fs.readFileSync(path.resolve(__dirname, 'data/helloworld', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/helloworld', options);
clause = new Clause(template);
clause.parse(helloWorldInput);
});
afterEach(() => {});
describe('#trigger', function () {
it('should trigger a smart clause', async function () {
const request = {
'$class': 'org.accordproject.helloworld.MyRequest',
'input': 'Accord Project'
};
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state);
result.should.not.be.null;
result.response.output.should.equal('Hello Fred Blogs (Accord Project)');
});
it('should trigger a smart clause with state initialization', async function () {
const result = await engine.init(clause);
result.state.should.not.be.null;
const state = result.state;
state.$class.should.equal('org.accordproject.runtime.State');
const request1 = {
'$class': 'org.accordproject.helloworld.MyRequest',
'input': 'Accord Project'
};
const result1 = await engine.trigger(clause, request1, state);
result1.should.not.be.null;
result1.response.output.should.equal('Hello Fred Blogs (Accord Project)');
});
});
describe('#trigger2', function () {
it('should trigger a smart clause', async function () {
try {
const request = {
'$class': 'org.accordproject.helloworld.MyRequest',
'input': 'Accord Project'
};
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state);
return result;
} catch (err) {
err.should.be.Error;
}
});
});
});
describe('EngineHelloWorldState', () => {
let engine;
let clause;
const helloWorldInput = fs.readFileSync(path.resolve(__dirname, 'data/helloworldstate', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/helloworldstate', options);
clause = new Clause(template);
clause.parse(helloWorldInput);
});
afterEach(() => {});
describe('#initialize', function () {
it('should initialize a state with counter set to 2', async function () {
const params = { startsAt : 2 };
const result = await engine.init(clause, null, null, params);
result.should.not.be.null;
result.state.counter.should.equal(2);
});
});
});
describe('EngineHelloModule', () => {
let engine;
let clause;
const helloWorldInput = fs.readFileSync(path.resolve(__dirname, 'data/hellomodule', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/hellomodule', options);
clause = new Clause(template);
clause.parse(helloWorldInput);
});
afterEach(() => {});
describe('#trigger', function () {
it('should trigger a smart clause', async function () {
const request = {
'$class': 'org.accordproject.hellomodule.MyRequest',
'input': 'Accord Project'
};
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state);
result.should.not.be.null;
result.response.output.should.equal('Hello Fred Blogs (Accord Project) [motd: PI/2.0 radians is 90.0 degrees]');
});
});
});
describe('EngineHelloEmit', () => {
let engine;
let clause;
const helloEmitInput = fs.readFileSync(path.resolve(__dirname, 'data/helloemit', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/helloemit', options);
clause = new Clause(template);
clause.parse(helloEmitInput);
});
afterEach(() => {});
describe('#triggerandemit', function () {
it('should trigger a smart clause which emits', async function () {
const request = {
'$class': 'org.accordproject.helloemit.MyRequest',
'input': 'Accord Project'
};
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state);
result.should.not.be.null;
result.response.output.should.equal('Hello Fred Blogs (Accord Project)');
result.emit[0].$class.should.equal('org.accordproject.helloemit.Greeting');
result.emit[0].message.should.equal('Voila!');
});
});
});
describe('EngineHelloEmitInit', () => {
let engine;
let clause;
const helloEmitInitInput = fs.readFileSync(path.resolve(__dirname, 'data/helloemitinit', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/helloemitinit', options);
clause = new Clause(template);
clause.parse(helloEmitInitInput);
});
afterEach(() => {});
describe('#triggerandemitinit', function () {
it('should trigger a smart clause which emits during initialization', async function () {
const result = await engine.init(clause);
result.should.not.be.null;
result.response.should.not.be.null;
result.emit[0].$class.should.equal('org.accordproject.helloemit.Greeting');
result.emit[0].message.should.equal('Voila!');
});
});
});
describe('EngineSaft', () => {
let engine;
let clause;
const saftInput = fs.readFileSync(path.resolve(__dirname, 'data/saft', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/saft', options);
clause = new Clause(template);
clause.parse(saftInput);
});
afterEach(() => {});
describe('#trigger', function () {
it('should trigger a smart clause', async function () {
const request = {};
const NS = 'org.accordproject.saft';
request.$class = `${NS}.Launch`;
request.exchangeRate = 100;
const state = {};
state.$class = 'org.accordproject.runtime.State';
const result = await engine.trigger(clause, request, state);
result.should.not.be.null;
result.response.tokenAmount.doubleValue.should.equal(100);
result.response.tokenAddress.should.equal('Daniel Charles Selman');
});
});
});
describe('BogusClauses', () => {
let engine;
let clause;
const testNoLogic = fs.readFileSync(path.resolve(__dirname, 'data/no-logic', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/no-logic', options);
clause = new Clause(template);
clause.parse(testNoLogic);
});
afterEach(() => {});
it('should throw error when no __dispatch in JavaScript logic', async () => {
const scriptManager = clause.getTemplate().getScriptManager();
return (() => scriptManager.hasDispatch()).should.throw('Function __dispatch was not found in logic');
});
it('should throw error when no __init in JavaScript logic', async () => {
const scriptManager = clause.getTemplate().getScriptManager();
return (() => scriptManager.hasInit()).should.throw('Function __init was not found in logic');
});
it('should throw an error when JavaScript logic is missing', async function() {
// Turn all JavaScript logic into something else
const scriptManager = clause.getTemplate().getScriptManager();
scriptManager.getAllScripts().forEach(function (element) {
if (element.getLanguage() === '.js') {
element.language = '.ergo';
}
}, this);
scriptManager.compiledScript = null;
(() => engine.getErgoEngine().cacheJsScript(scriptManager)).should.throw('Did not find any compiled JavaScript logic');
});
it('should throw an error when all logic is missing', async function() {
// Remove all scripts
const scriptManager = clause.getTemplate().getScriptManager();
scriptManager.clearScripts();
(() => engine.getErgoEngine().cacheJsScript(scriptManager)).should.throw('Did not find any compiled JavaScript logic');
});
});
describe('EngineInstallmentSaleErgo', () => {
let engine;
let clause;
const testLatePenaltyInput = fs.readFileSync(path.resolve(__dirname, 'data/installment-sale-ergo', 'text/sample.md'), 'utf8');
beforeEach(async function () {
engine = new Engine();
const template = await Template.fromDirectory('./test/data/installment-sale-ergo', options);
clause = new Clause(template);
clause.parse(testLatePenaltyInput);
});
afterEach(() => {});
describe('#trigger', function () {
it('should trigger a smart clause', async function () {
const request = {};
request.$class = 'org.accordproject.installmentsale.Installment';
request.amount = 2500.00;
const state = {};
state.$class = 'org.accordproject.installmentsale.InstallmentSaleState';
state.status = 'WaitingForFirstDayOfNextMonth';
state.balance_remaining = 10000.00;
state.total_paid = 0.00;
state.next_payment_month = 3.0;
const result = await engine.trigger(clause, request, state);
result.should.not.be.null;
result.response.balance.should.equal(7612.499999999999);
result.state.balance_remaining.should.equal(7612.499999999999);
result.state.total_paid.should.equal(2500.00);
});
it('should initialize a smart clause', async function () {
const result = await engine.init(clause);
result.should.not.be.null;
result.response.should.not.be.null;
result.state.balance_remaining.should.equal(10000.00);
result.state.total_paid.should.equal(0.0);
});
it('should initialize a smart clause and trigger one installment', async function () {
const result = await engine.init(clause);
result.should.not.be.null;
result.response.should.not.be.null;
result.state.balance_remaining.should.equal(10000.00);
result.state.total_paid.should.equal(0.0);
const request1 = {};
request1.$class = 'org.accordproject.installmentsale.Installment';
request1.amount = 2500.00;
const result1 = await engine.trigger(clause, request1, result.state);
result1.should.not.be.null;
result1.response.balance.should.equal(7612.499999999999);
result1.state.balance_remaining.should.equal(7612.499999999999);
result1.state.total_paid.should.equal(2500.00);
});
});
});