diff --git a/core/pom.xml b/core/pom.xml index f59e00aa5..c6a437e5a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -192,6 +192,11 @@ com.google.protobuf protobuf-java-util + + com.networknt + json-schema-validator + 1.4.0 + diff --git a/core/src/main/java/org/jsmart/zerocode/core/domain/ScenarioSpec.java b/core/src/main/java/org/jsmart/zerocode/core/domain/ScenarioSpec.java index d2425eb3c..93c11d565 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/domain/ScenarioSpec.java +++ b/core/src/main/java/org/jsmart/zerocode/core/domain/ScenarioSpec.java @@ -62,6 +62,9 @@ public Parameterized getParameterized() { return parameterized; } + public void addStep(Step step) { + this.steps.add(step); + } @Override public String toString() { return "ScenarioSpec{" + diff --git a/core/src/main/java/org/jsmart/zerocode/core/domain/Schema.java b/core/src/main/java/org/jsmart/zerocode/core/domain/Schema.java new file mode 100644 index 000000000..c08359741 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/domain/Schema.java @@ -0,0 +1,11 @@ +package org.jsmart.zerocode.core.domain; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@Repeatable( value = Schemas.class ) +public @interface Schema { + String value(); +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/domain/SchemaStep.java b/core/src/main/java/org/jsmart/zerocode/core/domain/SchemaStep.java new file mode 100644 index 000000000..1002e3598 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/domain/SchemaStep.java @@ -0,0 +1,48 @@ +package org.jsmart.zerocode.core.domain; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.tomakehurst.wiremock.common.Json; + +import java.util.List; + +public class SchemaStep extends Step{ + + private JsonNode response; + public SchemaStep(Integer loop, Retry retry, String name, String operation, String method, String url, JsonNode request, List validators, JsonNode sort, JsonNode assertions, JsonNode verify , JsonNode response , String verifyMode, boolean ignoreStep) { + super(loop, retry, name, operation, method, url, request, validators, sort, assertions, verify, verifyMode, ignoreStep); + + this.response = response; + } + + public JsonNode getResponse() + { + return response; + } + + public void setResponse(JsonNode response) { + this.response = response; + } + + @Override + public String toString() { + return "SchemaStep{" + + "loop=" + getLoop() + + ", retry='" + getRetry() + '\'' + + ", name='" + getName() + '\'' + + ", method='" + getMethod() + '\'' + + ", operation='" + getOperation() + '\'' + + ", url='" + getUrl() + '\'' + + ", request=" + getRequest() + + ", validators=" + getValidators() + + ", assertions=" + getAssertions() + + ", response=" + response + + ", verifyMode=" + getVerifyMode() + + ", verify=" + getVerify() + + ", id='" + getId() + '\'' + + ", stepFile=" + getStepFile() + + ", stepFiles=" + getStepFiles() + + ", parameterized=" + getParameterized() + + ", customLog=" + getCustomLog() + + '}'; + } +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/domain/Schemas.java b/core/src/main/java/org/jsmart/zerocode/core/domain/Schemas.java new file mode 100644 index 000000000..9f427ef09 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/domain/Schemas.java @@ -0,0 +1,12 @@ +package org.jsmart.zerocode.core.domain; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Schemas { + public Schema[] value() default {}; +} \ No newline at end of file diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepExecutionState.java b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepExecutionState.java index 814b7bd13..7bd108ec8 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepExecutionState.java +++ b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/StepExecutionState.java @@ -31,6 +31,16 @@ public void addRequest(String requestJson) { } + public String getResponse() + { + return paramMap.get("STEP.RESPONSE"); + } + + public String getRequest() + { + return paramMap.get("STEP.REQUEST"); + } + public void addResponse(String responseJson) { paramMap.put("STEP.RESPONSE", responseJson); } diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeJsonSchemaMatcher.java b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeJsonSchemaMatcher.java new file mode 100644 index 000000000..c76262ed7 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeJsonSchemaMatcher.java @@ -0,0 +1,10 @@ +package org.jsmart.zerocode.core.engine.preprocessor; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface ZeroCodeJsonSchemaMatcher { + + public boolean ismatching(JsonNode JsonFile , JsonNode JsonSchema); + + +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeJsonSchemaMatcherImpl.java b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeJsonSchemaMatcherImpl.java new file mode 100644 index 000000000..e0898c07d --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeJsonSchemaMatcherImpl.java @@ -0,0 +1,23 @@ +package org.jsmart.zerocode.core.engine.preprocessor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; +import java.util.Set; + + +public class ZeroCodeJsonSchemaMatcherImpl implements ZeroCodeJsonSchemaMatcher { + + + @Override + public boolean ismatching(JsonNode jsonFile, JsonNode schema) { + + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); + JsonSchema jsonSchema = factory.getSchema(schema); + Set errors = jsonSchema.validate(jsonFile); + return errors.isEmpty(); + } +} + 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 ca5518366..cdd87cc99 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 @@ -1,6 +1,7 @@ package org.jsmart.zerocode.core.runner; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.NullNode; import com.google.inject.Inject; @@ -13,6 +14,7 @@ import org.jsmart.zerocode.core.domain.Parameterized; import org.jsmart.zerocode.core.domain.ScenarioSpec; +import org.jsmart.zerocode.core.domain.SchemaStep; import org.jsmart.zerocode.core.domain.Step; import org.jsmart.zerocode.core.domain.builders.ZeroCodeExecReportBuilder; @@ -108,6 +110,8 @@ public class ZeroCodeMultiStepsScenarioRunnerImpl implements ZeroCodeMultiStepsS private Boolean stepOutcomeGreen; + private ScenarioSpec schemaScenario; + @Override public synchronized boolean runScenario(ScenarioSpec scenario, RunNotifier notifier, Description description) { @@ -133,6 +137,8 @@ public synchronized boolean runScenario(ScenarioSpec scenario, RunNotifier notif ScenarioSpec parameterizedScenario = parameterizedProcessor.resolveParameterized(scenario, scnCount); + schemaScenario = new ScenarioSpec(parameterizedScenario.getLoop() , parameterizedScenario.getIgnoreStepFailures() , parameterizedScenario.getScenarioName() , new ArrayList() , parameterizedScenario.getParameterized() , parameterizedScenario.getMeta()); + resultReportBuilder = newInstance() .loop(scnCount) .scenarioName(parameterizedScenario.getScenarioName()); @@ -255,6 +261,7 @@ private Boolean executeRetry(RunNotifier notifier, } executionResult = executeApi(logPrefixRelationshipId, thisStep, resolvedRequestJson, scenarioExecutionState); + // logging response final LocalDateTime responseTimeStamp = LocalDateTime.now(); correlLogger.aResponseBuilder() @@ -315,6 +322,16 @@ private Boolean executeRetry(RunNotifier notifier, continue; } + + + JsonNode schemaRequest = objectMapper.readTree(stepExecutionState.getRequest()+"\n" ); + JsonNode schemaResponse = objectMapper.readTree(executionResult); + + SchemaStep schemaStep = new SchemaStep(thisStep.getLoop() , thisStep.getRetry() , thisStep.getName() , thisStep.getOperation() , thisStep.getMethod() , thisStep.getUrl() ,schemaRequest , thisStep.getValidators() , thisStep.getSort() , null , null , schemaResponse , thisStep.getVerifyMode() , thisStep.getIgnoreStep()); + + schemaScenario.addStep(schemaStep); + + boolean ignoreStepFailures = scenario.getIgnoreStepFailures() == null ? false : scenario.getIgnoreStepFailures(); if (!failureResults.isEmpty()) { stepOutcomeGreen = notificationHandler.handleAssertion( @@ -602,4 +619,11 @@ else if (ofNullable(thisStep.getVerifyMode()).orElse("LENIENT").equals("STRICT") return failureResults; } + public ScenarioSpec getSchemaScenario() { + return schemaScenario; + } + + public void setSchemaScenario(ScenarioSpec scenarioSpec) { + this.schemaScenario = scenarioSpec; + } } diff --git a/core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeUnitRunner.java b/core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeUnitRunner.java index 6c659e247..89d4ea8b6 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeUnitRunner.java +++ b/core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeUnitRunner.java @@ -1,13 +1,10 @@ package org.jsmart.zerocode.core.runner; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.util.Modules; -import java.lang.annotation.Annotation; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; import org.jsmart.zerocode.core.di.main.ApplicationMainModule; import org.jsmart.zerocode.core.di.module.RuntimeHttpClientModule; import org.jsmart.zerocode.core.di.module.RuntimeKafkaClientModule; @@ -18,9 +15,11 @@ import org.jsmart.zerocode.core.domain.TargetEnv; import org.jsmart.zerocode.core.domain.UseHttpClient; import org.jsmart.zerocode.core.domain.UseKafkaClient; +import org.jsmart.zerocode.core.domain.Schema; import org.jsmart.zerocode.core.domain.builders.ZeroCodeExecReportBuilder; import org.jsmart.zerocode.core.domain.builders.ZeroCodeIoWriteBuilder; import org.jsmart.zerocode.core.engine.listener.TestUtilityListener; +import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeJsonSchemaMatcherImpl; import org.jsmart.zerocode.core.httpclient.BasicHttpClient; import org.jsmart.zerocode.core.httpclient.ssl.SslTrustHttpClient; import org.jsmart.zerocode.core.kafka.client.BasicKafkaClient; @@ -41,6 +40,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.annotation.Annotation; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + import static java.lang.System.getProperty; import static org.jsmart.zerocode.core.constants.ZeroCodeReportConstants.CHARTS_AND_CSV; import static org.jsmart.zerocode.core.constants.ZeroCodeReportConstants.ZEROCODE_JUNIT; @@ -64,7 +69,7 @@ public class ZeroCodeUnitRunner extends BlockJUnit4ClassRunner { private ZerocodeCorrelationshipLogger corrLogger; protected boolean testRunCompleted; protected boolean passed; - + protected Schema schemaAnno; private ZeroCodeMultiStepsScenarioRunner multiStepsRunner; /** @@ -118,11 +123,13 @@ protected void runChild(FrameworkMethod method, RunNotifier notifier) { jsonTestCaseAnno = evalScenarioToJsonTestCase(method.getMethod().getAnnotation(Scenario.class)); } + schemaAnno = method.getMethod().getAnnotation(Schema.class); + if (isIgnored(method)) { notifier.fireTestIgnored(description); - } else if (jsonTestCaseAnno != null) { + } else if (jsonTestCaseAnno != null ) { runLeafJsonTest(notifier, description, jsonTestCaseAnno); @@ -205,6 +212,7 @@ protected ZeroCodeReportGenerator getInjectedReportGenerator() { return getMainModuleInjector().getInstance(ZeroCodeReportGenerator.class); } + private void runLeafJsonTest(RunNotifier notifier, Description description, JsonTestCase jsonTestCaseAnno) { if (jsonTestCaseAnno != null) { currentTestCase = jsonTestCaseAnno.value(); @@ -221,7 +229,38 @@ private void runLeafJsonTest(RunNotifier notifier, Description description, Json LOGGER.debug("### Found currentTestCase : -" + child); passed = multiStepsRunner.runScenario(child, notifier, description); + //Schema validation + if( schemaAnno == null ) + { + LOGGER.warn("### No Json Schema was added for validation"); + } + else + { + LOGGER.debug("### Found Json Schema : - \n" + smartUtils.prettyPrintJson(smartUtils.readJsonAsString(schemaAnno.value()))); + // Schema Validation is performed here... + + ObjectMapper mapper = new ObjectMapper(); + + ZeroCodeJsonSchemaMatcherImpl zeroCodeJsonSchemaMatcher = new ZeroCodeJsonSchemaMatcherImpl(); + + JsonNode jsonFile = mapper.valueToTree(((ZeroCodeMultiStepsScenarioRunnerImpl)multiStepsRunner).getSchemaScenario()); + + String jsonScenarioFile = SmartUtils.readJsonAsString( schemaAnno.value() ); + + JsonNode schemaFile = mapper.readTree(jsonScenarioFile); + + if( zeroCodeJsonSchemaMatcher.ismatching(jsonFile , schemaFile) ) + { + LOGGER.debug("### Json Schema matches with the Json test file." ); + + } + else + { + LOGGER.error("### Json Schema does not matches with the Json test file."); + throw new Exception("Json Schema does not matches with the Json test file."); + } + } } catch (Exception ioEx) { ioEx.printStackTrace(); notifier.fireTestFailure(new Failure(description, ioEx)); @@ -240,6 +279,8 @@ private void runLeafJsonTest(RunNotifier notifier, Description description, Json notifier.fireTestFinished(description); } + + private List getSmartChildrenList() { List children = getChildren(); children.forEach( @@ -368,6 +409,4 @@ public String value() { return jsonTestCase.value() == null ? null : jsonTestCase; } - - } diff --git a/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworld/JustHelloWorldTest.java b/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworld/JustHelloWorldTest.java index 12046e8eb..fd8edbbaf 100644 --- a/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworld/JustHelloWorldTest.java +++ b/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworld/JustHelloWorldTest.java @@ -14,5 +14,4 @@ public class JustHelloWorldTest { @Scenario("helloworld/hello_world_status_ok_assertions.json") public void testGet() throws Exception { } - -} +} \ No newline at end of file diff --git a/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/hellowworldschema/JustHelloWorldSchemaTest.java b/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/hellowworldschema/JustHelloWorldSchemaTest.java new file mode 100644 index 000000000..2a49e6975 --- /dev/null +++ b/http-testing/src/test/java/org/jsmart/zerocode/testhelp/tests/hellowworldschema/JustHelloWorldSchemaTest.java @@ -0,0 +1,20 @@ +package org.jsmart.zerocode.testhelp.tests.hellowworldschema; + +import org.jsmart.zerocode.core.domain.Scenario; +import org.jsmart.zerocode.core.domain.Schema; +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 JustHelloWorldSchemaTest { + + @Test + @Scenario("helloworldschema/hello_world_status_ok_assertions.json") + @Schema("helloworldschema/hello_world_status_ok_assertions_schema.json") + public void testGet() throws Exception { + } + +} \ No newline at end of file diff --git a/http-testing/src/test/resources/helloworldschema/hello_world_status_ok_assertions.json b/http-testing/src/test/resources/helloworldschema/hello_world_status_ok_assertions.json new file mode 100644 index 000000000..0873b3a3f --- /dev/null +++ b/http-testing/src/test/resources/helloworldschema/hello_world_status_ok_assertions.json @@ -0,0 +1,20 @@ +{ + "scenarioName": "GIVEN- the GitHub REST end point, WHEN- I invoke GET, THEN- I will receive the 200 status with body", + "steps": [ + { + "name": "get_user_details", + "url": "/users/octocat", + "method": "GET", + "request": { + }, + "verify": { + "status": 200, + "body": { + "login" : "octocat", + "id" : 583231, + "type" : "User" + } + } + } + ] +} diff --git a/http-testing/src/test/resources/helloworldschema/hello_world_status_ok_assertions_schema.json b/http-testing/src/test/resources/helloworldschema/hello_world_status_ok_assertions_schema.json new file mode 100644 index 000000000..3e5bf9878 --- /dev/null +++ b/http-testing/src/test/resources/helloworldschema/hello_world_status_ok_assertions_schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "scenarioName": { + "type": "string" + }, + "steps": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "method": { + "type": "string" + }, + "request": { + "type": "object" + }, + "response": { + "type": "object", + "properties": { + "status": { + "type": "integer" + }, + "body": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + } + }, + "required": [ + "login", + "id", + "type" + ] + } + }, + "required": [ + "status", + "body" + ] + } + }, + "required": [ + "response" + ] + } + ] + } + }, + "required": [ + "scenarioName", + "steps" + ] +} \ No newline at end of file