├── .gitignore ├── README.md ├── pom.xml └── src ├── main └── java │ ├── io │ └── backend │ │ ├── commons │ │ ├── HttpStatuses.java │ │ └── RestResource.java │ │ ├── constants │ │ ├── ApiConstants.java │ │ └── ApiRoutes.java │ │ ├── entities │ │ ├── request │ │ │ ├── CreateUsersRequest.java │ │ │ └── discord │ │ │ │ └── SendMessageRequest.java │ │ └── response │ │ │ ├── CreateUserResponse.java │ │ │ ├── IfscCodeDetailsResponse.java │ │ │ ├── PostalCodeDetailsResponse.java │ │ │ ├── RickAndMortyResponse.java │ │ │ └── automationexercise │ │ │ └── GetAllProductListResponse.java │ │ ├── exceptions │ │ ├── ApiTestException.java │ │ ├── DateUtilsException.java │ │ ├── DiscordException.java │ │ └── TestUtilsException.java │ │ ├── services │ │ ├── discord │ │ │ ├── DiscordClient.java │ │ │ ├── DiscordController.java │ │ │ └── DiscordHelper.java │ │ └── rest │ │ │ ├── ApiClients.java │ │ │ ├── ApiControllers.java │ │ │ └── ApiHelpers.java │ │ └── utils │ │ ├── ConfigLoader.java │ │ ├── DateUtils.java │ │ ├── DiscordUtils.java │ │ ├── PropertiesHelper.java │ │ ├── RetryUtils.java │ │ └── TestUtils.java │ └── resource │ └── api.properties └── test ├── java └── io │ └── backend │ └── api │ ├── automationexercise │ └── GetAllProductsListTest.java │ ├── base │ └── BaseTest.java │ ├── constants │ └── TestGroups.java │ ├── ifsc │ └── tests │ │ └── IfscCodeTest.java │ ├── listeners │ └── ApiListeners.java │ ├── reqres │ └── tests │ │ └── ReqresTest.java │ ├── rickandmorty │ └── tests │ │ └── RickAndMortyCharacterTest.java │ ├── testdata │ └── ApiDataProvider.java │ └── zippo │ └── tests │ └── PostalCodeTest.java └── resources ├── logback.xml └── reportportal.properties /.gitignore: -------------------------------------------------------------------------------- 1 | ### Intellij template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | 5 | # User-specific stuff 6 | .idea/**/workspace.xml 7 | .idea/**/tasks.xml 8 | .idea/**/usage.statistics.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # AWS User-specific 13 | .idea/**/aws.xml 14 | 15 | # Generated files 16 | .idea/**/contentModel.xml 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Gradle 28 | .idea/**/gradle.xml 29 | .idea/**/libraries 30 | 31 | # Gradle and Maven with auto-import 32 | # When using Gradle or Maven with auto-import, you should exclude module files, 33 | # since they will be recreated, and may cause churn. Uncomment if using 34 | # auto-import. 35 | # .idea/artifacts 36 | # .idea/compiler.xml 37 | # .idea/jarRepositories.xml 38 | # .idea/modules.xml 39 | # .idea/*.iml 40 | # .idea/modules 41 | *.iml 42 | # *.ipr 43 | 44 | # CMake 45 | cmake-build-*/ 46 | 47 | # Mongo Explorer plugin 48 | .idea/**/mongoSettings.xml 49 | 50 | # File-based project format 51 | *.iws 52 | 53 | # IntelliJ 54 | out/ 55 | 56 | # mpeltonen/sbt-idea plugin 57 | .idea_modules/ 58 | 59 | # JIRA plugin 60 | atlassian-ide-plugin.xml 61 | 62 | # Cursive Clojure plugin 63 | .idea/replstate.xml 64 | 65 | # SonarLint plugin 66 | .idea/sonarlint/ 67 | 68 | # Crashlytics plugin (for Android Studio and IntelliJ) 69 | com_crashlytics_export_strings.xml 70 | crashlytics.properties 71 | crashlytics-build.properties 72 | fabric.properties 73 | 74 | # Editor-based Rest Client 75 | .idea/httpRequests 76 | 77 | # Android studio 3.1+ serialized cache file 78 | .idea/caches/build_file_checksums.ser 79 | 80 | ### Java template 81 | # Compiled class file 82 | *.class 83 | 84 | # Log file 85 | *.log 86 | 87 | # BlueJ files 88 | *.ctxt 89 | 90 | # Mobile Tools for Java (J2ME) 91 | .mtj.tmp/ 92 | 93 | # Package Files # 94 | *.jar 95 | *.war 96 | *.nar 97 | *.ear 98 | *.zip 99 | *.tar.gz 100 | *.rar 101 | 102 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 103 | hs_err_pid* 104 | replay_pid* 105 | 106 | ### Maven template 107 | target/ 108 | pom.xml.tag 109 | pom.xml.releaseBackup 110 | pom.xml.versionsBackup 111 | pom.xml.next 112 | release.properties 113 | dependency-reduced-pom.xml 114 | buildNumber.properties 115 | .mvn/timing.properties 116 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 117 | .mvn/wrapper/maven-wrapper.jar 118 | 119 | # Eclipse m2e generated files 120 | # Eclipse Core 121 | .project 122 | # JDT-specific (Eclipse Java Development Tools) 123 | .classpath 124 | 125 | ### Eclipse template 126 | .metadata 127 | bin/ 128 | tmp/ 129 | *.tmp 130 | *.bak 131 | *.swp 132 | *~.nib 133 | local.properties 134 | .settings/ 135 | .loadpath 136 | .recommenders 137 | 138 | # External tool builders 139 | .externalToolBuilders/ 140 | 141 | # Locally stored "Eclipse launch configurations" 142 | *.launch 143 | 144 | # PyDev specific (Python IDE for Eclipse) 145 | *.pydevproject 146 | 147 | # CDT-specific (C/C++ Development Tooling) 148 | .cproject 149 | 150 | # CDT- autotools 151 | .autotools 152 | 153 | # Java annotation processor (APT) 154 | .factorypath 155 | 156 | # PDT-specific (PHP Development Tools) 157 | .buildpath 158 | 159 | # sbteclipse plugin 160 | .target 161 | 162 | # Tern plugin 163 | .tern-project 164 | 165 | # TeXlipse plugin 166 | .texlipse 167 | 168 | # STS (Spring Tool Suite) 169 | .springBeans 170 | 171 | # Code Recommenders 172 | .recommenders/ 173 | 174 | # Annotation Processing 175 | .apt_generated/ 176 | .apt_generated_test/ 177 | 178 | # Scala IDE specific (Scala & Java development for Eclipse) 179 | .cache-main 180 | .scala_dependencies 181 | .worksheet 182 | 183 | SauceLabsWebTest.iml 184 | test-output/ 185 | .idea/modules.xml 186 | .idea/ 187 | 188 | logs/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ABOUT 2 | 3 | #### A REST API Test Framework for developing the regression suites. The test cases can be run locally or through CI/CD. 4 | 5 | # TEST FRAMEWORK DESIGN 6 | 7 | - Triggering the automation through a maven command or a testng xml file is the starting point. 8 | 9 | ![Charankumar_H_Test_Arch](https://github.com/user-attachments/assets/c7290a13-c39e-4e01-96c8-da743bfd43bc) 10 | 11 | # STEPS FOR INTEGRATING TESTNG & REPORT PORTAL 12 | 13 | 1. Download the latest docker-compose.yml 14 | from [here](https://github.com/reportportal/reportportal/blob/master/docker-compose.yml), a quicker way to download 15 | this is to use below command 16 | `curl https://raw.githubusercontent.com/reportportal/reportportal/master/docker-compose.yml -o docker-compose.yml` 17 | 2. Once downloaded, execute the below command to pull the required images and start containers 18 | `docker-compose -p reportportal up -d --force-recreate` 19 | 3. Verify login http://localhost:8080/ui/#login with `default\1q2w3e` or `superadmin\erebus` 20 | 4. Create a blank project and copy and paste the below config in `reportportal.properties` under `src/test/resources`. 21 | See the table below. 22 | 5. Read these great posts written below by [Automation Hacks](https://github.com/automationhacks) to configure the Report 23 | portal. 24 | By far, these are the only posts with accurate steps. 25 | 1. Further reading on setting up the reportportal 26 | is [here](https://automationhacks.io/2020/03/02/how-to-setup-reportportal-on-a-local-docker-instance/). 27 | 2. Further reading on configuring logback with reportportal to push logs 28 | is [here](https://automationhacks.io/2020/09/25/logging-integration-with-logback-testng-in-report-portal/). 29 | 30 | | Sl.No | Report portal Property Name | Report portal Property Value | 31 | |-------|-----------------------------|------------------------------| 32 | | 1 | rp.endpoint | http://localhost:8080 | 33 | | 2 | rp.api.key | | 34 | | 3 | rp.launch | Java launch | 35 | | 4 | rp.project | api_tests | 36 | 37 | #### REPORT PORTAL OUTPUTS 38 | 39 | Test_Logs_MVN_CMD 40 | Test_Logs_Pushed_Report_Portal 41 | Test_Suite_Launches_Report_Portal 42 | 43 | # STEPS FOR INTEGRATING TEST REPORTS [REPORT PORTAL URL] WITH DISCORD MESSAGE SERVICE 44 | 45 | 1. Create a discord account and follow the steps 46 | given [here](https://www.svix.com/resources/guides/how-to-make-webhook-discord/) to configure a message channel and 47 | send the test reports after the test execution. 48 | 2. Pass your channel's webhook token in the `ApiConstants` class. 49 | 3. Here we will send the Report Portal Launch URL along with test case metrics. So make sure that your report portal is 50 | up and running. 51 | 4. You're ready to execute your tests now. Follow the below section. 52 | 53 | #### DISCORD OUTPUTS 54 | 55 | Discord_Test_Report 56 | 57 | # STEPS FOR THE TEST EXECUTION 58 | 59 | The TestNG **has a default value of** `thread = 5` for parallel testing. 60 | To override the thread values use `-DthreadPoolSize=3 -Ddataproviderthreadcount=3` in the below maven command 61 | 62 | **1. git clone https://github.com/iamcharankumar/api_test_framework.git** 63 | 64 | **2. cd api_test_framework** 65 | 66 | **3. git pull** 67 | 68 | **4. mvn clean test -Dgroups=ALL_SMOKE,ALL_REGRESSION -Dthreads=3 -Ddataproviderthreadcount=3** 69 | 70 | **Note:** 71 | 72 | - To run specific test cases, use appropriate Test groups present in the 'TestGroups' class. 73 | 74 | #### DECLUTTERING MAVEN OUTPUT 75 | 76 | - Maven usually floods the console with logs during test execution, making it hard to spot what's important. 77 | A clean, minimal, Node.js-style output for a Java project felt impossible—until I 78 | found this [maven dependency](https://mvnrepository.com/artifact/me.fabriciorby/maven-surefire-junit5-tree-reporter). 79 | - Its purpose is simple: **"What happened to my test cases?"** That’s exactly what it shows—straight to the point, no 80 | clutter. 81 | - By following this [post](https://medium.com/wearewaes/my-journey-to-a-clear-test-output-in-maven-df82fe272249) 82 | by [Fabricio](https://github.com/fabriciorby), 83 | I was able to configure it easily and get the clean output shown below. 84 | - The output works locally and in GitHub Actions as well. 85 | - Huge respect and thanks to the author for this 86 | brilliant [work](https://github.com/fabriciorby/maven-surefire-junit5-tree-reporter?tab=readme-ov-file)! 87 | ❤️ 88 | 89 | API_Logs 90 | 91 | ## Star History 92 | 93 | [![Star History Chart](https://api.star-history.com/svg?repos=iamcharankumar/api_test_framework&type=Date)](https://star-history.com/#iamcharankumar/api_test_framework&Date) 94 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 4.0.0 7 | 8 | BackendTestAutomation 9 | BackendTest 10 | 0.0.1-SNAPSHOT 11 | 12 | BackendTest 13 | 14 | 17 15 | 17 16 | 17 | 18 | 19 | 20 | io.rest-assured 21 | rest-assured 22 | 5.4.0 23 | 24 | 25 | org.testng 26 | testng 27 | 7.7.0 28 | 29 | 30 | com.fasterxml.jackson.core 31 | jackson-databind 32 | 2.15.2 33 | 34 | 35 | org.projectlombok 36 | lombok 37 | 1.18.26 38 | 39 | 40 | org.slf4j 41 | slf4j-api 42 | 2.0.6 43 | 44 | 45 | com.github.dzieciou.testing 46 | curl-logger 47 | 3.0.0 48 | 49 | 50 | ch.qos.logback 51 | logback-classic 52 | 1.5.3 53 | 54 | 55 | net.jodah 56 | failsafe 57 | 2.4.4 58 | 59 | 60 | com.epam.reportportal 61 | agent-java-testng 62 | 5.4.1 63 | 64 | 65 | com.epam.reportportal 66 | logger-java-logback 67 | 5.2.2 68 | 69 | 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-surefire-plugin 76 | 3.5.2 77 | 78 | plain 79 | 80 | true 81 | 82 | 84 | methods 85 | 86 | 87 | listener 88 | 89 | io.backend.api.listeners.ApiListeners, 90 | com.epam.reportportal.testng.ReportPortalTestNGListener 91 | 92 | 93 | 94 | 95 | 96 | 97 | me.fabriciorby 98 | maven-surefire-junit5-tree-reporter 99 | 1.4.0 100 | 101 | 102 | org.apache.maven.surefire 103 | surefire-testng 104 | 3.5.2 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/main/java/io/backend/commons/HttpStatuses.java: -------------------------------------------------------------------------------- 1 | package io.backend.commons; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum HttpStatuses { 9 | //1xx: Informational 10 | CONTINUE(100, "Continue"), 11 | SWITCHING_PROTOCOLS(101, "Switching Protocols"), 12 | PROCESSING(102, "Processing"), 13 | EARLY_HINTS(103, "Early Hints"), 14 | 15 | //2xx: Success 16 | OK(200, "OK"), 17 | CREATED(201, "Created"), 18 | ACCEPTED(202, "Accepted"), 19 | NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"), 20 | NO_CONTENT(204, "No Content"), 21 | RESET_CONTENT(205, "Reset Content"), 22 | PARTIAL_CONTENT(206, "Partial Content"), 23 | MULTI_STATUS(207, "Multi-Status"), 24 | ALREADY_REPORTED(208, "Already Reported"), 25 | IM_USED(226, "IM Used"), 26 | 27 | //3xx: Redirection 28 | MULTIPLE_CHOICES(300, "Multiple Choice"), 29 | MOVED_PERMANENTLY(301, "Moved Permanently"), 30 | FOUND(302, "Found"), 31 | SEE_OTHER(303, "See Other"), 32 | NOT_MODIFIED(304, "Not Modified"), 33 | USE_PROXY(305, "Use Proxy"), 34 | TEMPORARY_REDIRECT(307, "Temporary Redirect"), 35 | PERMANENT_REDIRECT(308, "Permanent Redirect"), 36 | 37 | //4xx: Client Error 38 | BAD_REQUEST(400, "Bad Request"), 39 | UNAUTHORIZED(401, "Unauthorized"), 40 | PAYMENT_REQUIRED(402, "Payment Required"), 41 | FORBIDDEN(403, "Forbidden"), 42 | NOT_FOUND(404, "Not Found"), 43 | METHOD_NOT_ALLOWED(405, "Method Not Allowed"), 44 | NOT_ACCEPTABLE(406, "Not Acceptable"), 45 | PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"), 46 | REQUEST_TIMEOUT(408, "Request Timeout"), 47 | CONFLICT(409, "Conflict"), 48 | GONE(410, "Gone"), 49 | LENGTH_REQUIRED(411, "Length Required"), 50 | PRECONDITION_FAILED(412, "Precondition Failed"), 51 | REQUEST_TOO_LONG(413, "Payload Too Large"), 52 | REQUEST_URI_TOO_LONG(414, "URI Too Long"), 53 | UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), 54 | REQUESTED_RANGE_NOT_SATISFIABLE(416, "Range Not Satisfiable"), 55 | EXPECTATION_FAILED(417, "Expectation Failed"), 56 | MISDIRECTED_REQUEST(421, "Misdirected Request"), 57 | UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"), 58 | LOCKED(423, "Locked"), 59 | FAILED_DEPENDENCY(424, "Failed Dependency"), 60 | TOO_EARLY(425, "Too Early"), 61 | UPGRADE_REQUIRED(426, "Upgrade Required"), 62 | PRECONDITION_REQUIRED(428, "Precondition Required"), 63 | TOO_MANY_REQUESTS(429, "Too Many Requests"), 64 | REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"), 65 | UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"), 66 | 67 | //5xx: Server Error 68 | INTERNAL_SERVER_ERROR(500, "Internal Server Error"), 69 | NOT_IMPLEMENTED(501, "Not Implemented"), 70 | BAD_GATEWAY(502, "Bad Gateway"), 71 | SERVICE_UNAVAILABLE(503, "Service Unavailable"), 72 | GATEWAY_TIMEOUT(504, "Gateway Timeout"), 73 | HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version Not Supported"), 74 | VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates"), 75 | INSUFFICIENT_STORAGE(507, "Insufficient Storage"), 76 | LOOP_DETECTED(508, "Loop Detected"), 77 | NOT_EXTENDED(510, "Not Extended"), 78 | NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"); 79 | 80 | private final int code; 81 | private final String description; 82 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/commons/RestResource.java: -------------------------------------------------------------------------------- 1 | package io.backend.commons; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.dzieciou.testing.curl.CurlRestAssuredConfigFactory; 6 | import com.github.dzieciou.testing.curl.Options; 7 | import com.github.dzieciou.testing.curl.Platform; 8 | import groovy.json.JsonException; 9 | import io.restassured.RestAssured; 10 | import io.restassured.config.RestAssuredConfig; 11 | import io.restassured.response.Response; 12 | import lombok.AccessLevel; 13 | import lombok.NoArgsConstructor; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.slf4j.event.Level; 16 | 17 | import java.util.Objects; 18 | 19 | @Slf4j 20 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 21 | public class RestResource { 22 | 23 | private static RestResource instance; 24 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 25 | private static final Options OPTIONS = Options.builder().printMultiliner() 26 | .targetPlatform(Platform.WINDOWS).useShortForm().useLogLevel(Level.INFO).build(); 27 | private static final RestAssuredConfig CONFIG = CurlRestAssuredConfigFactory.createConfig(OPTIONS); 28 | private static final String CONTENT_TYPE = "application/json"; 29 | 30 | public static RestResource getInstance() { 31 | if (instance == null) { 32 | synchronized (RestResource.class) { 33 | if (instance == null) { 34 | instance = new RestResource(); 35 | } 36 | } 37 | } 38 | return instance; 39 | } 40 | 41 | public String serialize(Object classObject) throws JsonProcessingException { 42 | Objects.requireNonNull(classObject, "Serialization: Class object is null!"); 43 | return OBJECT_MAPPER.writeValueAsString(classObject); 44 | } 45 | 46 | public T deserialize(Response response, Class classVariable) throws JsonProcessingException { 47 | Objects.requireNonNull(response, "Deserialization: API Response is null!"); 48 | return OBJECT_MAPPER.readValue(response.asString(), classVariable); 49 | } 50 | 51 | public Response getApiResponse(String endPoint) { 52 | Response getResponse = RestAssured.given().log().all().request() 53 | .when().get(endPoint).then().log().all().extract().response(); 54 | Objects.requireNonNull(getResponse, "GET API Call failed!"); 55 | return getResponse; 56 | } 57 | 58 | public Response postApiResponse(String requestBody, String endPoint) throws JsonException { 59 | Response postResponse = RestAssured.given().contentType(CONTENT_TYPE) 60 | .body(requestBody).when().post(endPoint).then().log().all().extract().response(); 61 | Objects.requireNonNull(postResponse, "POST API Call Failed!"); 62 | return postResponse; 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/constants/ApiConstants.java: -------------------------------------------------------------------------------- 1 | package io.backend.constants; 2 | 3 | import io.backend.commons.RestResource; 4 | import io.backend.utils.ConfigLoader; 5 | import io.backend.utils.RetryUtils; 6 | import lombok.AccessLevel; 7 | import lombok.NoArgsConstructor; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 10 | public final class ApiConstants { 11 | 12 | public static final ConfigLoader CONFIG_LOADER = ConfigLoader.getInstance(); 13 | public static final RestResource REST_RESOURCE = RestResource.getInstance(); 14 | public static final RetryUtils RETRY_UTILS = new RetryUtils(); 15 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/constants/ApiRoutes.java: -------------------------------------------------------------------------------- 1 | package io.backend.constants; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public class ApiRoutes { 8 | public static final String GET_POSTAL_CODE_INFO = ApiConstants.CONFIG_LOADER.getZipposHost() + "/"; 9 | public static final String POST_CREATE_USER = ApiConstants.CONFIG_LOADER.getReqresHost() + "/api/users"; 10 | public static final String GET_RICK_AND_MORTY_CHARACTER = ApiConstants.CONFIG_LOADER.getRickAndMortyHost() + "/api/character/"; 11 | public static final String IFSC_CODE_DETAILS = ApiConstants.CONFIG_LOADER.getIfscCodeHost() + "/"; 12 | public static final String GET_ALL_PRODUCTS_LIST = ApiConstants.CONFIG_LOADER.getAutomationExerciseHost() + "/api/productsList"; 13 | 14 | // DISCORD API 15 | public static final String DISCORD_WEBHOOK = ApiConstants.CONFIG_LOADER.getDiscordHost() + "/api/webhooks/"; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/backend/entities/request/CreateUsersRequest.java: -------------------------------------------------------------------------------- 1 | package io.backend.entities.request; 2 | 3 | public record CreateUsersRequest(String name, String job) { 4 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/entities/request/discord/SendMessageRequest.java: -------------------------------------------------------------------------------- 1 | package io.backend.entities.request.discord; 2 | 3 | public record SendMessageRequest(String content) { 4 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/entities/response/CreateUserResponse.java: -------------------------------------------------------------------------------- 1 | package io.backend.entities.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public record CreateUserResponse(String id, String createdAt) { 7 | 8 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/entities/response/IfscCodeDetailsResponse.java: -------------------------------------------------------------------------------- 1 | package io.backend.entities.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | public record IfscCodeDetailsResponse( 8 | @JsonProperty(value = "BRANCH") String branch, 9 | @JsonProperty(value = "CENTRE") String centre, 10 | @JsonProperty(value = "DISTRICT") String district, 11 | @JsonProperty(value = "STATE") String state, 12 | @JsonProperty(value = "ADDRESS") String address, 13 | @JsonProperty(value = "CONTACT") String contact, 14 | @JsonProperty(value = "IMPS") boolean imps, 15 | @JsonProperty(value = "CITY") String city, 16 | @JsonProperty(value = "UPI") boolean upi, 17 | @JsonProperty(value = "MICR") String micr, 18 | @JsonProperty(value = "RTGS") boolean rtgs, 19 | @JsonProperty(value = "NEFT") boolean neft, 20 | @JsonProperty(value = "SWIFT") String swift, 21 | @JsonProperty(value = "ISO3166") String iso3166, 22 | @JsonProperty(value = "BANK") String bank, 23 | @JsonProperty(value = "IFSC") String ifsc 24 | ) { 25 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/entities/response/PostalCodeDetailsResponse.java: -------------------------------------------------------------------------------- 1 | package io.backend.entities.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | public record PostalCodeDetailsResponse(@JsonProperty("post code") String postCode, String country, 8 | @JsonProperty("country abbreviation") String countryAbbreviation) { 9 | 10 | @JsonIgnoreProperties(ignoreUnknown = true) 11 | private static record Places(@JsonProperty("place name") String placeName, String longitude, 12 | String state, @JsonProperty("state abbreviation") String stateAbbreviation, 13 | String latitude) { 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/entities/response/RickAndMortyResponse.java: -------------------------------------------------------------------------------- 1 | package io.backend.entities.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | import java.util.List; 6 | 7 | @JsonIgnoreProperties(ignoreUnknown = true) 8 | public record RickAndMortyResponse(int id, String name, String status, String type, String gender, Origin origin, 9 | Location location, String image, List episode, String url, String created) { 10 | 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public static record Origin(String name, String url) { 13 | 14 | } 15 | 16 | @JsonIgnoreProperties(ignoreUnknown = true) 17 | private static record Location(String name, String url) { 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/entities/response/automationexercise/GetAllProductListResponse.java: -------------------------------------------------------------------------------- 1 | package io.backend.entities.response.automationexercise; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | import java.util.List; 6 | 7 | @JsonIgnoreProperties(ignoreUnknown = true) 8 | public record GetAllProductListResponse(int responseCode, List products) { 9 | 10 | @JsonIgnoreProperties(ignoreUnknown = true) 11 | public static record Products(Integer id, String name, String price, String brand, Category category) { 12 | 13 | @JsonIgnoreProperties(ignoreUnknown = true) 14 | public static record Category(String category, Usertype usertype) { 15 | 16 | @JsonIgnoreProperties(ignoreUnknown = true) 17 | public static record Usertype(String usertype) { 18 | 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/exceptions/ApiTestException.java: -------------------------------------------------------------------------------- 1 | package io.backend.exceptions; 2 | 3 | public class ApiTestException extends RuntimeException { 4 | 5 | public ApiTestException(String errorMessage) { 6 | super(errorMessage); 7 | } 8 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/exceptions/DateUtilsException.java: -------------------------------------------------------------------------------- 1 | package io.backend.exceptions; 2 | 3 | public class DateUtilsException extends RuntimeException { 4 | 5 | public DateUtilsException(String errorMessage) { 6 | super(errorMessage); 7 | } 8 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/exceptions/DiscordException.java: -------------------------------------------------------------------------------- 1 | package io.backend.exceptions; 2 | 3 | public class DiscordException extends RuntimeException { 4 | 5 | public DiscordException(String errorMessage) { 6 | super(errorMessage); 7 | } 8 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/exceptions/TestUtilsException.java: -------------------------------------------------------------------------------- 1 | package io.backend.exceptions; 2 | 3 | public class TestUtilsException extends RuntimeException { 4 | 5 | public TestUtilsException(String errorMessage) { 6 | super(errorMessage); 7 | } 8 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/services/discord/DiscordClient.java: -------------------------------------------------------------------------------- 1 | package io.backend.services.discord; 2 | 3 | import io.backend.constants.ApiConstants; 4 | import io.backend.constants.ApiRoutes; 5 | import io.backend.entities.request.discord.SendMessageRequest; 6 | import io.backend.exceptions.DiscordException; 7 | import io.restassured.response.Response; 8 | import lombok.NoArgsConstructor; 9 | import lombok.SneakyThrows; 10 | 11 | @NoArgsConstructor 12 | public class DiscordClient { 13 | 14 | private static final String WEBHOOK_TOKEN = "{your_web_hook_token}"; 15 | 16 | @SneakyThrows 17 | public Response getSendMessageResponse(SendMessageRequest sendMessageRequest) { 18 | String discordWebHookEndPoint = ApiRoutes.DISCORD_WEBHOOK + WEBHOOK_TOKEN; 19 | String discordMessage = ApiConstants.REST_RESOURCE.serialize(sendMessageRequest); 20 | Response sendMessageResponse = ApiConstants.REST_RESOURCE.postApiResponse(discordMessage, discordWebHookEndPoint); 21 | if (sendMessageResponse != null) 22 | return sendMessageResponse; 23 | else 24 | throw new DiscordException("Client Exception: Discord Send Message API"); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/services/discord/DiscordController.java: -------------------------------------------------------------------------------- 1 | package io.backend.services.discord; 2 | 3 | import io.backend.commons.HttpStatuses; 4 | import io.backend.constants.ApiConstants; 5 | import io.backend.entities.request.discord.SendMessageRequest; 6 | import io.restassured.response.Response; 7 | import lombok.extern.slf4j.Slf4j; 8 | import net.jodah.failsafe.Failsafe; 9 | 10 | @Slf4j 11 | public class DiscordController { 12 | 13 | DiscordClient discordClient; 14 | 15 | public DiscordController() { 16 | this.discordClient = new DiscordClient(); 17 | } 18 | 19 | public String getSendMessageResponse(SendMessageRequest sendMessageRequest) { 20 | return Failsafe.with(ApiConstants.RETRY_UTILS.getRetryPolicyForDiscordException(2, 3)).get(() -> { 21 | Response sendMessageResponse = discordClient.getSendMessageResponse(sendMessageRequest); 22 | if (sendMessageResponse.getStatusCode() != HttpStatuses.NO_CONTENT.getCode()) 23 | log.error("Retrying for the Discord Send Message Code. Please stay with us..."); 24 | return sendMessageResponse.asString(); 25 | }); 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/services/discord/DiscordHelper.java: -------------------------------------------------------------------------------- 1 | package io.backend.services.discord; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | @Slf4j 8 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 9 | public class DiscordHelper { 10 | 11 | private static DiscordController discordController; 12 | 13 | public static DiscordController getDiscordController() { 14 | if (discordController == null) { 15 | log.info("Setting up the Discord Controller..."); 16 | discordController = new DiscordController(); 17 | } 18 | return discordController; 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/services/rest/ApiClients.java: -------------------------------------------------------------------------------- 1 | package io.backend.services.rest; 2 | 3 | import io.backend.constants.ApiConstants; 4 | import io.backend.constants.ApiRoutes; 5 | import io.backend.entities.request.CreateUsersRequest; 6 | import io.restassured.response.Response; 7 | import lombok.NoArgsConstructor; 8 | import lombok.SneakyThrows; 9 | 10 | import java.util.Objects; 11 | 12 | @NoArgsConstructor 13 | public class ApiClients { 14 | public Response getPostalCodeResponse(String country, String pinCode) { 15 | String postalCodeEndPoint = ApiRoutes.GET_POSTAL_CODE_INFO + country + "/" + pinCode; 16 | Response postalCodeResponse = ApiConstants.REST_RESOURCE.getApiResponse(postalCodeEndPoint); 17 | Objects.requireNonNull(postalCodeResponse, "Client Exception: Zippost Postal Code API"); 18 | return postalCodeResponse; 19 | } 20 | 21 | @SneakyThrows 22 | public Response createUserResponse(String name, String job) { 23 | CreateUsersRequest createUsersRequest = new CreateUsersRequest(name, job); 24 | String request = ApiConstants.REST_RESOURCE.serialize(createUsersRequest); 25 | Response createResponse = ApiConstants.REST_RESOURCE.postApiResponse(request, ApiRoutes.POST_CREATE_USER); 26 | Objects.requireNonNull(createResponse, "Client Exception: Reqres Create User API"); 27 | return createResponse; 28 | } 29 | 30 | public Response getRickAndMortyCharacterResponse(int characterId) { 31 | String rickAndMortyCharacterEndPoint = ApiRoutes.GET_RICK_AND_MORTY_CHARACTER + characterId; 32 | Response rickAndMortyCharacterResponse = ApiConstants.REST_RESOURCE.getApiResponse(rickAndMortyCharacterEndPoint); 33 | Objects.requireNonNull(rickAndMortyCharacterResponse, "Client Exception: Rick And Morty Character Details API"); 34 | return rickAndMortyCharacterResponse; 35 | } 36 | 37 | public Response getIfscCodeDetailsResponse(String ifscCode) { 38 | String ifscCodeDetailsEndPoint = ApiRoutes.IFSC_CODE_DETAILS + ifscCode; 39 | Response ifscCodeDetailsResponse = ApiConstants.REST_RESOURCE.getApiResponse(ifscCodeDetailsEndPoint); 40 | Objects.requireNonNull(ifscCodeDetailsResponse, "Client Exception: IFSC Code Details API"); 41 | return ifscCodeDetailsResponse; 42 | } 43 | 44 | public Response getAllProductsListResponse() { 45 | String productsListEndPoint = ApiRoutes.GET_ALL_PRODUCTS_LIST; 46 | Response productsListResponse = ApiConstants.REST_RESOURCE.getApiResponse(productsListEndPoint); 47 | Objects.requireNonNull(productsListResponse, "Client Exception: Automation Exercise Get All Products List API"); 48 | return productsListResponse; 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/services/rest/ApiControllers.java: -------------------------------------------------------------------------------- 1 | package io.backend.services.rest; 2 | 3 | import io.backend.commons.HttpStatuses; 4 | import io.backend.constants.ApiConstants; 5 | import io.backend.entities.response.CreateUserResponse; 6 | import io.backend.entities.response.IfscCodeDetailsResponse; 7 | import io.backend.entities.response.PostalCodeDetailsResponse; 8 | import io.backend.entities.response.RickAndMortyResponse; 9 | import io.backend.entities.response.automationexercise.GetAllProductListResponse; 10 | import io.backend.exceptions.ApiTestException; 11 | import io.restassured.response.Response; 12 | import lombok.extern.slf4j.Slf4j; 13 | import net.jodah.failsafe.Failsafe; 14 | 15 | @Slf4j 16 | public class ApiControllers { 17 | 18 | private final ApiClients API_CLIENTS; 19 | 20 | public ApiControllers() { 21 | this.API_CLIENTS = new ApiClients(); 22 | } 23 | 24 | public PostalCodeDetailsResponse getPostalCodeDetailsResponse(String country, String pinCode) { 25 | return Failsafe.with(ApiConstants.RETRY_UTILS.getRetryPolicyForZipposTestException(2, 3)).get(() -> { 26 | Response zipposPostalCodeResponse = API_CLIENTS.getPostalCodeResponse(country, pinCode); 27 | if (zipposPostalCodeResponse.getStatusCode() != HttpStatuses.OK.getCode()) { 28 | log.error("Retrying for the Zippos Postal Code. Please stay with us..."); 29 | throw new ApiTestException("Zippos Postal Code Details Status code mismatched!"); 30 | } 31 | return ApiConstants.REST_RESOURCE.deserialize(zipposPostalCodeResponse, PostalCodeDetailsResponse.class); 32 | }); 33 | } 34 | 35 | public CreateUserResponse getCreateUserResponse(String name, String job) { 36 | return Failsafe.with(ApiConstants.RETRY_UTILS.getRetryPolicyForReqresTestException(2, 3)).get(() -> { 37 | Response createUserResponse = API_CLIENTS.createUserResponse(name, job); 38 | if (createUserResponse.getStatusCode() != HttpStatuses.CREATED.getCode()) { 39 | log.error("Retrying for the Reqres Create User. Please stay with us..."); 40 | throw new ApiTestException("Reqres Create User status code mismatched!"); 41 | } 42 | return ApiConstants.REST_RESOURCE.deserialize(createUserResponse, CreateUserResponse.class); 43 | }); 44 | } 45 | 46 | 47 | public RickAndMortyResponse getRickAndMortyResponse(int characterId) { 48 | return Failsafe.with(ApiConstants.RETRY_UTILS.getRetryPolicyForRickAndMortyTestException(2, 3)).get(() -> { 49 | Response rickAndMortyCharacterResponse = API_CLIENTS.getRickAndMortyCharacterResponse(characterId); 50 | if (rickAndMortyCharacterResponse.getStatusCode() != HttpStatuses.OK.getCode()) { 51 | log.error("Retrying for the Rick And Morty Character. Please stay with us..."); 52 | throw new ApiTestException("Rick and Morty Get Character Details status code mismatched!"); 53 | } 54 | return ApiConstants.REST_RESOURCE.deserialize(rickAndMortyCharacterResponse, RickAndMortyResponse.class); 55 | }); 56 | } 57 | 58 | public IfscCodeDetailsResponse getIfscCodeDetailsResponse(String ifscCode) { 59 | return Failsafe.with(ApiConstants.RETRY_UTILS.getRetryPolicyForIfscCodeTestException(2, 3)).get(() -> { 60 | Response ifscCodeResponse = API_CLIENTS.getIfscCodeDetailsResponse(ifscCode); 61 | if (ifscCodeResponse.getStatusCode() != HttpStatuses.OK.getCode()) { 62 | log.error("Retrying for the IFSC Code Details. Please stay with us..."); 63 | throw new ApiTestException("IFSC Code Details Status code mismatched!"); 64 | } 65 | return ApiConstants.REST_RESOURCE.deserialize(ifscCodeResponse, IfscCodeDetailsResponse.class); 66 | }); 67 | } 68 | 69 | public GetAllProductListResponse getAllProductListResponse() { 70 | return Failsafe.with(ApiConstants.RETRY_UTILS.getRetryPolicyForAutomationExerciseException(2, 3)).get(() -> { 71 | Response productsListResponse = API_CLIENTS.getAllProductsListResponse(); 72 | if (productsListResponse.statusCode() != HttpStatuses.OK.getCode()) { 73 | log.error("Retrying for the Get All Products List. Please stay with us..."); 74 | throw new ApiTestException("Get All Products List status code mismatched!"); 75 | } 76 | return ApiConstants.REST_RESOURCE.deserialize(productsListResponse, GetAllProductListResponse.class); 77 | }); 78 | } 79 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/services/rest/ApiHelpers.java: -------------------------------------------------------------------------------- 1 | package io.backend.services.rest; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | @Slf4j 8 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 9 | public class ApiHelpers { 10 | 11 | private static ApiControllers apiControllers; 12 | 13 | public static ApiControllers getApiControllers() { 14 | if (apiControllers == null) { 15 | log.info("Setting up the Api Controllers..."); 16 | apiControllers = new ApiControllers(); 17 | } 18 | return apiControllers; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/backend/utils/ConfigLoader.java: -------------------------------------------------------------------------------- 1 | package io.backend.utils; 2 | 3 | import java.util.Objects; 4 | import java.util.Properties; 5 | 6 | public class ConfigLoader { 7 | 8 | private final Properties PROPERTIES; 9 | private static ConfigLoader instance; 10 | 11 | private ConfigLoader() { 12 | PROPERTIES = PropertiesHelper.loadProperties("./src/main/java/resource/api.properties"); 13 | } 14 | 15 | public static ConfigLoader getInstance() { 16 | if (instance == null) { 17 | synchronized (ConfigLoader.class) { 18 | if (instance == null) 19 | instance = new ConfigLoader(); 20 | } 21 | } 22 | return instance; 23 | } 24 | 25 | public String getZipposHost() { 26 | return getPropertyValue("ZIPPOS_HOST"); 27 | } 28 | 29 | public String getReqresHost() { 30 | return getPropertyValue("REQRES_HOST"); 31 | } 32 | 33 | public String getRickAndMortyHost() { 34 | return getPropertyValue("RICK_AND_MORTY_HOST"); 35 | } 36 | 37 | public String getIfscCodeHost() { 38 | return getPropertyValue("IFSC_CODE_HOST"); 39 | } 40 | 41 | public String getAutomationExerciseHost() { 42 | return getPropertyValue("AUTOMATION_EXERCISE"); 43 | } 44 | 45 | public String getDiscordHost() { 46 | return getPropertyValue("DISCORD_HOST"); 47 | } 48 | 49 | public String getPropertyValue(String propertyKey) { 50 | Objects.requireNonNull(propertyKey, "Property Value for Property Key: " + propertyKey + " is null or empty!"); 51 | return PROPERTIES.getProperty(propertyKey); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/utils/DateUtils.java: -------------------------------------------------------------------------------- 1 | package io.backend.utils; 2 | 3 | import io.backend.exceptions.DateUtilsException; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.Duration; 8 | import java.time.Instant; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class DateUtils { 12 | public static Instant getCurrentInstantTimeStamp() { 13 | return Instant.now(); 14 | } 15 | 16 | public static long getDurationBetweenTimeStamps(Instant startDate, Instant endDate) { 17 | if (startDate == null || endDate == null || startDate.isAfter(endDate)) 18 | throw new DateUtilsException("Calculation for Duration Between timestamps failed!"); 19 | return Duration.between(startDate, endDate).getSeconds(); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/utils/DiscordUtils.java: -------------------------------------------------------------------------------- 1 | package io.backend.utils; 2 | 3 | import io.backend.entities.request.discord.SendMessageRequest; 4 | import io.backend.exceptions.DiscordException; 5 | import io.backend.services.discord.DiscordHelper; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | @Slf4j 9 | public class DiscordUtils { 10 | public static String buildDiscordMessage(int passedTestCases, int failedTestCases, int skippedTestCases, int totalTestCases) { 11 | StringBuilder discordMessageBuilder = new StringBuilder(); 12 | discordMessageBuilder.append("\n******************************\n") 13 | .append("\nPASS: ").append(passedTestCases) 14 | .append("\nFAIL: ").append(failedTestCases) 15 | .append("\nSKIP: ").append(skippedTestCases) 16 | .append("\nTOTAL: ").append(totalTestCases).append("\n") 17 | .append("\nPASS% : ").append(TestUtils.calculateTestCasePercentage(passedTestCases, totalTestCases)) 18 | .append("\nFAIL% : ").append(TestUtils.calculateTestCasePercentage(failedTestCases, totalTestCases)) 19 | .append("\nReport Portal Run: ").append(TestUtils.getReportPortalLaunchUrl()) 20 | .append("\n******************************\n"); 21 | return String.valueOf(discordMessageBuilder); 22 | } 23 | 24 | public static void sendMessageToChannel(String message) { 25 | SendMessageRequest sendMessageRequest = new SendMessageRequest(message); 26 | String sendMessageResponse = DiscordHelper.getDiscordController().getSendMessageResponse(sendMessageRequest); 27 | if (sendMessageResponse.isEmpty()) 28 | return; 29 | else 30 | throw new DiscordException("DISCORD UTILS: Send Message Failed!"); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/utils/PropertiesHelper.java: -------------------------------------------------------------------------------- 1 | package io.backend.utils; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Properties; 9 | 10 | @Slf4j 11 | public class PropertiesHelper { 12 | 13 | public static Properties loadProperties(String fileName) { 14 | Properties properties; 15 | InputStream fileInputStream; 16 | log.info("Loading the Properties file...."); 17 | try { 18 | fileInputStream = new FileInputStream(fileName); 19 | properties = new Properties(); 20 | properties.load(fileInputStream); 21 | } catch (IOException e) { 22 | throw new RuntimeException("Properties File failed to load!" + e.getMessage()); 23 | } 24 | return properties; 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/utils/RetryUtils.java: -------------------------------------------------------------------------------- 1 | package io.backend.utils; 2 | 3 | import io.backend.exceptions.ApiTestException; 4 | import io.backend.exceptions.DiscordException; 5 | import net.jodah.failsafe.RetryPolicy; 6 | import org.apache.http.NoHttpResponseException; 7 | 8 | import java.net.ConnectException; 9 | import java.net.SocketException; 10 | import java.time.Duration; 11 | 12 | public class RetryUtils { 13 | 14 | public RetryPolicy getDefaultRetryPolicy(int delayInSeconds, int maxRetries) { 15 | return new RetryPolicy<>() 16 | .handle(ConnectException.class) 17 | .handle(SocketException.class) 18 | .handle(NoHttpResponseException.class) 19 | .withDelay(Duration.ofSeconds(delayInSeconds)) 20 | .withMaxRetries(maxRetries); 21 | } 22 | 23 | public RetryPolicy getRetryPolicyForZipposTestException(int delayInSeconds, int maxRetries) { 24 | return getDefaultRetryPolicy(delayInSeconds, maxRetries).handle(ApiTestException.class); 25 | } 26 | 27 | public RetryPolicy getRetryPolicyForReqresTestException(int delayInSeconds, int maxRetries) { 28 | return getDefaultRetryPolicy(delayInSeconds, maxRetries).handle(ApiTestException.class); 29 | } 30 | 31 | public RetryPolicy getRetryPolicyForRickAndMortyTestException(int delayInSeconds, int maxRetries) { 32 | return getDefaultRetryPolicy(delayInSeconds, maxRetries).handle(ApiTestException.class); 33 | } 34 | 35 | public RetryPolicy getRetryPolicyForIfscCodeTestException(int delayInSeconds, int maxRetries) { 36 | return getDefaultRetryPolicy(delayInSeconds, maxRetries).handle(ApiTestException.class); 37 | } 38 | 39 | public RetryPolicy getRetryPolicyForAutomationExerciseException(int delayInSeconds, int maxRetries) { 40 | return getDefaultRetryPolicy(delayInSeconds, maxRetries).handle(ApiTestException.class); 41 | } 42 | 43 | public RetryPolicy getRetryPolicyForDiscordException(int delayInSeconds, int maxRetries) { 44 | return getDefaultRetryPolicy(delayInSeconds, maxRetries).handle(DiscordException.class); 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/io/backend/utils/TestUtils.java: -------------------------------------------------------------------------------- 1 | package io.backend.utils; 2 | 3 | import com.epam.reportportal.listeners.ListenerParameters; 4 | import com.epam.reportportal.service.Launch; 5 | import io.backend.exceptions.TestUtilsException; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.testng.ITestResult; 8 | 9 | import java.util.Optional; 10 | 11 | @Slf4j 12 | public class TestUtils { 13 | 14 | public static String concatenateTestMethodTestData(ITestResult result, Object[] testParameters) { 15 | StringBuilder testName = new StringBuilder(); 16 | if (result != null) { 17 | if (result.getMethod().isDataDriven()) { 18 | testName.append(result.getName()).append("_"); 19 | for (Object object : testParameters) { 20 | testName.append(object.toString()).append("_"); 21 | } 22 | return testName.substring(0, testName.length() - 1); 23 | } else 24 | return result.getName(); 25 | } else 26 | throw new TestUtilsException("Test Method and Test Data Concatenation Failed!"); 27 | } 28 | 29 | public static int calculateTestCasePercentage(int numberOfTestCases, int totalTestCases) { 30 | if (totalTestCases > 0) { 31 | double testCasePercentage = ((double) numberOfTestCases / totalTestCases) * 100; 32 | return (int) testCasePercentage; 33 | } else 34 | throw new TestUtilsException("Cannot be divided by Zero! Total Test Cases cannot be Zero."); 35 | } 36 | 37 | public static String getReportPortalLaunchUrl() { 38 | Launch launch = Optional.ofNullable(Launch.currentLaunch()).filter(currentLaunch -> currentLaunch != Launch.NOOP_LAUNCH) 39 | .orElse(null); 40 | if (launch == null) 41 | return "Report Portal Listener is disabled!"; 42 | ListenerParameters parameters = launch.getParameters(); 43 | String launchUuid = launch.getLaunch().blockingGet(); 44 | String baseUrl = parameters.getBaseUrl(); 45 | return baseUrl + "/ui/#" + parameters.getProjectName() + "/launches/all/" + launchUuid; 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/resource/api.properties: -------------------------------------------------------------------------------- 1 | ZIPPOS_HOST=https://api.zippopotam.us 2 | REQRES_HOST=https://reqres.in 3 | RICK_AND_MORTY_HOST=https://rickandmortyapi.com 4 | IFSC_CODE_HOST=https://ifsc.razorpay.com 5 | AUTOMATION_EXERCISE=https://automationexercise.com 6 | DISCORD_HOST=https://discord.com -------------------------------------------------------------------------------- /src/test/java/io/backend/api/automationexercise/GetAllProductsListTest.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.automationexercise; 2 | 3 | import io.backend.api.base.BaseTest; 4 | import io.backend.api.constants.TestGroups; 5 | import io.backend.api.testdata.ApiDataProvider; 6 | import io.backend.commons.HttpStatuses; 7 | import io.backend.entities.response.automationexercise.GetAllProductListResponse; 8 | import io.backend.exceptions.ApiTestException; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.testng.annotations.Test; 11 | import org.testng.asserts.SoftAssert; 12 | 13 | import java.util.List; 14 | import java.util.Objects; 15 | 16 | @Slf4j 17 | public class GetAllProductsListTest extends BaseTest { 18 | 19 | @Test(description = "To verify, all the product details.", dataProvider = "get-all-products", dataProviderClass = ApiDataProvider.class, 20 | groups = {TestGroups.ALL_REGRESSION, TestGroups.AUTOMATION_EXERCISE_REGRESSION}) 21 | public void testGetAllProductsList(GetAllProductListResponse productListResponse, int expectedId, String expectedName, 22 | String expectedPrice, String expectedBrand, String expectedUserType, String expectedCategory) { 23 | SoftAssert softAssert = new SoftAssert(); 24 | softAssert.assertEquals(productListResponse.responseCode(), HttpStatuses.OK.getCode(), "Get All Products List Response Code Mismatched!"); 25 | List productsList = productListResponse.products(); 26 | int actualId = productsList.stream().filter(Objects::nonNull) 27 | .filter(id -> id.id().equals(expectedId)).findFirst() 28 | .orElseThrow(() -> new ApiTestException("Test Data: Invalid Product ID!")).id(); 29 | softAssert.assertEquals(actualId, expectedId, "Get All Products List ID Mismatched!"); 30 | String actualName = productsList.stream().filter(Objects::nonNull) 31 | .filter(name -> name.name().equals(expectedName)).findFirst() 32 | .orElseThrow(() -> new ApiTestException("Test Data: Invalid Product Name!")).name(); 33 | softAssert.assertEquals(actualName, expectedName, "Get All Products List Name Mismatched!"); 34 | String actualPrice = productsList.stream().filter(Objects::nonNull) 35 | .filter(price -> price.price().equals(expectedPrice)).findFirst() 36 | .orElseThrow(() -> new ApiTestException("Test Data: Invalid Product Price!")).price(); 37 | softAssert.assertEquals(actualPrice, expectedPrice, "Get All Products List Price Mismatched!"); 38 | String actualBrand = productsList.stream().filter(Objects::nonNull) 39 | .filter(brand -> brand.brand().equals(expectedBrand)).findFirst() 40 | .orElseThrow(() -> new ApiTestException("Test Data: ")).brand(); 41 | softAssert.assertEquals(actualBrand, expectedBrand, "Get All Products List Brand Mismatched!"); 42 | String actualCategory = productsList.stream().filter(Objects::nonNull) 43 | .filter(category -> category.category().category().equals(expectedCategory)).findFirst() 44 | .orElseThrow(() -> new ApiTestException("Test Data: Invalid Product Category!")).category().category(); 45 | softAssert.assertEquals(actualCategory, expectedCategory, "Get All Products List Category Mismatched!"); 46 | String actualUserType = productsList.stream().filter(Objects::nonNull) 47 | .filter(userType -> userType.category().usertype().usertype().equals(expectedUserType)) 48 | .findFirst().orElseThrow(() -> new ApiTestException("Test Data: Invalid user type!")) 49 | .category().usertype().usertype(); 50 | softAssert.assertEquals(actualUserType, expectedUserType, "Get All Products List User Type Mismatched!"); 51 | softAssert.assertAll(); 52 | log.info("Verified the Product Details: {}, {}, {}, {}, {}, {}", actualId, actualName, actualPrice, actualBrand, actualCategory, actualUserType); 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/java/io/backend/api/base/BaseTest.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.base; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.testng.annotations.AfterMethod; 5 | import org.testng.annotations.BeforeMethod; 6 | 7 | import java.lang.reflect.Method; 8 | 9 | 10 | @Slf4j 11 | public abstract class BaseTest { 12 | 13 | @BeforeMethod(alwaysRun = true) 14 | protected void setUp(Method method) { 15 | log.info("Started Executing the test method {}, in the thread: {}", method.getName(), Thread.currentThread().getId()); 16 | } 17 | 18 | @AfterMethod(alwaysRun = true) 19 | protected void tearDown(Method method) { 20 | log.info("Completed Executing the test method {} , in the thread: {}", method.getName(), Thread.currentThread().getId()); 21 | } 22 | } -------------------------------------------------------------------------------- /src/test/java/io/backend/api/constants/TestGroups.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.constants; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public final class TestGroups { 8 | public static final String ALL_SMOKE = "ALL_SMOKE"; 9 | public static final String ALL_REGRESSION = "ALL_REGRESSION"; 10 | public static final String IFSC_SMOKE = "IFSC_SMOKE"; 11 | public static final String IFSC_REGRESSION = "IFSC_REGRESSION"; 12 | public static final String REQ_RES_SMOKE = "REQ_RES_SMOKE"; 13 | public static final String REQ_RES_REGRESSION = "REQ_RES_REGRESSION"; 14 | public static final String RICK_MORTY_SMOKE = "RICK_MORTY_SMOKE"; 15 | public static final String RICK_MORTY_REGRESSION = "RICK_MORTY_REGRESSION"; 16 | public static final String ZIPPOS_SMOKE = "ZIPPOS_SMOKE"; 17 | public static final String ZIPPOS_REGRESSION = "ZIPPOS_REGRESSION"; 18 | public static final String AUTOMATION_EXERCISE_REGRESSION = "AUTOMATION_EXERCISE_REGRESSION"; 19 | } -------------------------------------------------------------------------------- /src/test/java/io/backend/api/ifsc/tests/IfscCodeTest.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.ifsc.tests; 2 | 3 | import io.backend.api.base.BaseTest; 4 | import io.backend.api.constants.TestGroups; 5 | import io.backend.api.testdata.ApiDataProvider; 6 | import io.backend.entities.response.IfscCodeDetailsResponse; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.testng.Assert; 9 | import org.testng.annotations.Test; 10 | 11 | @Slf4j 12 | public class IfscCodeTest extends BaseTest { 13 | @Test(description = "To verify, the ifsc code details.", dataProvider = "ifsc-code", dataProviderClass = ApiDataProvider.class, 14 | groups = {TestGroups.IFSC_SMOKE, TestGroups.IFSC_REGRESSION, TestGroups.ALL_SMOKE, TestGroups.ALL_REGRESSION}) 15 | public void testIfscCodeDetails(IfscCodeDetailsResponse ifscCodeDetailsResponse, String ifscCode) { 16 | Assert.assertEquals(ifscCodeDetailsResponse.ifsc(), ifscCode, "IFSC Code Mismatched!"); 17 | log.info("Verified the IFSC Code Details for the given IFSC Code {}", ifscCode); 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/java/io/backend/api/listeners/ApiListeners.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.listeners; 2 | 3 | import io.backend.utils.DateUtils; 4 | import io.backend.utils.DiscordUtils; 5 | import io.backend.utils.TestUtils; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.testng.*; 8 | 9 | import java.time.Instant; 10 | 11 | @Slf4j 12 | public class ApiListeners implements ITestListener, ISuiteListener { 13 | 14 | private Instant startDate; 15 | 16 | @Override 17 | public void onStart(ISuite suite) { 18 | startDate = DateUtils.getCurrentInstantTimeStamp(); 19 | log.info("API Test Suite '{}' Started executing at {}.", suite.getName(), startDate); 20 | } 21 | 22 | @Override 23 | public void onFinish(ITestContext context) { 24 | Instant endDate = DateUtils.getCurrentInstantTimeStamp(); 25 | long timeElapsed = DateUtils.getDurationBetweenTimeStamps(startDate, endDate); 26 | log.info("API Suite Finished executing in {} seconds.", timeElapsed); 27 | int passedTestCases = context.getPassedTests().size(); 28 | int failedTestCases = context.getFailedTests().size(); 29 | int skippedTestCases = context.getSkippedTests().size(); 30 | int totalTestCases = passedTestCases + failedTestCases + skippedTestCases; 31 | String discordMessage = DiscordUtils.buildDiscordMessage(passedTestCases, failedTestCases, skippedTestCases, totalTestCases); 32 | DiscordUtils.sendMessageToChannel(discordMessage); 33 | } 34 | 35 | @Override 36 | public void onTestSuccess(ITestResult result) { 37 | log.info("Test Method {} is PASS.", TestUtils.concatenateTestMethodTestData(result, result.getParameters())); 38 | } 39 | 40 | @Override 41 | public void onTestFailure(ITestResult result) { 42 | log.info("Test Method {} is FAIL.", TestUtils.concatenateTestMethodTestData(result, result.getParameters())); 43 | 44 | } 45 | 46 | @Override 47 | public void onTestSkipped(ITestResult result) { 48 | log.info("Test Method {} SKIP.", TestUtils.concatenateTestMethodTestData(result, result.getParameters())); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/io/backend/api/reqres/tests/ReqresTest.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.reqres.tests; 2 | 3 | import io.backend.api.base.BaseTest; 4 | import io.backend.api.constants.TestGroups; 5 | import io.backend.api.testdata.ApiDataProvider; 6 | import io.backend.entities.response.CreateUserResponse; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.testng.annotations.Test; 9 | import org.testng.asserts.SoftAssert; 10 | 11 | @Slf4j 12 | public class ReqresTest extends BaseTest { 13 | 14 | @Test(description = "To verify, postal code details for the given country and pin code", dataProvider = "create-user", 15 | dataProviderClass = ApiDataProvider.class, groups = {TestGroups.REQ_RES_SMOKE, TestGroups.REQ_RES_REGRESSION, 16 | TestGroups.ALL_SMOKE, TestGroups.ALL_REGRESSION}) 17 | public void testUserDetails(CreateUserResponse createUserResponse) { 18 | SoftAssert softAssert = new SoftAssert(); 19 | softAssert.assertFalse(createUserResponse.id().isEmpty(), "Create User ID is empty!"); 20 | softAssert.assertFalse(createUserResponse.createdAt().isEmpty(), "Create User date is empty!"); 21 | softAssert.assertAll(); 22 | log.info("Verified the Create User API with id {}", createUserResponse.id()); 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/java/io/backend/api/rickandmorty/tests/RickAndMortyCharacterTest.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.rickandmorty.tests; 2 | 3 | import io.backend.api.base.BaseTest; 4 | import io.backend.api.constants.TestGroups; 5 | import io.backend.api.testdata.ApiDataProvider; 6 | import io.backend.entities.response.RickAndMortyResponse; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.testng.annotations.Test; 9 | import org.testng.asserts.SoftAssert; 10 | 11 | @Slf4j 12 | public class RickAndMortyCharacterTest extends BaseTest { 13 | @Test(description = "To verify, all the rick and morty character details.", dataProvider = "rick-and-morty-characters", 14 | dataProviderClass = ApiDataProvider.class, groups = {TestGroups.RICK_MORTY_SMOKE, TestGroups.RICK_MORTY_REGRESSION, 15 | TestGroups.ALL_SMOKE, TestGroups.ALL_REGRESSION}) 16 | public void testRickAndMortyCharacters(RickAndMortyResponse rickAndMortyResponse, int characterId, 17 | String characterName, String characterStatus, String characterOrigin) { 18 | SoftAssert softAssert = new SoftAssert(); 19 | softAssert.assertEquals(rickAndMortyResponse.id(), characterId, "Rick And Morty Character Id Failed!"); 20 | softAssert.assertEquals(rickAndMortyResponse.name(), characterName, "Rick And Morty Character Name Failed!"); 21 | softAssert.assertEquals(rickAndMortyResponse.status(), characterStatus, "Rick And Morty Character Status Failed!"); 22 | softAssert.assertEquals(rickAndMortyResponse.origin().name(), characterOrigin, "Rick And Morty Character Origin Failed!"); 23 | softAssert.assertAll(); 24 | log.info("Verified the Character ID {}, Character Name {} and Character Status {}", characterId, characterName, characterStatus); 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/java/io/backend/api/testdata/ApiDataProvider.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.testdata; 2 | 3 | import io.backend.entities.response.CreateUserResponse; 4 | import io.backend.entities.response.IfscCodeDetailsResponse; 5 | import io.backend.entities.response.PostalCodeDetailsResponse; 6 | import io.backend.entities.response.RickAndMortyResponse; 7 | import io.backend.entities.response.automationexercise.GetAllProductListResponse; 8 | import io.backend.services.rest.ApiHelpers; 9 | import org.testng.annotations.DataProvider; 10 | 11 | public class ApiDataProvider { 12 | 13 | @DataProvider(name = "postal-codes", parallel = true) 14 | private Object[][] postalCodes() { 15 | return new Object[][]{ 16 | {getPostalCodeDetailsResponse("us", "90210"), "us", "90210", "United States"} 17 | }; 18 | } 19 | 20 | private PostalCodeDetailsResponse getPostalCodeDetailsResponse(String country, String pinCode) { 21 | return ApiHelpers.getApiControllers().getPostalCodeDetailsResponse(country, pinCode); 22 | } 23 | 24 | @DataProvider(name = "create-employee", parallel = true) 25 | private Object[][] createEmployee() { 26 | return new Object[][]{ 27 | {"test", "123", "23"} 28 | }; 29 | } 30 | 31 | @DataProvider(name = "create-user", parallel = true) 32 | private Object[][] createUser() { 33 | return new Object[][]{ 34 | {getCreateUserResponse("morpheus", "leader")} 35 | }; 36 | } 37 | 38 | private CreateUserResponse getCreateUserResponse(String name, String job) { 39 | return ApiHelpers.getApiControllers().getCreateUserResponse(name, job); 40 | } 41 | 42 | @DataProvider(name = "rick-and-morty-characters", parallel = true) 43 | private Object[][] rickAndMortyCharacters() { 44 | return new Object[][]{ 45 | {getRickAndMortyResponse(50), 50, "Blim Blam", "Alive", "unknown"}, 46 | {getRickAndMortyResponse(290), 290, "Rick Sanchez", "Dead", "Earth (Evil Rick's Target Dimension)"}, 47 | {getRickAndMortyResponse(303), 303, "Samantha", "Alive", "Earth (C-137)"}, 48 | {getRickAndMortyResponse(473), 473, "Bartender Morty", "Alive", "unknown"}, 49 | {getRickAndMortyResponse(572), 572, "Robot Snake", "unknown", "Snake Planet"}, 50 | {getRickAndMortyResponse(653), 653, "Plane Crash Survivor", "unknown", "Near-Duplicate Reality"} 51 | }; 52 | } 53 | 54 | private RickAndMortyResponse getRickAndMortyResponse(int characterId) { 55 | return ApiHelpers.getApiControllers().getRickAndMortyResponse(characterId); 56 | } 57 | 58 | @DataProvider(name = "ifsc-code", parallel = true) 59 | private Object[][] ifscCodes() { 60 | return new Object[][]{ 61 | {getIfscCodeDetailsResponse("YESB0DNB002"), "YESB0DNB002"}, 62 | {getIfscCodeDetailsResponse("HDFC0000260"), "HDFC0000260"} 63 | }; 64 | } 65 | 66 | private IfscCodeDetailsResponse getIfscCodeDetailsResponse(String ifscCode) { 67 | return ApiHelpers.getApiControllers().getIfscCodeDetailsResponse(ifscCode); 68 | } 69 | 70 | @DataProvider(name = "get-all-products", parallel = true) 71 | private Object[][] allProductsList() { 72 | final GetAllProductListResponse productListResponse = ApiHelpers.getApiControllers().getAllProductListResponse(); 73 | return new Object[][]{ 74 | {productListResponse, 1, "Blue Top", "Rs. 500", "Polo", "Women", "Tops"}, 75 | {productListResponse, 2, "Men Tshirt", "Rs. 400", "H&M", "Men", "Tshirts"}, 76 | {productListResponse, 3, "Sleeveless Dress", "Rs. 1000", "Madame", "Women", "Dress"}, 77 | {productListResponse, 4, "Stylish Dress", "Rs. 1500", "Madame", "Women", "Dress"}, 78 | {productListResponse, 43, "GRAPHIC DESIGN MEN T SHIRT - BLUE", "Rs. 1389", "Mast & Harbour", "Men", "Tshirts"} 79 | }; 80 | } 81 | } -------------------------------------------------------------------------------- /src/test/java/io/backend/api/zippo/tests/PostalCodeTest.java: -------------------------------------------------------------------------------- 1 | package io.backend.api.zippo.tests; 2 | 3 | 4 | import io.backend.api.base.BaseTest; 5 | import io.backend.api.constants.TestGroups; 6 | import io.backend.api.testdata.ApiDataProvider; 7 | import io.backend.entities.response.PostalCodeDetailsResponse; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.testng.annotations.Test; 10 | import org.testng.asserts.SoftAssert; 11 | 12 | @Slf4j 13 | public class PostalCodeTest extends BaseTest { 14 | 15 | @Test(description = "To verify, postal code details for the given country and pin code", dataProvider = "postal-codes", 16 | dataProviderClass = ApiDataProvider.class, groups = {TestGroups.ZIPPOS_SMOKE, TestGroups.ZIPPOS_REGRESSION, 17 | TestGroups.ALL_SMOKE, TestGroups.ALL_REGRESSION}) 18 | public void testPostalCodeDetails(PostalCodeDetailsResponse postalCodeDetailsResponse, String country, String pinCode, String fullCountryName) { 19 | SoftAssert softAssert = new SoftAssert(); 20 | softAssert.assertEquals(postalCodeDetailsResponse.country(), fullCountryName, "Postal Code Country Mismatched!"); 21 | softAssert.assertEquals(postalCodeDetailsResponse.postCode(), pinCode, "Post Code pin code Mismatched!"); 22 | softAssert.assertAll(); 23 | log.info("Verified the Postal Code details with country {} and pin code {}", 24 | postalCodeDetailsResponse.country(), postalCodeDetailsResponse.postCode()); 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ${user.dir}/logs/api_tests.log 16 | 17 | 18 | ${user.dir}/logs/api_tests_%d{yyyy-MM-dd}.gz 19 | 20 | 30 21 | 3GB 22 | 23 | 24 | %-4relative [%thread] %-5level %logger{35} - %msg%n 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/test/resources/reportportal.properties: -------------------------------------------------------------------------------- 1 | rp.endpoint=http://localhost:8080 2 | rp.api.key=backend-test_UKmvCvgUTPCve6ifzutRGaJQU-FowFl2YsAT1lZb3kdIQbIWTAJB9yu5NnMdg5Hg 3 | rp.launch=api_test_launch 4 | rp.project=api_tests --------------------------------------------------------------------------------