From f675fe7ab5488775616ebdfaf36b59c5cdb222b9 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Sat, 30 May 2026 16:56:42 +0530 Subject: [PATCH] fix(test): make external model fixtures hermetic Signed-off-by: Rishabh Jain --- test/TemplateArchiveProcessor.test.ts | 121 +++++++----------- test/TemplateMarkInterpreter.test.ts | 3 +- ...dproject.org.accordproject.party@0.2.0.cto | 22 ++++ .../@models.accordproject.org.money@0.3.0.cto | 34 +++++ .../@models.accordproject.org.time@0.3.0.cto | 36 ++++++ test/support/externalModels.ts | 46 +++++++ 6 files changed, 184 insertions(+), 78 deletions(-) create mode 100644 test/models/@models.accordproject.org.accordproject.party@0.2.0.cto create mode 100644 test/models/@models.accordproject.org.money@0.3.0.cto create mode 100644 test/models/@models.accordproject.org.time@0.3.0.cto create mode 100644 test/support/externalModels.ts diff --git a/test/TemplateArchiveProcessor.test.ts b/test/TemplateArchiveProcessor.test.ts index 470622f..97d3965 100644 --- a/test/TemplateArchiveProcessor.test.ts +++ b/test/TemplateArchiveProcessor.test.ts @@ -1,110 +1,77 @@ -import {Template} from '@accordproject/cicero-core'; +import { Template } from '@accordproject/cicero-core'; import { TemplateArchiveProcessor, InitResponse, TriggerResponse } from '../src/TemplateArchiveProcessor'; +import { mockExternalModelFetches } from './support/externalModels'; + +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', () => { + beforeEach(() => { + mockExternalModelFetches(); + }); + + 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 options = {}; - const result = await templateArchiveProcessor.draft(data, 'markdown', options); + const templateArchiveProcessor = await createTemplateArchiveProcessor(); + const result = await templateArchiveProcessor.draft(data, 'markdown', {}); 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: InitResponse = await templateArchiveProcessor.init(data); const payload = response.state as { count?: number }; expect(payload.count).toBe(0); }); test('should compile logic', async () => { - const template = await Template.fromDirectory('test/archives/latedeliveryandpenalty-typescript', {offline: true}); - const templateArchiveProcessor = new TemplateArchiveProcessor(template); + const templateArchiveProcessor = await createTemplateArchiveProcessor(); const compiledCode = await templateArchiveProcessor.compileLogic(); expect(compiledCode['logic/logic.ts']).toBeDefined(); expect(compiledCode['logic/logic.ts'].code).toContain('LateDeliveryAndPenalty'); }); 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); - - // then we trigger the template const response: TriggerResponse = await templateArchiveProcessor.trigger(data, request, stateResponse.state); - // we should have a result const resultPayload = response.result as { penalty?: number }; expect(resultPayload.penalty).toBe(2625); - // the state should have been updated const statePayload = response.state as { count?: number }; expect(statePayload.count).toBe(1); - // the events should have been emitted const eventPayload = response.events[0] as { penaltyCalculated?: boolean }; expect(eventPayload.penaltyCalculated).toBe(true); }); diff --git a/test/TemplateMarkInterpreter.test.ts b/test/TemplateMarkInterpreter.test.ts index 4ceccba..92ea385 100644 --- a/test/TemplateMarkInterpreter.test.ts +++ b/test/TemplateMarkInterpreter.test.ts @@ -4,6 +4,7 @@ import { TemplateMarkInterpreter } from '../src'; import { TemplateMarkTransformer } from '@accordproject/markdown-template'; import { readFileSync, readdirSync } from 'fs'; import * as path from 'path'; +import { loadOfflineExternalModels } from './support/externalModels'; const CLAUSE_LIBRARY = { 'clauses': [ @@ -70,8 +71,8 @@ describe('templatemark interpreter', () => { const data = JSON.parse(readFileSync(`${GOOD_TEMPLATES_ROOT}/${templateName}/data.json`, 'utf-8')); const modelManager = new ModelManager(); + loadOfflineExternalModels(modelManager); modelManager.addCTOModel(model, undefined, true); - await modelManager.updateExternalModels(); const engine = new TemplateMarkInterpreter(modelManager, CLAUSE_LIBRARY); const templateMarkTransformer = new TemplateMarkTransformer(); diff --git a/test/models/@models.accordproject.org.accordproject.party@0.2.0.cto b/test/models/@models.accordproject.org.accordproject.party@0.2.0.cto new file mode 100644 index 0000000..987da2b --- /dev/null +++ b/test/models/@models.accordproject.org.accordproject.party@0.2.0.cto @@ -0,0 +1,22 @@ +/* + * 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. + */ + +concerto version "^3.0.0" + +namespace org.accordproject.party@0.2.0 + +/* A party to a contract */ +participant Party identified by partyId { + o String partyId +} diff --git a/test/models/@models.accordproject.org.money@0.3.0.cto b/test/models/@models.accordproject.org.money@0.3.0.cto new file mode 100644 index 0000000..9da7fcb --- /dev/null +++ b/test/models/@models.accordproject.org.money@0.3.0.cto @@ -0,0 +1,34 @@ +/* + * 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. + */ + +concerto version "^3.0.0" + +namespace org.accordproject.money@0.3.0 + +/** + * Represents an amount of money. + */ +concept MonetaryAmount { + o Double doubleValue + o CurrencyCode currencyCode +} + +/** + * Currency codes required by the offline template fixtures. + */ +enum CurrencyCode { + o EUR + o GBP + o USD +} diff --git a/test/models/@models.accordproject.org.time@0.3.0.cto b/test/models/@models.accordproject.org.time@0.3.0.cto new file mode 100644 index 0000000..ee903b5 --- /dev/null +++ b/test/models/@models.accordproject.org.time@0.3.0.cto @@ -0,0 +1,36 @@ +/* + * 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. + */ + +concerto version "^3.0.0" + +namespace org.accordproject.time@0.3.0 + +/** + * Units for a duration. + */ +enum TemporalUnit { + o seconds + o minutes + o hours + o days + o weeks +} + +/** + * A duration. For example, 6 hours. + */ +concept Duration { + o Long amount + o TemporalUnit unit +} diff --git a/test/support/externalModels.ts b/test/support/externalModels.ts new file mode 100644 index 0000000..62b5edf --- /dev/null +++ b/test/support/externalModels.ts @@ -0,0 +1,46 @@ +import { readFileSync } from 'fs'; +import path from 'path'; +import { ModelManager } from '@accordproject/concerto-core'; + +const TEST_MODELS_DIR = path.resolve(__dirname, '..', 'models'); +const ARCHIVE_MODELS_DIR = path.resolve(__dirname, '..', 'archives', 'latedeliveryandpenalty-typescript', 'model'); + +const VENDORED_MODEL_FILES = [ + '@models.accordproject.org.money@0.3.0.cto', + '@models.accordproject.org.accordproject.party@0.2.0.cto', + '@models.accordproject.org.time@0.3.0.cto' +]; + +export function loadOfflineExternalModels(modelManager: ModelManager) { + VENDORED_MODEL_FILES.forEach((fileName) => { + const model = readFileSync(path.join(TEST_MODELS_DIR, fileName), 'utf-8'); + modelManager.addCTOModel(model, fileName); + }); +} + +export function mockExternalModelFetches() { + const originalFetch = global.fetch.bind(global); + const modelByUrl = new Map([ + ['https://models.accordproject.org/money@0.3.0.cto', readFileSync(path.join(TEST_MODELS_DIR, '@models.accordproject.org.money@0.3.0.cto'), 'utf-8')], + ['https://models.accordproject.org/accordproject/party@0.2.0.cto', readFileSync(path.join(TEST_MODELS_DIR, '@models.accordproject.org.accordproject.party@0.2.0.cto'), 'utf-8')], + ['https://models.accordproject.org/time@0.3.0.cto', readFileSync(path.join(TEST_MODELS_DIR, '@models.accordproject.org.time@0.3.0.cto'), 'utf-8')], + ['https://models.accordproject.org/accordproject/contract@0.2.0.cto', readFileSync(path.join(ARCHIVE_MODELS_DIR, '@models.accordproject.org.accordproject.contract@0.2.0.cto'), 'utf-8')], + ['https://models.accordproject.org/accordproject/runtime@0.2.0.cto', readFileSync(path.join(ARCHIVE_MODELS_DIR, '@models.accordproject.org.accordproject.runtime@0.2.0.cto'), 'utf-8')] + ]); + + return jest.spyOn(global, 'fetch').mockImplementation(async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; + const model = modelByUrl.get(url); + + if (model !== undefined) { + return new Response(model, { + status: 200, + headers: { + 'content-type': 'text/plain' + } + }); + } + + return originalFetch(input, init); + }); +}