diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..3007d5256 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: authorjapps # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: https://paypal.me/authorjapps # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..4f1b5e0c7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,46 @@ +# + +## Fixed Which Issue? +- [ ] Which issue or ticket was(will be) fixed by this PR? (capture the issue link here) + +PR Branch +**_#ADD LINK TO THE PR BRANCH_** + +## Motivation and Context + +## Checklist: + +* [ ] 1. New Unit tests were added + * [ ] 1.1 Covered in existing Unit tests + +* [ ] 2. Integration tests were added + * [ ] 2.1 Covered in existing Integration tests + +* [ ] 3. Test names are meaningful + +* [ ] 3.1 Feature manually tested and outcome is successful + +* [ ] 4. PR doesn't break any of the earlier features for end users + * [ ] 4.1 WARNING! This might break one or more earlier earlier features, hence left a comment tagging all reviewrs + +* [ ] 5. PR doesn't break the HTML report features directly + * [ ] 5.1 Yes! I've manually run it locally and seen the HTML reports are generated perfectly fine + * [ ] 5.2 Yes! I've opened the generated HTML reports from the `/target` folder and they look fine + +* [ ] 6. PR doesn't break any HTML report features indirectly + * [ ] 6.1 I have not added or amended any dependencies in this PR + * [ ] 6.2 I have double checked, the new dependency added or removed has not affected the report generation indirectly + * [ ] 6.3 Yes! I've seen the Sample report screenshots [here](https://github.com/authorjapps/zerocode/issues/694#issuecomment-2505958433), and HTML report of the current PR looks simillar. + +* [ ] 7. Branch build passed in CI + +* [ ] 8. No 'package.*' in the imports + +* [ ] 9. Relevant DOcumentation page added or updated with clear instructions and examples for the end user + * [ ] 9.1 Not applicable. This was only a code refactor change, no functional or behaviourial changes were introduced + +* [ ] 10. Http test added to `http-testing-examples` module(if applicable) ? + * [ ] 10.1 Not applicable. The changes did not affect HTTP automation flow + +* [ ] 11. Kafka test added to `kafka-testing-examples` module(if applicable) ? + * [ ] 11.1 Not applicable. The changes did not affect Kafka automation flow diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..a5de58fd7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,55 @@ +name: Zerocode TDD CI Build In Action + +on: + workflow_dispatch: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + build: + strategy: + matrix: + version: [8, 11, 17, 21, 23] + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setting up JDK ${{ matrix.version }} + uses: actions/setup-java@v4 + with: + java-version: "${{ matrix.version }}" + distribution: 'temurin' + cache: 'maven' + + - name: Running Kafka + run: docker compose -f docker/compose/kafka-schema-registry.yml up -d && sleep 10 + + - name: Running PostgreSQL (to test DB SQL Executor) + run: docker compose -f docker/compose/pg_compose.yml up -d + + - name: Building and testing the changes + run: mvn clean test -ntp + + - if: always() + name: Junit html report + uses: javiertuya/junit-report-action@v1.3.0 + with: + surefire-files: "**/target/surefire-reports/TEST-*.xml" + report-dir: target/reports + + - if: always() + name: Interactive and granular reports + run: cp core/target/*.html target/reports/ && cp core/target/*.csv target/reports/ + + - if: always() + name: Publish test report files + uses: actions/upload-artifact@v4.6.2 + with: + name: "test-report-java${{ matrix.version }}" + path: | + target/reports + !target/reports/someFileName.html diff --git a/.gitignore b/.gitignore index 1b257ebfb..b4e7d865c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ package.properties .project .settings -*/target \ No newline at end of file +*/target +.vscode \ No newline at end of file diff --git a/.travis.yml b/.travis_NOT_IN_USE.yml similarity index 89% rename from .travis.yml rename to .travis_NOT_IN_USE.yml index 2a1b32383..5ced11fb9 100644 --- a/.travis.yml +++ b/.travis_NOT_IN_USE.yml @@ -1,7 +1,8 @@ #specific JDK or multiple JDK can be specified here. There is an issue with mvn surefire and openJDK8. #It is better to switch to openJDK language: java - +jdk: + - openjdk8 #This helps to avoid downloading artefacts every time from MVN central. #If you find any CI build failing due to unreflected dependency changes, kindly clear the CI cache @@ -17,6 +18,7 @@ services: #Still docker images are pulled every time. We need to cahche it for best practice. before_install: - wget https://raw.githubusercontent.com/authorjapps/zerocode-docker-factory/master/compose/kafka-schema-registry.yml + - if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_TOKEN" ] ; then docker login -u $DOCKER_USERNAME -p $DOCKER_TOKEN; fi - docker-compose -f kafka-schema-registry.yml up -d #Just compile and run tests, also print version at the beginning. diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 000000000..e4f8654cb --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,117 @@ +# Building ZeroCode + +## Without executing the tests +``` +mvn clean install -DskipTests +``` +## With tests executed(core) +Either you can cd to the `core` dir and build/run the project +``` +cd core +mvn clean install +or +mvn clean test +``` +_This way we don't need to run any Kafka containers as we are not firing Kafka tests._ + +#### or + +You can issue a mvn build command for the specific module(being on the parent dir) +``` +mvn -pl core clean install + +or + +mvn -pl core clean test +``` + +## Runing Only The Integration tests(core) +Right click and run the "integrationtests" package. It picks the tests ending with "*Test.java" +integration_tests_only_running_ + + +## With tests executed(kafka) +Some tests require a running Kafka (and some related components like kafka-rest, and kafka-schema-registry). + +Location: +https://github.com/authorjapps/zerocode/blob/master/docker/compose/kafka-schema-registry.yml + +Download the file, and run(or `cd to the docker/compose` dir and run) +``` +docker-compose -f kafka-schema-registry.yml up -d + +Note: +The above command brings up all necessary Kafka components in the docker containers. +There is no need of bringing up more containers. + +We have provided other compose-files just in-case anyone has to experiment tests with +single-node or multi-node cluster(s) independently. +``` + +In the [zerocode-docker-factory repository](https://github.com/authorjapps/zerocode-docker-factory/) ([direct download link](https://raw.githubusercontent.com/authorjapps/zerocode-docker-factory/master/compose/kafka-schema-registry.yml)) +you'll find 'kafka-schema-registry.yml', a docker-compose file that provides these components. + +Then you can run: +``` +mvn clean install <---- To build and install all the modules + +mvn -pl kafka-testing clean test <---- To run all the tests + +mvn -pl kafka-testing clean install <---- To run all the tests and install it to .m2 repo + +``` + +More info on the docker-compose file can be found in the [wiki](https://github.com/authorjapps/zerocode-docker-factory/wiki/Docker-container-for-Kafka-and-Schema-Registry) + +## With tests executed(all) +As explained above, in the root/parent folder, please issue the below command(the usual way) + +``` +mvn clean install <---- To build and install all the modules +``` + +## Compiling in ARM Processors +You might get the following error when you do a "mvn clean install -DskipTests" + +```java +[ERROR] Failed to execute goal com.github.os72:protoc-jar-maven-plugin:3.11.4:run (default) on project kafka-testing: + Error extracting protoc for version 3.11.4: Unsupported platform: protoc-3.11.4-osx-aarch_64.exe -> [Help 1] + +// +// more details >> +// +[INFO] ZeroCode TDD Parent ................................ SUCCESS [ 0.504 s] +[INFO] Zerocode TDD Core .................................. SUCCESS [ 2.365 s] +[INFO] Zerocode Http Testing With Simple YAML and JSON DSL SUCCESS [ 0.413 s] +[INFO] Zerocode Kafka Testing With Simple YAML and JSON DSL FAILURE [ 0.507 s] +[INFO] Zerocode JUnit5 Jupiter Load Testing ............... SKIPPED +[INFO] Zerocode Automated Testing Maven Archetype ......... SKIPPED +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD FAILURE +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 3.858 s +[INFO] Finished at: 2023-12-16T10:31:44Z +[INFO] ------------------------------------------------------------------------ +[ERROR] Failed to execute goal com.github.os72:protoc-jar-maven-plugin:3.11.4:run (default) on project kafka-testing: Error extracting protoc for version 3.11.4: Unsupported platform: protoc-3.11.4-osx-aarch_64.exe -> [Help 1] + +``` + +### Fix: +Go to --> .../zerocode/kafka-testing/pom.xml --> Comment the following line: + +```shell + +``` +Then execute ""mvn clean install -DskipTests"" --> It should be SUCCESS. + +Raise an Issue if you want to locally execute the tests involving "protos" and you aren't able to do it. + +Visit here for more details: +- https://github.com/os72/protoc-jar-maven-plugin?tab=readme-ov-file +- https://github.com/os72/protoc-jar/issues/93 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbab32473..9004aa714 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,4 @@ + # How to contribute # Thank you for your interest in contributing to this project. @@ -38,13 +39,22 @@ ISSUE-14 # SSL enabled http client 1. Please join our [mailing-lists][] and [gitter chat room][] for seeking community help. +## Write Unit tests +- For any feature you develop or include in a pull request (PR), ensure it is covered by unit tests. +- All unit tests should be placed in their respective package folders. +- Follow existing tests and mimic the pattern + +## Write Integration tests +- For any feature you develop or include in a pull request (PR), ensure it is also covered by integration tests as well. +- All integration tests should be placed under the [integrationtests](https://github.com/authorjapps/zerocode/tree/master/core/src/test/java/org/jsmart/zerocode/integrationtests) parent folder. +- Follow existing tests and mimic the pattern + ## Note Any contribution submitted by an author for inclusion in this repository shall be licensed under this [LICENSE](https://github.com/authorjapps/zerocode/blob/master/LICENSE) [forking]: https://help.github.com/articles/fork-a-repo [pull request]: https://help.github.com/articles/creating-a-pull-request [mailing-lists]: https://groups.google.com/forum/#!forum/zerocode-automation -[gitter chat room]: https://gitter.im/zerocode-testing/help-and-usage diff --git a/ISSUES.md b/ISSUES.md new file mode 100644 index 000000000..29d40834f --- /dev/null +++ b/ISSUES.md @@ -0,0 +1,4 @@ +## How To Raise Issues Correctly +Visit this [link](https://github.com/authorjapps/zerocode/wiki/Guidelines-for-raising-issues#right-way) + +Visit this [page](https://github.com/authorjapps/zerocode/wiki/Guidelines-for-raising-issues) to learn more in a context. diff --git a/README.md b/README.md index dc39e56f5..dc3db5470 100644 --- a/README.md +++ b/README.md @@ -1,2275 +1,141 @@ Zerocode Zerocode === -[![License](https://img.shields.io/hexpm/l/plug.svg)](https://github.com/authorjapps/zerocode/blob/master/LICENSE) -[![Build Status](https://travis-ci.org/authorjapps/zerocode.svg?branch=master)](https://travis-ci.org/authorjapps/zerocode) -[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/zerocode-testing/help-and-usage) -[![Performance Testing](https://img.shields.io/badge/performance-testing-ff69b4.svg)](https://github.com/authorjapps/zerocode/wiki/Load-or-Performance-Testing-(IDE-based)) -[![Twitter Follow](https://img.shields.io/twitter/follow/ZerocodeEasyTDD.svg?style=social&label=Follow)](https://twitter.com/ZerocodeEasyTDD) -> _Automated API testing was never so easy before._ +A no-code automated testing framework for Data streams(Kafka), microservices APIs, and databases using JSON. -Zerocode makes it easy to create and maintain automated tests with absolute minimum overhead for [REST](https://github.com/authorjapps/zerocode/wiki/User-journey:-Create,-Update-and-GET-Employee-Details),[SOAP](https://github.com/authorjapps/zerocode/blob/master/README.md#soap-method-invocation-example-with-xml-input), [Kafka](https://github.com/authorjapps/zerocode/wiki/Kafka-Testing-Introduction), [DB services](https://github.com/authorjapps/zerocode/wiki/Sample-DB-SQL-Executor) and more. Jump to the [quick-start section](https://github.com/authorjapps/zerocode/blob/master/README.md#getting-started-) or [HelloWorld](https://github.com/authorjapps/zerocode/blob/master/README.md#hello-world-) section to explore more. -Zerocode is used by many companies such as Vocalink, HSBC, HomeOffice(Gov) and [others](https://github.com/authorjapps/zerocode/blob/master/README.md#smart-projects-using-zerocode) to achieve zero-defect production drop of their micro-services. +[![API](https://img.shields.io/badge/api-automation-blue)](https://github.com/authorjapps/zerocode/wiki/What-is-Zerocode-Testing) +[![Performance Testing](https://img.shields.io/badge/performance-testing-8A2BE2)](https://github.com/authorjapps/zerocode/wiki/Load-or-Performance-Testing-(IDE-based)) +[![Kafka Testing](https://img.shields.io/badge/kafka-testing-blue)](https://zerocode-tdd.tddfy.com/kafka/Kafka-Testing-Introduction) -It is a light-weight, simple and extensible open-source framework for writing test intentions in simple JSON format that facilitates both declarative configuration and automation. The [framework manages](https://github.com/authorjapps/zerocode/wiki/What-is-Zerocode-Testing) the step-chaining, request payload handling and response assertions at the same time, same place using [JSON Path](https://github.com/json-path/JsonPath/blob/master/README.md#path-examples). - -For example, if our REST API returns the following from URL "`https://localhost:8080/api/customers/123`" with status `200`, -```javaScript -{ - "id": 123, - "type": "Premium High Value", - "addresses": [ - { - "type":"holiday", - "line1":"Mars" - } - ] -} -``` - -then, we can easily validate the above API using `Zerocode` like below. - -> _The beauty here is, we can use the JSON payload structure as it is without any manipulation._ - - -```javaScript -{ - "url": "api/customers/123", - "operation": "GET", - "request": {}, - "verifications": { - "status": 200, - "body": { - "id": 123, - "type": "Premium High Value", - "addresses": [ - { - "type":"holiday", - "line1":"Mars" - } - ] - } - } -} -``` - -Or - -```javaScript -{ - ... - "verifications": { - "body": { - "id": "$NOT.NULL", // A not-null indeterministic value - "addresses.SIZE": "$GT.0" // Only the length validation(not the contents) - Greater Than 0. - } - } -} -``` - -Or - -```javaScript -{ - ... - "verifications": { - "body": { - "type": "$CONTAINS.STRING:Premium High" // Matches only part of the value - } - } -} -``` - -Or - -```javaScript -{ - ... - "verifications": { - "body": { - "addresses[?(@.type=='Holiday')].line1.SIZE": 1 // Indeterministic element position in an array - } - } -} -``` - -and run it simply by pointing to the above JSON file from a "JUnit" @Test method. - -```java - @Test - @Scenario("test_customer_get_api.json") - public void getCustomerHappy(){ - // No code goes here. This remains empty. - } -``` - -Looks simple n easy? Why not give a try? See the [quick-start section](https://github.com/authorjapps/zerocode/blob/master/README.md#getting-started-) or [HelloWorld](https://github.com/authorjapps/zerocode/blob/master/README.md#hello-world-) section. - -Configuring Custom Http Client -=== -`@UseHttpClient` enables us to use any project specific custom Http client. See an example [here](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldcustomclient/GitHubSecurityHeaderTokenTest.java). -e.g. -```java -@UseHttpClient(CustomHttpClient.class) -public class GitHubSecurityHeaderTokenTest { -} -``` -But this feature is optional and the framework defaults to use Apache `HttpClients` for both http and https connections. - -Running a Single Scenario Test -=== -`ZeroCodeUnitRunner` is the JUnit runner which enables us to run a single or more test-cases from a JUnit test-class. -e.g. -```java -@TargetEnv("app_sit1.properties") -@RunWith(ZeroCodeUnitRunner.class) -public class GitHubHelloWorldTest { - - @Test - @Scenario("screening_tests/test_happy_flow.json") - public void testHappyFlow(){ - } - - @Test - @Scenario("screening_tests/test_negative_flow.json") - public void testNegativeFlow(){ - } - -} -``` - -Running a Suite of Tests -=== - -+ Selecting all tests as usual `JUnit Suite` - -```java -@RunWith(Suite.class) -@Suite.SuiteClasses({ - HelloWorldSimpleTest.class, - HelloWorldMoreTest.class, -}) - -public class HelloWorldJunitSuite { - // This class remains empty -} -``` - -Or -+ Selecting tests by cherry-picking from test resources -```java -@TargetEnv("app_dev1.properties") -@UseHttpClient(CustomHttpClient.class) -@RunWith(ZeroCodePackageRunner.class) -@Scenarios({ - @Scenario("path1/test_case_scenario_1.json"), - @Scenario("path2/test_case_scenario_2.json"), -}) -public class HelloWorldSelectedGitHubSuite { - // This space remains empty -} -``` - -Python -=== -If you are looking for simillar REST API testing DSL in Python(YAML), -Then visit this open-source [pyresttest](https://github.com/svanoort/pyresttest#sample-test) lib in the GitHub. - -In the below example - -- `name` is equivalent to `scenarioName` -- `method` is equivalent to `operation` -- `validators` is equivalent to `verifications` or `assertions` of Zerocode - -```yaml -- test: # create entity by PUT - - name: "Create or update a person" - - url: "/api/person/1/" - - method: "PUT" - - body: '{"first_name": "Gaius","id": 1,"last_name": "Baltar","login": "gbaltar"}' - - headers: {'Content-Type': 'application/json'} - - validators: # This is how we do more complex testing! - - compare: {header: content-type, comparator: contains, expected:'json'} - - compare: {jsonpath_mini: 'login', expected: 'gbaltar'} # JSON extraction - - compare: {raw_body:"", comparator:contains, expected: 'Baltar' } # Tests on raw response -``` - -The [Quick-Start](https://github.com/svanoort/pyresttest/blob/master/quickstart.md) guide explains how to bring up a REST end point and run the tests. - -Load Testing -=== -Use Zerocode declarative [parallel load generation](https://github.com/authorjapps/zerocode/blob/master/README.md#generating-load-for-performance-testing-aka-stress-testing) on the target system. - -Declarative TestCase - Hooking BDD Scenario Steps -=== - -![declarative_reduced](https://user-images.githubusercontent.com/12598420/53393304-ee26b280-3993-11e9-8522-983635c054d7.png) - -It eliminates the repetitive code such as Java step definitions, test assertions, payload parsing, API calls such as Http, Kafka, DB Services and much more. See an example [how](https://github.com/authorjapps/zerocode/wiki/User-journey:-Create,-Update-and-GET-Employee-Details). It's powerful JSON comparison and assertions make the testing cycle a lot easy and clean. - -It has got best of best ideas and practices from the community to keep it super simple and the adoption is rapidly growing among the developer/tester community. It alleviates pain and brings the simplicity in validating the APIs. - -It also helps in mocking/stubbing interfacing APIs during the testing cycle in a declarative-fashion as a [test-step](https://github.com/authorjapps/zerocode/blob/master/README.md#using-wiremock-for-mocking-dependent-end-points) as well as [standalone](https://github.com/authorjapps/api-mock-maker) mock-server deployed locally or into cloud. Its approach to IDE based performance testing to generate load/stress on the target application is quite simple, flexible and efficient - enabling us to simply reuse the test(s) from our regression pack. - -Here the host and port are maintained in a properties file to enable easy environment-switching. -``` -host_env1.properties --------------------- -web.application.endpoint.host=https://api.github.com -web.application.endpoint.port=443 -``` - -e.g. Our below User-Journey or AC(Acceptance Criteria) or a Scenario, -```JSON -AC1: -GIVEN- the POST api end point '/api/v1/users' to create an user, -WHEN- I invoke the API, -THEN- I will receive the 201 response status with the a {created-User-Id} -AND- I will validate the response - -AC2: -GIVEN- the REST api GET end point '/api/v1/users/${id}', -WHEN- I invoke the API, -THEN- I will receive the 200(Ok) response status with body(user details) and headers -AND- I will validate the response -``` -translates to the below executable JSON steps in `Zerocode` - Simple and clean !
-_(See here [a full blown CRUD operation scenario](https://github.com/authorjapps/zerocode/wiki/User-journey:-Create,-Update-and-GET-Employee-Details) with POST, PUT, GET, DELETE example.)_
- -post_get_user - -That's it, the simple JSON steps. No other step definition coding needed.
-~~_No feature files, no extra plugins, no assertThat(...), no statements or grammar syntax overhead._~~ - -And it is **declarative** JSON DSL, with the `request/response` fields available for the step chaining via the `JSON Path`. - -See the [Table Of Contents](https://github.com/authorjapps/zerocode#table-of-contents--) for usages and examples. - -Maven and CI ๐Ÿ”จ -==== -**Latest release: [1.3.x](https://search.maven.org/search?q=zerocode-tdd)** ๐Ÿน - -**Continuous Integration:** [![Build Status](https://travis-ci.org/authorjapps/zerocode.svg?branch=master)](https://travis-ci.org/authorjapps/zerocode)
-**HelloWorld:** [Calling a GitHub api](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/resources/helloworld/hello_world_status_ok_assertions.json) step and executing [Test](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworld/JustHelloWorldTest.java) code.
-**Help and Usage:** [Table of Contents](https://github.com/authorjapps/zerocode#table-of-contents--)
-**Wiki:** [About Zerocode](https://github.com/authorjapps/zerocode/wiki)
-**License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
-**Mailing List:** [Mailing List](https://groups.google.com/forum/#!forum/zerocode-automation)
-**Chat Room:** [Gitter Chat ](https://gitter.im/zerocode-testing/help-and-usage)
- -> The purpose of Zerocode lib is to make our API tests easy to **write**, easy to **change**, easy to **share**. - -Maven dependency xml: -```xml - - org.jsmart - zerocode-tdd - 1.3.x - -``` - -> _Jump to [Getting Started](https://github.com/authorjapps/zerocode/blob/master/README.md#getting-started-)_ - -
- -Hello World ๐Ÿ™Œ -==== - -In a typical TDD approach, Zerocode is used in various phases of a project to pass though various quality gates. -This makes the TDD cycle very very easy, clean and efficient. -e.g. -+ NFR - Performance Testing -+ NFR - Security Testing -+ DEV - Integration Testing -+ DEV - Dev Build/In-Memory Testing -+ CI - End to End Testing Build -+ CI - SIT(System Integration Testing) Build -+ CI - Contract Test Build -+ CI - DataBase Integrity Testing -+ MANUAL - Manual Testing like usual REST clients(Postman or Insomnia etc) -+ MOCK - API Mocking/Service Virtualization - - -Clone or download the below quick-start repos to run these from your local IDE or maven. - - * Quick start - [**Hello World** examples](https://github.com/authorjapps/zerocode-hello-world)
- - * Quick start - [**Hello World Kafka Testing** examples](https://github.com/authorjapps/hello-kafka-stream-testing)
- - * Quick start - [**API Contracts testing** - Interfacing applications](https://github.com/authorjapps/consumer-contract-tests)
- - * Quick start - [**Performance** testing - Varying **Load/Stress** generation](https://github.com/authorjapps/performance-tests)
- - * Quick start - [**Spring Boot** application - **Integration testing** - In-Memory](https://github.com/authorjapps/spring-boot-integration-test)
- - * Quick start - [**Performance testing** - Resusing Spring JUnit tests(`less common`) - JUnit-Spring-Zerocode](https://github.com/authorjapps/zerocode-spring-junit)
- - * Quick start - [**Kotlin Integration** - A Simple Kotlin Application - Dev and Test Best Practice](https://github.com/BeTheCodeWithYou/SpringBoot-Kotlin)
- - -To build any of the above projects, we can use the following command -``` -mvn clean install -DskipTests -``` - -For selected module build -> mvn clean install -pl core,http-testing - -
- -Upcoming Releases ๐Ÿผ -==== -+ Kafka - Testing Distributed Data Stream application (Easy and fun) ๐Ÿ”œ
- + Multi Topic `produce` and `consume` ๐Ÿ”œ
- + KSQL Integration ๐Ÿ”œ
- + `produce` and `consume` JSON messages - + Test `avro` schema registry along with REST Proxy -+ WebHook and WebSocket HelloWord Examples - -
- -Supported testing frameworks: -=== - * [JUnit](http://junit.org) - * [JUnit5 Jupiter Support](https://github.com/authorjapps/zerocode/wiki/JUnit5-Jupiter-Parallel-Load-Extension) - -
- -~~Testing no more a harder, slower and sleepless task~~ - -See the [HelloWorldTest](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworld/JustHelloWorldTest.java) and [more](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldmore/JustHelloWorldMoreTest.java) +**Latest release:๐Ÿน** [![Maven](https://maven-badges.herokuapp.com/maven-central/org.jsmart/zerocode-tdd/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.jsmart/zerocode-tdd/)
+**CI Testing:** ![example workflow](https://github.com/authorjapps/zerocode/actions/workflows/main.yml/badge.svg)
+**Issue Discussions:** [Slack](https://join.slack.com/t/zerocode-workspace/shared_invite/enQtNzYxMDAwNTQ3MjY1LTA2YmJjODJhNzQ4ZjBiYTQwZDBmZmNkNmExYjA3ZDk2OGFiZWFmNWJlNGRkOTdiMDQ4ZmQyNzcyNzVjNWQ4ODQ)
+**Mailing List:** [Mailing List](https://groups.google.com/forum/#!forum/zerocode-automation)
+**License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
-Kafka Testing -=== -Visit the page [Kafka Testing Introduction](https://github.com/authorjapps/zerocode/wiki/Kafka-Testing-Introduction) for step-by-step approach. - -#### Current Release Covers -+ Kafka - Testing Distributed Data Stream application (Easy and fun)
- + Simple `produce` and `consume` - + `produce` and `consume` RAW messages - + `produce` and `consume` JSON messages - + Test `avro` schema registry along with REST Proxy -+ Kafka - HelloWorld examples and Wiki on dockerized testinng
-
+Zerocode makes it easy to create and maintain automated tests with absolute minimum overhead for [REST](https://github.com/authorjapps/zerocode/wiki/User-journey:-Create,-Update-and-GET-Employee-Details),[SOAP](https://github.com/authorjapps/zerocode/blob/master/README.md#soap-method-invocation-example-with-xml-input), [Kafka Real Time Data Streams](https://github.com/authorjapps/zerocode/wiki/Kafka-Testing-Introduction) and much more. +It has the best of best ideas and practices from the community to keep it super simple, and the adoption is rapidly growing among the developers & testers community. -DataBase(DB) Integration Testing +Documentation === -Visit the page [Database Validation](https://github.com/authorjapps/zerocode/wiki/Sample-DB-SQL-Executor) for step-by-step approach. - -
+Visit here : ++ [Documentation](https://zerocode-tdd.tddfy.com) - Indexed & instantly finds you the results ++ Want to amend or improve any documentation? Steps and guidelines are [here](https://github.com/authorjapps/zerocode/wiki/Documentation-Steps) -Maven Dependencies +IDE Support By === -[Search in the Maven Portal](https://search.maven.org/search?q=zerocode-tdd) or [View in Maven repo](https://mvnrepository.com/artifact/org.jsmart/zerocode-tdd) - -[More (Wiki) >>](https://github.com/authorjapps/zerocode/wiki) - -
+[Jetbrains IDE](https://www.jetbrains.com/idea/) -Smart Projects Using Zerocode -=== - + [Vocalink (A Mastercard company)](https://www.vocalink.com/) - REST API testing for virtualization software - + [HSBC Bank](https://www.hsbc.co.uk/) - MuleSoft application REST API Contract testing, E2E Integration Testing, Oracle DB API testing, SOAP testing and Load/Stress aka Performance testing - + [Barclays Bank](https://www.barclays.co.uk/) - Micro-Services API Contract Validation for System APIs build using Spring Boot - + [Home Office(GOV.UK)](https://www.gov.uk/government/organisations/home-office) - Micro-Services REST API Contract testing, HDFS/Hbase REST end point testing, Kafka Data-pipeline testing, Authentication testing. -Latest news/releases/features +Maven Dependency === -Follow us(Twitter) -download - -
++ [New releases - zerocode-tdd](https://maven-badges.herokuapp.com/maven-central/org.jsmart/zerocode-tdd/) +[![Maven](https://maven-badges.herokuapp.com/maven-central/org.jsmart/zerocode-tdd/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.jsmart/zerocode-tdd/) ++ _[Older releases - zerocode-rest-bdd](https://maven-badges.herokuapp.com/maven-central/org.jsmart/zerocode-rest-bdd/)_ +[![Maven](https://maven-badges.herokuapp.com/maven-central/org.jsmart/zerocode-rest-bdd/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.jsmart/zerocode-rest-bdd/) -Getting started โ›นโ€โ™‚ +Introduction === +Zerocode is a modern, lightweight, and extensible open-source framework designed for writing executable test scenarios using simple JSON or YAML formats. It supports both declarative configuration and automation, making it user-friendly and efficient. -Add these `two` maven dependencies: -```xml - - org.jsmart - zerocode-tdd - 1.3.x - test - - - - junit - junit - 4.12 - test - -``` - -Then annotate our `JUnit` test method pointing to the JSON file as below and `run` as a unit test. -That's it. Done. - -```java -@TargetEnv("github_host.properties") -@RunWith(ZeroCodeUnitRunner.class) -public class JustHelloWorldTest { - - @Test - @Scenario("helloworld/hello_world_status_ok_assertions.json") - public void testGet() throws Exception { - - } -} -``` -Where, We just need the below `hello_world_status_ok_assertions.json`. +In essence, Zerocode simplifies the complexities of modern API and data-streaming automation, including Kafka. The framework seamlessly handles response validations, target API invocations, load/stress testing, and security testing, all through straightforward YAML/JSON/Fluent steps. +For example, if your REST API URL `https://localhost:8080/api/v1/customers/123` with `GET` method and `"Content-Type": "application/json"` returns the following payload and a `http` status code `200(OK)` , ```javaScript +Response: { - "scenarioName": "Invoke the GET api and assert the response", - "steps": [ + "id": 123, + "type": "Premium High Value", + "addresses": [ { - "name": "get_user_details", - "url": "/users/octocat", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body": { - "login" : "octocat", - "id" : 33847731, - "type" : "User" - } - } + "type":"home", + "line1":"10 Random St" } ] } ``` -the `github_host.properties` looks as below: -``` -web.application.endpoint.host=https://api.github.com -web.application.endpoint.port=443 -web.application.endpoint.context= -``` - -And the assertThat(...), GIVEN-WHEN-THEN steps become implicit. We don't have to deal with them explicitly as the framework handles these complexities and makes the testing very very easy
- -~~GIVEN- the GitHub REST api GET end point,~~
-~~WHEN- I invoke the API,~~
-~~THEN- I will receive 200(OK) status with body and assert the response~~
- -or - -~~GIVEN- the GitHub REST url and the method GET,~~
-~~WHEN- I invoke the API,~~
-~~THEN- I will receive 200(OK) status with body~~
-~~AND assert the response~~
- -or - -~~GIVEN- the GET methos~~
-~~AND the http url of GitHub api~~
-~~WHEN- I invoke the API using a HTTP client,~~
-~~THEN- I will receive 200(OK) status with body~~
-~~AND assert the response~~
- -or - - -~~HttpResponse response =~~ - -~~aHttpClient.get("https:///users/octocat")~~ - - ~~.header("accept", "application/json")~~ - - ~~.execute();~~ - -~~User user = response.getUser();~~ - -~~assertThat(response.getStatusCode(), is(200))~~ - -~~assertThat(user.getId(), is(33847731))~~ - -~~assertThat(user.getLogin(), is("octocat"))~~ - -~~assertThat(user.getType(), is("user"))~~ - -
- -See more usages and examples below. - -## Table of Contents - -- [Help and usage](#1) -- [Overriding with Custom HttpClient with Project demand, See also SSL Trusted Http Client](#16) -- [Externalize host and port to properties files](#17) -- [Using any properties file key-value in the steps](#17.1) -- [Single Scenario with single step](#2) -- [Generating Load or stress for performance testing](#27) -- [Step with more assertions](#3) -- [Running with step loop](#4) -- [Running with scenario loop](#5) -- [Generated reports and charts](#6) -- [More assertion with handy place holders](#7) -- [General Place holders](#8) -- [Chaining multiple steps for a scenario](#10) -- [Enabling ignoreStepFailures for executing all steps in a scenario](#10.1) -- [Generating random strings, random numbers and static strings](#11) -- [Asserting general and exception messages](#12) -- [Asserting with LT(lesser than) and GT(greater than)](#13) -- [Dealing with arrays](#9) -- [Asserting an empty array](#14) -- [Asserting an array SIZE](#141) -- [Calling java methods(apis) for specific tasks)](#15) -- [Generating IDs and sharing across steps](#18) -- [Bare JSON String without curly braces, still a valid JSON](#19) -- [Passing Headers to the REST API](#20) -- [Passing "Content-Type": "application/x-www-form-urlencoded" header](#20.1) -- [Handling Content-Type with charset-16 or charset-32](#20.2) -- [Setting Jenkins env propperty and picking environment specific properties file](#21) -- [LocalDate and LocalDateTime format example](#22) -- [SOAP method invocation example using xml input](#23) -- [SOAP method invocation where Corporate Proxy enabled](#24) -- [MIME Type Converters- XML to JSON, prettyfy XML etc](#25) -- [Using WireMock for mocking dependent end points](#26) -- [Basic http authentication step using zerocode](#28) -- [Sending query params in URL or separately](#29) -- [General place holders and assertion place holder table](#99) -- [References and Dicussions](#100) - - -### examples: - -#### 1: -#### Help and usage - -Download this help and usage project to try it yourself. - -- HelloWorld project: https://github.com/authorjapps/zerocode-hello-world - -- Simple steps to run: https://github.com/authorjapps/zerocode-hello-world#zerocode-hello-world - -- Git [Clone](https://github.com/authorjapps/zerocode-hello-world) or [Download](https://github.com/authorjapps/zerocode-hello-world/archive/master.zip) the zip file(contains a maven project) to run locally - +then, we can easily validate the above API using `Zerocode` like below. -#### 2: -#### Single Scenario with single step ++ Using JSON -A scenario might consists of one or more steps. Let's start with single step Test Case: -```javaScript +```JSON { - "scenarioName": "Vanilla - Will Get Google Employee Details", - "steps": [ - { - "name": "step1_get_google_emp_details", - "url": "http://localhost:9998/google-emp-services/home/employees/999", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200 - } + "url": "api/v1/customers/123", + "method": "GET", + "request": { + "headers": { + "Content-Type": "application/json" } - ] -} -``` - -Note: -The above JSON block is a test case where we asked the test framework to hit the -> REST end point : http://localhost:9998/google-emp-services/home/employees/999 - -> with method: GET - -> and asserting the REST response with an - -> expected status: 200 - -> where, step "name" is a meaningful step name, which is significant when multiple steps are run. See a multi-step example. - -Note: -> scenarioname : is free text - -> step name: free text without any space - - -The above test case will PASS as the end point actually responds as below. Look at the "response" section below. -```javaScript + }, + "retry": { + "max": 3, + "delay": 1000 + }, + "verify": { + "status": 200, + "headers": { + "Content-Type" : [ "application/json; charset=utf-8" ] + }, + "body": { + "id": 123, + "type": "Premium Visa", + "addresses": [ { - "name": "Sample_Get_Employee_by_Id", - "operation": "GET", - "url": "/google-emp-services/home/employees/999", - "response": { - "status": 200, - "body": { - "id": 999, - "name": "Larry P", - "availability": true, - "addresses":[ - { - "gpsLocation": "x3000-y5000z-70000" - }, - { - "gpsLocation": "x3000-y5000z-70000S" - } - ] - } - } + "type": "Billing", + "line1": "10 Random St" } -``` - -The following Test Case will fail. Why? - -Because you are asserting with an expected status as 500, but the end point actually returns 200. - -```javaScript -{ - "scenarioName": "Vanilla - Will Get Google Employee Details", - "steps": [ - { - "name": "step1_get_google_emp_details", - "url": "http://localhost:9998/google-emp-services/home/employees/999", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 500 - } - } - ] -} -``` - -#### 27: -#### Generating load for performance testing aka stress testing -+ Browse or clone this [sample performance-tests repo](https://github.com/authorjapps/performance-tests) with examples. - + Take advantage of the following two extended Junit load runners from the lib- - -> @RunWith(ZeroCodeLoadRunner.class) - -and - -> @RunWith(ZeroCodeMultiLoadRunner.class) - -- Load a single scenario using `ZeroCodeLoadRunner` (See example of [ZeroCodeMultiLoadRunner here](https://github.com/authorjapps/performance-tests#multi-scenario-parallel-load)) - -```java -@LoadWith("load_config_sample.properties") -@TestMapping(testClass = TestGitGubEndPoint.class, testMethod = "testGitHubGET_load") -@RunWith(ZeroCodeLoadRunner.class) -public class LoadGetEndPointTest { + ] + } + }, + "verifyMode": "LENIENT" } ``` -- The load generation properties are set here `load_config_sample.properties`. Learn [more >>](https://github.com/authorjapps/zerocode/wiki/Load-or-Performance-Testing-(IDE-based)#how-to-run-tests-in-parallel-in-context-of-one-or-more-scenarios-) -```properties -number.of.threads=2 -ramp.up.period.in.seconds=10 -loop.count=1 -abort.after.time.lapsed.in.seconds=600 -``` -- The test case for GET api is mapped or fed into the load runner as below: -> @TestMapping(testClass = TestGitGubEndPoint.class, testMethod = "testGitHubGET_load") ++ or Using YAML -which verifies the response in the `assertions` section - +```yaml -```javascript -{ - "scenarioName": "Load testing- Git Hub GET API", - "steps": [ - { - "name": "get_user_details", - "url": "/users/octocat", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body": { - "login" : "octocat", - "id" : 583231, - "avatar_url" : "https://avatars3.githubusercontent.com/u/583231?v=4", - "type" : "User", - "name" : "The Octocat", - "company" : "GitHub" - } - } - } - ] -} -``` -- In one of the response during the load, if the `actual response` does not match the `expected response` i.e. in the `assertions` section above, then the test will fail. -- [Browse the above example](https://github.com/authorjapps/zerocode-hello-world) in GitHub. +--- +url: api/v1/customers/123 +method: GET +request: + headers: + Content-Type: application/json +retry: + max: 3 + delay: 1000 +verify: + status: 200 + headers: + Content-Type: + - application/json; charset=utf-8 + body: + id: 123 + type: Premium Visa + addresses: + - type: Billing + line1: 10 Random St +verifyMode: LENIENT +``` + + +> _The beauty here is, we can use the payload/headers structure for validation as it is without any manipulation +> or +> use a flat JSON path to skip the hassles of the entire object hierarchies._ + + +Looks simple & easy? Why not give it a try? Visit the [quick-start guide](https://zerocode-tdd.tddfy.com/microservices/start) or -- [Download as zip](https://github.com/authorjapps/zerocode-hello-world/archive/master.zip) the above maven project to run from your IDE. - -[More (Learn advantages of load testing using your IDE(Eclipse or Intellij etc)) >>](https://github.com/authorjapps/zerocode/wiki/Load-or-Performance-Testing-(IDE-based)) - -#### 3: -#### Single step with more assertions - -```javaScript -{ - "scenarioName": "Vanilla - Will Get Google Employee Details", - "steps": [ - { - "name": "step1_get_google_emp_details", - "url": "http://localhost:9998/google-emp-services/home/employees/999", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body": { - "id": 999, - "name": "Larry P", - "availability": true, - "addresses":[ - { - "gpsLocation": "x3000-y5000z-70000" - }, - { - "gpsLocation": "x3000-y5000z-70000S" - } - ] - } - } - } - ] -} -``` - -The above Test Case will PASS as the assertions section has all expected values matching the end point's response. - -#### 4: -#### Running with step _loop_ - -- Usage: See here: [Step loop](https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios/02_using_step_loop.json) - -- _loop_ field in a step will execute the step that many number of time. - -```javaScript -{ - "scenarioName": "Vanilla - Execute multiple times - Step", - "steps": [ - { - "loop": 2, - "name": "get_room_details", - "url": "http://localhost:9998/google-emp-services/home/employees/101", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body": { - "id": 101 - } - } - } - ] -} -``` - - -#### 5: -#### Running with scenario _loop_ - -- Usage: See here: [Scenario loop](https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios/03_using_scenario_loop.json) -Runs the entire scenario two times i.e. executing both the steps once for each time. - -```javaScript -{ - "scenarioName": "Vanilla - Execute multiple times - Scenario", - "loop": 2, - "steps": [ - { - "name": "get_room_details", - "url": "http://localhost:9998/google-emp-services/home/employees/101", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body": { - "id": 101 - } - } - }, - { - "name": "get_another_room_details", - "url": "http://localhost:9998/google-emp-services/home/employees/102", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body": { - "id": 102 - } - } - } - ] -} -``` - - -#### 6: -#### Generated reports and charts - -_(For Gradle build setup - See [here - Wiki](https://github.com/authorjapps/zerocode/wiki/Gradle-build-for-JUnit-Smart-Chart-and-CSV-Reports))_ - -Generated test statistics reports. See the '/target' folder after every run. -e.g. Look for- - -> target/zerocode-junit-granular-report.csv - -> target/zerocode-junit-interactive-fuzzy-search.html - -See some sample reports below: - -##### Spike Chart: - -1. [Full coverage CSV report](https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/zz_reports/zerocode_full_report_2016-07-30T11-44-14.512.csv) - -1. [Interactive - Chart(Filter by Author, Test name, status etc)](http://htmlpreview.github.io/?https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/zz_reports/zerocode-interactive.html) - - -##### CSV Report: - -- See here : [Full coverage CSV report](https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/zz_reports/zerocode_full_report_2016-07-30T11-44-14.512.csv) - -``` -If target folder has permission issue, the library alerts with- ----------------------------------------------------------------------------------------- -Somehow the 'target/zerocode-test-reports' is not present or has no report JSON files. -Possible reasons- - 1) No tests were activated or made to run via ZeroCode runner. -or- - 2) You have simply used @RunWith(...) and ignored all tests -or- - 3) Permission issue to create/write folder/files - 4) Please fix it by adding/activating at least one test case or fix the file permission issue ----------------------------------------------------------------------------------------- -``` - -#### 7: -#### More assertion with handy place holders - -- Link: [See test cases folder](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios) - - -#### 8: -#### REST endpoint calls with General Place holders - - -- Link: [See test cases folder](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios) - - -#### 9: -#### Step dealing with arrays - -- Link: [See test cases folder](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios) - -##### Finding the occurance of an element in the array response -e.g. your actual response is like below, -Your use-case is, `Dan` and `Mike` might not be returned in the same order always, but they appear only once in the array. -``` -Url: "/api/v1/screening/persons", -Operation: "GET", -Response: -{ - "status": 200, - "body": { - "type" : "HIGH-VALUE", - "persons":[ - { - "id": "120.100.80.03", - "name": "Dan" - }, - { - "id": "120.100.80.11", - "name": "Mike" - } - ] - } -} -``` -To assert the above situation, you can find the element using `JSON path` as below and verify 'Dan' was returned only once in the array and 'Emma' was present in the 'persons' array. -(See more JSON paths [here](https://github.com/json-path/JsonPath)) -``` -{ - "scenarioName": "Scenario- Get all person details", - "steps": [ - { - "name": "get_screening_details", - "url": "/api/v1/screening/persons", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body": { - "type": "HIGH-VALUE", - "persons.SIZE": 2, - "persons[?(@.name=='Dan')].id.SIZE": 1, - "persons[?(@.name=='Mike')].id.SIZE": 1, - "persons[?(@.name=='Emma')].id.SIZE": 0 - } - } - } - ] -} -``` -What `persons[?(@.name=='Dan')].id.SIZE` means is- -> In the `persons` array check every element with the name `Dan`, if found pick the `id` of element and return all of the `id`s as an array, then do `.SIZE` on the `id`s array and return a count. - -Note- -Even if a single matching element is found, the return is always an array type. Also if you do a `.length()` on the returned `id`s e.g. `persons[?(@.name=='Dan')].id.length()`, that's also an array i.e. `[2]` instead of simple `2`. That's how JSON path behaves. Hence `.SIZE` helps to achieve this. - -Run [the above test case](https://github.com/authorjapps/consumer-contract-tests/blob/master/src/test/resources/contract_tests/screeningservice/find_element_in_array_via_jsonpath.json) from [here - testFindElementInArray()](https://github.com/authorjapps/consumer-contract-tests/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/screeningservice/ScreeningServiceContractTest.java). - - -#### 10: -#### Chaining multiple steps for a scenario - -Chaining steps: Multi-Step REST calls with earlier response(IDs etc) as input to next step - -```javaScript -{ - "scenarioName": "12_chaining_multiple_steps_using_previous_response", - "steps": [ - { - "name": "create_new_employee", - "url": "http://localhost:9998/google-emp-services/home/employees", - "operation": "POST", - "request": {}, - "assertions": { - "status": 201, - "body": { - "id": 1000 - } - } - }, - { - "name": "get_and_verify_created_employee", - "url": "http://localhost:9998/google-emp-services/home/employees/$%7B$.create_new_employee.response.body.id%7D", //<--- ID from previous response // - "operation": "GET", - "request": {}, - "assertions": { - "status": 200, - "body": { - "id": 1000, - "name": "${$.create_new_employee.response.body.name}", - "addresses": [ - { - "gpsLocation": "${$.create_new_employee.response.body.addresses[0].gpsLocation}" - }, - { - "gpsLocation": "${$.create_new_employee.response.body.addresses[1].gpsLocation}" - } - ] - } - } - } - ] -} -``` - -- Example : [Scenario with two steps - 1st create and then get](https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios/12_chaining_multiple_steps_with_prev_response.json) -- Link: [See test cases folder](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios) - -#### 10.1: -#### Enabling ignoreStepFailures for executing all steps in a scenario - -Setting `"ignoreStepFailures": true` will allow to execute the next step even if the earlier step failed. - -e.g. -``` -{ - "scenarioName": "Multi step - ignoreStepFailures", - "ignoreStepFailures": true, - "steps": [ - -``` - -See HelloWorld repo for a running example. - - -#### 11: -#### Generating random strings, random numbers and static strings - -Random UUID- -```javaScript -{ - "scenarioName": "random_UUID", - "steps": [ - { - "name": "create_new_employee", - "url": "http://localhost:9998/google-emp-services/home/employees", - "operation": "POST", - "request": { - "body": { - "id": "${RANDOM.UUID}", //<-- Everytime it creates unique uuid. See below example. - "name": "Elen M" - } - }, - "assertions": { - "status": 201 - } - } - ] -} - -Resolves to- -{ - "scenarioName": "random_UUID", - "steps": [ - { - "name": "create_new_employee", - "url": "http://localhost:9998/google-emp-services/home/employees", - "operation": "POST", - "request": { - "body": { - "id": "94397df8-0e9e-4479-a2f9-9af509fb5998", //<-- Every time it runs, it creates an unique uuid - "name": "Elen M" - } - }, - "assertions": { - "status": 201 - } - } - ] -} -``` - -Random String of specific length- -```javaScript -{ - "scenarioName": "13_random_and_static_string_number_place_holders", - "steps": [ - { - "name": "create_new_employee", - "url": "http://localhost:9998/google-emp-services/home/employees", - "operation": "POST", - "request": { - "body": { - "id": 1000, - "name": "Larry ${RANDOM.STRING:5}", //<-- Random number of length 5 chars - "password": "${RANDOM.STRING:10}" //<-- Random number of length 10 chars - } - }, - "assertions": { - "status": 201 - } - } - ] -} -``` - -resolves to the below POST request to the end point: -```javaScript -step:create_new_employee -url:http://localhost:9998/google-emp-services/home/employees -method:POST -request: -{ - "body" : { - "id" : 1000, - "name" : "Larry tzezq", - "password" : "czljtmzotu" - } -} - -``` - -See full log in the log file, looks like this: -```javaScript -requestTimeStamp:2016-08-01T15:37:20.555 -step:create_new_employee -url:http://localhost:9998/google-emp-services/home/employees -method:POST -request: -{ - "body" : { - "id" : 1000, - "name" : "Larry tzezq", - "password" : "czljtmzotu" - } -} - -Response: -{ - "status" : 201, - ... -} -*responseTimeStamp:2016-08-01T15:37:20.707 -*Response delay:152.0 milli-secs ----------> Assertion: <---------- -{ - "status" : 201 -} --done- - ---------- RELATIONSHIP-ID: 4cfd3bfb-a537-49a2-84a2-0457c4e65803 --------- -requestTimeStamp:2016-08-01T15:37:20.714 -step:again_try_to_create_employee_with_same_name_n_password -url:http://localhost:9998/google-emp-services/home/employees -method:POST -request: -{ - "body" : { - "id" : 1000, - "name" : "Larry tzezq", - "password" : "czljtmzotu" - } -} ---------- RELATIONSHIP-ID: 4cfd3bfb-a537-49a2-84a2-0457c4e65803 --------- -Response: -{ - "status" : 201, - ... -} -*responseTimeStamp:2016-08-01T15:37:20.721 -*Response delay:7.0 milli-secs ----------> Assertion: <---------- -{ - "status" : 201 -} --done- - -``` - -- Link: [See test cases folder](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios) - - -#### 12: -#### Asserting general and exception messages - -Asserting with $CONTAINS.STRING: - -```javaScript -{ - ... - ... - "assertions": { - "status": 200, - "body": { - "name": "$CONTAINS.STRING:Larry" //<-- PASS: If the "name" field in the response contains "Larry". - } - } -} -``` - -- Similar way exception messages can be asserted for part or full message. - -- Link: [See test cases folder](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios) - - -#### 13: -#### Asserting with $GT or $LT - -$GT. - -```javaScript -{ - ... - ... - "assertions": { - "status": "$GT.198" //<--- PASS: 200 is greater than 198 - } -} - -``` - -$LT. -```javaScript -{ - ... - ... - "assertions": { - "status": "$LT.500" //<--- PASS: 200 is lesser than 500 - } -} - -``` - -- Link: [See full examples](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios) - - -#### 14: -#### Asserting an empty array with $[] - -```javaScript - { - ... - ... - "assertions": { - "status": 200, - "body": { - "id": "$NOT.NULL", - "vehicles": "$[]" //<--- PASS: if the response has empty "vehicles" - } - } - } -``` - -- Link: [See full examples](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios) - -#### 141: -#### Asserting an array SIZE -If your response contains the below: -``` -e.g. http response body: -{ - "results": [ - { - "id": 1, - "name": "Elon Musk" - }, - { - "id": 2, - "name": "Jeff Bezos" - } - ] -} -``` - -Then you can assert many ways for the desired result- - -```javaScript - { - ... - "assertions": { - "results.SIZE": 2 - } - } - --or- - { - ... - "assertions": { - "results.SIZE": "$GT.1" - } - } --or- - { - ... - "assertions": { - "results.SIZE": "$LT.3" - } - } -etc -``` - -See more SIZE examples [here](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/resources/helloworld_array_size/hello_world_array_size_assertions_test.json) in the [hello-world repo](https://github.com/authorjapps/zerocode-hello-world). - - -#### 15: -#### Calling java methods(apis) for doing specific tasks: -+ Sample tests are [here](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldjavaexec/HelloWorldJavaMethodExecTest.java) - + Example of request response as JSON - [See here](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/resources/helloworldjavaexec/hello_world_javaexec_req_resp_as_json.json) - + Example of passing a simple string e.g. DB SQL query for Postgres, MySql, Oracle etc - [See step-by-step details Wiki](https://github.com/authorjapps/zerocode/wiki/Sample-DB-SQL-Executor) - -- You can clone and execute from this repo [here](https://github.com/authorjapps/zerocode-hello-world) - -In case of - Java method request, response as JSON: -```javaScript -{ - "scenarioName": "Java method request, response as JSON", - "steps": [ - { - "name": "execute_java_method", - "url": "org.jsmart.zerocode.zerocodejavaexec.OrderCreator", - "operation": "createOrder", - "request": { - "itemName" : "Mango", - "quantity" : 15000 - }, - "assertions": { - "orderId" : 1020301, - "itemName" : "Mango", - "quantity" : 15000 - } - } - ] -} -``` - -Sample Java class and method used in the above step- -```java -public class OrderCreator { - - public Order createOrder(Order order){ - /** - * TODO- Suppose you process the "order" received, and finally return the "orderProcessed". - * Here it is hardcoded for simplicity and understanding purpose only - */ - - Order orderProcessed = new Order(1020301, order.getItemName(), order.getQuantity()); - - return orderProcessed; - } -} -``` -Order pojo looks like below, [full pojo src here](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/main/java/org/jsmart/zerocode/zerocodejavaexec/pojo/Order.java)- -```java -public class Order { - private Integer orderId; - private String itemName; - private Long quantity; - - @JsonCreator - public Order( - @JsonProperty("orderId")Integer orderId, - @JsonProperty("itemName")String itemName, - @JsonProperty("quantity")Long quantity) { - this.orderId = orderId; - this.itemName = itemName; - this.quantity = quantity; - } - - public Integer getOrderId() { - return orderId; - } - - public String getItemName() { - return itemName; - } - - public Long getQuantity() { - return quantity; - } - -``` - - -More examples here- - -- Multiple host in a properties file [See here an example test](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/resources/helloworldjavaexec/read_config_properties_into_test_case_1.json) - -- More [examples here](https://github.com/authorjapps/zerocode-hello-world/tree/master/src/test/resources/helloworldjavaexec) - - -#### 16: -#### Overriding with Custom HttpClient with Project demand - -See here how to pass custom headers in the HttpClient : [See usage of @UseHttpClient](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/HelloWorldCustomHttpClientSuite.java) - -See here custom one : [See usage of @UseHttpClient](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/main/java/org/jsmart/zerocode/zerocodejavaexec/httpclient/CustomHttpClient.java) - -e.g. -```java -@TargetEnv("github_host.properties") -@UseHttpClient(CustomHttpClient.class) -@RunWith(ZeroCodePackageRunner.class) -@TestPackageRoot("helloworld_github_REST_api") //<--- Root of the folder in test/resources to pick all tests -public class HelloWorldCustomHttpClientSuite { -} -``` - -#### 17: -#### Externalizing RESTful host and port into properties file(s). - -Note: -Each runner is capable of running with a properties file which can have host and port for specific to this runner. -- So one can have a single properties file per runner which means you can run the tests against multiple environments --OR- -- can have a single properties file shared across all the runners means all tests run against the same environment. - -** Note - As per Latest config update, we have updated endpoint configuration fields. -From the release 1.2.8 onwards we will be allowing `web.` and deprecating `restful.` in endpoint configurations. -We will take away support for `restful.` from endpoint configuration in the future releases. -Version 1.2.8 will work for both as we have made the framework backward compatible. - -e.g. - -"config_hosts_sample.properties" - -``` -web.application.endpoint.host=http://{host-name-or-ip} - -web.application.endpoint.port=9998 - -web.application.endpoint.context=/google-emp-services -``` - -The runner looks like this: -``` -@TargetEnv("config_hosts_sample.properties") -@RunWith(ZeroCodeUnitRunner.class) -public class ScreeningServiceContractTest { - - @Test - @Scenario("contract_tests/screeningservice/get_screening_details_by_custid.json") - public void testScreeningLocalAndGlobal() throws Exception { - } -} -``` - -- See example here of a test scenario: [hello-world test scenario](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/resources/helloworld/hello_world_status_ok_assertions.json) -``` -{ - "scenarioName": "GIVEN- the GitHub REST api, WHEN- I invoke GET, THEN- I will receive the 200 status with body", - "steps": [ - { - "name": "get_user_details", - "url": "/users/octocat", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body": { - "login" : "octocat", - "type" : "User" - } - } - } - ] -} -``` - -- See tests here using `ZeroCodeUnitRunner.class`: [hello-world via JUnit @Test](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworld/JustHelloWorldTest.java) -``` -@TargetEnv("github_host.properties") -@RunWith(ZeroCodeUnitRunner.class) -public class JustHelloWorldTest { - - @Test - @Scenario("helloworld/hello_world_status_ok_assertions.json") - public void testGet() throws Exception { - - } -} -``` - -- See tests here using `ZeroCodePackageRunner.class`: [hello-world suite](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/HelloWorldGitHubSuite.java) -``` -@TargetEnv("github_host.properties") -@UseHttpClient(SslTrustHttpClient.class) //<--- Optional, Needed for https/ssl connections. -@RunWith(ZeroCodePackageRunner.class) -@TestPackageRoot("helloworld_github_REST_api") //<--- Root of the package to pick all tests including sub-folders -public class HelloWorldGitHubSuite { - -} -``` - -- See tests here using `@RunWith(Suite.class)`: [Contract-test suite](https://github.com/authorjapps/consumer-contract-tests/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/ContractTestSuite.java) -``` -@Suite.SuiteClasses({ - RegulatoryServiceContractTest.class, - IdCheckServiceContractTest.class, - CorpLoanServiceContractTest.class, - ScreeningServiceContractTest.class -}) -@RunWith(Suite.class) -public class ContractTestSuite { - -} -``` - -#### 17.1: -#### Using any properties file key-value in the steps - -You can directly use the existing properties or introduce new common properties to be used in the test steps. -Usage: `${my_new_url}`, `${web.application.endpoint.host}`, `${X-APP-SAML-TOKEN}` etc - -This is particularly useful when you want to introduce one or more common properties to use across the test suite. :+1: -(Clone [HelloWorld repo](https://github.com/authorjapps/zerocode-hello-world) to run this from your IDE) - -e.g. - -"config_hosts_sample.properties" - -``` -web.application.endpoint.host=http://{host-name-or-ip} -web.application.endpoint.port=9998 -web.application.endpoint.context=/google-emp-services -# or e.g. some new properties you introduced -my_new_url=http://localhost:9998 -X-APP-SAML-TOKEN=token-xyz -``` - -Then, you can simply use the properties as below. -```json -{ - "scenarioName": "New property keys from host config file", - "steps": [ - { - "name": "get_api_call", - "url": "${web.application.endpoint.host}:${web.application.endpoint.port}/home/bathroom/1", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200 - } - }, - { - "name": "get_call_via_new_url", - "url": "${my_new_url}/home/bathroom/1", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200 - } - } - - ] -} -``` - - -#### 18: -#### Generating IDs and sharing across steps - -- [See a running example](https://github.com/authorjapps/helpme/tree/master/zerocode-rest-help/src/test/resources/tests/01_vanila_placeholders) - - -#### 19: -#### Bare JSON String, still a valid JSON - -- [See a running example](https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios/14_bare_string_json.json) - - -#### 20: -#### Passing Headers to the REST API - -- [See a running example](https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios/16_passing_headers_to_rest_apis.json) - - -#### 20.1: -#### Passing "Content-Type": "application/x-www-form-urlencoded" header -It is very easy to send this content-type in the header and assert the response. - -When you use this header, then you just need to put the `Key-Value` or `Name-Value` content under request `body` or request `queryParams` section. That's it. - -e.g. -```javaScript - "request": { - "headers": { - "Content-Type": "application/x-www-form-urlencoded" - }, - "body": { - "unit-no": "12-07", - "block-number": 33, - "state/region": "Singapore North", - "country": "Singapore", - "pin": "87654321", - } - } -``` - -- What happens if my **Key** contains a `space` or front slash `/` etc? - -This is automatically taken care by `Apache Http Client`. That means it gets converted to the equivalent encoded char which is understood by the server(e.g. Spring boot or Jersey or Tomcat etc ). - -e.g. -The above name-value pair behind the scene is sent to the server as below: -> unit-no=12-07&country=Singapore&block-number=33&pin=87654321&state%2Fregion=Singapore+North - -See more examples and usages in the [Wiki >>](https://github.com/authorjapps/zerocode/wiki/application-x-www-form-urlencoded-urlencoded-with-KeyValue-params) - -#### 20.2: -#### Handling Content-Type with charset-16 or charset-32 -When the http server sends response with charset other than utf-8 i.e. utf-16 or utf-32 etc, then the Zerocode framework automatically handles it correctly. -See [Wiki - Charset in response](https://github.com/authorjapps/zerocode/wiki/Charset-UTF-8-or-UTF-16-or-UTF-32-etc-in-the-http-response) for details on how it handles. - -Also the framework enables you to override this behaviour/handling by overriding method `createCharsetResponse` in the class `BasicHttpClient.java`. See an example in the working code example of HelloWorld repo. - -#### 21: -#### Passing environment param via Jenkins and dynamically picking environment specific properties file in CI -- [See a running example of passing envronment param and value](https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/java/org/jsmart/zerocode/testhelp/tests/EnvPropertyHelloWorldTest.java) -```java -package org.jsmart.zerocode.testhelp.tests; - -import org.jsmart.zerocode.core.domain.EnvProperty; -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; - -@EnvProperty("_${env}") //any meaningful string e.g. `env.name` or `envName` or `app.env` etc -@TargetEnv("hello_world_host.properties") -@RunWith(ZeroCodeUnitRunner.class) -public class EnvPropertyHelloWorldTest { - - @Test - @Scenario("hello_world/hello_world_get.json") - public void testRunAgainstConfigPropertySetViaJenkins() throws Exception { - - } -} - -/** - Set "env=ci" in Jenkins (or via .profile in a Unix machine, System/User properties in Windows) - then the runner picks "hello_world_host_ci.properties" and runs. - if -Denv=sit, then runner looks for and picks "hello_world_host_sit.properties" and runs. - -If `env` not supplied, then defaults to "hello_world_host.properties" which by default mentioned mentioned via @TargetEnv - - -or- - - Configure the below `mvn goal` when you run via Jenkins goal in the specific environment e.g. - - - For CI : - mvn clean install -Denv=ci - - For SIT: - mvn clean install -Denv=sit - - and make sure: - hello_world_host_ci.properties and hello_world_host_sit.properties etc are available in the resources folder or class path. - */ -``` - - -#### 22: - -#### LocalDate and LocalDateTime format example - -```javaScript -{ - "id": 1000, - "createdDay": "${LOCAL.DATE.TODAY:yyyy-MM-dd}", - "createdDayTimeStamp": "${LOCAL.DATETIME.NOW:yyyy-MM-dd'T'HH:mm:ss.nnnnnnnnn}", - "randomUniqueValue": "${LOCAL.DATETIME.NOW:yyyyMMdd'T'HHmmssnnnnnnnnn}" -} - -resolved to ===> below date and datetime - -{ - "id": 1000, - "createdDay": "2018-02-14", - "createdDayTimeStamp": "2018-02-14T21:52:45.180000000", - "randomUniqueValue": "20180214T215245180000000" -} - -``` - -e.g formats: -``` -output: 2018-02-11 // "uuuu-MM-dd" -output: 2018 02 11 // "uuuu MM dd" -output: 2018 // "yyyy" -output: 2018-Feb-11 // "uuuu-MMM-dd" -output: 2018-02-11 // "uuuu-LL-dd" -Default: date.toString(): 2018-02-11 -``` - -Note: -`uuuu` prints same as `yyyy` - -``` -output: 2018-02-11T21:31:21.041000000 // "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS" -output: 2018-02-11T21:31:21.41000000 // "uuuu-MM-dd'T'HH:mm:ss.n" -output: 2018-02-11T21:31:21.041000000 // "uuuu-MM-dd'T'HH:mm:ss.nnnnnnnnn" -output: 2018-02-11T21:31:21.77481041 // "uuuu-MM-dd'T'HH:mm:ss.A" -output: 2018-02-14 // "uuuu-MM-dd" or "yyyy-MM-dd" -Default: date.toString(): 2018-02-11T21:31:20.989 // .toString() -``` -### See here more- -https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html - -``` - H hour-of-day (0-23) number 0 - m minute-of-hour number 30 - s second-of-minute number 55 - S fraction-of-second fraction 978 - A milli-of-day number 1234 - n nano-of-second number 987654321 - N nano-of-day number 1234000000 -``` -All letters 'A' to 'Z' and 'a' to 'z' are reserved as pattern letters. The following pattern letters are defined: -``` - Symbol Meaning Presentation Examples - ------ ------- ------------ ------- - G era text AD; Anno Domini; A - u year year 2004; 04 - y year-of-era year 2004; 04 - D day-of-year number 189 - M/L month-of-year number/text 7; 07; Jul; July; J - d day-of-month number 10 -``` - -#### 23: - -#### SOAP method invocation example with xml input - -You can invoke SOAP as below which is already supported by zerocode lib, or you can write your own SOAP executor using Java(if -you want to, but you don't have to). -(If you want- Then, in the README file go to section -> "Calling java methods(apis) for specific tasks" ) - -```javaScript -{ - "scenarioName": "GIVEN a SOAP end poinr WHEN I invoke a method with a request XML, THEN I will ge the SOAP response in XML", - "steps": [ - { - "name": "invoke_currency_conversion", - "url": "http:///", - "operation": "POST", - "request": { - "headers": { - "Content-Type": "text/xml; charset=utf-8", - "SOAPAction": "" - //"SOAPAction": "\"\"" - }, - "body": "escaped request XML message ie the soap:Envelope message" - -or- // pick from- src/test/resources/soap_requests/xml_files/soap_request.xml - "body": "${XML.FILE:soap_requests/xml_files/soap_request.xml}" - }, - "assertions": { - "status": 200 - } - } - ] -} -``` - -e.g. below- -This example invokes a free SOAP service over internet. -Note: -If this service is down, the invocation might fail. -So better to test against an available SOAP service to you or a local stub service. - -```javaScript -{ - "scenarioName": "GIVEN a SOAP end point WHEN I invoke a method with a request XML, THEN I will get response in XML", - "steps": [ - { - "name": "invoke_currency_conversion", - "url": "http://www.webservicex.net/CurrencyConvertor.asmx", - "operation": "POST", - "request": { - "headers": { - "Content-Type": "text/xml; charset=utf-8", - "SOAPAction": "http://www.webservicex.net/ConversionRate" - //"SOAPAction": "\"http://www.webservicex.net/ConversionRate/"" - }, - "body": "\n\n \n \n AFA\n GBP\n \n \n" - // -or- - // "body": "${XML.FILE:soap_requests/xml_files/soap_request.xml}" - }, - "assertions": { - "status": 200 - } - } - ] -} -``` - -You should received the below- -``` -Response: -{ - "status" : 200, - "headers" : { - "Date" : [ "Fri, 16 Feb 2018 05:38:27 GMT" ], - "Server" : [ "Microsoft-IIS/7.0" ] - }, - - "rawBody" : "-1" -} -*responseTimeStamp:2018-02-16T05:38:35.254 -*Response delay:653.0 milli-secs - ``` - - -#### 24: -#### SOAP method invocation where Corporate Proxy enabled -You need to use a HttpClient ie override the BasicHttpClient and set proxies to it as below- -```java - Step-1) - CredentialsProvider credsProvider = createProxyCredentialsProvider(proxyHost, proxyPort, proxyUserName, proxyPassword); - - Step-2) - HttpHost proxy = new HttpHost(proxyHost, proxyPort); - - Step-3) method Step-1 - private CredentialsProvider createProxyCredentialsProvider(String proxyHost, int proxyPort, String proxyUserName, String proxyPassword) { - - CredentialsProvider credsProvider = new BasicCredentialsProvider(); - - credsProvider.setCredentials( - - new AuthScope(proxyHost, proxyPort), - - new UsernamePasswordCredentials(proxyUserName, proxyPassword)); - - return credsProvider; - } - - Step-4) - Set the values from Step-1 and Step-2 - - HttpClients.custom() - - .setSSLContext(sslContext) - - .setSSLHostnameVerifier(new NoopHostnameVerifier()) - - .setDefaultCookieStore(cookieStore) - - .setDefaultCredentialsProvider(credsProvider) //<------------- From Step-1 - - .setProxy(proxy) //<------------- From Step-2 - - .build(); -``` - -You can inject the Corporate Proxy details to the custom {{HttpClient}} li below from a config file simply by annotating -the key names from the host config file which is used by the runner for mentioning host and port. -e.g. below: -See an example here- -https://github.com/authorjapps/zerocode/blob/master/src/main/java/org/jsmart/zerocode/core/httpclient/soap/SoapCorporateProxySslHttpClient.java - -Usage example here: -https://github.com/authorjapps/zerocode/blob/master/src/test/java/org/jsmart/zerocode/core/soap/SoapCorpProxySslHttpClientTest.java - -How to use? -```java -@UseHttpClient(SoapCorporateProxySslHttpClient.class) -@TargetEnv("soap_host_with_corp_proxy.properties") -@RunWith(ZeroCodeUnitRunner.class) -public class SoapCorpProxySslHttpClientTest { - - @Ignore - @Test - @Scenario("foo/bar/soap_test_case_file.json") - public void testSoapWithCorpProxyEnabled() throws Exception { - - } -} -``` - -Explanation below- - -```java -@TargetEnv("hello_world_host.properties") -@RunWith(ZeroCodeUnitRunner.class) -public class HelloWorldTest { - // @Test - // tests here -} - -soap_host_with_corp_proxy.properties ---------------------------- -# Web Server host and port -web.application.endpoint.host=https://soap-server-host/ServiceName -web.application.endpoint.port=443 - -# Web Service context; Leave it blank in case you do not have a common context -web.application.endpoint.context= - -#sample test purpose - if you remove this from ehre, then make sure to remove from Java file -corporate.proxy.host=http://exam.corporate-proxy-host.co.uk -corporate.proxy.port=80 -corporate.proxy.username=HAVYSTARUSER -corporate.proxy.password=i#am#here#for#soap# - - -Your HttpClient: ----------------- -See- -https://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientProxyAuthentication.java - -public class YourHttpClient { - - @Inject - @Named("corporate.proxy.host") - private String proxyHost; - - @Inject - @Named("corporate.proxy.port") - private String proxyPort; - - @Inject - @Named("corporate.proxy.username") - private String proxyUserName; - - @Inject - @Named("corporate.proxy.password") - private String proxyPassword; - - // Build the client using these. -} -``` - -#### 25: -#### MIME Type Converters- XML to JSON, prettyfy XML etc -e.g. -##### xmlToJson -```javaScript -{ - "name": "xml_to_json", - "url": "org.jsmart.zerocode.converter.MimeTypeConverter", - "operation": "xmlToJson", - "request": "\n\n \n \n AFA\n GBP\n \n \n", - "assertions": { - "soap:Envelope": { - "xmlns:xsd": "http://www.w3.org/2001/XMLSchema", - "xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/", - "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "soap:Body": { - "ConversionRate": { - "xmlns": "http://www.webservicex.net/", - "FromCurrency": "AFA", - "ToCurrency": "GBP" - } - } - } - } - } -``` - -##### jsonToJson -Various input and output. Depending upon the usecase, you can use that method. - -```javaScript -{ - "scenarioName": "Given a json string or json block, convert to equivalent json block", - "steps": [ - { - "name": "json_block_to_json", - "url": "org.jsmart.zerocode.converter.MimeTypeConverter", - "operation": "jsonBlockToJson", - "request": { - "headers": { - "hdrX": "valueX" - }, - "body": { - "id": 1001, - "addresses": [ - { - "postCode": "PXY" - }, - { - "postCode": "LMZ DDD" - } - ] - } - }, - "assertions": { - "headers": { - "hdrX": "valueX" - }, - "body": { - "id": 1001, - "addresses": [ - { - "postCode": "PXY" - }, - { - "postCode": "${$.json_block_to_json.request.body.addresses[1].postCode}" - } - ] - } - } - }, - { - "name": "json_to_json", - "url": "org.jsmart.zerocode.converter.MimeTypeConverter", - "operation": "jsonToJson", - "request": "${$.json_block_to_json.request.headers}", - "assertions": { - "hdrX": "valueX" - } - }, - { - "name": "body_json_to_json", - "url": "org.jsmart.zerocode.converter.MimeTypeConverter", - "operation": "jsonToJson", - "request": "${$.json_block_to_json.request.body}", - "assertions": { - "id": 1001, - "addresses": [ - { - "postCode": "PXY" - }, - { - "postCode": "LMZ DDD" - } - ] - } - }, - { - "name": "json_node_to_json", - "url": "org.jsmart.zerocode.converter.MimeTypeConverter", - "operation": "jsonBlockToJson", - "request": { - "headers": { - "hdrX": "valueX" - }, - "body": { - "id": 1001, - "addresses": [ - { - "postCode": "PXY" - } - ] - } - }, - "assertions": { - "headers": { - "hdrX": "valueX" - }, - "body": { - "id": 1001, - "addresses": [ - { - "postCode": "${$.json_block_to_json.request.body.addresses[0].postCode}" - } - ] - } - } - } - ] -} -``` -Available methods are- -* xmlToJson -* jsonToJson -* jsonBlockToJson -* jsonNodeToJson -* prettyXml - -#### 26: -#### Using WireMock for mocking dependent end points -See Issue #47 for the scenarios when WireMock becomes handy. -See examples here- -https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/resources/wiremock_tests/mock_via_wiremock_then_test_the_end_point.json - - -The below JSON block step will mock two end points using WireMock. -1. GET: /api/v1/amazon/customers/UK001 (no headers) -2. GET: /api/v1/amazon/customers/cust-007 (with headers) - -```javaScript - { - "name": "setup_mocks", - "url": "/$MOCK", - "operation": "$USE.WIREMOCK", - "request": { - "mocks": [ - { - "name": "mocking_a_GET_endpoint", - "operation": "GET", - "url": "/api/v1/amazon/customers/UK001", - "response": { - "status": 200, - "headers": { - "Accept": "application/json" - }, - "body": { - "id": "UK001", - "name": "Adam Smith", - "Age": "33" - } - } - }, - { - "name": "mocking_a_GET_endpoint_with_headers", - "operation": "GET", - "url": "/api/v1/amazon/customers/cust-007", - "request": { - "headers": { - "api_key": "key-01-01", - "api_secret": "secret-01-01" - } - }, - "response": { - "status": 200, - "body": { - "id": "cust-007", - "type": "Premium" - } - } - } - ] - }, - "assertions": { - "status": 200 - } - } - -``` - -#### 28: -#### Http Basic authentication step using zerocode -+ How can I do basic http authentication in ZeroCode ? - + Ans: You can do this in so many ways, it depends on your project requirement. Most simplest one is to pass the base64 basicAuth in the request headers as below - e.g. `USERNAME/PASSWORD` as `charaanuser/passtwitter` - -Note- -Zerocode framework helps you to achieve this, but has nothing to do with Basic-Auth. It uses `Apache Http Client` behind the scenes, this means whatever you can do using `Apache Http Client`, you can do it simply using `Zerocode`. - -+ Positive scenario -```javaScript -{ - "name": "get_book_using_basic_auth", - "url": "http://localhost:8088/api/v1/white-papers/WP-001", - "operation": "GET", - "request": { - "headers": { - "Authorization": "Basic Y2hhcmFhbnVzZXI6cGFzc3R3aXR0ZXI=" // You can generate this using Postman or java code - } - }, - "assertions": { - "status": 200, // 401 - if unauthorised. See negatibe test below - "body": { - "id": "WP-001", - "type": "pdf", - "category": "Mule System API" - } - } -} -``` - -+ Negative scenario -``` -{ - "name": "get_book_using_wrong_auth", - "url": "http://localhost:8088/api/v1/white-papers/WP-001", - "operation": "GET", - "request": { - "headers": { - "Authorization": "Basic aWRONG-PASSWORD" - } - }, - "assertions": { - "status": 401 //401(or simillar code whatever the server responds), you can assert here. - "body": { - "message": "Unauthorised" - } - } -} -``` -+ If your requirement is to put basic auth for all the API tests e.g. GET, POST, PUT, DELETE etc commonly in the regression suite, then you can put this `"Authorization"` header into your SSL client code. -You can refer to an example [test here](https://github.com/authorjapps/consumer-contract-tests/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/basicauth/BasicAuthContractTest.java). - -+ In your custom http client, you add the header to the request at one place, which is common to all the API tests. -See: `org.jsmart.zerocode.httpclient.CorpBankApcheHttpClient#addBasicAuthHeader` in the [http-client code](https://github.com/authorjapps/consumer-contract-tests/blob/master/src/main/java/org/jsmart/zerocode/httpclient/CorpBankApcheHttpClient.java) it uses. - -#### 29: -#### Sending query params in URL or separately -You can pass query params in the usual way in the URL e.g. `?page=1&page_size=5` -or- -You can pass them in the request as below. -``` -... - "request": { - "queryParams":{ - "page":1, - "per_page":6 - } - } -... -``` -See below both the examples( See this in the hello-world repo in action i.e. the [Test-Case](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/resources/helloworld_queryparams/github_get_repos_by_query_params.json) and the [JUnit Test](https://github.com/authorjapps/zerocode-hello-world/blob/master/src/test/java/org/jsmart/zerocode/testhelp/tests/helloworldqueryparams/HelloWorldQueryParamsTest.java) ) -``` -{ - "scenarioName": "Git Hub GET API - Fetch by queryParams", - "steps": [ - { - "name": "get_repos_by_query", - "url": "https://api.github.com/users/octocat/repos?page=1&per_page=6", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body.SIZE": 6 - } - }, - { - "name": "get_repos_by_query_params", - "url": "https://api.github.com/users/octocat/repos", - "operation": "GET", - "request": { - "queryParams":{ - "page":1, - "per_page":6 - } - }, - "assertions": { - "status": 200, - "body.SIZE": 6 - } - }, - { - "name": "get_all_reposs_without_query", // without the query params, which fetches everything. - "url": "https://api.github.com/users/octocat/repos", - "operation": "GET", - "request": { - }, - "assertions": { - "status": 200, - "body.SIZE": 8 - } - } - ] -} -``` - - -#### 99: -#### Place holders for End Point Mocking - -| Place Holder | Output | More | -| ------------- |:-------------| -----| -| /$MOCK | Signifies that this step will be used for mocking end points | Start with a front slash | -| $USE.WIREMOCK | Framework will use wiremock APIs to mock the end points defined in "mocks" section | Can use other mechanisms e.g. local REST api simulators | - -#### General place holders - -| Place Holder | Output | More | -| ------------- |:-------------| -----| -| ${RANDOM.NUMBER} | Replaces with a random number | Random number is generated using current timestamp in milli-sec | -| ${RANDOM.UUID} | Replaces with a random UUID | Random number is generated using java.util.UUID e.g. 077e6162-3b6f-4ae2-a371-2470b63dgg00 | -| ${RANDOM.STRING:10} | Replaces a random string consists of ten english alpphabets | The length can be dynamic | -| ${RANDOM.STRING:4} | Replaces with a random string consists of four english alpphabets | The length can be dynamic | -| ${STATIC.ALPHABET:5} | Replaces with abcde ie Static string of length 5| String starts from "a" and continues, repeats after "z"| -| ${STATIC.ALPHABET:7} | Replaces with abcdefg ie Static string of length 7| String starts from a"" and continues, repeats after "z"| -| ${LOCAL.DATE.TODAY:yyyy-MM-dd} | Resolves this today's date in the format yyyy-MM-dd or any suppliedformat| See format examples here https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios/18_date_and_datetime_today_generator.json | -| ${LOCAL.DATETIME.NOW:yyyy-MM-dd'T'HH:mm:ss.nnnnnnnnn} | Resolves this today's datetime stamp in any supplied format| See format examples here https://github.com/authorjapps/helpme/blob/master/zerocode-rest-help/src/test/resources/tests/00_sample_test_scenarios/18_date_and_datetime_today_generator.json | - -#### Assertion place holders - -| Place Holder | Output | More | -| ------------- |:-------------| -----| -| $NOT.NULL | Assertion passes if a not null value was present in the response | Otherwise fails | -| $NULL | Assertion passes if a null value was present in the response | Otherwise fails | -| $[] | Assertion passes if an empty array was present in the response | Otherwise fails | -| $EQ.99 | Assertion passes if a numeric value equals to 99 was present in the response | Can be any int, long, float etc | -| $NOT.EQ.99 | Assertion passes if a numeric value is not equals to 99 was present in the response | Can be any int, long, float etc | -| $GT.99 | Assertion passes if a value greater than 99 was present in the response | Can be any int, long, float etc | -| $LT.99 | Assertion passes if a value lesser than 99 was present in the response | Can be any int, long, float etc | -| $CONTAINS.STRING:id was cust-001 | Assertion passes if the node response contains string "id was cust-001" | Otherwise fails | -| $CONTAINS.STRING.IGNORECASE:id WaS CuSt-001 | Assertion passes if the response value contains string "id was cust-001" with case insensitive | Otherwise fails | -| $MATCHES.STRING:`\\d{4}-\\d{2}-\\d{2}` | Assertion passes if the response value contains e.g. `"1989-07-09"` matching regex `\\d{4}-\\d{2}-\\d{2}` | Otherwise fails | -| $LOCAL.DATETIME.BEFORE:2017-09-14T09:49:34.000Z | Assertion passes if the actual date is earlier than this date | Otherwise fails | -| $LOCAL.DATETIME.AFTER:2016-09-14T09:49:34.000Z | Assertion passes if the actual date is later than this date | Otherwise fails | -| $ONE.OF:[First Val, Second Val, Nth Val] | Assertion passes if `currentStatus` actual value is one of the expected values supplied in the `array` | Otherwise fails. E.g. `"currentStatus": "$ONE.OF:[Found, Searching, Not Found]"` | - -#### Assertion Path holders - -| Place Holder | Output | More | -| ------------- |:-------------| -----| -| `".SIZE":"$GT.2"` | e.g. `"persons.SIZE" : "$GT.2"` - Assertion passes if the array contains more than 2 elements | Search for `dealing with arrays` in this README for more usages | -| `".SIZE":"$LT.4"` | e.g. `"persons.SIZE" : "$LT.4"` - Assertion passes if the array contains less than 4 elements | Search for `dealing with arrays` in this README for more usages | -| `".SIZE":3` | e.g. `"persons.SIZE" : 3` - Assertion passes if the array has exactly 3 elements | Search for `dealing with arrays` in this README for more usages | +[user's guide](https://github.com/authorjapps/zerocode/wiki#developer-guide) to check out more scenarios. +Zerocode-TDD is used by many companies such as Vocalink, HSBC, HomeOffice(Gov) and [many others](https://github.com/authorjapps/zerocode/wiki#smart-projects-using-zerocode) to achieve accurate production drop of their micro-services, data-pipelines etc. -#### 100: -#### JSON Slice And Dice - Solved -+ [Exapnd, Collapse, Remove Node and Traverse etc](https://jsoneditoronline.org/) - + Tree structure viewing - Good for array traversing - + Remove a node -> Click on left arrow -+ [Beautify, Minify, Copy Jayway JSON Pth](http://jsonpathfinder.com/) -+ [JSON Path Evaluator](http://jsonpath.herokuapp.com/?path=$.store.book[*].author) +Also, learn more about [Validators Vs Matchers](https://zerocode-tdd.tddfy.com/assertions/Validators-and-Matchers) here. -#### Video tutorials -* [RESTful testing with test cases in JSON](https://youtu.be/nSWq5SuyqxE) - YouTube -* [Zerocode - Simple and powerful testing library - HelloWorld](https://www.youtube.com/watch?v=YCV1cqGt5e0) - YouTube -* [Zerocode Query Params Demo](https://www.youtube.com/watch?v=a7JhwMxVcCM) - YouTube +Happy Testing! ๐Ÿผ -#### References, Dicussions and articles -* [Performance testing using JUnit and maven](https://www.codeproject.com/Articles/1251046/How-to-do-performance-testing-using-JUnit-and-Mave) - Codeproject -* [REST API or SOAP End Point Testing](https://www.codeproject.com/Articles/1242569/REST-API-or-SOAP-End-Point-Testing-with-ZeroCode-J) - Codeproject -* [DZone- MuleSoft API Testing With Zerocode Test Framework](https://dzone.com/articles/zerocode-test-framework-for-restsoap-api-tddbdd-ap) - DZone -* [Testing need not be harder or slower, it should be easier and faster](https://dzone.com/articles/rest-api-testing-using-the-zerocode-json-based-bdd) - DZone -* [Kafka - Quick and Practical Testing With Zerocode](https://dzone.com/articles/a-quick-and-practical-example-of-kafka-testing) - DZone -* [Kotlin Apps Testing With Zerocode](https://dzone.com/articles/kotlin-spring-bootspring-data-h2-db-rest-api) - DZone +๐Ÿ”† Visit [Documentation](https://zerocode-tdd.tddfy.com) - Indexed, searchable & instantly finds you the results diff --git a/_config.yml b/_config.yml index c4192631f..8bbf79438 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-cayman \ No newline at end of file +theme: jekyll-theme-hacker diff --git a/core/pom.xml b/core/pom.xml index 6addbdc12..f9192e4ce 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -4,12 +4,10 @@ zerocode-tdd-parent org.jsmart - 1.3.9-SNAPSHOT + 1.3.46-SNAPSHOT zerocode-tdd - org.jsmart - jar Zerocode TDD Core Zerocode TDD framework for API test automation @@ -25,7 +23,8 @@ https://github.com/authorjapps/zerocode scm:git:git@github.com:authorjapps/zerocode.git scm:git:git@github.com:authorjapps/zerocode.git - + HEAD + @@ -49,20 +48,20 @@ - - - 4.12 - 2.6.2 - - + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + com.google.code.gson gson @@ -77,7 +76,7 @@ org.apache.velocity - velocity + velocity-engine-core com.fasterxml.jackson.dataformat @@ -95,31 +94,24 @@ ch.qos.logback logback-classic - - ch.qos.logback - logback-core - org.jboss.resteasy resteasy-jaxrs - com.jayway.jsonpath - json-path + org.jboss.resteasy + resteasy-client - commons-lang - commons-lang + com.jayway.jsonpath + json-path + com.google.classpath-explorer classpath-explorer - - org.jukito - jukito - com.google.inject guice @@ -144,34 +136,67 @@ com.fasterxml.jackson.core jackson-databind - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - junit junit + + org.mockito + mockito-core + test + org.apache.httpcomponents httpclient - ${httpclient.version} + + + org.apache.httpcomponents + httpmime + + + org.apache.commons + commons-collections4 + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-text org.jsmart micro-simulator test + + commons-dbutils + commons-dbutils + com.h2database h2 test + + org.postgresql + postgresql + + com.aventstack extentreports + + com.google.protobuf + protobuf-java + + + com.google.protobuf + protobuf-java-util + @@ -185,11 +210,6 @@ org.apache.maven.plugins maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java-compiler-source.version} - ${java-compiler-target.version} - @@ -202,20 +222,13 @@ - org.apache.maven.plugins maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java-compiler-source.version} - ${java-compiler-target.version} - org.apache.maven.plugins maven-source-plugin - 2.2.1 attach-sources @@ -228,7 +241,6 @@ org.apache.maven.plugins maven-javadoc-plugin - attach-javadocs @@ -241,7 +253,6 @@ org.apache.maven.plugins maven-gpg-plugin - 1.5 sign-artifacts @@ -267,7 +278,6 @@ org.apache.maven.plugins maven-release-plugin - 2.5 true false diff --git a/core/src/main/java/org/jsmart/zerocode/converter/Converter.java b/core/src/main/java/org/jsmart/zerocode/converter/Converter.java index c8c41d2cc..3d489fc4a 100644 --- a/core/src/main/java/org/jsmart/zerocode/converter/Converter.java +++ b/core/src/main/java/org/jsmart/zerocode/converter/Converter.java @@ -7,6 +7,8 @@ public interface Converter { Object xmlToJson(String xmlObject); + Object stringToJson(String jsonString) throws IOException; + Object jsonToJson(String jsonString) throws IOException; Object jsonBlockToJson(JsonNode jsonNode) throws IOException; diff --git a/core/src/main/java/org/jsmart/zerocode/converter/MimeTypeConverter.java b/core/src/main/java/org/jsmart/zerocode/converter/MimeTypeConverter.java index b8bbe14ac..15f28b062 100644 --- a/core/src/main/java/org/jsmart/zerocode/converter/MimeTypeConverter.java +++ b/core/src/main/java/org/jsmart/zerocode/converter/MimeTypeConverter.java @@ -48,7 +48,9 @@ public Object xmlToJson(String xmlContent) { } /** - * Converts input JSON string (usually escaped e.g. "{\"a\": \"b\", \"active\": true}" ) to JSON block + * Converts input JSON string (usually escaped e.g. "{\"a\": \"b\", \"active\": true}" ) to JSON block. + * This helps in picking a value from the JSON block via Jayway Path and assert. + * * See also- * - method jsonBlockToJson for unescaped json to json block. * - method jsonNodeToJson for unescaped json to json block. @@ -58,10 +60,15 @@ public Object xmlToJson(String xmlContent) { * @throws IOException - This method might throw IOException */ @Override - public Object jsonToJson(String jsonString) throws IOException { + public Object stringToJson(String jsonString) throws IOException { return mapper.readValue(jsonString, JsonNode.class); } + @Override + public Object jsonToJson(String jsonString) throws IOException { + return stringToJson(jsonString); + } + /** * Converts JSON Block({"a": "b", "active": true}) to JSON block * See also- jsonNodeToJson which is identical to jsonBlockToJson. @@ -77,7 +84,7 @@ public static String prettyXml(String input) { final String formattedXml = prettyXmlWithIndentType(input, 2); - LOGGER.info("\n--------------------- Pretty XML -------------------------\n" + LOGGER.debug("\n--------------------- Pretty XML -------------------------\n" + formattedXml + "\n------------------------- * -----------------------------\n"); diff --git a/core/src/main/java/org/jsmart/zerocode/converter/SoapMocker.java b/core/src/main/java/org/jsmart/zerocode/converter/SoapMocker.java index 3edea3fab..e83ff5814 100644 --- a/core/src/main/java/org/jsmart/zerocode/converter/SoapMocker.java +++ b/core/src/main/java/org/jsmart/zerocode/converter/SoapMocker.java @@ -1,10 +1,8 @@ package org.jsmart.zerocode.converter; -import org.jsmart.zerocode.core.utils.SmartUtils; - -import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.jsmart.zerocode.core.utils.SmartUtils; public class SoapMocker { @@ -17,8 +15,7 @@ public Object soapResponseXml(String nothing){ return singleKeyValueMap; - } catch (IOException e) { - e.printStackTrace(); + } catch (RuntimeException e) { throw new RuntimeException("something wrong happened here" + e); } } diff --git a/core/src/main/java/org/jsmart/zerocode/core/AddService.java b/core/src/main/java/org/jsmart/zerocode/core/AddService.java index 65c06e3d3..e11937894 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/AddService.java +++ b/core/src/main/java/org/jsmart/zerocode/core/AddService.java @@ -7,7 +7,7 @@ public class AddService { private static final Logger logger = LoggerFactory.getLogger(AddService.class); public int add(int i, int i1) { - logger.info("i= " + i + ", j= " + i1); + logger.debug("i= " + i + ", j= " + i1); return i + i1; } @@ -17,12 +17,18 @@ public Integer square(Integer number) { } public Integer squareMyNumber(MyNumber myNumber) { - logger.info("Calculating Square of " + myNumber.getNumber()); + logger.debug("Calculating Square of " + myNumber.getNumber()); return myNumber.getNumber() * myNumber.getNumber(); } + public Double squareRoot(Double number) { + if (number < 0.0) + throw new RuntimeException("Can not square root a negative number"); + return Math.sqrt(number); + } + public Integer anInteger() { - logger.info("Returning a number "); + logger.debug("Returning a number "); return 30; } diff --git a/core/src/main/java/org/jsmart/zerocode/core/domain/reports/ZeroCodeReportProperties.java b/core/src/main/java/org/jsmart/zerocode/core/constants/ZeroCodeReportConstants.java similarity index 52% rename from core/src/main/java/org/jsmart/zerocode/core/domain/reports/ZeroCodeReportProperties.java rename to core/src/main/java/org/jsmart/zerocode/core/constants/ZeroCodeReportConstants.java index dbb3f5b24..c296ddc21 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/domain/reports/ZeroCodeReportProperties.java +++ b/core/src/main/java/org/jsmart/zerocode/core/constants/ZeroCodeReportConstants.java @@ -1,6 +1,6 @@ -package org.jsmart.zerocode.core.domain.reports; +package org.jsmart.zerocode.core.constants; -public interface ZeroCodeReportProperties { +public interface ZeroCodeReportConstants { String RESULT_PASS = "PASSED"; String RESULT_FAIL = "FAILED"; String TEST_STEP_CORRELATION_ID = "TEST-STEP-CORRELATION-ID:"; @@ -9,13 +9,21 @@ public interface ZeroCodeReportProperties { String TARGET_FULL_REPORT_CSV_FILE_NAME = "zerocode-junit-granular-report.csv"; String TARGET_FILE_NAME = "target/zerocode-junit-interactive-fuzzy-search.html"; String HIGH_CHART_HTML_FILE_NAME = "zerocode_results_chart"; - String AUTHOR_MARKER = "@@"; - String ANONYMOUS_AUTHOR = "Anonymous"; + String AUTHOR_MARKER_OLD = "@@"; //Deprecated + String AUTHOR_MARKER_NEW = "@"; + String CATEGORY_MARKER = "#"; + String ANONYMOUS_CAT = "Anonymous"; String REPORT_TITLE_DEFAULT = "Zerocode Test Report"; String REPORT_DISPLAY_NAME_DEFAULT = "Zerocode Interactive Report"; String DEFAULT_REGRESSION_CATEGORY = "Regression"; + String DEFAULT_REGRESSION_AUTHOR = "All"; String LINK_LABEL_NAME = "Spike Chart(Click here)"; String ZEROCODE_JUNIT = "zerocode.junit"; String CHARTS_AND_CSV = "gen-smart-charts-csv-reports"; + // Custom js and css for extent report + String EXTENT_ADDITIONAL_JS = "document.querySelector('.vheader').insertAdjacentHTML('afterbegin'," + + "'
" + + "
')"; + String EXTENT_ADDITIONAL_CSS = "#theme-selector{padding-right:12px;padding-left:12px;margin-right:10px}"; } diff --git a/core/src/main/java/org/jsmart/zerocode/core/constants/ZerocodeConstants.java b/core/src/main/java/org/jsmart/zerocode/core/constants/ZerocodeConstants.java new file mode 100644 index 000000000..e533aa4cf --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/constants/ZerocodeConstants.java @@ -0,0 +1,15 @@ +package org.jsmart.zerocode.core.constants; + +import static org.jsmart.zerocode.core.utils.SmartUtils.readJsonAsString; + +public interface ZerocodeConstants { + String PROPERTY_KEY_HOST = "restful.application.endpoint.host"; + String PROPERTY_KEY_PORT = "restful.application.endpoint.context"; + + String KAFKA = "kafka"; + String KAFKA_TOPIC = "kafka-topic:"; + String OK = "Ok"; + String FAILED = "Failed"; + + String DSL_FORMAT = readJsonAsString("dsl_formats/dsl_parameterized_values.json"); +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java new file mode 100644 index 000000000..ff8162d13 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java @@ -0,0 +1,135 @@ +package org.jsmart.zerocode.core.db; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.lang3.StringUtils; +import org.jsmart.zerocode.core.di.provider.CsvParserProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Data loading in the database from a CSV external source + */ +class DbCsvLoader { + private static final Logger LOGGER = LoggerFactory.getLogger(DbCsvLoader.class); + private Connection conn; + private CsvParserProvider csvParser; + + public DbCsvLoader(Connection conn, CsvParserProvider csvParser) { + this.conn = conn; + this.csvParser = csvParser; + } + + /** + * Loads rows in CSV format (csvLines) into a table in the database + * and returns the total number of rows. + */ + public int loadCsv(String table, List csvLines, boolean withHeaders, String nullString) throws SQLException { + if (csvLines == null || csvLines.isEmpty()) + return 0; + + List lines = parseLines(table, csvLines); + + String[] headers = buildHeaders(lines.get(0), withHeaders); + List paramset = buildParameters(table, headers, lines, withHeaders, nullString); + if (paramset.isEmpty()) // can have headers, but no rows + return 0; + + String sql = buildSql(table, headers, paramset.get(0).length); + LOGGER.info("Loading CSV using this sql: {}", sql); + + QueryRunner runner = new QueryRunner(); + int insertCount = 0; + for (int i = 0 ; i < paramset.size(); i++) { + insertRow(runner, i, sql, paramset.get(i)); + insertCount++; + } + LOGGER.info("Total of rows inserted: {}", insertCount); + return insertCount; + } + + private List parseLines(String table, List lines) { + int numCol = 0; // will check that every row has same columns than the first + List parsedLines = new ArrayList<>(); + for (int i = 0; i buildParameters(String table, String[] headers, List lines, boolean withHeaders, String nullString) { + DbValueConverter converter = new DbValueConverter(conn, table); + List paramset = new ArrayList<>(); + for (int i = withHeaders ? 1 : 0; i < lines.size(); i++) { + String[] parsedLine = lines.get(i); + parsedLine = processNulls(parsedLine, nullString); + Object[] params; + try { + params = converter.convertColumnValues(headers, parsedLine); + LOGGER.info(" row [{}] params: {}", i + 1, Arrays.asList(params).toString()); + } catch (Exception e) { // Not only SQLException as converter also does parsing + String message = String.format("Error matching data type of parameters and table columns at CSV row %d", i + 1); + LOGGER.error(message); + LOGGER.error("Exception message: {}", e.getMessage()); + throw new RuntimeException(message, e); + } + paramset.add(params); + } + return paramset; + } + + private String[] processNulls(String[] line, String nullString) { + for (int i = 0; i < line.length; i++) { + if (StringUtils.isBlank(nullString) && StringUtils.isBlank(line[i])) { + line[i] = null; + } else if (!StringUtils.isBlank(nullString)) { + if (StringUtils.isBlank(line[i])) // null must be empty string + line[i] = ""; + else if (nullString.trim().equalsIgnoreCase(line[i].trim())) + line[i] = null; + } + } + return line; + } + + private String buildSql(String table, String[] headers, int columnCount) { + String placeholders = IntStream.range(0, columnCount) + .mapToObj(i -> "?").collect(Collectors.joining(",")); + return "INSERT INTO " + table + + (headers.length > 0 ? " (" + String.join(",", headers) + ")" : "") + + " VALUES (" + placeholders + ");"; + } + + private void insertRow(QueryRunner runner, int rowId, String sql, Object[] params) { + try { + runner.update(conn, sql, params); + } catch (SQLException e) { + String message = String.format("Error inserting data at CSV row %d", rowId + 1); + LOGGER.error(message); + LOGGER.error("Exception message: {}", e.getMessage()); + throw new RuntimeException(message, e); + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvRequest.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvRequest.java new file mode 100644 index 000000000..cebffe1cc --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvRequest.java @@ -0,0 +1,101 @@ +package org.jsmart.zerocode.core.db; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class DbCsvRequest { + private final String tableName; + private final List csvSource; + private final Boolean withHeaders; + private final String nullString; + + public DbCsvRequest( + @JsonProperty(value="tableName", required=true) String tableName, + @JsonProperty("csvSource") JsonNode csvSourceJsonNode, + @JsonProperty("withHeaders") Boolean withHeaders, + @JsonProperty("nullString") String nullString) { + this.tableName = tableName; + this.withHeaders = Optional.ofNullable(withHeaders).orElse(false); + this.nullString = Optional.ofNullable(nullString).orElse(""); + this.csvSource = Optional.ofNullable(csvSourceJsonNode).map(this::getCsvSourceFrom).orElse(Collections.emptyList()); + } + + public String getTableName() { + return tableName; + } + + public List getCsvSource() { + return csvSource; + } + + public boolean getWithHeaders() { + return withHeaders; + } + + public String getNullString() { + return nullString; + } + + // Code below is duplicated from org.jsmart.zerocode.core.domain.Parametrized.java and not included in tests. + // TODO Consider some refactoring later and review error message when file not found + + private List getCsvSourceFrom(JsonNode csvSourceJsonNode) { + try { + if (csvSourceJsonNode.isArray()) { + return readCsvSourceFromJson(csvSourceJsonNode); + + } else { + return readCsvSourceFromExternalCsvFile(csvSourceJsonNode); + } + } catch (IOException e) { + throw new RuntimeException("Error deserializing csvSource", e); + } + } + + private List readCsvSourceFromJson(JsonNode csvSourceJsonNode) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectReader reader = mapper.readerFor(new TypeReference>() { + }); + return reader.readValue(csvSourceJsonNode); + } + + private List readCsvSourceFromExternalCsvFile(JsonNode csvSourceJsonNode) throws IOException { + String csvSourceFilePath = csvSourceJsonNode.textValue(); + if (StringUtils.isNotBlank(csvSourceFilePath)) { + Path path = Paths.get("./src/test/resources/",csvSourceFilePath); + List csvSourceFileLines = Files.lines(path) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + //if (this.ignoreHeader) { + // return csvSourceFileLines.stream() + // .skip(1) + // .collect(Collectors.toList()); + //} + return csvSourceFileLines; + } + return Collections.emptyList(); + } + + @Override + public String toString() { + return "Parameterized{" + + "tableName=" + tableName + + ", csvSource=" + csvSource + + ", withHeaders=" + withHeaders + + ", nullString=" + nullString + + '}'; + } +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java new file mode 100644 index 000000000..6c9b6c8fc --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java @@ -0,0 +1,119 @@ +package org.jsmart.zerocode.core.db; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import org.apache.commons.dbutils.DbUtils; +import org.jsmart.zerocode.core.di.provider.CsvParserProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Interaction with a database using SQL to read/write + * Requires the appropriated connection data in the target environment + * properties, see src/test/resources/db_test.properties + */ +public class DbSqlExecutor { + private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlExecutor.class); + public static final String SQL_RESULTS_KEY = "rows"; + public static final String CSV_RESULTS_KEY = "size"; + + // Optional to log the explanatory error message if the env variables are no defined + @Inject(optional = true) + @Named("db.driver.url") private String url; + + @Inject(optional = true) + @Named("db.driver.user") private String user; + + @Inject(optional = true) + @Named("db.driver.password") private String password; + + @Inject + private CsvParserProvider csvParser; + + /** + * The LOADCSV operation inserts the content of a CSV file into a table, + * and returns the number of records inserted under the key "size" + */ + public Map LOADCSV(DbCsvRequest request) { // uppercase for consistency with http api operations + return loadcsv(request); + } + + public Map loadcsv(DbCsvRequest request) { + Connection conn = createAndGetConnection(); + try { + LOGGER.info("Load CSV, request -> {} ", request); + DbCsvLoader runner = new DbCsvLoader(conn, csvParser); + long result = runner.loadCsv(request.getTableName(), request.getCsvSource(), + request.getWithHeaders(), request.getNullString()); + Map response = new HashMap<>(); + response.put(CSV_RESULTS_KEY, result); + return response; + } catch (Exception e) { + String message = "Failed to load CSV"; + LOGGER.error(message, e); + throw new RuntimeException(message, e); + } finally { + closeConnection(conn); + } + } + + /** + * The EXECUTE operation returns the records retrieved by the SQL specified in the request + * under the key "rows" (select), or an empty object (insert, update) + */ + public Map EXECUTE(DbSqlRequest request) { + return execute(request); + } + + public Map execute(DbSqlRequest request) { + Connection conn = createAndGetConnection(); + try { + LOGGER.info("Execute SQL, request -> {} ", request); + DbSqlRunner runner = new DbSqlRunner(conn); + List> results = runner.execute(request.getSql(), request.getSqlParams()); + Map response = new HashMap<>(); + if (results == null) { // will return empty node, use "verify":{} + response.put(SQL_RESULTS_KEY, new ObjectMapper().createObjectNode()); + } else { + response.put(SQL_RESULTS_KEY, results); + } + return response; + } catch (SQLException e) { + String message = "Failed to execute SQL"; + LOGGER.error(message, e); + throw new RuntimeException(message, e); + } finally { + closeConnection(conn); + } + } + + /** + * Returns a new JDBC connection using DriverManager. + * Override this method in case you get the connections using another approach + * (e.g. DataSource) + */ + protected Connection createAndGetConnection() { + LOGGER.info("Create and get connection, url: {}, user: {}", url, user); + try { + return DriverManager.getConnection(url, user, password); + } catch (SQLException e) { + String message = "Failed to create connection, Please check the target environment properties " + + "to connect the database (db.driver.url, db.driver.user and db.driver.password)"; + LOGGER.error(message, e); + throw new RuntimeException(message, e); + } + } + + protected void closeConnection(Connection conn) { + DbUtils.closeQuietly(conn); + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRequest.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRequest.java new file mode 100644 index 000000000..c89b84c12 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRequest.java @@ -0,0 +1,37 @@ +package org.jsmart.zerocode.core.db; + +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DbSqlRequest { + private final String sql; + private final Object[] sqlParams; + + @JsonCreator + public DbSqlRequest( + @JsonProperty("sql") String sql, + @JsonProperty("sqlParams") Object[] sqlParams) { + this.sql = sql; + this.sqlParams = sqlParams; + } + + public String getSql() { + return sql; + } + + public Object[] getSqlParams() { + return sqlParams; + } + + @Override + public String toString() { + return "Request{" + + "sql=" + sql + + ", sqlParams=" + (sqlParams == null ? "[]" : Arrays.asList(sqlParams).toString()) + + '}'; + } +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRunner.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRunner.java new file mode 100644 index 000000000..81c58c5de --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRunner.java @@ -0,0 +1,45 @@ +package org.jsmart.zerocode.core.db; + +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.handlers.MapListHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +/** + * Execution of SQL statements against a database + */ +class DbSqlRunner { + private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlRunner.class); + private Connection conn; + + public DbSqlRunner(Connection conn) { + this.conn = conn; + } + + /** + * Executes a SQL statement with parameters (optional) and returns a list of maps + * with the ResultSet content (select) or null (insert, update) + */ + public List> execute(String sql, Object[] params) throws SQLException { + // There is only one execute operation instead of separate update and query. + // The DbUtils execute method returns a list containing each ResultSet (each is a list of maps): + // - Empty (insert and update) + // - With one or more ResultSets (select): use the first one + // - Note that some drivers never return more than one ResultSet (e.g. H2) + QueryRunner runner = new QueryRunner(); + List>> result = runner.execute(conn, sql, new MapListHandler(), params); + if (result.isEmpty()) { + return null; + } else { + if (result.size() > 1) + LOGGER.warn("The SQL query returned more than one ResultSet, keeping only the first one"); + return result.get(0); + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbValueConverter.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbValueConverter.java new file mode 100644 index 000000000..364325369 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbValueConverter.java @@ -0,0 +1,161 @@ +package org.jsmart.zerocode.core.db; + +import static org.apache.commons.lang3.time.DateUtils.parseDate; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.ParseException; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Conversion of string values to be inserted in the database + * into objects compatible with the java sql type of the target columns. + */ +public class DbValueConverter { + private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlExecutor.class); + + private Connection conn; + private String table; + private DatabaseMetaData databaseMetaData; + public Map columnTypes; // java.sql.Types + + public DbValueConverter(Connection conn, String table) { + this.conn = conn; + this.table = table; + try { + initializeMetadata(); + } catch (Exception e) { + logInitializeError(); + } + } + + private void initializeMetadata() throws SQLException { + LOGGER.info("Metadata initialization for table: {}", table); + columnTypes = new LinkedHashMap<>(); // must keep column order + databaseMetaData = conn.getMetaData(); + + table = convertToStoredCase(table); // to locate table name in metadata + LOGGER.info("Database storesLowerCaseIdentifiers={}, storesUpperCaseIdentifiers={}", + databaseMetaData.storesLowerCaseIdentifiers(), databaseMetaData.storesUpperCaseIdentifiers()); + + try (ResultSet rs = databaseMetaData.getColumns(null, null, table, "%")) { + while (rs.next()) { + String storedName = rs.getString("COLUMN_NAME"); + int typeValue = rs.getInt("DATA_TYPE"); + // internally, key is lowercase to allow case insensitive lookups + columnTypes.put(storedName.toLowerCase(), typeValue); + } + } + LOGGER.info("Mapping from java columns to sql types: {}", columnTypes.toString()); + if (columnTypes.isEmpty()) + logInitializeError(); + } + + private String convertToStoredCase(String identifier) throws SQLException { + if (databaseMetaData.storesLowerCaseIdentifiers()) // e.g. Postgres + identifier = identifier.toLowerCase(); + else if (databaseMetaData.storesUpperCaseIdentifiers()) // e.g. H2 + identifier = identifier.toUpperCase(); + return identifier; + } + + private void logInitializeError() { + LOGGER.error("Initialization of metadata for table {} failed. " + + "Errors may appear when matching query parameters to their data types", table); + } + + /** + * Given an array of column names and other array with their corresponding values (as strings), + * transforms each value to the compatible data type that allow to be inserted in the database. + * If the column names are missing, uses all columns in the current table as the column names. + */ + Object[] convertColumnValues(String[] columns, String[] values) { + if (ArrayUtils.isEmpty(columns)) // if no specified, use all columns in the table + columns = columnTypes.keySet().toArray(new String[0]); + + Object[] converted = new Object[values.length]; + for (int i = 0; i < values.length; i++) { + converted[i] = i < columns.length && i < values.length + ? convertColumnValue(columns[i], values[i]) + : values[i]; + } + return converted; + } + + private Object convertColumnValue(String column, String value) { + try { + return convertColumnValueWithThrow(column, value); + } catch (ParseException e) { + LOGGER.error("Can't convert the data type of value {} at column {}", value, column); + return value; + } + } + + /** + * Converts the string representation of a data type value into the appropriate simple SQL data type. + * If a data type is not handled by this method (or is string), returns the input string value as fallback. + * + * See table B-1 in JDBC 4.2 Specification + */ + private Object convertColumnValueWithThrow(String column, String value) throws ParseException { + if (value == null) + return null; + if (!columnTypes.containsKey(column.toLowerCase())) // fallback if no metadata + return value; + + int sqlType = columnTypes.get(column.toLowerCase()); + return convertColumnValueFromJavaSqlType(sqlType, value); + } + + private Object convertColumnValueFromJavaSqlType(int sqlType, String value) throws ParseException { + switch (sqlType) { + case java.sql.Types.NUMERIC: + case java.sql.Types.DECIMAL: return java.math.BigDecimal.valueOf(Double.parseDouble(value)); + + case java.sql.Types.BIT: //accepts "1" as true (e.g. SqlServer) + case java.sql.Types.BOOLEAN: return Boolean.valueOf("1".equals(value) ? "true" : value); + + case java.sql.Types.TINYINT: return Byte.valueOf(value); + case java.sql.Types.SMALLINT: return Short.valueOf(value); + case java.sql.Types.INTEGER: return Integer.valueOf(value); + case java.sql.Types.BIGINT: return Long.valueOf(value); + + case java.sql.Types.REAL: return Float.valueOf(value); + case java.sql.Types.FLOAT: return Double.valueOf(value); + case java.sql.Types.DOUBLE: return Double.valueOf(value); + + case java.sql.Types.DATE: return new java.sql.Date(parseDate(value, getDateFormats()).getTime()); + case java.sql.Types.TIME: return new java.sql.Time(parseDate(value, getTimeFormats()).getTime()); + case java.sql.Types.TIMESTAMP: return new java.sql.Timestamp(parseDate(value, getTimestampFormats()).getTime()); + default: + return value; + // Not including: binary and advanced datatypes (e.g. blob, array...) + } + } + + // Supported date time formats are the common ISO-8601 formats + // defined in org.apache.commons.lang3.time.DateFormatUtils, + // as well as their variants that specify milliseconds. + // This may be made user configurable later, via properties and/or embedded in the payload + + private String[] getDateFormats() { + return new String[] { "yyyy-MM-dd" }; + } + + private String[] getTimeFormats() { + return new String[] { "HH:mm:ssZ", "HH:mm:ss.SSSZ", "HH:mm:ss", "HH:mm:ss.SSS" }; + } + + private String[] getTimestampFormats() { + return new String[] { "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSZ", + "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSS" }; + } + +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/main/ApplicationMainModule.java b/core/src/main/java/org/jsmart/zerocode/core/di/main/ApplicationMainModule.java index 3038172ae..0e0cab725 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/di/main/ApplicationMainModule.java +++ b/core/src/main/java/org/jsmart/zerocode/core/di/main/ApplicationMainModule.java @@ -1,29 +1,38 @@ package org.jsmart.zerocode.core.di.main; - import com.google.inject.AbstractModule; import com.google.inject.name.Names; +import java.util.Properties; +import java.util.logging.Logger; +import org.jsmart.zerocode.core.di.module.CsvParserModule; import org.jsmart.zerocode.core.di.module.GsonModule; import org.jsmart.zerocode.core.di.module.HttpClientModule; import org.jsmart.zerocode.core.di.module.ObjectMapperModule; import org.jsmart.zerocode.core.di.module.PropertiesInjectorModule; -import org.jsmart.zerocode.core.engine.executor.JavaExecutor; -import org.jsmart.zerocode.core.engine.executor.JavaExecutorImpl; -import org.jsmart.zerocode.core.engine.executor.JsonServiceExecutor; -import org.jsmart.zerocode.core.engine.executor.JsonServiceExecutorImpl; +import org.jsmart.zerocode.core.engine.executor.ApiServiceExecutor; +import org.jsmart.zerocode.core.engine.executor.ApiServiceExecutorImpl; +import org.jsmart.zerocode.core.engine.executor.httpapi.HttpApiExecutor; +import org.jsmart.zerocode.core.engine.executor.httpapi.HttpApiExecutorImpl; +import org.jsmart.zerocode.core.engine.executor.javaapi.JavaMethodExecutor; +import org.jsmart.zerocode.core.engine.executor.javaapi.JavaMethodExecutorImpl; +import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeAssertionsProcessor; +import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeAssertionsProcessorImpl; import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeExternalFileProcessor; import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeExternalFileProcessorImpl; -import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeJsonTestProcesor; -import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeJsonTestProcesorImpl; +import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeParameterizedProcessor; +import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeParameterizedProcessorImpl; +import org.jsmart.zerocode.core.engine.sorter.ZeroCodeSorter; +import org.jsmart.zerocode.core.engine.sorter.ZeroCodeSorterImpl; +import org.jsmart.zerocode.core.engine.validators.ZeroCodeValidator; +import org.jsmart.zerocode.core.engine.validators.ZeroCodeValidatorImpl; import org.jsmart.zerocode.core.report.ZeroCodeReportGenerator; import org.jsmart.zerocode.core.report.ZeroCodeReportGeneratorImpl; import org.jsmart.zerocode.core.runner.ZeroCodeMultiStepsScenarioRunner; import org.jsmart.zerocode.core.runner.ZeroCodeMultiStepsScenarioRunnerImpl; -import java.util.Properties; -import java.util.logging.Logger; - -import static org.jsmart.zerocode.core.di.PropertyKeys.*; +import static org.jsmart.zerocode.core.utils.PropertiesProviderUtils.checkAndLoadOldProperties; +import static org.jsmart.zerocode.core.utils.PropertiesProviderUtils.loadAbsoluteProperties; +import static org.jsmart.zerocode.core.utils.SmartUtils.isValidAbsolutePath; public class ApplicationMainModule extends AbstractModule { private static final Logger LOGGER = Logger.getLogger(ApplicationMainModule.class.getName()); @@ -43,17 +52,22 @@ public void configure() { install(new HttpClientModule()); install(new GsonModule()); install(new PropertiesInjectorModule(serverEnv)); + install(new CsvParserModule()); //install(new KafkaModule()); /* * Bind Direct classes, classes to interfaces etc */ bind(ZeroCodeMultiStepsScenarioRunner.class).to(ZeroCodeMultiStepsScenarioRunnerImpl.class); - bind(JsonServiceExecutor.class).to(JsonServiceExecutorImpl.class); - bind(JavaExecutor.class).to(JavaExecutorImpl.class); - bind(ZeroCodeJsonTestProcesor.class).to(ZeroCodeJsonTestProcesorImpl.class); + bind(ApiServiceExecutor.class).to(ApiServiceExecutorImpl.class); + bind(HttpApiExecutor.class).to(HttpApiExecutorImpl.class); + bind(JavaMethodExecutor.class).to(JavaMethodExecutorImpl.class); + bind(ZeroCodeAssertionsProcessor.class).to(ZeroCodeAssertionsProcessorImpl.class); + bind(ZeroCodeValidator.class).to(ZeroCodeValidatorImpl.class); bind(ZeroCodeReportGenerator.class).to(ZeroCodeReportGeneratorImpl.class); bind(ZeroCodeExternalFileProcessor.class).to(ZeroCodeExternalFileProcessorImpl.class); + bind(ZeroCodeParameterizedProcessor.class).to(ZeroCodeParameterizedProcessorImpl.class); + bind(ZeroCodeSorter.class).to(ZeroCodeSorterImpl.class); // ------------------------------------------------ // Bind properties for localhost, CI, DIT, SIT etc @@ -63,6 +77,11 @@ public void configure() { public Properties getProperties(String host) { final Properties properties = new Properties(); + + if(isValidAbsolutePath(host)){ + return loadAbsoluteProperties(host, properties); + } + try { properties.load(getClass().getClassLoader().getResourceAsStream(host)); @@ -73,30 +92,11 @@ public Properties getProperties(String host) { checkAndLoadOldProperties(properties); } catch (Exception e) { - LOGGER.info("###Oops!Exception### while reading target env file: " + host + ". Have you mentioned env details?"); + LOGGER.warning("###Oops!Exception### while reading target env file: " + host + ". Have you mentioned env details?"); throw new RuntimeException("could not read the target-env properties file --" + host + "-- from the classpath."); } return properties; } - private void checkAndLoadOldProperties(Properties properties) { - - if(properties.get(WEB_APPLICATION_ENDPOINT_HOST) == null && properties.get(RESTFUL_APPLICATION_ENDPOINT_HOST) != null){ - Object oldPropertyValue = properties.get(RESTFUL_APPLICATION_ENDPOINT_HOST); - properties.setProperty(WEB_APPLICATION_ENDPOINT_HOST, oldPropertyValue != null ? oldPropertyValue.toString() : null); - } - - if(properties.get(WEB_APPLICATION_ENDPOINT_PORT) == null && properties.get(RESTFUL_APPLICATION_ENDPOINT_PORT) != null){ - Object oldPropertyValue = properties.get(RESTFUL_APPLICATION_ENDPOINT_PORT); - properties.setProperty(WEB_APPLICATION_ENDPOINT_PORT, oldPropertyValue != null ? oldPropertyValue.toString() : null); - } - - if(properties.get(WEB_APPLICATION_ENDPOINT_CONTEXT) == null && properties.get(RESTFUL_APPLICATION_ENDPOINT_CONTEXT) != null){ - Object oldPropertyValue = properties.get(RESTFUL_APPLICATION_ENDPOINT_CONTEXT); - properties.setProperty(WEB_APPLICATION_ENDPOINT_CONTEXT, oldPropertyValue != null ? oldPropertyValue.toString() : null); - } - - } - } diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/module/CsvParserModule.java b/core/src/main/java/org/jsmart/zerocode/core/di/module/CsvParserModule.java new file mode 100644 index 000000000..c0b13ac2a --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/di/module/CsvParserModule.java @@ -0,0 +1,16 @@ +package org.jsmart.zerocode.core.di.module; + +import com.google.inject.Binder; +import com.google.inject.Module; +import jakarta.inject.Singleton; +import org.jsmart.zerocode.core.di.provider.CsvParserInterface; +import org.jsmart.zerocode.core.di.provider.CsvParserProvider; + +public class CsvParserModule implements Module { + + @Override + public void configure(Binder binder) { + binder.bind(CsvParserInterface.class).toProvider(CsvParserProvider.class).in(Singleton.class); + } +} + diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/module/GsonModule.java b/core/src/main/java/org/jsmart/zerocode/core/di/module/GsonModule.java index 3a8e4f2f5..1b912a015 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/di/module/GsonModule.java +++ b/core/src/main/java/org/jsmart/zerocode/core/di/module/GsonModule.java @@ -3,10 +3,9 @@ import com.google.gson.Gson; import com.google.inject.Binder; import com.google.inject.Module; +import jakarta.inject.Singleton; import org.jsmart.zerocode.core.di.provider.GsonSerDeProvider; -import javax.inject.Singleton; - public class GsonModule implements Module { diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/module/HttpClientModule.java b/core/src/main/java/org/jsmart/zerocode/core/di/module/HttpClientModule.java index b5fa1f7d3..300697b2b 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/di/module/HttpClientModule.java +++ b/core/src/main/java/org/jsmart/zerocode/core/di/module/HttpClientModule.java @@ -2,11 +2,10 @@ import com.google.inject.Binder; import com.google.inject.Module; +import jakarta.inject.Singleton; import org.jsmart.zerocode.core.di.provider.DefaultGuiceHttpClientProvider; import org.jsmart.zerocode.core.httpclient.BasicHttpClient; -import javax.inject.Singleton; - public class HttpClientModule implements Module { @Override diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/module/ObjectMapperModule.java b/core/src/main/java/org/jsmart/zerocode/core/di/module/ObjectMapperModule.java index 34f622898..f5d7e3371 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/di/module/ObjectMapperModule.java +++ b/core/src/main/java/org/jsmart/zerocode/core/di/module/ObjectMapperModule.java @@ -3,16 +3,19 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Binder; import com.google.inject.Module; +import com.google.inject.name.Names; +import jakarta.inject.Singleton; import org.jsmart.zerocode.core.di.provider.ObjectMapperProvider; - -import javax.inject.Singleton; - +import org.jsmart.zerocode.core.di.provider.YamlObjectMapperProvider; public class ObjectMapperModule implements Module { @Override public void configure(Binder binder) { binder.bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class).in(Singleton.class); + binder.bind(ObjectMapper.class) + .annotatedWith(Names.named("YamlMapper")) + .toProvider(YamlObjectMapperProvider.class).in(Singleton.class); } } diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/module/OptionalTypeAdapter.java b/core/src/main/java/org/jsmart/zerocode/core/di/module/OptionalTypeAdapter.java new file mode 100644 index 000000000..70d102fda --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/di/module/OptionalTypeAdapter.java @@ -0,0 +1,38 @@ +package org.jsmart.zerocode.core.di.module; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.Optional; + +public class OptionalTypeAdapter extends TypeAdapter> { + private final TypeAdapter delegate; + + public OptionalTypeAdapter(TypeAdapter delegate) { + this.delegate = delegate; + } + + @Override + public void write(JsonWriter out, Optional value) throws IOException { + if (value == null || !value.isPresent()) { + out.nullValue(); + } else { + delegate.write(out, value.get()); + } + } + + @Override + public Optional read(JsonReader in) throws IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return Optional.empty(); + } else { + return Optional.ofNullable(delegate.read(in)); + } + } + + public static TypeAdapter> factory(TypeAdapter delegate) { + return new OptionalTypeAdapter<>(delegate); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/module/OptionalTypeAdapterFactory.java b/core/src/main/java/org/jsmart/zerocode/core/di/module/OptionalTypeAdapterFactory.java new file mode 100644 index 000000000..e2030f39a --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/di/module/OptionalTypeAdapterFactory.java @@ -0,0 +1,29 @@ +package org.jsmart.zerocode.core.di.module; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Optional; + +public class OptionalTypeAdapterFactory implements TypeAdapterFactory { + + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (!Optional.class.isAssignableFrom(typeToken.getRawType())) { + return null; + } + + Type type = typeToken.getType(); + if (type instanceof ParameterizedType) { + Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + TypeAdapter elementAdapter = gson.getAdapter(TypeToken.get(elementType)); + return (TypeAdapter) OptionalTypeAdapter.factory(elementAdapter); + } + + return null; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserConfig.java b/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserConfig.java new file mode 100644 index 000000000..4ca7c4496 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserConfig.java @@ -0,0 +1,72 @@ +package org.jsmart.zerocode.core.di.provider; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * / + * Configuration class for parsing CSV files with Jackson's CSV module, + * using a custom deserializer to maintain compatibility with non-standard formats previously handled by uniVocity. + * + */ +public class CsvParserConfig { + + public static class CustomStringDeserializer extends StdDeserializer { + // Immutable map of replacement rules: pattern โ†’ replacement + private static final Map REPLACEMENTS = createReplacements(); + + /** + * Creates an immutable map of replacement rules for escape patterns. + * @return A map containing patterns and their replacements. + */ + private static Map createReplacements() { + Map map = new HashMap<>(); + map.put("\\'", "'"); // Backslash-escaped single quote + map.put("''", "'"); // Double single quote (single-quoted CSV) + map.put("\\\\", "\\"); // Double backslash to preserve literal backslash + return Collections.unmodifiableMap(map); + } + + public CustomStringDeserializer() { + super(String.class); + } + + /** + * Deserializes a String value from the CSV parser, applying custom escape pattern replacements. + *

+ * The method processes the input string to handle non-standard escape patterns required + * for the expected output (e.g., ["a'c", "d\"f", "x\y"]). It uses a stream-based + * approach to apply replacements only when the pattern is present, ensuring efficiency. + *

+ * Without this deserializer, Jackson's default CSV parser may: + *

    + *
  • Strip literal backslashes (e.g., x\y becomes xy).
  • + *
  • Misinterpret single-quote escaping (e.g., \' or '').
  • + *
+ *

+ * This implementation ensures compatibility with the previous CSV parsing library's behavior + * and handles inputs like "a'c","d""f","x\y" or "a\'c","d\"f","x\y". + * + * @return The processed string with escape patterns replaced, or null if the input is null. + * @throws IOException If an I/O error occurs during parsing. + */ + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + final String value = p.getText(); + if (Objects.isNull(value)) { + return null; + } + return REPLACEMENTS.entrySet().stream() + .filter(entry -> value.contains(entry.getKey())) + .reduce(value, (current, entry) -> current.replace(entry.getKey(), entry.getValue()), (v1, v2) -> v1); + } + } + +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserInterface.java b/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserInterface.java new file mode 100644 index 000000000..0ea6c59f5 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserInterface.java @@ -0,0 +1,21 @@ +package org.jsmart.zerocode.core.di.provider; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * Defines a contract for parsing a single line of CSV data into a string array. + * Implementations should handle CSV lines with a specified format, such as + * comma-separated fields with optional quoting and escaping. + */ +public interface CsvParserInterface { + /** + * Parses a single line of CSV data into an array of fields. + * + * @param line the CSV line to parse, which may include quoted fields and escaped characters + * @return an array of strings representing the parsed fields; may be empty if the line is invalid + * @throws IOException if an error occurs during parsing, such as malformed CSV data + */ + String[] parseLine(final String line) throws IOException; +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserProvider.java b/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserProvider.java new file mode 100644 index 000000000..327113614 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserProvider.java @@ -0,0 +1,66 @@ +package org.jsmart.zerocode.core.di.provider; + + +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import com.fasterxml.jackson.dataformat.csv.CsvParser; +import com.fasterxml.jackson.dataformat.csv.CsvSchema; +import jakarta.inject.Provider; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class CsvParserProvider implements Provider { + private static final Logger logger = LoggerFactory.getLogger(CsvParserProvider.class); + private static final JacksonCsvParserAdapter instance; + public static final String LINE_SEPARATOR = "\n"; + private static final String CARRIAGE_RETURN = "\r"; + + static { + final CsvSchema schema = createCsvSchema(); + final CsvMapper csvMapper = new CsvMapper(); + csvMapper.enable(CsvParser.Feature.TRIM_SPACES); + csvMapper.enable(CsvParser.Feature.ALLOW_TRAILING_COMMA); + csvMapper.registerModule(new SimpleModule() + .addDeserializer(String.class, new CsvParserConfig.CustomStringDeserializer())); + final ObjectReader mapper = csvMapper + .enable(CsvParser.Feature.TRIM_SPACES) + .readerFor(String[].class) + .with(schema); + instance = new JacksonCsvParserAdapter(mapper); + } + + @Override + public JacksonCsvParserAdapter get() { + return instance; + } + + public String[] parseLine(final String line) { + try { + return instance.parseLine(sanitizeLine(line)); + } catch (final IOException e) { + logger.warn("Failed to parse line: {}", line, e); + return new String[0]; + } + } + + private String sanitizeLine(final String line) { + if (StringUtils.isNotBlank(line) && !line.contains(CARRIAGE_RETURN)) { + return line; + } + return line.replace(CARRIAGE_RETURN, StringUtils.SPACE); + } + + private static CsvSchema createCsvSchema() { + return CsvSchema.builder() + .setColumnSeparator(',') + .setQuoteChar('\'') + .setNullValue("") + .setLineSeparator(LINE_SEPARATOR) + .build(); + } + +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/provider/DefaultGuiceHttpClientProvider.java b/core/src/main/java/org/jsmart/zerocode/core/di/provider/DefaultGuiceHttpClientProvider.java index 9424fdd36..978efa20e 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/di/provider/DefaultGuiceHttpClientProvider.java +++ b/core/src/main/java/org/jsmart/zerocode/core/di/provider/DefaultGuiceHttpClientProvider.java @@ -1,10 +1,9 @@ package org.jsmart.zerocode.core.di.provider; +import jakarta.inject.Provider; import org.jsmart.zerocode.core.httpclient.BasicHttpClient; import org.jsmart.zerocode.core.httpclient.ssl.SslTrustHttpClient; -import javax.inject.Provider; - public class DefaultGuiceHttpClientProvider implements Provider { @Override diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/provider/GsonSerDeProvider.java b/core/src/main/java/org/jsmart/zerocode/core/di/provider/GsonSerDeProvider.java index ff7546efe..6a0031538 100644 --- a/core/src/main/java/org/jsmart/zerocode/core/di/provider/GsonSerDeProvider.java +++ b/core/src/main/java/org/jsmart/zerocode/core/di/provider/GsonSerDeProvider.java @@ -1,16 +1,82 @@ package org.jsmart.zerocode.core.di.provider; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import jakarta.inject.Provider; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.jsmart.zerocode.core.di.module.OptionalTypeAdapterFactory; -import javax.inject.Provider; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; public class GsonSerDeProvider implements Provider { @Override public Gson get() { + return new GsonBuilder() + .registerTypeAdapterFactory(KafkaHeadersAdapter.FACTORY) + .registerTypeAdapterFactory(new OptionalTypeAdapterFactory()) + .create(); + } + + static class KafkaHeadersAdapter extends TypeAdapter { + + static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() == Headers.class) { + return (TypeAdapter) new KafkaHeadersAdapter(gson); + } + return null; + } + }; + + private final Gson gson; + + public KafkaHeadersAdapter(Gson gson) { + this.gson = gson; + } + + @Override + public void write(JsonWriter writer, Headers value) throws IOException { + if (value == null || !value.iterator().hasNext()) { + writer.nullValue(); + } else { + Map headers = new HashMap<>(); + value.forEach(header -> headers.put(header.key(), new String(header.value()))); + gson.getAdapter(Map.class).write(writer, headers); + } + } + + @Override + public Headers read(JsonReader reader) throws IOException { + Headers headers = null; + JsonToken peek = reader.peek(); + if (JsonToken.NULL.equals(peek)) { + reader.nextNull(); + } else { + Map map = gson.getAdapter(Map.class).read(reader); - return (new Gson()); + headers = new RecordHeaders(); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + headers.add(key, value == null ? null : value.getBytes()); + } + } + return headers; + } } } diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/provider/JacksonCsvParserAdapter.java b/core/src/main/java/org/jsmart/zerocode/core/di/provider/JacksonCsvParserAdapter.java new file mode 100644 index 000000000..efb476568 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/di/provider/JacksonCsvParserAdapter.java @@ -0,0 +1,23 @@ +package org.jsmart.zerocode.core.di.provider; + +import com.fasterxml.jackson.databind.ObjectReader; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.io.StringReader; + +public class JacksonCsvParserAdapter implements CsvParserInterface { + + private final ObjectReader mapper; + public JacksonCsvParserAdapter(final ObjectReader mapper) { + this.mapper = mapper; + } + @Override + public String[] parseLine(final String line) throws IOException { + if (StringUtils.isEmpty(line)) return null ; + if(line.trim().isEmpty()) { + return new String[]{ null }; + } + return mapper.readValue(new StringReader(line)); + } +} diff --git a/core/src/main/java/org/jsmart/zerocode/core/di/provider/JsonPathJacksonProvider.java b/core/src/main/java/org/jsmart/zerocode/core/di/provider/JsonPathJacksonProvider.java new file mode 100644 index 000000000..867571a63 --- /dev/null +++ b/core/src/main/java/org/jsmart/zerocode/core/di/provider/JsonPathJacksonProvider.java @@ -0,0 +1,37 @@ +package org.jsmart.zerocode.core.di.provider; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.Option; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.json.JsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import com.jayway.jsonpath.spi.mapper.MappingProvider; +import jakarta.inject.Provider; +import java.util.EnumSet; +import java.util.Set; + +public class JsonPathJacksonProvider implements Provider { + @Override + public Configuration.Defaults get() { + return new Configuration.Defaults() { + + private final JsonProvider jsonProvider = new JacksonJsonProvider(); + private final MappingProvider mappingProvider = new JacksonMappingProvider(); + + @Override + public JsonProvider jsonProvider() { + return jsonProvider; + } + + @Override + public MappingProvider mappingProvider() { + return mappingProvider; + } + + @Override + public Set