From f68b27b23e1bedd6f0026c4727edef4cbc25c742 Mon Sep 17 00:00:00 2001 From: nchandra Date: Thu, 22 Oct 2026 22:57:19 +0100 Subject: [PATCH 1/3] stepFiles with PARAM (parameters section) --- .gitignore | 3 +- .../org/jsmart/zerocode/core/domain/Step.java | 28 +++ .../StepFileParametersResolver.java | 162 ++++++++++++++ .../ZeroCodeExternalFileProcessor.java | 9 + .../ZeroCodeExternalFileProcessorImpl.java | 26 +++ .../engine/tokens/ZeroCodeValueTokens.java | 1 + .../ZeroCodeMultiStepsScenarioRunnerImpl.java | 20 +- .../StepFileParametersResolverTest.java | 200 ++++++++++++++++++ ...ZeroCodeExternalFileProcessorImplTest.java | 68 +++++- .../StepFilesParametersTest.java | 18 ++ .../multi_step/param_echo_steps.json | 21 ++ .../step_files_parameters_test.json | 49 +++++ .../step_file_parameters/login_step.json | 14 ++ .../step_file_parameters/no_param_steps.json | 11 + .../step_file_parameters/user_steps.json | 31 +++ http-testing-examples/pom.xml | 1 + .../HelloWorldStepFileParametersTest.java | 39 ++++ .../common/create_and_verify_user.json | 29 +++ ..._users_with_step_file_parameters_test.json | 35 +++ .../localhost_REST_fake_end_points_stubs.json | 58 +++++ 20 files changed, 818 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolver.java create mode 100644 core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolverTest.java create mode 100644 core/src/test/java/org/jsmart/zerocode/integrationtests/StepFilesParametersTest.java create mode 100644 core/src/test/resources/integration_test_files/multi_step/param_echo_steps.json create mode 100644 core/src/test/resources/integration_test_files/multi_step/step_files_parameters_test.json create mode 100644 core/src/test/resources/unit_test_files/step_file_parameters/login_step.json create mode 100644 core/src/test/resources/unit_test_files/step_file_parameters/no_param_steps.json create mode 100644 core/src/test/resources/unit_test_files/step_file_parameters/user_steps.json create mode 100644 http-testing-examples/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldstepfileparameters/HelloWorldStepFileParametersTest.java create mode 100644 http-testing-examples/src/test/resources/helloworld_step_file_parameters/common/create_and_verify_user.json create mode 100644 http-testing-examples/src/test/resources/helloworld_step_file_parameters/create_users_with_step_file_parameters_test.json diff --git a/.gitignore b/.gitignore index b4e7d865c..40dd365ca 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ package.properties .settings */target -.vscode \ No newline at end of file +.vscode +/jira/ diff --git a/core/src/main/java/org/jsmart/zerocode/core/domain/Step.java b/core/src/main/java/org/jsmart/zerocode/core/domain/Step.java index a95dffb9b..7559407db 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/domain/Step.java +++ b/core/src/main/java/org/jsmart/zerocode/core/domain/Step.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; @@ -33,10 +34,16 @@ public class Step { @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) private List stepFiles; + private JsonNode parameters; private List parameterized; private List parameterizedCsv; private String customLog; + // Not part of the DSL: + // set only on steps read from a stepFile/stepFiles, to show where they came from + @JsonIgnore + private String stepFileParent; + public Integer getLoop() { return loop; } @@ -109,6 +116,14 @@ public void setStepFile(JsonNode stepFile) { this.stepFile = stepFile; } + public JsonNode getParameters() { + return parameters; + } + + public void setParameters(JsonNode parameters) { + this.parameters = parameters; + } + public List getParameterized() { return parameterized; @@ -128,6 +143,18 @@ public void setParameterizedCsv(List parameterizedCsv) { public String getCustomLog(){ return customLog; } + /** + * @return the "name (id: ...)" of the step that referenced the stepFile/stepFiles this step came from, + * or null when this step is written directly in the scenario + */ + public String getStepFileParent() { + return stepFileParent; + } + + public void setStepFileParent(String stepFileParent) { + this.stepFileParent = stepFileParent; + } + public void setCustomLog(String customLog) { this.customLog = customLog; } public boolean getIgnoreStep() { @@ -181,6 +208,7 @@ public String toString() { ", id='" + id + '\'' + ", stepFile=" + stepFile + ", stepFiles=" + stepFiles + + ", parameters=" + parameters + ", parameterized=" + parameterized + ", customLog=" + customLog + '}'; diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolver.java b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolver.java new file mode 100644 index 000000000..fa7c63993 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolver.java @@ -0,0 +1,162 @@ +package org.jsmart.zerocode.core.engine.preprocessor; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.jsmart.zerocode.core.domain.Step; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.stream.Collectors.joining; +import static org.apache.commons.lang3.StringUtils.removeStart; +import static org.jsmart.zerocode.core.engine.tokens.ZeroCodeValueTokens.JSON_PAYLOAD_FILE; +import static org.jsmart.zerocode.core.engine.tokens.ZeroCodeValueTokens.STEP_FILE_PARAM; +import static org.jsmart.zerocode.core.engine.tokens.ZeroCodeValueTokens.YAML_PAYLOAD_FILE; +import static org.jsmart.zerocode.core.utils.SmartUtils.getJsonFilePhToken; + +/** + * Replaces ${PARAM.name} (or {{PARAM.name}}) tokens inside an inlined "stepFile"/"stepFiles" with the + * values from the "parameters" of the referencing step. + * + * Runs before the usual token resolution, so a parameter value may itself carry tokens such as + * ${RANDOM.NUMBER} or ${$.earlier_step.response.body.id}, which are then resolved as normal. + * The parameters apply only to the steps of that one reference and do not leak into the scenario. + */ +public class StepFileParametersResolver { + private static final String QUOTED_PARAM = Pattern.quote(STEP_FILE_PARAM); + private static final Pattern PARAM_TOKEN_PATTERN = Pattern.compile( + "\\$\\{" + QUOTED_PARAM + "([^}]+)\\}|\\{\\{" + QUOTED_PARAM + "([^}]+)\\}\\}"); + private static final String STEP_FILE = "stepFile"; + private static final String STEP_FILES = "stepFiles"; + private static final String INLINE_SOURCE = ""; + + private final ObjectMapper objectMapper; + + public StepFileParametersResolver(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Fills in the ${PARAM.x} values of the step file content. + * + * @param rawStep the step before the file was read, used to name the file in error messages + * @param inlinedStep the step after the file was read + * @return the step with its parameters filled in, or inlinedStep as-is if it has no step file + */ + public Step resolve(Step rawStep, Step inlinedStep) { + final JsonNode parameters = inlinedStep.getParameters(); + final boolean hasParameters = parameters != null && !parameters.isNull(); + + if (!hasStepFileContent(inlinedStep)) { + failIfParametersPresent(hasParameters, inlinedStep); + return inlinedStep; + } + if (hasParameters && !parameters.isObject()) { + throw new RuntimeException(describeSource(rawStep, inlinedStep.getId()) + + " has \"parameters\" that is not a JSON object: " + parameters); + } + + final ParameterContext context = new ParameterContext( + hasParameters ? parameters : JsonNodeFactory.instance.objectNode(), + describeSource(rawStep, inlinedStep.getId())); + + final ObjectNode stepNode = objectMapper.valueToTree(inlinedStep); + final String stepFileField = inlinedStep.getStepFile() != null ? STEP_FILE : STEP_FILES; + stepNode.set(stepFileField, substitute(stepNode.get(stepFileField), context)); + + return toStep(stepNode, inlinedStep.getId()); + } + + private boolean hasStepFileContent(Step step) { + final List stepFiles = step.getStepFiles(); + return step.getStepFile() != null || (stepFiles != null && !stepFiles.isEmpty()); + } + + private void failIfParametersPresent(boolean hasParameters, Step step) { + if (!hasParameters) { + return; + } + throw new RuntimeException("Step (name: " + step.getName() + ", id: " + step.getId() + + ") has \"parameters\", but \"parameters\" is only supported together with \"stepFile\" or \"stepFiles\""); + } + + private JsonNode substitute(JsonNode node, ParameterContext context) { + if (node.isTextual()) { + return TextNode.valueOf(replaceParamTokens(node.asText(), context)); + } + if (node.isArray()) { + final ArrayNode resolvedArray = JsonNodeFactory.instance.arrayNode(); + node.forEach(element -> resolvedArray.add(substitute(element, context))); + return resolvedArray; + } + if (node.isObject()) { + final ObjectNode resolvedObject = JsonNodeFactory.instance.objectNode(); + node.fields().forEachRemaining(field -> resolvedObject.set(field.getKey(), substitute(field.getValue(), context))); + return resolvedObject; + } + return node; + } + + private String replaceParamTokens(String text, ParameterContext context) { + final Matcher matcher = PARAM_TOKEN_PATTERN.matcher(text); + final StringBuffer resolved = new StringBuffer(); + while (matcher.find()) { + final String paramName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); + final String paramValue = context.valueOf(paramName, matcher.group(0)); + matcher.appendReplacement(resolved, Matcher.quoteReplacement(paramValue)); + } + matcher.appendTail(resolved); + return resolved.toString(); + } + + private Step toStep(ObjectNode stepNode, String stepId) { + try { + return objectMapper.treeToValue(stepNode, Step.class); + } catch (JsonProcessingException exx) { + throw new RuntimeException("Failed to apply \"parameters\" to step file content of step id: " + stepId, exx); + } + } + + private String describeSource(Step rawStep, String stepId) { + final String sourceFiles = rawStep.getStepFile() != null + ? STEP_FILE + " '" + fileNameOf(rawStep.getStepFile()) + "'" + : STEP_FILES + " '" + rawStep.getStepFiles().stream().map(this::fileNameOf).collect(joining(", ")) + "'"; + return sourceFiles + " (id: " + stepId + ")"; + } + + private String fileNameOf(JsonNode fileReference) { + if (!fileReference.isTextual()) { + return INLINE_SOURCE; + } + final String token = getJsonFilePhToken(fileReference.asText()); + if (token == null) { + return fileReference.asText(); + } + return removeStart(removeStart(token, JSON_PAYLOAD_FILE), YAML_PAYLOAD_FILE); + } + + private static class ParameterContext { + private final JsonNode parameters; + private final String sourceDescription; + + ParameterContext(JsonNode parameters, String sourceDescription) { + this.parameters = parameters; + this.sourceDescription = sourceDescription; + } + + String valueOf(String paramName, String paramToken) { + final JsonNode value = parameters.get(paramName); + if (value == null) { + throw new RuntimeException(sourceDescription + " uses " + paramToken + + ", but '" + paramName + "' is not in \"parameters\""); + } + return value.isValueNode() ? value.asText() : value.toString(); + } + } +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessor.java b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessor.java index 2775b401a..bd025977c 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessor.java +++ b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessor.java @@ -8,5 +8,14 @@ public interface ZeroCodeExternalFileProcessor { Step resolveExtJsonFile(Step thisStep); + /** + * Replaces the ${PARAM.name} tokens of an inlined stepFile/stepFiles with the step's "parameters". + * + * @param rawStep the step as written in the scenario, before the external file was inlined + * @param inlinedStep the step after {@link #resolveExtJsonFile(Step)} + * @return the step with its step file parameters resolved + */ + Step resolveStepFileParameters(Step rawStep, Step inlinedStep); + List createFromStepFile(Step thisStep, String stepId); } diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessorImpl.java b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessorImpl.java index 04cb9b1a6..ffe30a54d 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessorImpl.java +++ b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessorImpl.java @@ -46,9 +46,12 @@ @Singleton public class ZeroCodeExternalFileProcessorImpl implements ZeroCodeExternalFileProcessor { private static final Logger LOGGER = getLogger(ZeroCodeExternalFileProcessorImpl.class); + private static final String UNNAMED_STEP_FILE_PARENT = "(stepFile)"; private final ObjectMapper objectMapper; + private final StepFileParametersResolver stepFileParametersResolver; + @Inject @Named("YamlMapper") private ObjectMapper yamlMapper; @@ -56,6 +59,7 @@ public class ZeroCodeExternalFileProcessorImpl implements ZeroCodeExternalFilePr @Inject public ZeroCodeExternalFileProcessorImpl(ObjectMapper objectMapper) { this.objectMapper = objectMapper; + this.stepFileParametersResolver = new StepFileParametersResolver(objectMapper); } /** @@ -97,6 +101,11 @@ public Step resolveExtJsonFile(Step thisStep) { } + @Override + public Step resolveStepFileParameters(Step rawStep, Step inlinedStep) { + return stepFileParametersResolver.resolve(rawStep, inlinedStep); + } + @Override public List createFromStepFile(Step thisStep, String stepId) { List thisSteps = new ArrayList<>(); @@ -118,9 +127,26 @@ public List createFromStepFile(Step thisStep, String stepId) { throw new RuntimeException(e); } } + final String stepFileParent = describeStepFileParent(thisStep); + thisSteps.forEach(innerStep -> innerStep.setStepFileParent(stepFileParent)); return thisSteps; } + private String describeStepFileParent(Step parentStep) { + final String name = parentStep.getName(); + final String id = parentStep.getId(); + if (name == null && id == null) { + return UNNAMED_STEP_FILE_PARENT; + } + if (name == null) { + return "(id: " + id + ")"; + } + if (id == null) { + return name; + } + return name + " (id: " + id + ")"; + } + /** * Digs deep into the nested map and looks for external file reference,if found, replaces the place holder with * the file content. This is handy when the engineers wants to drive the common contents from a central place. diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/tokens/ZeroCodeValueTokens.java b/core/src/main/java/org/jsmart/zerocode/core/engine/tokens/ZeroCodeValueTokens.java index 0d04d354d..72fc828c8 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/engine/tokens/ZeroCodeValueTokens.java +++ b/core/src/main/java/org/jsmart/zerocode/core/engine/tokens/ZeroCodeValueTokens.java @@ -34,6 +34,7 @@ public class ZeroCodeValueTokens { public static final String ABS_PATH = "ABS.PATH:"; public static final String JSON_CONTENT = "JSON.CONTENT:"; public static final String URL_ENCODED = "URLENCODED:"; + public static final String STEP_FILE_PARAM = "PARAM."; public static Map globalTokenCache = new HashMap<>(); 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 e6d89e012..d326dcb49 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 @@ -182,7 +182,9 @@ private Boolean executeRetryWithSteps(RunNotifier notifier, Description description, ScenarioExecutionState scenarioExecutionState, ScenarioSpec scenario, Step thisStep) { + final Step rawStep = thisStep; thisStep = extFileProcessor.resolveExtJsonFile(thisStep); + thisStep = extFileProcessor.resolveStepFileParameters(rawStep, thisStep); thisStep = zeroCodeAssertionsProcessor.resolveJsonContent(thisStep, scenarioExecutionState); List thisSteps = extFileProcessor.createFromStepFile(thisStep, thisStep.getId()); @@ -233,12 +235,12 @@ private Boolean executeRetry(RunNotifier notifier, retryTillSuccess = true; } - String thisStepName = thisStep.getName(); + final String thisStepName = displayNameOf(thisStep); for (int retryCounter = 0; retryCounter < retryMaxTimes; retryCounter++) { if(retryCounter > 0){ LOGGER.warn("\n\n------------>Retrying...[step][attempt-{}][executions-{}]:'{}' -> '{}'", - retryCounter, (retryCounter+1), scenario.getScenarioName(), thisStep.getName()); + retryCounter, (retryCounter+1), scenario.getScenarioName(), thisStepName); } try { if (retryCounter > 0 && !isEmpty(thisStep.getRetry().getWithSteps())) { @@ -406,6 +408,18 @@ private Boolean executeRetry(RunNotifier notifier, return null; } + /** + * Name to show in logs and reports. A step from a stepFile/stepFiles is shown with the step + * that used it, e.g. "create_admin_reuse (id: create_admin) -> get_user". + * The step's own name stays the same, so ${$.get_user...} still works. + */ + private String displayNameOf(Step thisStep) { + if (thisStep.getStepFileParent() == null) { + return thisStep.getName(); + } + return thisStep.getStepFileParent() + " -> " + thisStep.getName(); + } + private String expectedValidatorsAsJson(Step thisStep) throws JsonProcessingException { if(thisStep.getValidators() == null){ return "No validators were found for this step"; @@ -421,7 +435,7 @@ private String executeApi(String logPrefixRelationshipId, String url = thisStep.getUrl(); String operationName = thisStep.getOperation(); String stepId = thisStep.getId(); - String thisStepName = thisStep.getName(); + String thisStepName = displayNameOf(thisStep); // -------------------------------- // Resolve the URL patterns if any diff --git a/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolverTest.java b/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolverTest.java new file mode 100644 index 000000000..47049473c --- /dev/null +++ b/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolverTest.java @@ -0,0 +1,200 @@ +package org.jsmart.zerocode.core.engine.preprocessor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jsmart.zerocode.core.di.provider.ObjectMapperProvider; +import org.jsmart.zerocode.core.domain.Step; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +import static com.jayway.jsonpath.JsonPath.read; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; + +public class StepFileParametersResolverTest { + private static final String STEP_FILES_PATH = "unit_test_files/step_file_parameters/user_steps.json"; + private static final String STEP_FILE_PATH = "unit_test_files/step_file_parameters/login_step.json"; + private static final String NO_PARAM_STEP_FILES_PATH = "unit_test_files/step_file_parameters/no_param_steps.json"; + + private final ObjectMapper objectMapper = new ObjectMapperProvider().get(); + private final ZeroCodeExternalFileProcessorImpl externalFileProcessor = new ZeroCodeExternalFileProcessorImpl(objectMapper); + + @Test + public void testResolveStepFilesWillReplaceParametersInUrlRequestAndAssertions() throws IOException { + final Step rawStep = stepFilesStep(STEP_FILES_PATH, + "{\"username\": \"alice\", \"role\": \"ADMIN\", \"retries\": 3, \"active\": true}"); + + final String innerStepsJson = resolveInnerStepsJson(rawStep); + + assertThat(read(innerStepsJson, "$[0].request.body.username"), is("alice")); + assertThat(read(innerStepsJson, "$[0].request.body.role"), is("ADMIN")); + assertThat(read(innerStepsJson, "$[1].url"), is("/api/v1/users/alice")); + assertThat(read(innerStepsJson, "$[1].assertions.body.username"), is("alice")); + } + + @Test + public void testResolveDoubleCurlyBraceTokenWillReplaceParameter() throws IOException { + final Step rawStep = stepFilesStep(STEP_FILES_PATH, + "{\"username\": \"alice\", \"role\": \"ADMIN\", \"retries\": 3, \"active\": true}"); + + final String innerStepsJson = resolveInnerStepsJson(rawStep); + + assertThat(read(innerStepsJson, "$[1].assertions.body.role"), is("ADMIN")); + } + + @Test + public void testResolveNumberAndBooleanParametersWillGetTextForTypeCasts() throws IOException { + final Step rawStep = stepFilesStep(STEP_FILES_PATH, + "{\"username\": \"alice\", \"role\": \"ADMIN\", \"retries\": 3, \"active\": true}"); + + final String innerStepsJson = resolveInnerStepsJson(rawStep); + + assertThat(read(innerStepsJson, "$[0].request.body.retries"), is("(int)3")); + assertThat(read(innerStepsJson, "$[0].request.body.active"), is("(boolean)true")); + } + + @Test + public void testResolveSingleStepFileWillReplaceParameters() throws IOException { + final Step rawStep = parseStep("{\"id\": \"login_as_admin\", " + + "\"stepFile\": \"${JSON.FILE:" + STEP_FILE_PATH + "}\", " + + "\"parameters\": {\"username\": \"alice\", \"password\": \"secret\"}}"); + + final Step resolvedStep = resolve(rawStep); + final String innerStepJson = objectMapper.writeValueAsString(resolvedStep.getStepFile()); + + assertThat(read(innerStepJson, "$.request.body.username"), is("alice")); + assertThat(read(innerStepJson, "$.request.body.password"), is("secret")); + } + + @Test + public void testResolveParameterValueWithOtherTokensWillKeepThemForLaterResolution() throws IOException { + final Step rawStep = stepFilesStep(STEP_FILES_PATH, + "{\"username\": \"user_${RANDOM.NUMBER}\", \"role\": \"${$.signup.response.body.role}\", " + + "\"retries\": 3, \"active\": true}"); + + final String innerStepsJson = resolveInnerStepsJson(rawStep); + + assertThat(read(innerStepsJson, "$[0].request.body.username"), is("user_${RANDOM.NUMBER}")); + assertThat(read(innerStepsJson, "$[0].request.body.role"), is("${$.signup.response.body.role}")); + } + + @Test + public void testResolveStepFilesWillKeepIdAndParametersOnReferencingStep() throws IOException { + final Step rawStep = stepFilesStep(STEP_FILES_PATH, + "{\"username\": \"alice\", \"role\": \"ADMIN\", \"retries\": 3, \"active\": true}"); + + final Step resolvedStep = resolve(rawStep); + + assertThat(resolvedStep.getId(), is("create_admin")); + assertThat(resolvedStep.getParameters().get("username").asText(), is("alice")); + } + + @Test + public void testResolveWithMissingParameterWillFailWithClearMessage() throws IOException { + final Step rawStep = stepFilesStep(STEP_FILES_PATH, + "{\"username\": \"alice\", \"retries\": 3, \"active\": true}"); + + final RuntimeException exception = assertThrows(RuntimeException.class, () -> resolve(rawStep)); + + assertThat(exception.getMessage(), is("stepFiles '" + STEP_FILES_PATH + "' (id: create_admin) " + + "uses ${PARAM.role}, but 'role' is not in \"parameters\"")); + } + + @Test + public void testResolveStepFileWithoutParametersWillFailForUsedParameter() throws IOException { + final Step rawStep = parseStep("{\"id\": \"login_as_admin\", " + + "\"stepFile\": \"${JSON.FILE:" + STEP_FILE_PATH + "}\"}"); + + final RuntimeException exception = assertThrows(RuntimeException.class, () -> resolve(rawStep)); + + assertThat(exception.getMessage(), is("stepFile '" + STEP_FILE_PATH + "' (id: login_as_admin) " + + "uses ${PARAM.username}, but 'username' is not in \"parameters\"")); + } + + @Test + public void testResolveInlineStepFileWithMissingParameterWillFailNamingInlineSource() throws IOException { + final Step rawStep = parseStep("{\"id\": \"inline_step\", " + + "\"stepFile\": {\"name\": \"echo\", \"url\": \"\", \"operation\": \"\", " + + "\"request\": {\"user\": \"${PARAM.username}\"}, \"assertions\": {}}}"); + + final RuntimeException exception = assertThrows(RuntimeException.class, () -> resolve(rawStep)); + + assertThat(exception.getMessage(), is("stepFile '' (id: inline_step) " + + "uses ${PARAM.username}, but 'username' is not in \"parameters\"")); + } + + @Test + public void testResolveNonObjectParametersWillFailWithClearMessage() throws IOException { + final Step rawStep = stepFilesStep(STEP_FILES_PATH, "[\"alice\"]"); + + final RuntimeException exception = assertThrows(RuntimeException.class, () -> resolve(rawStep)); + + assertThat(exception.getMessage(), is("stepFiles '" + STEP_FILES_PATH + "' (id: create_admin) " + + "has \"parameters\" that is not a JSON object: [\"alice\"]")); + } + + @Test + public void testResolveParametersWithoutStepFileWillFailWithClearMessage() throws IOException { + final Step rawStep = parseStep("{\"name\": \"plain\", \"url\": \"\", \"operation\": \"\", " + + "\"request\": {}, \"assertions\": {}, \"parameters\": {\"username\": \"alice\"}}"); + + final RuntimeException exception = assertThrows(RuntimeException.class, () -> resolve(rawStep)); + + assertThat(exception.getMessage(), is("Step (name: plain, id: null) has \"parameters\", " + + "but \"parameters\" is only supported together with \"stepFile\" or \"stepFiles\"")); + } + + @Test + public void testResolveStepWithoutStepFileWillGetSameStep() throws IOException { + final Step rawStep = parseStep("{\"name\": \"plain\", \"url\": \"\", \"operation\": \"\", " + + "\"request\": {\"user\": \"${PARAM.username}\"}, \"assertions\": {}}"); + + final Step resolvedStep = resolve(rawStep); + + assertThat(resolvedStep, sameInstance(rawStep)); + } + + @Test + public void testResolveStepFilesWithoutParamTokensWillGetUnchangedSteps() throws IOException { + final Step rawStep = parseStep("{\"id\": \"plain_files\", " + + "\"stepFiles\": \"${JSON.FILE:" + NO_PARAM_STEP_FILES_PATH + "}\"}"); + final Step inlinedStep = externalFileProcessor.resolveExtJsonFile(rawStep); + + final Step resolvedStep = externalFileProcessor.resolveStepFileParameters(rawStep, inlinedStep); + + assertThat(objectMapper.writeValueAsString(resolvedStep), is(objectMapper.writeValueAsString(inlinedStep))); + } + + @Test + public void testCreateFromResolvedStepFilesWillGetInnerStepsWithParameters() throws IOException { + final Step rawStep = stepFilesStep(STEP_FILES_PATH, + "{\"username\": \"bob\", \"role\": \"VIEWER\", \"retries\": 1, \"active\": false}"); + + final List innerSteps = externalFileProcessor.createFromStepFile(resolve(rawStep), rawStep.getId()); + + assertThat(innerSteps.size(), is(2)); + assertThat(innerSteps.get(1).getUrl(), is("/api/v1/users/bob")); + } + + private Step stepFilesStep(String stepFilesPath, String parametersJson) throws IOException { + return parseStep("{\"id\": \"create_admin\", " + + "\"stepFiles\": \"${JSON.FILE:" + stepFilesPath + "}\", " + + "\"parameters\": " + parametersJson + "}"); + } + + private Step parseStep(String stepJson) throws IOException { + return objectMapper.readValue(stepJson, Step.class); + } + + private Step resolve(Step rawStep) { + final Step inlinedStep = externalFileProcessor.resolveExtJsonFile(rawStep); + return externalFileProcessor.resolveStepFileParameters(rawStep, inlinedStep); + } + + private String resolveInnerStepsJson(Step rawStep) throws IOException { + return objectMapper.writeValueAsString(resolve(rawStep).getStepFiles()); + } +} diff --git a/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessorImplTest.java b/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessorImplTest.java index e30837331..b571d0c0b 100644 --- a/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessorImplTest.java +++ b/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeExternalFileProcessorImplTest.java @@ -10,10 +10,12 @@ import java.io.IOException; +import java.util.List; import java.util.Map; import static com.jayway.jsonpath.JsonPath.read; import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.nullValue; import static org.jsmart.zerocode.core.utils.SmartUtils.readJsonAsString; import static org.junit.Assert.assertThat; @@ -112,4 +114,68 @@ public void test_textNodeExtFile() throws IOException { assertThat(read(resultJsonStep, "$.request.headers.api_key"), is("hello key")); } -} \ No newline at end of file + @Test + public void testCreateFromStepFilesWithNameAndIdWillTagStepsWithNameAndId() throws IOException { + final List innerSteps = createInnerSteps("\"name\": \"create_admin_reuse\", \"id\": \"create_admin\", "); + + assertThat(innerSteps.get(0).getStepFileParent(), is("create_admin_reuse (id: create_admin)")); + assertThat(innerSteps.get(1).getStepFileParent(), is("create_admin_reuse (id: create_admin)")); + } + + @Test + public void testCreateFromStepFilesWithIdOnlyWillTagStepsWithId() throws IOException { + final List innerSteps = createInnerSteps("\"id\": \"create_admin\", "); + + assertThat(innerSteps.get(0).getStepFileParent(), is("(id: create_admin)")); + } + + @Test + public void testCreateFromStepFilesWithNameOnlyWillTagStepsWithName() throws IOException { + final List innerSteps = createInnerSteps("\"name\": \"create_admin_reuse\", "); + + assertThat(innerSteps.get(0).getStepFileParent(), is("create_admin_reuse")); + } + + @Test + public void testCreateFromStepFilesWithoutNameOrIdWillTagStepsAsStepFile() throws IOException { + final List innerSteps = createInnerSteps(""); + + assertThat(innerSteps.get(0).getStepFileParent(), is("(stepFile)")); + } + + @Test + public void testCreateFromSingleStepFileWillTagStepWithId() throws IOException { + final Step parentStep = objectMapper.readValue("{\"id\": \"single_step_file\", " + + "\"stepFile\": \"${JSON.FILE:integration_test_files/multi_step/single_step.json}\"}", Step.class); + + final List innerSteps = externalFileProcessor.createFromStepFile( + externalFileProcessor.resolveExtJsonFile(parentStep), parentStep.getId()); + + assertThat(innerSteps.get(0).getStepFileParent(), is("(id: single_step_file)")); + } + + @Test + public void testSerializeStepFromStepFilesWillGetNoStepFileParentField() throws IOException { + final List innerSteps = createInnerSteps("\"id\": \"create_admin\", "); + + final String innerStepJson = objectMapper.writeValueAsString(innerSteps.get(0)); + + assertThat(innerStepJson.contains("stepFileParent"), is(false)); + } + + @Test + public void testParseStepWithoutStepFileWillGetNoStepFileParent() throws IOException { + final Step plainStep = objectMapper.readValue("{\"name\": \"plain\", \"url\": \"\", \"operation\": \"\", " + + "\"request\": {}, \"assertions\": {}}", Step.class); + + assertThat(plainStep.getStepFileParent(), is(nullValue())); + } + + private List createInnerSteps(String parentNameAndIdJson) throws IOException { + final Step parentStep = objectMapper.readValue("{" + parentNameAndIdJson + + "\"stepFiles\": \"${JSON.FILE:integration_test_files/multi_step/multi_step_file.json}\"}", Step.class); + return externalFileProcessor.createFromStepFile( + externalFileProcessor.resolveExtJsonFile(parentStep), parentStep.getId()); + } + +} diff --git a/core/src/test/java/org/jsmart/zerocode/integrationtests/StepFilesParametersTest.java b/core/src/test/java/org/jsmart/zerocode/integrationtests/StepFilesParametersTest.java new file mode 100644 index 000000000..b9e58c89d --- /dev/null +++ b/core/src/test/java/org/jsmart/zerocode/integrationtests/StepFilesParametersTest.java @@ -0,0 +1,18 @@ +package org.jsmart.zerocode.integrationtests; + +import org.jsmart.zerocode.core.domain.HostProperties; +import org.jsmart.zerocode.core.domain.JsonTestCase; +import org.jsmart.zerocode.core.tests.customrunner.TestOnlyZeroCodeUnitRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +@HostProperties(host="http://localhost", port=9998, context = "") +@RunWith(TestOnlyZeroCodeUnitRunner.class) +public class StepFilesParametersTest { + + @Test + @JsonTestCase("integration_test_files/multi_step/step_files_parameters_test.json") + public void testSameStepFileWithDifferentParametersWillPassForEachReuse() throws Exception { + + } +} diff --git a/core/src/test/resources/integration_test_files/multi_step/param_echo_steps.json b/core/src/test/resources/integration_test_files/multi_step/param_echo_steps.json new file mode 100644 index 000000000..392e6dfba --- /dev/null +++ b/core/src/test/resources/integration_test_files/multi_step/param_echo_steps.json @@ -0,0 +1,21 @@ +[ + { + "name": "echo_user", + "url": "", + "operation": "", + "request": { + "username": "${PARAM.username}", + "role": "${PARAM.role}", + "retries": "(int)${PARAM.retries}", + "active": "(boolean)${PARAM.active}", + "tag": "${PARAM.tag}" + }, + "assertions": { + "username": "${PARAM.username}", + "role": "${PARAM.role}", + "retries": "(int)${PARAM.retries}", + "active": "(boolean)${PARAM.active}", + "tag": "$CONTAINS.STRING:t_" + } + } +] diff --git a/core/src/test/resources/integration_test_files/multi_step/step_files_parameters_test.json b/core/src/test/resources/integration_test_files/multi_step/step_files_parameters_test.json new file mode 100644 index 000000000..c029e759b --- /dev/null +++ b/core/src/test/resources/integration_test_files/multi_step/step_files_parameters_test.json @@ -0,0 +1,49 @@ +{ + "scenarioName": "Reuse a step file twice with different parameters", + "steps": [ + { + "name": "signup", + "url": "", + "operation": "", + "request": { + "role": "ADMIN" + }, + "assertions": {} + }, + { + "id": "create_admin", + "stepFiles": "${JSON.FILE:integration_test_files/multi_step/param_echo_steps.json}", + "parameters": { + "username": "alice", + "role": "${$.signup.request.role}", + "retries": 3, + "active": true, + "tag": "t_${RANDOM.NUMBER}" + } + }, + { + "id": "create_viewer", + "stepFiles": "${JSON.FILE:integration_test_files/multi_step/param_echo_steps.json}", + "parameters": { + "username": "bob", + "role": "VIEWER", + "retries": 1, + "active": false, + "tag": "t_${RANDOM.NUMBER}" + } + }, + { + "name": "latest_echo_user_is_viewer", + "url": "", + "operation": "", + "request": { + "username": "${$.echo_user.request.username}", + "retries": "${$.echo_user.response.retries}" + }, + "assertions": { + "username": "bob", + "retries": "1" + } + } + ] +} diff --git a/core/src/test/resources/unit_test_files/step_file_parameters/login_step.json b/core/src/test/resources/unit_test_files/step_file_parameters/login_step.json new file mode 100644 index 000000000..6eed770fe --- /dev/null +++ b/core/src/test/resources/unit_test_files/step_file_parameters/login_step.json @@ -0,0 +1,14 @@ +{ + "name": "login", + "url": "/api/v1/login", + "method": "POST", + "request": { + "body": { + "username": "${PARAM.username}", + "password": "${PARAM.password}" + } + }, + "assertions": { + "status": 200 + } +} diff --git a/core/src/test/resources/unit_test_files/step_file_parameters/no_param_steps.json b/core/src/test/resources/unit_test_files/step_file_parameters/no_param_steps.json new file mode 100644 index 000000000..f1104f6a3 --- /dev/null +++ b/core/src/test/resources/unit_test_files/step_file_parameters/no_param_steps.json @@ -0,0 +1,11 @@ +[ + { + "name": "plain_step", + "url": "/api/v1/health", + "method": "GET", + "request": {}, + "assertions": { + "status": 200 + } + } +] diff --git a/core/src/test/resources/unit_test_files/step_file_parameters/user_steps.json b/core/src/test/resources/unit_test_files/step_file_parameters/user_steps.json new file mode 100644 index 000000000..e894bd5ca --- /dev/null +++ b/core/src/test/resources/unit_test_files/step_file_parameters/user_steps.json @@ -0,0 +1,31 @@ +[ + { + "name": "create_user", + "url": "/api/v1/users", + "method": "POST", + "request": { + "body": { + "username": "${PARAM.username}", + "role": "${PARAM.role}", + "retries": "(int)${PARAM.retries}", + "active": "(boolean)${PARAM.active}" + } + }, + "assertions": { + "status": 201 + } + }, + { + "name": "get_user", + "url": "/api/v1/users/${PARAM.username}", + "method": "GET", + "request": {}, + "assertions": { + "status": 200, + "body": { + "username": "${PARAM.username}", + "role": "{{PARAM.role}}" + } + } + } +] diff --git a/http-testing-examples/pom.xml b/http-testing-examples/pom.xml index c4b086ea3..920d1a44a 100644 --- a/http-testing-examples/pom.xml +++ b/http-testing-examples/pom.xml @@ -65,6 +65,7 @@ org.jsmart.zerocode.testhelp.tests.helloworldfileupload.HelloWorldFileUploadTest org.jsmart.zerocode.testhelp.tests.helloworldretrywithsteps.HelloWorldRetryWithStepsTest org.jsmart.zerocode.testhelp.tests.helloworldurlencoded.HelloWorldUrlEncodedTest + org.jsmart.zerocode.testhelp.tests.helloworldstepfileparameters.HelloWorldStepFileParametersTest diff --git a/http-testing-examples/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldstepfileparameters/HelloWorldStepFileParametersTest.java b/http-testing-examples/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldstepfileparameters/HelloWorldStepFileParametersTest.java new file mode 100644 index 000000000..b2ec593a1 --- /dev/null +++ b/http-testing-examples/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldstepfileparameters/HelloWorldStepFileParametersTest.java @@ -0,0 +1,39 @@ +package org.jsmart.zerocode.testhelp.tests.helloworldstepfileparameters; + +import org.jsmart.zerocode.core.domain.Scenario; +import org.jsmart.zerocode.core.domain.TargetEnv; +import org.jsmart.zerocode.core.runner.ZeroCodeUnitRunner; +import org.jsmart.zerocode.testhelp.localserver.RunMeFirstLocalMockRESTServer; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Shows how the same "stepFiles" is reused twice in one scenario with different "parameters", + * read inside the step file as ${PARAM.username} and ${PARAM.role}. + *

+ * Starts and stops the local mock REST server itself, hence needs no manual setup. + */ +@TargetEnv("hello_world_host.properties") +@RunWith(ZeroCodeUnitRunner.class) +public class HelloWorldStepFileParametersTest { + + private static RunMeFirstLocalMockRESTServer mockRESTServer; + + @BeforeClass + public static void startMockServer() { + mockRESTServer = new RunMeFirstLocalMockRESTServer(RunMeFirstLocalMockRESTServer.PORT); + mockRESTServer.start(); + } + + @AfterClass + public static void stopMockServer() { + mockRESTServer.stop(); + } + + @Test + @Scenario("helloworld_step_file_parameters/create_users_with_step_file_parameters_test.json") + public void testSameStepFileWithDifferentParametersWillCreateAndVerifyEachUser() throws Exception { + } +} diff --git a/http-testing-examples/src/test/resources/helloworld_step_file_parameters/common/create_and_verify_user.json b/http-testing-examples/src/test/resources/helloworld_step_file_parameters/common/create_and_verify_user.json new file mode 100644 index 000000000..7826e77cb --- /dev/null +++ b/http-testing-examples/src/test/resources/helloworld_step_file_parameters/common/create_and_verify_user.json @@ -0,0 +1,29 @@ +[ + { + "name": "create_user", + "url": "/api/v1/users", + "operation": "POST", + "request": { + "body": { + "username": "${PARAM.username}", + "role": "${PARAM.role}" + } + }, + "assertions": { + "status": 201 + } + }, + { + "name": "get_user", + "url": "/api/v1/users/${PARAM.username}", + "operation": "GET", + "request": {}, + "assertions": { + "status": 200, + "body": { + "username": "${PARAM.username}XXX", + "role": "${PARAM.role}" + } + } + } +] diff --git a/http-testing-examples/src/test/resources/helloworld_step_file_parameters/create_users_with_step_file_parameters_test.json b/http-testing-examples/src/test/resources/helloworld_step_file_parameters/create_users_with_step_file_parameters_test.json new file mode 100644 index 000000000..37108fca8 --- /dev/null +++ b/http-testing-examples/src/test/resources/helloworld_step_file_parameters/create_users_with_step_file_parameters_test.json @@ -0,0 +1,35 @@ +{ + "scenarioName": "Reuse the same step file to create an admin and a viewer user", + "steps": [ + { + "id": "create_admin", + "name": "create_admin_reuse", + "stepFiles": "${JSON.FILE:helloworld_step_file_parameters/common/create_and_verify_user.json}", + "parameters": { + "username": "alice", + "role": "ADMIN" + } + }, + { + "id": "create_viewer", + "stepFiles": "${JSON.FILE:helloworld_step_file_parameters/common/create_and_verify_user.json}", + "parameters": { + "username": "bob", + "role": "VIEWER" + } + }, + { + "name": "latest_user_is_viewer", + "url": "/api/v1/users/${$.get_user.response.body.username}", + "operation": "GET", + "request": {}, + "assertions": { + "status": 200, + "body": { + "username": "bob", + "role": "VIEWER" + } + } + } + ] +} diff --git a/http-testing-examples/src/test/resources/localhost_stubs/localhost_REST_fake_end_points_stubs.json b/http-testing-examples/src/test/resources/localhost_stubs/localhost_REST_fake_end_points_stubs.json index bbf1cfe12..e3ef834fc 100644 --- a/http-testing-examples/src/test/resources/localhost_stubs/localhost_REST_fake_end_points_stubs.json +++ b/http-testing-examples/src/test/resources/localhost_stubs/localhost_REST_fake_end_points_stubs.json @@ -245,6 +245,64 @@ "question": "zerocode" } } + }, + { + "name": "Mock POST user alice with role ADMIN", + "operation": "POST", + "url": "/api/v1/users", + "ignoreBody": false, + "body": { + "username": "alice", + "role": "ADMIN" + }, + "response": { + "status": 201, + "body": { + "username": "alice", + "role": "ADMIN" + } + } + }, + { + "name": "Mock GET user alice", + "operation": "GET", + "url": "/api/v1/users/alice", + "response": { + "status": 200, + "body": { + "username": "alice", + "role": "ADMIN" + } + } + }, + { + "name": "Mock POST user bob with role VIEWER", + "operation": "POST", + "url": "/api/v1/users", + "ignoreBody": false, + "body": { + "username": "bob", + "role": "VIEWER" + }, + "response": { + "status": 201, + "body": { + "username": "bob", + "role": "VIEWER" + } + } + }, + { + "name": "Mock GET user bob", + "operation": "GET", + "url": "/api/v1/users/bob", + "response": { + "status": 200, + "body": { + "username": "bob", + "role": "VIEWER" + } + } } ] From ef7cc1f2554302d264d973719460d6ec7e60fe5b Mon Sep 17 00:00:00 2001 From: nchandra Date: Fri, 23 Oct 2026 08:11:42 +0100 Subject: [PATCH 2/3] Examples added to comments --- .../preprocessor/StepFileParametersResolver.java | 12 +++++++++++- .../runner/ZeroCodeMultiStepsScenarioRunnerImpl.java | 4 +++- .../preprocessor/StepFileParametersResolverTest.java | 1 + .../HelloWorldStepFileParametersTest.java | 1 - .../common/create_and_verify_user.json | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolver.java b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolver.java index fa7c63993..9e39c86ee 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolver.java +++ b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolver.java @@ -23,9 +23,10 @@ /** * Replaces ${PARAM.name} (or {{PARAM.name}}) tokens inside an inlined "stepFile"/"stepFiles" with the * values from the "parameters" of the referencing step. - * + * - * Runs before the usual token resolution, so a parameter value may itself carry tokens such as * ${RANDOM.NUMBER} or ${$.earlier_step.response.body.id}, which are then resolved as normal. + * - * The parameters apply only to the steps of that one reference and do not leak into the scenario. */ public class StepFileParametersResolver { @@ -54,10 +55,12 @@ public Step resolve(Step rawStep, Step inlinedStep) { final boolean hasParameters = parameters != null && !parameters.isNull(); if (!hasStepFileContent(inlinedStep)) { + // If this is regular DSL step, then "parameters" not allowed, throw an exception. failIfParametersPresent(hasParameters, inlinedStep); return inlinedStep; } if (hasParameters && !parameters.isObject()) { + // "parameters" should be of shape JSON object throw new RuntimeException(describeSource(rawStep, inlinedStep.getId()) + " has \"parameters\" that is not a JSON object: " + parameters); } @@ -103,6 +106,13 @@ private JsonNode substitute(JsonNode node, ParameterContext context) { return node; } + /** + * Replaces every ${PARAM.x} and {{PARAM.x}} in one text value. Other tokens are left for later resolution. + * e.g. + * with "parameters": { "username": "alice", "role": "ADMIN", "retries": 3, "tag": "t_${RANDOM.NUMBER}" } + * - "/api/v1/users/${PARAM.username}" -> "/api/v1/users/alice" + * - "{{PARAM.role}}:${PARAM.retries}:${PARAM.tag}" -> "ADMIN:3:t_${RANDOM.NUMBER}" + */ private String replaceParamTokens(String text, ParameterContext context) { final Matcher matcher = PARAM_TOKEN_PATTERN.matcher(text); final StringBuffer resolved = new StringBuffer(); 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 d326dcb49..1aafa19e3 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 @@ -410,7 +410,9 @@ private Boolean executeRetry(RunNotifier notifier, /** * Name to show in logs and reports. A step from a stepFile/stepFiles is shown with the step - * that used it, e.g. "create_admin_reuse (id: create_admin) -> get_user". + * that used it, + * e.g. + * "create_admin_reuse (id: create_admin) -> get_user". * The step's own name stays the same, so ${$.get_user...} still works. */ private String displayNameOf(Step thisStep) { diff --git a/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolverTest.java b/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolverTest.java index 47049473c..ae4924bd9 100644 --- a/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolverTest.java +++ b/core/src/test/java/org/jsmart/zerocode/core/engine/preprocessor/StepFileParametersResolverTest.java @@ -35,6 +35,7 @@ public void testResolveStepFilesWillReplaceParametersInUrlRequestAndAssertions() assertThat(read(innerStepsJson, "$[1].assertions.body.username"), is("alice")); } + // Making sure both type supported: ${..} and {{..}} @Test public void testResolveDoubleCurlyBraceTokenWillReplaceParameter() throws IOException { final Step rawStep = stepFilesStep(STEP_FILES_PATH, diff --git a/http-testing-examples/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldstepfileparameters/HelloWorldStepFileParametersTest.java b/http-testing-examples/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldstepfileparameters/HelloWorldStepFileParametersTest.java index b2ec593a1..ccc55c23f 100644 --- a/http-testing-examples/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldstepfileparameters/HelloWorldStepFileParametersTest.java +++ b/http-testing-examples/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldstepfileparameters/HelloWorldStepFileParametersTest.java @@ -12,7 +12,6 @@ /** * Shows how the same "stepFiles" is reused twice in one scenario with different "parameters", * read inside the step file as ${PARAM.username} and ${PARAM.role}. - *

* Starts and stops the local mock REST server itself, hence needs no manual setup. */ @TargetEnv("hello_world_host.properties") diff --git a/http-testing-examples/src/test/resources/helloworld_step_file_parameters/common/create_and_verify_user.json b/http-testing-examples/src/test/resources/helloworld_step_file_parameters/common/create_and_verify_user.json index 7826e77cb..521717671 100644 --- a/http-testing-examples/src/test/resources/helloworld_step_file_parameters/common/create_and_verify_user.json +++ b/http-testing-examples/src/test/resources/helloworld_step_file_parameters/common/create_and_verify_user.json @@ -21,7 +21,7 @@ "assertions": { "status": 200, "body": { - "username": "${PARAM.username}XXX", + "username": "${PARAM.username}", "role": "${PARAM.role}" } } From 6f3780595726821978adbcaf5a289fd617117a54 Mon Sep 17 00:00:00 2001 From: nchandra Date: Fri, 23 Oct 2026 12:35:03 +0100 Subject: [PATCH 3/3] newVersion 1.4.5-SNAPSHOT-INPROGRESS --- core/pom.xml | 2 +- http-testing-examples/pom.xml | 2 +- junit5-testing-examples/pom.xml | 2 +- kafka-testing-examples/pom.xml | 2 +- pom.xml | 2 +- zerocode-maven-archetype/pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index d5560e441..107e62022 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.jsmart zerocode-tdd-parent - 1.4.4 + 1.4.5 zerocode-tdd diff --git a/http-testing-examples/pom.xml b/http-testing-examples/pom.xml index 920d1a44a..40ed522d0 100644 --- a/http-testing-examples/pom.xml +++ b/http-testing-examples/pom.xml @@ -4,7 +4,7 @@ zerocode-tdd-parent org.jsmart - 1.4.4 + 1.4.5 org.jsmart diff --git a/junit5-testing-examples/pom.xml b/junit5-testing-examples/pom.xml index f5c7bca46..7387898fd 100644 --- a/junit5-testing-examples/pom.xml +++ b/junit5-testing-examples/pom.xml @@ -4,7 +4,7 @@ zerocode-tdd-parent org.jsmart - 1.4.4 + 1.4.5 zerocode-tdd-jupiter diff --git a/kafka-testing-examples/pom.xml b/kafka-testing-examples/pom.xml index bc6ea6fc5..c1ae0ba97 100644 --- a/kafka-testing-examples/pom.xml +++ b/kafka-testing-examples/pom.xml @@ -4,7 +4,7 @@ zerocode-tdd-parent org.jsmart - 1.4.4 + 1.4.5 kafka-testing-examples diff --git a/pom.xml b/pom.xml index 5ac70e4b1..28ad9af90 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ zerocode-tdd-parent org.jsmart - 1.4.4 + 1.4.5 pom ZeroCode TDD Parent diff --git a/zerocode-maven-archetype/pom.xml b/zerocode-maven-archetype/pom.xml index 7f199dc96..2598541ae 100644 --- a/zerocode-maven-archetype/pom.xml +++ b/zerocode-maven-archetype/pom.xml @@ -4,7 +4,7 @@ org.jsmart zerocode-tdd-parent - 1.4.4 + 1.4.5 zerocode-maven-archetype