diff --git a/src/TemplateArchiveProcessor.ts b/src/TemplateArchiveProcessor.ts index 5708205..0fc0cc2 100644 --- a/src/TemplateArchiveProcessor.ts +++ b/src/TemplateArchiveProcessor.ts @@ -38,6 +38,34 @@ export type InitResponse = { state: State; } +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function validateTriggerResponse(result: unknown): asserts result is TriggerResponse { + if (!isObject(result)) { + throw new Error('Invalid trigger result: expected an object'); + } + if (!isObject(result.result)) { + throw new Error('Invalid trigger result: missing result object'); + } + if (!isObject(result.state)) { + throw new Error('Invalid trigger result: missing state object'); + } + if (!Array.isArray(result.events)) { + throw new Error('Invalid trigger result: events must be an array'); + } +} + +function validateInitResponse(result: unknown): asserts result is InitResponse { + if (!isObject(result)) { + throw new Error('Invalid init result: expected an object'); + } + if (!isObject(result.state)) { + throw new Error('Invalid init result: missing state object'); + } +} + /** * A template archive processor: can draft content using the * templatemark for the archive and trigger the logic of the archive @@ -124,7 +152,9 @@ export class TemplateArchiveProcessor { arguments: [data, request, state, resolvedTime, resolvedOffset] }); if(evalResponse.result) { - return evalResponse.result; + const executionResult = await evalResponse.result; + validateTriggerResponse(executionResult); + return executionResult; } else { throw new Error('Trigger failed with message: ' + evalResponse.message); @@ -175,7 +205,9 @@ export class TemplateArchiveProcessor { arguments: [data, resolvedTime, resolvedOffset] }); if(evalResponse.result) { - return evalResponse.result; + const executionResult = await evalResponse.result; + validateInitResponse(executionResult); + return executionResult; } else { throw new Error('Init failed with message: ' + evalResponse.message); diff --git a/test/TemplateArchiveProcessor.test.ts b/test/TemplateArchiveProcessor.test.ts index c82026b..2991d26 100644 --- a/test/TemplateArchiveProcessor.test.ts +++ b/test/TemplateArchiveProcessor.test.ts @@ -1,56 +1,50 @@ import {Template} from '@accordproject/cicero-core'; import { TemplateArchiveProcessor } from '../src/TemplateArchiveProcessor'; +import { JavaScriptEvaluator } from '../src/JavaScriptEvaluator'; + +const data = { + "$class": "io.clause.latedeliveryandpenalty@0.1.0.TemplateModel", + "forceMajeure": true, + "penaltyDuration": { + "$class": "org.accordproject.time@0.3.0.Duration", + "amount": 2, + "unit": "days" + }, + "penaltyPercentage": 10.5, + "capPercentage": 55, + "termination": { + "$class": "org.accordproject.time@0.3.0.Duration", + "amount": 15, + "unit": "days" + }, + "fractionalPart": "days", + "clauseId": "c88e5ed7-c3e0-4249-a99c-ce9278684ac8", + "$identifier": "c88e5ed7-c3e0-4249-a99c-ce9278684ac8" +}; + +const request = { + goodsValue: 100 +}; + +async function createTemplateArchiveProcessor() { + const template = await Template.fromDirectory('test/archives/latedeliveryandpenalty-typescript', {offline: true}); + return new TemplateArchiveProcessor(template); +} describe('template archive processor', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + test('should draft a template', async () => { - const template = await Template.fromDirectory('test/archives/latedeliveryandpenalty-typescript', {offline: true}); - const templateArchiveProcessor = new TemplateArchiveProcessor(template); - const data = { - "$class": "io.clause.latedeliveryandpenalty@0.1.0.TemplateModel", - "forceMajeure": true, - "penaltyDuration": { - "$class": "org.accordproject.time@0.3.0.Duration", - "amount": 2, - "unit": "days" - }, - "penaltyPercentage": 10.5, - "capPercentage": 55, - "termination": { - "$class": "org.accordproject.time@0.3.0.Duration", - "amount": 15, - "unit": "days" - }, - "fractionalPart": "days", - "clauseId": "c88e5ed7-c3e0-4249-a99c-ce9278684ac8", - "$identifier": "c88e5ed7-c3e0-4249-a99c-ce9278684ac8" - }; + const templateArchiveProcessor = await createTemplateArchiveProcessor(); const options = {}; const result = await templateArchiveProcessor.draft(data, 'markdown', options); expect(result).toMatchSnapshot(); }); test('should init a template', async () => { - const template = await Template.fromDirectory('test/archives/latedeliveryandpenalty-typescript', {offline: true}); - const templateArchiveProcessor = new TemplateArchiveProcessor(template); - const data = { - "$class": "io.clause.latedeliveryandpenalty@0.1.0.TemplateModel", - "forceMajeure": true, - "penaltyDuration": { - "$class": "org.accordproject.time@0.3.0.Duration", - "amount": 2, - "unit": "days" - }, - "penaltyPercentage": 10.5, - "capPercentage": 55, - "termination": { - "$class": "org.accordproject.time@0.3.0.Duration", - "amount": 15, - "unit": "days" - }, - "fractionalPart": "days", - "clauseId": "c88e5ed7-c3e0-4249-a99c-ce9278684ac8", - "$identifier": "c88e5ed7-c3e0-4249-a99c-ce9278684ac8" - }; + const templateArchiveProcessor = await createTemplateArchiveProcessor(); const response = await templateArchiveProcessor.init(data); // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload:any = response.state; @@ -58,30 +52,7 @@ describe('template archive processor', () => { }); test('should trigger a template', async () => { - const template = await Template.fromDirectory('test/archives/latedeliveryandpenalty-typescript', {offline: true}); - const templateArchiveProcessor = new TemplateArchiveProcessor(template); - const data = { - "$class": "io.clause.latedeliveryandpenalty@0.1.0.TemplateModel", - "forceMajeure": true, - "penaltyDuration": { - "$class": "org.accordproject.time@0.3.0.Duration", - "amount": 2, - "unit": "days" - }, - "penaltyPercentage": 10.5, - "capPercentage": 55, - "termination": { - "$class": "org.accordproject.time@0.3.0.Duration", - "amount": 15, - "unit": "days" - }, - "fractionalPart": "days", - "clauseId": "c88e5ed7-c3e0-4249-a99c-ce9278684ac8", - "$identifier": "c88e5ed7-c3e0-4249-a99c-ce9278684ac8" - }; - const request = { - goodsValue: 100 - }; + const templateArchiveProcessor = await createTemplateArchiveProcessor(); // first we init the template const stateResponse = await templateArchiveProcessor.init(data); @@ -100,4 +71,27 @@ describe('template archive processor', () => { // the events should have been emitted expect(payload.events[0].penaltyCalculated).toBe(true); }); + + test.each([ + [{ state: {}, events: [] }, 'Invalid trigger result: missing result object'], + [{ result: {}, events: [] }, 'Invalid trigger result: missing state object'], + [{ result: {}, state: {}, events: {} }, 'Invalid trigger result: events must be an array'] + ])('should validate malformed trigger result %j', async (triggerResult, errorMessage) => { + const templateArchiveProcessor = await createTemplateArchiveProcessor(); + const stateResponse = await templateArchiveProcessor.init(data); + jest.spyOn(JavaScriptEvaluator.prototype, 'evalDangerously').mockResolvedValueOnce({ + result: Promise.resolve(triggerResult) + }); + + await expect(templateArchiveProcessor.trigger(data, request, stateResponse.state)).rejects.toThrow(errorMessage); + }); + + test('should validate malformed init result', async () => { + const templateArchiveProcessor = await createTemplateArchiveProcessor(); + jest.spyOn(JavaScriptEvaluator.prototype, 'evalDangerously').mockResolvedValueOnce({ + result: Promise.resolve({}) + }); + + await expect(templateArchiveProcessor.init(data)).rejects.toThrow('Invalid init result: missing state object'); + }); });