Repository: authorjapps/zerocode
Pull request #667 (2024-06-09 06:34:28)

ISSUE-659 # add ability to refer CSV source value by header
# <Feature Title>

## Fixes Issue
- [x] Which issue or ticket was(will be) fixed by this PR? [ISSUE-659](https://github.com/authorjapps/zerocode/issues/659)

**[PR Branch](https://github.com/luke-zhou/zerocode/tree/master)**

## Motivation and Context
This PR is aiming to solve the [ISSUE-659](https://github.com/authorjapps/zerocode/issues/659)

## Checklist:

* [x] New Unit tests were added
  * [ ] Covered in existing Unit tests

* [x] Integration tests were added
  * [ ] Covered in existing Integration tests

* [x] Test names are meaningful

* [x] Feature manually tested and outcome is successful

* [x] PR doesn't break any of the earlier features for end users
  * [ ] WARNING! This might break one or more earlier earlier features, hence left a comment tagging all reviewrs

* [x] Branch build passed in CI

* [x] No 'package.*' in the imports

* [ ] Relevant DOcumentation page added or updated with clear instructions and examples for the end user
  * [ ] Not applicable. This was only a code refactor change, no functional or behaviourial changes were introduced

* [x] Http test added to `http-testing` module(if applicable) ?
  * [ ] Not applicable. The changes did not affect HTTP automation flow

* [ ] Kafka test added to `kafka-testing` module(if applicable) ?
  * [x] Not applicable. The changes did not affect Kafka automation flow



Patch:
diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImpl.java b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImpl.java
index cf486129..30ff1666 100644
--- a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImpl.java
+++ b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImpl.java
@@ -7,13 +7,14 @@ import com.univocity.parsers.csv.CsvParser;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.text.StringSubstitutor;
 import org.jsmart.zerocode.core.domain.ScenarioSpec;
+import org.jsmart.zerocode.core.utils.TokenUtils;
 import org.slf4j.Logger;
 
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.IntStream;
 
 import static org.jsmart.zerocode.core.constants.ZerocodeConstants.DSL_FORMAT;
 import static org.jsmart.zerocode.core.di.provider.CsvParserProvider.LINE_SEPARATOR;
@@ -115,10 +116,13 @@ public class ZeroCodeParameterizedProcessorImpl implements ZeroCodeParameterized
                 return scenario;
             }
 
-            Map<String, Object> valuesMap = new HashMap<>();
+            String[] headers = retrieveCsvHeaders(parameterizedCsvList.get(0));
+
+            paramIndex = headers == null ? paramIndex : paramIndex+1;
+
             String csvLine = parameterizedCsvList.get(paramIndex);
 
-            resolveCsvLine(valuesMap, csvLine);
+            Map<String, Object> valuesMap = resolveCsvLine(csvLine, headers);
 
             String resultantStepJson = replaceWithValues(stepJson, valuesMap);
 
@@ -129,11 +133,25 @@ public class ZeroCodeParameterizedProcessorImpl implements ZeroCodeParameterized
         }
     }
 
-    private void resolveCsvLine(Map<String, Object> valuesMap, String csvLine) {
+    private String[] retrieveCsvHeaders(String csvHeaderLine) {
+        String[] parsedHeaderLine = csvParser.parseLine(csvHeaderLine + LINE_SEPARATOR);
+        boolean hasHeader = parsedHeaderLine.length > 0 && Arrays.stream(parsedHeaderLine).allMatch(s -> s.matches("^\\|.*\\|$"));
+        return !hasHeader ? null : Arrays.stream(parsedHeaderLine).map(s -> s.substring(1,s.length()-1)).toArray(String[]::new);
+    }
+
+    private Map<String, Object> resolveCsvLine(String csvLine, String[] headers) {
+        Map<String, Object> valuesMap = new HashMap<>();
         String[] parsedLine = csvParser.parseLine(csvLine + LINE_SEPARATOR);
-        AtomicLong index = new AtomicLong(0);
-        Arrays.stream(parsedLine)
-                .forEach(thisValue -> valuesMap.put(index.getAndIncrement() + "", thisValue));
+        IntStream.range(0, parsedLine.length).forEach(i -> valuesMap.put(i + "", parsedLine[i]));
+
+        if (headers != null){
+            IntStream.range(0, headers.length).forEach(i -> {
+                if(!headers[i].contains(" ") && !headers[i].isEmpty()){
+                    valuesMap.put("PARAM."+headers[i], TokenUtils.resolveKnownTokens(parsedLine[i]).toString());
+                }
+            });
+        }
+        return valuesMap;
     }
 
     private String replaceWithValues(String stepJson, Map<String, Object> valuesMap) {
diff --git a/core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeMultiStepsScenarioRunnerImpl.java b/core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeMultiStepsScenarioRunnerImpl.java
index 93ee71ca..ca551836 100644
--- a/core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeMultiStepsScenarioRunnerImpl.java
+++ b/core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeMultiStepsScenarioRunnerImpl.java
@@ -10,9 +10,13 @@ import com.univocity.parsers.csv.CsvParser;
 import static java.util.Optional.ofNullable;
 import static org.apache.commons.collections4.CollectionUtils.isEmpty;
 import static org.jsmart.zerocode.core.constants.ZerocodeConstants.KAFKA_TOPIC;
+
+import org.jsmart.zerocode.core.domain.Parameterized;
 import org.jsmart.zerocode.core.domain.ScenarioSpec;
 import org.jsmart.zerocode.core.domain.Step;
 import org.jsmart.zerocode.core.domain.builders.ZeroCodeExecReportBuilder;
+
+import static org.jsmart.zerocode.core.di.provider.CsvParserProvider.LINE_SEPARATOR;
 import static org.jsmart.zerocode.core.domain.builders.ZeroCodeExecReportBuilder.newInstance;
 import org.jsmart.zerocode.core.domain.builders.ZeroCodeIoWriteBuilder;
 import org.jsmart.zerocode.core.engine.assertion.FieldAssertionMatcher;
@@ -30,7 +34,6 @@ import org.jsmart.zerocode.core.logbuilder.ZerocodeCorrelationshipLogger;
 import org.jsmart.zerocode.core.utils.ApiTypeUtils;
 import static org.jsmart.zerocode.core.utils.ApiTypeUtils.apiType;
 import static org.jsmart.zerocode.core.utils.RunnerUtils.getFullyQualifiedUrl;
-import static org.jsmart.zerocode.core.utils.RunnerUtils.getParameterSize;
 import static org.jsmart.zerocode.core.utils.SmartUtils.prettyPrintJson;
 import org.junit.runner.Description;
 import org.junit.runner.notification.RunNotifier;
@@ -42,6 +45,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Arrays;
 import java.util.function.BiConsumer;
 
 @Singleton
@@ -551,6 +555,24 @@ public class ZeroCodeMultiStepsScenarioRunnerImpl implements ZeroCodeMultiStepsS
         return scenarioLoopTimes;
     }
 
+    private int getParameterSize(Parameterized parameterized) {
+        if (parameterized == null) {
+            return 0;
+        }
+
+        List<Object> valueSource = parameterized.getValueSource();
+        List<String> csvSource = parameterized.getCsvSource();
+        int csvSourceSize = 0;
+
+        if (csvSource != null && !csvSource.isEmpty()){
+            String[] parsedHeaderLine = csvParser.parseLine(csvSource.get(0) + LINE_SEPARATOR);
+            boolean hasHeader = parsedHeaderLine.length > 0 && Arrays.stream(parsedHeaderLine).allMatch(s -> s.matches("^\\|.*\\|$"));
+            csvSourceSize = hasHeader ? csvSource.size() -1 : csvSource.size();
+        }
+
+        return valueSource != null ? valueSource.size() : csvSourceSize;
+    }
+
     private List<FieldAssertionMatcher> compareStepResults(Step thisStep, String actualResult, String expectedResult, String resolvedScenarioState) {
         List<FieldAssertionMatcher> failureResults = new ArrayList<>();
 
diff --git a/core/src/main/java/org/jsmart/zerocode/core/utils/RunnerUtils.java b/core/src/main/java/org/jsmart/zerocode/core/utils/RunnerUtils.java
index 9465182b..9c0516f8 100644
--- a/core/src/main/java/org/jsmart/zerocode/core/utils/RunnerUtils.java
+++ b/core/src/main/java/org/jsmart/zerocode/core/utils/RunnerUtils.java
@@ -120,18 +120,6 @@ public class RunnerUtils {
         return stepLoopTimes > 0 ? stepLoopTimes: MIN_COUNT;
     }
 
-    public static int getParameterSize(Parameterized parameterized) {
-        if (parameterized == null) {
-            return 0;
-        }
-
-        List<Object> valueSource = parameterized.getValueSource();
-        List<String> csvSource = parameterized.getCsvSource();
-
-        return valueSource != null ? valueSource.size() :
-                (csvSource != null ? csvSource.size() : 0);
-    }
-
     public static void handleTestCompleted(RunListener reportListener, Logger logger) {
         if (CHARTS_AND_CSV.equals(getProperty(ZEROCODE_JUNIT))) {
             /**


Test patch:
diff --git a/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImplTest.java b/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImplTest.java
index 3bdc973f..6f2e03a8 100644
--- a/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImplTest.java
+++ b/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImplTest.java
@@ -1,10 +1,12 @@
 package org.jsmart.zerocode.core.engine.preprocessor;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.univocity.parsers.csv.CsvParser;
 import jakarta.inject.Inject;
 import org.jsmart.zerocode.core.di.main.ApplicationMainModule;
 import org.jsmart.zerocode.core.domain.ScenarioSpec;
+import org.jsmart.zerocode.core.domain.Step;
 import org.jsmart.zerocode.core.utils.SmartUtils;
 import org.jukito.JukitoRunner;
 import org.jukito.TestModule;
@@ -75,7 +77,7 @@ public class ZeroCodeParameterizedProcessorImplTest {
     @Test
     public void testProcessParameterized_csv() throws Exception {
         String jsonDocumentAsString = smartUtils
-                .getJsonDocumentAsString("unit_test_files/engine_unit_test_jsons/11_scenario_parameterized_csv.json");
+                .getJsonDocumentAsString("unit_test_files/engine_unit_test_jsons/11.1_scenario_parameterized_csv.json");
         ScenarioSpec scenarioSpec = mapper.readValue(jsonDocumentAsString, ScenarioSpec.class);
 
         ScenarioSpec scenarioSpecResolved = parameterizedProcessor.resolveParameterized(scenarioSpec, 0);
@@ -87,4 +89,23 @@ public class ZeroCodeParameterizedProcessorImplTest {
         assertThat(scenarioSpecResolved.getSteps().get(0).getAssertions().get("status").asInt(), is(400));
 
     }
+
+    @Test
+    public void testProcessParameterized_csv_with_named_random() throws Exception {
+        String jsonDocumentAsString = smartUtils
+                .getJsonDocumentAsString("unit_test_files/engine_unit_test_jsons/11.2_scenario_parameterized_csv_with_named_random.json");
+        ScenarioSpec scenarioSpec = mapper.readValue(jsonDocumentAsString, ScenarioSpec.class);
+
+        ScenarioSpec scenarioSpecResolved = parameterizedProcessor.resolveParameterized(scenarioSpec, 0);
+        Step step = scenarioSpecResolved.getSteps().get(0);
+        assertThat(step.getUrl(), is("/anUrl/${RANDOM.NUMBER}/${RANDOM.NUMBER}"));
+        JsonNode queryParams = step.getRequest().get("queryParams");
+        assertThat(queryParams.get("id1"), is(queryParams.get("id2")));
+        assertThat(queryParams.get("addressId1"), is(queryParams.get("addressId2")));
+        assertThat(scenarioSpecResolved.getSteps().get(0).getAssertions().get("status").asInt(), is(200));
+
+        scenarioSpecResolved = parameterizedProcessor.resolveParameterized(scenarioSpec, 1);
+        assertThat(scenarioSpecResolved.getSteps().get(0).getUrl(), is("/anUrl/11/22"));
+        assertThat(scenarioSpecResolved.getSteps().get(0).getAssertions().get("status").asInt(), is(400));
+    }
 }
\ No newline at end of file
diff --git a/core/src/test/java/org/jsmart/zerocode/core/utils/SmartUtilsTest.java b/core/src/test/java/org/jsmart/zerocode/core/utils/SmartUtilsTest.java
index d8e22b66..247d04ef 100644
--- a/core/src/test/java/org/jsmart/zerocode/core/utils/SmartUtilsTest.java
+++ b/core/src/test/java/org/jsmart/zerocode/core/utils/SmartUtilsTest.java
@@ -76,7 +76,7 @@ public class SmartUtilsTest {
     @Test
     public void willReadAllfileNamesFrom_TestResource() {
         List<String> allTestCaseFiles = SmartUtils.getAllEndPointFiles("unit_test_files/engine_unit_test_jsons");
-        assertThat(allTestCaseFiles.size(), is(22));
+        assertThat(allTestCaseFiles.size(), is(23));
         assertThat(allTestCaseFiles.get(0), is("unit_test_files/engine_unit_test_jsons/00_test_json_single_step_verifications.json"));
     }
 
diff --git a/core/src/test/java/org/jsmart/zerocode/parameterized/ParameterisedCsvDemoTest.java b/core/src/test/java/org/jsmart/zerocode/parameterized/ParameterisedCsvDemoTest.java
index 8f36a1d4..bf97a093 100644
--- a/core/src/test/java/org/jsmart/zerocode/parameterized/ParameterisedCsvDemoTest.java
+++ b/core/src/test/java/org/jsmart/zerocode/parameterized/ParameterisedCsvDemoTest.java
@@ -14,4 +14,9 @@ public class ParameterisedCsvDemoTest {
     @JsonTestCase("integration_test_files/parameterized/parameterized_sample_csv_test.json")
     public void testParameterizedCsv() throws Exception {
     }
+
+    @Test
+    @JsonTestCase("integration_test_files/parameterized/parameterized_sample_csv_with_headers_test.json")
+    public void testParameterizedCsvWithHeaders() throws Exception {
+    }
 }
diff --git a/core/src/test/resources/integration_test_files/parameterized/parameterized_sample_csv_with_headers_test.json b/core/src/test/resources/integration_test_files/parameterized/parameterized_sample_csv_with_headers_test.json
new file mode 100644
index 00000000..2b1eff2a
--- /dev/null
+++ b/core/src/test/resources/integration_test_files/parameterized/parameterized_sample_csv_with_headers_test.json
@@ -0,0 +1,29 @@
+{
+    "scenarioName": "Parameterized test scenario demo",
+    "steps": [
+        {
+            "name": "get_user",
+            "url": "",
+            "operation": "",
+            "request": {
+                "status": "${2}",
+                "body": {
+                    "login": "octocat-${PARAM.id}-${PARAM.AddressId}"
+                }
+            },
+            "assertions": {
+                "status": "${$.get_user.response.status}",
+                "body": {
+                    "login": "octocat-${PARAM.id}-${PARAM.AddressId}"
+                }
+            }
+        }
+    ],
+    "parameterized": {
+        "csvSource":[
+            "|id|,              |AddressId|,        |status|",
+            "${RANDOM.NUMBER},  ${RANDOM.NUMBER},   200",
+            "11,                22,                 400"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/resources/unit_test_files/engine_unit_test_jsons/11_scenario_parameterized_csv.json b/core/src/test/resources/unit_test_files/engine_unit_test_jsons/11.1_scenario_parameterized_csv.json
similarity index 100%
rename from core/src/test/resources/unit_test_files/engine_unit_test_jsons/11_scenario_parameterized_csv.json
rename to core/src/test/resources/unit_test_files/engine_unit_test_jsons/11.1_scenario_parameterized_csv.json
diff --git a/core/src/test/resources/unit_test_files/engine_unit_test_jsons/11.2_scenario_parameterized_csv_with_named_random.json b/core/src/test/resources/unit_test_files/engine_unit_test_jsons/11.2_scenario_parameterized_csv_with_named_random.json
new file mode 100644
index 00000000..7ec92490
--- /dev/null
+++ b/core/src/test/resources/unit_test_files/engine_unit_test_jsons/11.2_scenario_parameterized_csv_with_named_random.json
@@ -0,0 +1,29 @@
+{
+    "scenarioName": "For deserialize only",
+    "ignoreStepFailures": true,
+    "steps": [
+        {
+            "name": "step1",
+            "url": "/anUrl/${0}/${1}",
+            "operation": "GET",
+            "request": {
+                "queryParams": {
+                    "id1": "${PARAM.id}",
+                    "id2": "${PARAM.id}",
+                    "addressId1": "${PARAM.AddressId}",
+                    "addressId2": "${PARAM.AddressId}"
+                }
+            },
+            "assertions": {
+                "status": "${PARAM.status}"
+            }
+        }
+    ],
+    "parameterized": {
+        "csvSource": [
+            "|id|,                  |AddressId|,              |status|",
+            "${RANDOM.NUMBER},      ${RANDOM.NUMBER},           200",
+            "11,                    22,                         400"
+        ]
+    }
+}
diff --git a/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldnamedcsvparam/HelloWorldNamedCsvParamsTest.java b/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldnamedcsvparam/HelloWorldNamedCsvParamsTest.java
new file mode 100644
index 00000000..e670a25d
--- /dev/null
+++ b/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldnamedcsvparam/HelloWorldNamedCsvParamsTest.java
@@ -0,0 +1,18 @@
+package org.jsmart.zerocode.testhelp.tests.helloworldnamedcsvparam;
+
+import org.jsmart.zerocode.core.domain.Scenario;
+import org.jsmart.zerocode.core.domain.TargetEnv;
+import org.jsmart.zerocode.core.runner.ZeroCodeUnitRunner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@TargetEnv("github_host.properties")
+@RunWith(ZeroCodeUnitRunner.class)
+public class HelloWorldNamedCsvParamsTest {
+
+    @Test
+    @Scenario("parameterized_csv/hello_world_test_named_parameterized.json")
+    public void testNamedParameterized() throws Exception {
+    }
+
+}
diff --git a/http-testing/src/test/resources/parameterized_csv/hello_world_test_named_parameterized.json b/http-testing/src/test/resources/parameterized_csv/hello_world_test_named_parameterized.json
new file mode 100644
index 00000000..cb25c51e
--- /dev/null
+++ b/http-testing/src/test/resources/parameterized_csv/hello_world_test_named_parameterized.json
@@ -0,0 +1,30 @@
+{
+    "scenarioName": "Named Parameterized Scenario for ---> ${0}",
+    "steps": [
+        {
+            "name": "get_user_details",
+            "url": "/users/${PARAM.USERID}",
+            "method": "GET",
+            "request": {
+            },
+            "assertions": {
+                "status": 200,
+                "body": {
+                    "login" : "${PARAM.USERID}",
+                    "type" : "User",
+                    "name" : "${PARAM.USERNAME}",
+                    "location" : "${PARAM.ADDRESS}",
+                    "id" : "(int)${PARAM.USERNO}",
+                    "site_admin" : "(boolean)${PARAM.ISADMIN}"
+                }
+            }
+        }
+    ],
+    "parameterized": {
+        "csvSource":[
+            "|USERID|,     |USERNAME|,  |ADDRESS|,     |USERNO|,        |ISADMIN|",
+            "octocat,      The Octocat, San Francisco, 583231,          false",
+            "siddhagalaxy, Sidd,        UK,            33847730,        false"
+        ]
+    }
+}