diff --git a/.github/workflows/test-rule-scenarios.yml b/.github/workflows/test-rule-scenarios.yml new file mode 100644 index 0000000..1ea8286 --- /dev/null +++ b/.github/workflows/test-rule-scenarios.yml @@ -0,0 +1,23 @@ +name: Test Rule Scenarios + +on: + push: + branches: '*' + pull_request: + branches: '*' + +jobs: + test-rule-scenarios: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: npm install + + - name: Run Jest rule scenario tests + run: npm test src/testAllRules.spec.ts + + - name: Run Jest rule Excel scenario tests + run: npm test src/testAllRulesExcel.spec.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2818935..ce9b946 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,11 +2,9 @@ name: Test on: push: - branches: - - '*' + branches: '*' pull_request: - branches: - - '*' + branches: '*' jobs: test: diff --git a/package-lock.json b/package-lock.json index ffe9d55..d8c2ca9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@gorules/zen-engine": "^0.21.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", @@ -33,6 +34,7 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", + "node-xlsx": "^0.24.0", "prettier": "^3.0.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", @@ -1018,6 +1020,96 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@gorules/zen-engine": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@gorules/zen-engine/-/zen-engine-0.21.0.tgz", + "integrity": "sha512-aobKQTZ0YtlFcJljJOqIzOQJeQd+rrsZpVMS4GtBucnfOh3lOVBkn5Kv5+Scoizi9scm/G2jUOkK76ro/R5w7w==", + "engines": { + "node": ">= 14" + }, + "optionalDependencies": { + "@gorules/zen-engine-darwin-arm64": "0.21.0", + "@gorules/zen-engine-darwin-x64": "0.21.0", + "@gorules/zen-engine-linux-arm64-gnu": "0.21.0", + "@gorules/zen-engine-linux-x64-gnu": "0.21.0", + "@gorules/zen-engine-win32-x64-msvc": "0.21.0" + } + }, + "node_modules/@gorules/zen-engine-darwin-arm64": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@gorules/zen-engine-darwin-arm64/-/zen-engine-darwin-arm64-0.21.0.tgz", + "integrity": "sha512-binj2rpYptU3/LsO6Ww+2BIn3v0Pn+fezrYyhilYrJb8GVS6YdWK+fi8aNoQZ0DQiep9OMvOVuLjgY4G5DaSFg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 14" + } + }, + "node_modules/@gorules/zen-engine-darwin-x64": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@gorules/zen-engine-darwin-x64/-/zen-engine-darwin-x64-0.21.0.tgz", + "integrity": "sha512-BzCnLZL9d2uyZWMMcPPsX20xs3iTp2cfm6N2laElOAVcfwzrfaDFl0Mk6gyVKhnxE0222YRoW8fC+gfEqgsaBA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 14" + } + }, + "node_modules/@gorules/zen-engine-linux-arm64-gnu": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@gorules/zen-engine-linux-arm64-gnu/-/zen-engine-linux-arm64-gnu-0.21.0.tgz", + "integrity": "sha512-q+SbFoju8+m/nITiKrMo1HoT3CL+aSrVdajsZ6Fu3snzUsYDebgswKb2UWWiIPO2xy/D8E9BADb2UJsr4bDhmw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 14" + } + }, + "node_modules/@gorules/zen-engine-linux-x64-gnu": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@gorules/zen-engine-linux-x64-gnu/-/zen-engine-linux-x64-gnu-0.21.0.tgz", + "integrity": "sha512-zvWfcvB/Q8RaHZkzgbc9JcsLilhhONBilzcdqt0685fuZpZlc049JICG0/80kC1qlZT9KcDpckZXH7BweEnX2Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 14" + } + }, + "node_modules/@gorules/zen-engine-win32-x64-msvc": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@gorules/zen-engine-win32-x64-msvc/-/zen-engine-win32-x64-msvc-0.21.0.tgz", + "integrity": "sha512-mGnJmhC1KWyin7mjJ1EtpFHKlczciB4L5o+y2DmCSMZgTHVFHBxkxB/78zbWlxRoKtF+smKq/FBvrA4uqqK5nQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 14" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -6779,6 +6871,21 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/node-xlsx": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/node-xlsx/-/node-xlsx-0.24.0.tgz", + "integrity": "sha512-1olwK48XK9nXZsyH/FCltvGrQYvXXZuxVitxXXv2GIuRm51aBi1+5KwR4rWM4KeO61sFU+00913WLZTD+AcXEg==", + "dev": true, + "dependencies": { + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" + }, + "bin": { + "node-xlsx": "dist/bin/cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7885,11 +7992,11 @@ } }, "node_modules/side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -8997,6 +9104,19 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/xlsx": { + "version": "0.20.2", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz", + "integrity": "sha512-+nKZ39+nvK7Qq6i0PvWWRA4j/EkfWOtkP/YhMtupm+lJIiHxUrgTr1CcKv1nBk1rHtkRRQ3O2+Ih/q/sA+FXZA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index ea9061e..0823b8b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@gorules/zen-engine": "^0.21.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", @@ -45,6 +46,7 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", + "node-xlsx": "^0.24.0", "prettier": "^3.0.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", diff --git a/src/InputOutputTest.xlsx b/src/InputOutputTest.xlsx new file mode 100644 index 0000000..a8468cd Binary files /dev/null and b/src/InputOutputTest.xlsx differ diff --git a/src/api/decisions/decisions.controller.ts b/src/api/decisions/decisions.controller.ts new file mode 100644 index 0000000..518587f --- /dev/null +++ b/src/api/decisions/decisions.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Param, HttpException, HttpStatus } from '@nestjs/common'; +import { DecisionsService } from './decisions.service'; + +@Controller('api/decisions') +export class DecisionsController { + constructor(private readonly decisionsService: DecisionsService) {} + + @Get('/:ruleId') + async getSubmissions(@Param('ruleId') ruleId: string) { + try { + // TODO: Get inputs from request + return await this.decisionsService.runDecision({ + numberOfChildren: 2, + familyComposition: 'single', + familyUnitInPayForDecember: true, + }); + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Get('/:formId/:id') + async getSubmissionById(@Param('formId') formId: string, @Param('id') id: string) {} +} diff --git a/src/api/decisions/decisions.service.ts b/src/api/decisions/decisions.service.ts new file mode 100644 index 0000000..aab11c1 --- /dev/null +++ b/src/api/decisions/decisions.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { runDecision } from '../../services/gorules'; + +@Injectable() +export class DecisionsService { + constructor() {} + + async runDecision(inputs: object) { + return runDecision('wintersupplement', inputs); + } +} diff --git a/src/app.module.ts b/src/app.module.ts index 41845f1..532140f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,8 @@ import { MongooseModule } from '@nestjs/mongoose'; import { RuleData, RuleDataSchema } from './api/ruleData/ruleData.schema'; import { RuleDataController } from './api/ruleData/ruleData.controller'; import { RuleDataService } from './api/ruleData/ruleData.service'; +import { DecisionsController } from './api/decisions/decisions.controller'; +import { DecisionsService } from './api/decisions/decisions.service'; import { SubmissionsController } from './api/submissions/submissions.controller'; import { SubmissionsService } from './api/submissions/submissions.service'; @@ -11,9 +13,9 @@ import { SubmissionsService } from './api/submissions/submissions.service'; imports: [ ConfigModule.forRoot(), MongooseModule.forRoot(process.env.MONGODB_URL), - MongooseModule.forFeature([{ name: RuleData.name, schema: RuleDataSchema }]), // import model + MongooseModule.forFeature([{ name: RuleData.name, schema: RuleDataSchema }]), ], - controllers: [RuleDataController, SubmissionsController], - providers: [RuleDataService, SubmissionsService], + controllers: [RuleDataController, DecisionsController, SubmissionsController], + providers: [RuleDataService, DecisionsService, SubmissionsService], }) export class AppModule {} diff --git a/src/rules/wintersupplement.json b/src/rules/wintersupplement.json new file mode 100644 index 0000000..78a418a --- /dev/null +++ b/src/rules/wintersupplement.json @@ -0,0 +1,263 @@ +{ + "tests": [{ + "name": "Default test", + "input": { + "numberOfChildren": 2, + "familyComposition": "single", + "familyUnitInPayForDecember": true + }, + "output": { + "baseAmount": 60, + "childrenAmount": 40, + "isEligible": true, + "supplementAmount": 100 + } + }], + "contentType": "application/vnd.gorules.decision", + "nodes": [ + { + "id": "8ac97728-c53d-441b-8c4f-cbce96bbbfb1", + "name": "Winter Supplement Request", + "type": "inputNode", + "position": { + "x": -900, + "y": 30 + } + }, + { + "id": "86049028-7a4f-4b79-a1b1-025b98061ee0", + "name": "Winter Supplement Response", + "type": "outputNode", + "position": { + "x": 890, + "y": 150 + } + }, + { + "id": "d5e41add-1cb0-4e32-8667-ffd548e523bf", + "name": "Child Calculation", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "722b3945-02f5-4210-a9e4-b496d7d9438b", + "399837f9-5fd5-4936-8262-a47c36287cf7": "numberOfChildren*20", + "4513898e-c063-4670-a99a-804148934985": "" + } + ], + "inputs": [ + { + "id": "4513898e-c063-4670-a99a-804148934985", + "name": "Number of Children", + "type": "expression", + "field": "numberOfChildren" + } + ], + "outputs": [ + { + "id": "399837f9-5fd5-4936-8262-a47c36287cf7", + "name": "Children Amount", + "type": "expression", + "field": "childrenAmount", + "defaultValue": "0" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 120, + "y": 30 + } + }, + { + "id": "c3ab217c-22fa-4896-8f29-4a359e12f483", + "name": "Spouse Calculation", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "f3164738-59c2-4e3b-84d5-ceaf3e04aff2", + "3991b5b8-b68c-493d-a31b-226a0756ff28": "\"single\"", + "c526b490-0906-4505-aff8-d161e8feed4b": "60" + }, + { + "_id": "1a7b357d-bbd8-481b-9c24-d91686c83e67", + "3991b5b8-b68c-493d-a31b-226a0756ff28": "\"couple\"", + "c526b490-0906-4505-aff8-d161e8feed4b": "120" + } + ], + "inputs": [ + { + "id": "3991b5b8-b68c-493d-a31b-226a0756ff28", + "name": "Family Composition", + "type": "expression", + "field": "familyComposition", + "defaultValue": "single" + } + ], + "outputs": [ + { + "id": "c526b490-0906-4505-aff8-d161e8feed4b", + "name": "Base Amount", + "type": "expression", + "field": "baseAmount" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 120, + "y": -80 + } + }, + { + "id": "84cf05d5-9ef3-4e83-bb7c-2b686e6b7815", + "name": "Total Supplement", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "aa135592-8426-42ae-8d23-4512caeea78e", + "16683437-061d-4caf-804f-aefeeddc9b32": "baseAmount+childrenAmount", + "6abe1cb3-a3da-4770-abdc-25de26f96a88": "", + "dc3e0c07-45e0-4220-ba8e-f3d654be8f0a": "" + } + ], + "inputs": [ + { + "id": "6abe1cb3-a3da-4770-abdc-25de26f96a88", + "name": "Base Amount", + "type": "expression", + "field": "baseAmount", + "defaultValue": "0" + }, + { + "id": "dc3e0c07-45e0-4220-ba8e-f3d654be8f0a", + "name": "Children Amount", + "type": "expression", + "field": "childrenAmount", + "defaultValue": "0" + } + ], + "outputs": [ + { + "id": "16683437-061d-4caf-804f-aefeeddc9b32", + "name": "Supplement Amount", + "type": "expression", + "field": "supplementAmount" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 480, + "y": -20 + } + }, + { + "id": "bd7103da-9a6e-4fbd-ba14-12008e3cd61c", + "name": "Should Calculate Supplement", + "type": "switchNode", + "content": { + "statements": [ + { + "id": "38203cc4-5089-4ed7-b19c-58a99b65e545", + "condition": "isEligible" + } + ] + }, + "position": { + "x": -225, + "y": -80 + } + }, + { + "id": "7b088ca1-2314-45ec-835c-c38e66f7cb5c", + "name": "IsEligible", + "type": "expressionNode", + "content": { + "expressions": [ + { + "id": "3c63ac7c-49a8-4e3f-b23c-53e78666b29a", + "key": "isEligible", + "value": "familyUnitInPayForDecember" + } + ] + }, + "position": { + "x": -555, + "y": 150 + } + } + ], + "edges": [ + { + "id": "39711638-645a-4f35-b85d-3b619f47cc8e", + "type": "edge", + "sourceId": "bd7103da-9a6e-4fbd-ba14-12008e3cd61c", + "targetId": "d5e41add-1cb0-4e32-8667-ffd548e523bf", + "sourceHandle": "38203cc4-5089-4ed7-b19c-58a99b65e545" + }, + { + "id": "363f09ea-7a0f-4b9c-bf2c-aa00ef6925e0", + "type": "edge", + "sourceId": "bd7103da-9a6e-4fbd-ba14-12008e3cd61c", + "targetId": "c3ab217c-22fa-4896-8f29-4a359e12f483", + "sourceHandle": "38203cc4-5089-4ed7-b19c-58a99b65e545" + }, + { + "id": "d5c6c7df-dc16-47d4-b84d-e005893ee2d1", + "type": "edge", + "sourceId": "8ac97728-c53d-441b-8c4f-cbce96bbbfb1", + "targetId": "7b088ca1-2314-45ec-835c-c38e66f7cb5c" + }, + { + "id": "19ebf821-284b-4b01-99cc-5e51837d2408", + "type": "edge", + "sourceId": "7b088ca1-2314-45ec-835c-c38e66f7cb5c", + "targetId": "bd7103da-9a6e-4fbd-ba14-12008e3cd61c" + }, + { + "id": "aed0501a-8138-4cfa-b7f3-7b8eee7807c9", + "type": "edge", + "sourceId": "7b088ca1-2314-45ec-835c-c38e66f7cb5c", + "targetId": "86049028-7a4f-4b79-a1b1-025b98061ee0" + }, + { + "id": "533fcf7b-c45d-45fd-b1e9-3099217d9ded", + "type": "edge", + "sourceId": "8ac97728-c53d-441b-8c4f-cbce96bbbfb1", + "targetId": "bd7103da-9a6e-4fbd-ba14-12008e3cd61c" + }, + { + "id": "6fa54de0-abe4-4581-9ffb-54bbc1faa0f9", + "type": "edge", + "sourceId": "c3ab217c-22fa-4896-8f29-4a359e12f483", + "targetId": "86049028-7a4f-4b79-a1b1-025b98061ee0" + }, + { + "id": "4969d7e2-7417-4185-922a-a128ab998f20", + "type": "edge", + "sourceId": "d5e41add-1cb0-4e32-8667-ffd548e523bf", + "targetId": "86049028-7a4f-4b79-a1b1-025b98061ee0" + }, + { + "id": "7e990235-16d1-4220-9621-baf08c49e3b2", + "type": "edge", + "sourceId": "c3ab217c-22fa-4896-8f29-4a359e12f483", + "targetId": "84cf05d5-9ef3-4e83-bb7c-2b686e6b7815" + }, + { + "id": "793c31a0-6a07-4d94-b79b-bad5fd4e6a1c", + "type": "edge", + "sourceId": "d5e41add-1cb0-4e32-8667-ffd548e523bf", + "targetId": "84cf05d5-9ef3-4e83-bb7c-2b686e6b7815" + }, + { + "id": "8b1fc9c9-65ab-4024-8994-832f0bb32201", + "type": "edge", + "sourceId": "84cf05d5-9ef3-4e83-bb7c-2b686e6b7815", + "targetId": "86049028-7a4f-4b79-a1b1-025b98061ee0" + } + ] +} \ No newline at end of file diff --git a/src/rules/wintersupplement.tests.json b/src/rules/wintersupplement.tests.json new file mode 100644 index 0000000..8787321 --- /dev/null +++ b/src/rules/wintersupplement.tests.json @@ -0,0 +1,69 @@ +{ + "tests": [{ + "name": "Ineligible", + "input": { + "numberOfChildren": 2, + "familyComposition": "single", + "familyUnitInPayForDecember": false + }, + "output": { + "isEligible": false + } + }, + { + "name": "Single, no children", + "input": { + "numberOfChildren": 0, + "familyComposition": "single", + "familyUnitInPayForDecember": true + }, + "output": { + "baseAmount": 60, + "childrenAmount": 0, + "isEligible": true, + "supplementAmount": 60 + } + }, + { + "name": "Single, 2 children", + "input": { + "numberOfChildren": 2, + "familyComposition": "single", + "familyUnitInPayForDecember": true + }, + "output": { + "baseAmount": 60, + "childrenAmount": 40, + "isEligible": true, + "supplementAmount": 100 + } + }, + { + "name": "Couple, 1 child", + "input": { + "numberOfChildren": 1, + "familyComposition": "couple", + "familyUnitInPayForDecember": true + }, + "output": { + "baseAmount": 120, + "childrenAmount": 20, + "isEligible": true, + "supplementAmount": 140 + } + }, + { + "name": "Couple, 10 children", + "input": { + "numberOfChildren": 10, + "familyComposition": "couple", + "familyUnitInPayForDecember": true + }, + "output": { + "baseAmount": 120, + "childrenAmount": 200, + "isEligible": true, + "supplementAmount": 320 + } + }] +} \ No newline at end of file diff --git a/src/services/gorules.ts b/src/services/gorules.ts new file mode 100644 index 0000000..fa9159f --- /dev/null +++ b/src/services/gorules.ts @@ -0,0 +1,10 @@ +import { ZenEngine } from '@gorules/zen-engine'; +import { readFile } from 'fs/promises'; + +export const runDecision = async (rulename: string, inputs: object) => { + const content = await readFile('src/rules/wintersupplement.json'); + const engine = new ZenEngine(); + const decision = engine.createDecision(content); + const result = await decision.evaluate(inputs); + return result; +}; diff --git a/src/testAllRules.spec.ts b/src/testAllRules.spec.ts new file mode 100644 index 0000000..9d15f23 --- /dev/null +++ b/src/testAllRules.spec.ts @@ -0,0 +1,48 @@ +import { readdirSync, readFileSync, existsSync } from 'fs'; +import * as path from 'path'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DecisionsService } from './api/decisions/decisions.service'; + +describe('Test all rules', () => { + let service: DecisionsService; + + const directoryPath = path.join(__dirname, '/rules'); + const files = readdirSync(directoryPath); + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DecisionsService], + }).compile(); + service = module.get(DecisionsService); + }); + + files.forEach((file) => { + if (path.extname(file) === '.json' && !file.includes('.tests.json')) { + describe(`Testing ${file}`, () => { + const testFile = file.replace('.json', '.tests.json'); + const testFilePath = path.join(directoryPath, testFile); + + // Check if tests file does not exist + if (!existsSync(testFilePath)) { + console.warn(`Test file ${testFile} does not exist.`); + return; + } + + const { tests } = JSON.parse(readFileSync(testFilePath, 'utf-8')); + + tests.forEach((test: { name: string; input: object; output: object }, index) => { + // Check if test is not valid (missing name, input, or output) + if (!test.name || !test.input || !test.output) { + console.warn(`Test at index ${index} in ${testFile} is invalid: ${JSON.stringify(test)}`); + return; + } + + it(`Scenario: ${test.name}`, async () => { + const { result } = await service.runDecision(test.input); + expect(result).toEqual(test.output); + }); + }); + }); + } + }); +}); diff --git a/src/testAllRulesExcel.spec.ts b/src/testAllRulesExcel.spec.ts new file mode 100644 index 0000000..e530cdf --- /dev/null +++ b/src/testAllRulesExcel.spec.ts @@ -0,0 +1,68 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DecisionsService } from './api/decisions/decisions.service'; +import xlsx from 'node-xlsx'; + +// Helper function to parse Excel data +const parseExcelFile = (filePath: string) => { + const sheets = xlsx.parse(filePath); + return sheets.map((sheet) => { + const [header, inputOutputNames, ...rows] = sheet.data; + const nameColumnIndex = header.indexOf('NAME'); + const inputColStart = header.indexOf('INPUTS'); + const outputColStart = header.indexOf('OUTPUTS'); + const inputColEnd = outputColStart - 1; + const outputColEnd = inputOutputNames.length; + + const inputNames = inputOutputNames.slice(inputColStart, inputColEnd + 1); + const outputNames = inputOutputNames.slice(outputColStart, outputColEnd); + + return { + name: sheet.name, + nameColumnIndex, + inputNames, + outputNames, + rows, + inputColStart, + outputColStart, + }; + }); +}; + +// Load and parse the Excel file +const excelFilePath = `${__dirname}/InputOutputTest.xlsx`; +const excelSheets = parseExcelFile(excelFilePath); + +describe('Test all excel rules', () => { + let service: DecisionsService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DecisionsService], + }).compile(); + service = module.get(DecisionsService); + }); + + // Iterate over each sheet in the Excel file + excelSheets.forEach((sheet) => { + describe(`Testing ${sheet.name}`, () => { + sheet.rows.forEach((row, rowIndex) => { + const scenarioName = row[sheet.nameColumnIndex]; + + it(`Scenario: ${scenarioName}`, async () => { + const input = sheet.inputNames.reduce((acc, inputName, index) => { + acc[inputName] = row[sheet.inputColStart + index]; + return acc; + }, {}); + + const output = sheet.outputNames.reduce((acc, outputName, index) => { + acc[outputName] = row[sheet.outputColStart + index]; + return acc; + }, {}); + + const { result } = await service.runDecision(input); + expect(result).toEqual(output); + }); + }); + }); + }); +});