diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index d0bbc96..bd685b4 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -461,11 +461,13 @@ describe('ScenarioDataService', () => { inputs: { familyComposition: 'single', numberOfChildren: 2 }, outputs: { isEligible: true, baseAmount: 100 }, expectedResults: {}, + resultMatch: false, }, 'Scenario 2': { inputs: { familyComposition: 'couple', numberOfChildren: 3 }, outputs: { isEligible: false, baseAmount: 200 }, expectedResults: {}, + resultMatch: false, }, }; @@ -555,6 +557,26 @@ describe('ScenarioDataService', () => { expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); + + it('should escape inputs and outputs containing commas or quotes', async () => { + const goRulesJSONFilename = 'test.json'; + const ruleContent = { nodes: [], edges: [] }; + const ruleRunResults = { + 'Scenario 1': { + inputs: { input1: 'value, with, commas', input2: 'value "with" quotes' }, + outputs: { output1: 'result, with, commas', output2: 'result "with" quotes' }, + expectedResults: {}, + }, + }; + + jest.spyOn(service, 'runDecisionsForScenarios').mockResolvedValue(ruleRunResults); + + const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename, ruleContent); + + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: input1,Input: input2\nScenario 1,Fail,"value, with, commas",value "with" quotes`; + + expect(csvContent.trim()).toBe(expectedCsvContent.trim()); + }); }); describe('processProvidedScenarios', () => { diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index bee024c..af12f24 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -164,29 +164,43 @@ export class ScenarioDataService { newScenarios, ); - const inputKeys = extractUniqueKeys(ruleRunResults, 'inputs'); - const outputKeys = extractUniqueKeys(ruleRunResults, 'result'); - const expectedResultsKeys = extractUniqueKeys(ruleRunResults, 'expectedResults'); + const keys = { + inputs: extractUniqueKeys(ruleRunResults, 'inputs'), + expectedResults: extractUniqueKeys(ruleRunResults, 'expectedResults'), + result: extractUniqueKeys(ruleRunResults, 'result'), + }; const headers = [ 'Scenario', 'Results Match Expected (Pass/Fail)', - ...inputKeys.map((key) => `Input: ${key}`), - ...expectedResultsKeys.map((key) => `Expected Result: ${key}`), - ...outputKeys.map((key) => `Result: ${key}`), + ...this.prefixKeys(keys.inputs, 'Input'), + ...this.prefixKeys(keys.expectedResults, 'Expected Result'), + ...this.prefixKeys(keys.result, 'Result'), ]; - const rows = Object.entries(ruleRunResults).map(([scenarioName, scenarioData]) => { - const resultsMatch = scenarioData.resultMatch ? 'Pass' : 'Fail'; - const inputs = inputKeys.map((key) => scenarioData.inputs[key] ?? ''); - const outputs = outputKeys.map((key) => scenarioData.result[key] ?? ''); - const expectedResults = expectedResultsKeys.map((key) => scenarioData.expectedResults[key] ?? ''); + const rows = Object.entries(ruleRunResults).map(([scenarioName, data]) => [ + this.escapeCSVField(scenarioName), + data.resultMatch ? 'Pass' : 'Fail', + ...this.mapFields(data.inputs, keys.inputs), + ...this.mapFields(data.expectedResults, keys.expectedResults), + ...this.mapFields(data.result, keys.result), + ]); - return [scenarioName, resultsMatch, ...inputs, ...expectedResults, ...outputs]; - }); + return [headers, ...rows].map((row) => row.join(',')).join('\n'); + } + + private prefixKeys(keys: string[], prefix: string): string[] { + return keys.map((key) => `${prefix}: ${key}`); + } + + private mapFields(data: Record, keys: string[]): string[] { + return keys.map((key) => this.escapeCSVField(data[key])); + } - const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n'); - return csvContent; + private escapeCSVField(field: any): string { + if (field == null) return ''; + const stringField = typeof field === 'string' ? field : String(field); + return stringField.includes(',') ? `"${stringField.replace(/"/g, '""')}"` : stringField; } /**