diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..2e07beb
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,33 @@
+name: Publish to GitHub Packages
+
+on:
+ push:
+ branches:
+ - develop
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Cache Maven dependencies
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven
+
+ - name: Build and Publish to GitHub Packages
+ run: mvn clean install deploy
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a32aab2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
+/.idea
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..3e54c11
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,59 @@
+
+ 4.0.0
+
+ com.tappayments.automation
+ qa-common-utils
+ 1.0.1
+ jar
+
+ qa-common-utils
+ http://maven.apache.org
+
+
+ UTF-8
+ 17
+ 17
+
+
+
+
+ io.rest-assured
+ rest-assured
+ 5.5.0
+
+
+
+ org.testng
+ testng
+ 7.8.0
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.17.2
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ provided
+
+
+
+ com.aventstack
+ extentreports
+ 5.1.2
+
+
+
+
+
+ github
+ https://maven.pkg.github.com/Tap-Payments/QA-Common-Utils
+
+
+
+
diff --git a/src/main/java/com/tappayments/automation/App.java b/src/main/java/com/tappayments/automation/App.java
new file mode 100644
index 0000000..5dd67b0
--- /dev/null
+++ b/src/main/java/com/tappayments/automation/App.java
@@ -0,0 +1,9 @@
+package com.tappayments.automation;
+
+public class App
+{
+ public static void main( String[] args )
+ {
+ System.out.println( "QA Common util project." );
+ }
+}
diff --git a/src/main/java/com/tappayments/automation/config/ExtentReportManager.java b/src/main/java/com/tappayments/automation/config/ExtentReportManager.java
new file mode 100644
index 0000000..ee15e8e
--- /dev/null
+++ b/src/main/java/com/tappayments/automation/config/ExtentReportManager.java
@@ -0,0 +1,82 @@
+package com.tappayments.automation.config;
+
+import com.aventstack.extentreports.ExtentReports;
+import com.aventstack.extentreports.markuputils.CodeLanguage;
+import com.aventstack.extentreports.markuputils.ExtentColor;
+import com.aventstack.extentreports.markuputils.MarkupHelper;
+import com.aventstack.extentreports.reporter.ExtentSparkReporter;
+import com.aventstack.extentreports.reporter.configuration.Theme;
+import com.tappayments.automation.utils.CommonConstants;
+import io.restassured.http.Header;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+public class ExtentReportManager {
+
+ public static ExtentReports extentReports;
+
+ public static ExtentReports createInstance(String fileName, String reportName, String documentTitle){
+
+ ExtentSparkReporter extentSparkReporter = new ExtentSparkReporter(fileName);
+ extentSparkReporter.config().setReportName(reportName);
+ extentSparkReporter.config().setDocumentTitle(documentTitle);
+ extentSparkReporter.config().setTheme(Theme.STANDARD);
+ extentSparkReporter.config().setEncoding(CommonConstants.UTF_8);
+
+ extentReports = new ExtentReports();
+ extentReports.attachReporter(extentSparkReporter);
+
+ return extentReports;
+ }
+
+ public static String getExtentReportNameWithTimeStamp(){
+
+ DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyy_MM_dd_HH_mm_ss");
+ LocalDateTime localDateTime = LocalDateTime.now();
+ String formattedTime = dateTimeFormatter.format(localDateTime);
+ return "QA_Automation_Extent_Report_" + formattedTime + ".html";
+ }
+
+ public static void logPassDetails(String log){
+
+ ExtentReportSetup.extentTest.get().pass(MarkupHelper.createLabel(log, ExtentColor.GREEN));
+ }
+
+ public static void logFailureDetails(String log){
+
+ ExtentReportSetup.extentTest.get().fail(MarkupHelper.createLabel(log, ExtentColor.RED));
+ }
+
+ public static void logExceptionDetails(String log){
+
+ ExtentReportSetup.extentTest.get().fail(log);
+ }
+
+ public static void logInfoDetails(String log){
+
+ ExtentReportSetup.extentTest.get().info(MarkupHelper.createLabel(log, ExtentColor.GREY));
+ }
+
+ public static void logJson(String json){
+
+ ExtentReportSetup.extentTest.get().info(MarkupHelper.createCodeBlock(json, CodeLanguage.JSON));
+ }
+
+ public static void logHeaders(List headerList){
+
+ String[][] headers = headerList.stream()
+ .map(header -> new String[]{
+ header.getName(), header.getValue()
+ })
+ .toArray(String[][]::new);
+
+ ExtentReportSetup.extentTest.get().info(MarkupHelper.createTable(headers));
+ }
+
+ public static void logWarningDetails(String log){
+
+ ExtentReportSetup.extentTest.get().warning(MarkupHelper.createLabel(log, ExtentColor.YELLOW));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/tappayments/automation/config/ExtentReportSetup.java b/src/main/java/com/tappayments/automation/config/ExtentReportSetup.java
new file mode 100644
index 0000000..d20ff84
--- /dev/null
+++ b/src/main/java/com/tappayments/automation/config/ExtentReportSetup.java
@@ -0,0 +1,100 @@
+package com.tappayments.automation.config;
+
+import com.aventstack.extentreports.ExtentReports;
+import com.aventstack.extentreports.ExtentTest;
+import org.testng.ITestContext;
+import org.testng.ITestListener;
+import org.testng.ITestResult;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ExtentReportSetup implements ITestListener {
+
+ private static ExtentReports extentReports;
+ public static ThreadLocal extentTest = new ThreadLocal<>();
+ private static Map categoryNodes = new HashMap<>();
+ private static Map subCategoryNodes = new HashMap<>();
+
+ public void onStart(ITestContext context){
+
+ // Generate the extent report name with timestamp
+ String fileName = ExtentReportManager.getExtentReportNameWithTimeStamp();
+
+ // Get platform-specific file separator ("/" for Mac/Linux, "\\" for Windows)
+ String separator = System.getProperty("file.separator");
+
+ // Construct the full path to the report directory
+ String fullReportPath = System.getProperty("user.dir")
+ + separator + "extent.reports"
+ + separator + fileName;
+ extentReports = ExtentReportManager.createInstance(fullReportPath, "QA Automation API Report", "QA Automation API Report");
+ }
+
+ public void onFinish(ITestContext context){
+
+ if(extentReports != null)
+ extentReports.flush();
+ }
+
+ public void onTestStart(ITestResult result) {
+
+ String[] groups = result.getMethod().getGroups();
+
+ if (groups.length > 0) {
+
+ String mainCategory = "";
+ String subCategory = "";
+ String sectionTag = "";
+ String author = "";
+
+ for (String group : groups) {
+ if (group.startsWith("MC:")) {
+ mainCategory = group.substring(3); // Remove "MC:" prefix
+ } else if (group.startsWith("SC:")) {
+ subCategory = group.substring(3); // Remove "SC:" prefix
+ } else if (group.startsWith("SECTION:")) {
+ sectionTag = group.substring(8); // Remove "TAG:" prefix
+ } else if (group.startsWith("AUTHOR:")) {
+ author = group.substring(7); // Remove "AUTHOR:" prefix
+ }
+ }
+
+ // Level 3: Specific Test Case Node
+ String testCaseName = result.getMethod().getDescription();
+
+ // Create or retrieve the main category node
+ ExtentTest mainCategoryNode = categoryNodes.computeIfAbsent(mainCategory, k -> extentReports.createTest(k));
+
+ // Create or retrieve the subcategory node under the main category
+ String finalSubCategory = subCategory;
+ ExtentTest subCategoryNode = subCategoryNodes.computeIfAbsent(mainCategory + subCategory, k -> mainCategoryNode.createNode(finalSubCategory));
+
+ // Create a node for the specific test case
+ ExtentTest testNode = subCategoryNode.createNode(testCaseName);
+ subCategoryNode.assignCategory(mainCategory);
+ subCategoryNode.assignAuthor(author);
+
+ testNode.assignCategory(sectionTag);
+ extentTest.set(testNode);
+ }
+ }
+
+ public void onTestFailure(ITestResult result){
+
+ ExtentReportManager.logFailureDetails(result.getThrowable().getMessage());
+
+ String stackTrace = Arrays.toString(result.getThrowable().getStackTrace());
+ stackTrace = stackTrace.replaceAll(",", "
");
+
+ String formattedTrace = """
+
+ Click here to see detail exception logs
+ """ + stackTrace + """
+
+ """;
+
+ ExtentReportManager.logExceptionDetails(formattedTrace);
+ }
+}
diff --git a/src/main/java/com/tappayments/automation/utils/CommonAutomationUtils.java b/src/main/java/com/tappayments/automation/utils/CommonAutomationUtils.java
new file mode 100644
index 0000000..502fc3e
--- /dev/null
+++ b/src/main/java/com/tappayments/automation/utils/CommonAutomationUtils.java
@@ -0,0 +1,229 @@
+package com.tappayments.automation.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.restassured.response.Response;
+import org.testng.Assert;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.*;
+import static org.hamcrest.Matchers.emptyOrNullString;
+
+public class CommonAutomationUtils {
+
+ public static String stringToJson(Object object){
+
+ String payload = "";
+ ObjectMapper mapper = new ObjectMapper();
+ try {
+ payload = mapper.writeValueAsString(object);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ return payload;
+ }
+
+ public static void verifyCommonResponseSuccessValidation(Response response, int statusCode){
+
+ verifyCommonResponseSuccessValidation(response, statusCode, Map.of());
+ }
+
+ public static void verifyCommonResponseSuccessValidation(Response response, int statusCode, Map additionalParamChecks){
+
+ verifyStatusCode(response, statusCode);
+ verifyExactMatch(response, Map.of("live_mode",false, "status", "INITIATED", "response.code", "101"));
+ if(!additionalParamChecks.isEmpty())
+ verifyExactMatch(response, additionalParamChecks);
+ verifyNonEmpty(response, List.of("transaction.created"));
+ }
+
+ public static void verifyCommonResponseFailedValidation(Response response, int statusCode, Map parameterChecks){
+
+ verifyStatusCode(response, statusCode);
+ verifyExactMatch(response, parameterChecks);
+ }
+
+ public static void verifyCommonResponseFailedValidation(JsonNode jsonResponse, int responseCode, int expectedResponseCode, String code, String error){
+
+ String errorCode = jsonResponse.at("/errors/0/code").asText();
+ String errorType = jsonResponse.at("/errors/0/error").asText();
+
+ Assert.assertEquals(responseCode, expectedResponseCode, "Response code doesn't match.");
+ Assert.assertEquals(errorCode, code, "Error code doesn't match!");
+ Assert.assertEquals(errorType, error, "Error type doesn't match!");
+ }
+
+ public static void verifyCommonResponseFailedDescriptionValidation(JsonNode jsonResponse, int responseCode, int expectedResponseCode, String code, String error){
+
+ String errorCode = jsonResponse.at("/errors/0/code").asText();
+ String errorType = jsonResponse.at("/errors/0/description").asText();
+
+ Assert.assertEquals(responseCode, expectedResponseCode, "Response code doesn't match.");
+ Assert.assertEquals(errorCode, code, "Error code doesn't match!");
+ Assert.assertEquals(errorType, error, "Error description doesn't match!");
+ }
+
+ public static void verifyStatusCode(Response response, int statusCode){
+
+ response.then().statusCode(statusCode);
+ }
+
+ public static void verifyExactMatch(Response response, Map mactches){
+
+ mactches.forEach((path, expectedValue) -> {
+ if (expectedValue instanceof Double)
+ response.then() .body(path, equalTo(((Double) expectedValue).floatValue()));
+ else if (expectedValue instanceof String)
+ response.then().body(path, equalTo((String) expectedValue));
+ else if (expectedValue instanceof Integer)
+ response.then().body(path, equalTo((Integer) expectedValue));
+ else if (expectedValue instanceof Boolean)
+ response.then().body(path, is(expectedValue));
+ else
+ throw new IllegalArgumentException("Unsupported type : " + expectedValue.getClass());
+ });
+ }
+
+ public static void verifyNonEmpty(Response response, List checks){
+
+ checks.forEach((path) -> {
+ response.then().body(path, not(emptyOrNullString()));
+ });
+ }
+
+ public static void verifyNonAvailableKey(Response response, List paths){
+
+ paths.forEach((fullPath) -> {
+ String parentPath = fullPath.contains(".") ? fullPath.substring(0, fullPath.lastIndexOf(".")) : "";
+ String key = fullPath.contains(".") ? fullPath.substring(fullPath.lastIndexOf(".") + 1) : fullPath;
+
+ if (!parentPath.isEmpty()) {
+ // Check that the key is not present within the specific object
+ response.then().body(parentPath, not(hasKey(key)));
+ } else {
+ // Check that the key is not present at the root level
+ response.then().body("$", not(hasKey(key)));
+ }
+ });
+ }
+
+ public static JsonNode convertToJson(String json){
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode jsonNode = null;
+ try {
+ // Convert JSON string to JsonNode
+ jsonNode = objectMapper.readTree(json);
+ } catch (JsonProcessingException e) {
+ // Handle parsing errors specifically
+ System.err.println("Error processing JSON: " + e.getMessage());
+ } catch (NullPointerException e) {
+ // Handle cases where expected fields are missing in JSON
+ System.err.println("Missing field in JSON: " + e.getMessage());
+ } catch (Exception e) {
+ // Catch any other unforeseen exceptions
+ System.err.println("An unexpected error occurred: " + e.getMessage());
+ }
+
+ return jsonNode;
+ }
+
+ public static String modifyJson(String jsonPayload, String operation, String jsonKey, Object newValue) {
+ ObjectMapper mapper = new ObjectMapper();
+ try {
+ JsonNode rootNode = mapper.readTree(jsonPayload);
+
+ if (rootNode instanceof ObjectNode rootObjectNode) {
+ switch (operation) {
+ case "MODIFY" -> applyModification(rootObjectNode, jsonKey, newValue);
+ case "DELETE" -> deleteKey(rootObjectNode, jsonKey);
+ default -> throw new IllegalArgumentException("Unsupported operation: " + operation);
+ }
+ return mapper.writeValueAsString(rootObjectNode);
+ } else {
+ throw new RuntimeException("Expected JSON root element to be an ObjectNode");
+ }
+
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void applyModification(ObjectNode objectNode, String jsonKey, Object newValue) {
+ if (jsonKey.contains(".")) {
+ String[] keyHierarchy = jsonKey.split("\\.");
+ JsonNode targetNode = traverseToNode(objectNode, keyHierarchy);
+ if (targetNode instanceof ObjectNode targetObjectNode) {
+ assignValue(targetObjectNode, keyHierarchy[keyHierarchy.length - 1], newValue);
+ } else if (targetNode instanceof ArrayNode targetArrayNode) {
+ assignValue(targetArrayNode, keyHierarchy[keyHierarchy.length - 1], newValue);
+ } else {
+ throw new RuntimeException("Expected a JSON ObjectNode at " + keyHierarchy[keyHierarchy.length - 2]);
+ }
+ } else {
+ assignValue(objectNode, jsonKey, newValue);
+ }
+ }
+
+ private static void deleteKey(ObjectNode objectNode, String jsonKey) {
+ if (jsonKey.contains(".")) {
+ String[] keyHierarchy = jsonKey.split("\\.");
+ JsonNode targetNode = traverseToNode(objectNode, keyHierarchy);
+ if (targetNode instanceof ObjectNode targetObjectNode) {
+ targetObjectNode.remove(keyHierarchy[keyHierarchy.length - 1]);
+ } else if (targetNode instanceof ArrayNode targetArrayNode) {
+ ObjectNode firstObjectNode = (ObjectNode) targetArrayNode.get(0);
+ deleteKey(firstObjectNode, keyHierarchy[keyHierarchy.length - 1]);
+ } else {
+ throw new RuntimeException("Expected a JSON ObjectNode at " + keyHierarchy[keyHierarchy.length - 2]);
+ }
+ } else {
+ objectNode.remove(jsonKey);
+ }
+ }
+
+ private static JsonNode traverseToNode(ObjectNode objectNode, String[] keyHierarchy) {
+ JsonNode currentNode = objectNode;
+ for (int i = 0; i < keyHierarchy.length - 1; i++) {
+ if (currentNode.isArray()) {
+ currentNode = currentNode.get(0);
+ }
+ currentNode = currentNode.path(keyHierarchy[i]);
+ if (!currentNode.isObject() && !currentNode.isArray()) {
+ throw new RuntimeException("Expected a JSON ObjectNode at " + keyHierarchy[i]);
+ }
+ }
+ return currentNode;
+ }
+
+ private static void assignValue(JsonNode node, String jsonKey, Object newValue) {
+ if (node.isObject()) {
+ ObjectNode targetObjectNode = (ObjectNode) node;
+ if (newValue instanceof Double) {
+ targetObjectNode.put(jsonKey, (Double) newValue);
+ } else if (newValue instanceof Integer) {
+ targetObjectNode.put(jsonKey, (Integer) newValue);
+ } else if (newValue instanceof String) {
+ targetObjectNode.put(jsonKey, (String) newValue);
+ } else if (newValue instanceof Boolean) {
+ targetObjectNode.put(jsonKey, (Boolean) newValue);
+ } else {
+ throw new IllegalArgumentException("Unsupported value type: " + newValue.getClass());
+ }
+ } else if (node.isArray()) {
+ ArrayNode targetArrayNode = (ArrayNode) node;
+ for (JsonNode arrayElement : targetArrayNode) {
+ if (arrayElement.isObject()) {
+ assignValue(arrayElement, jsonKey, newValue);
+ }
+ }
+ } else {
+ throw new IllegalArgumentException("Unsupported node type: " + node.getClass());
+ }
+ }
+}
diff --git a/src/main/java/com/tappayments/automation/utils/CommonConstants.java b/src/main/java/com/tappayments/automation/utils/CommonConstants.java
new file mode 100644
index 0000000..f369c43
--- /dev/null
+++ b/src/main/java/com/tappayments/automation/utils/CommonConstants.java
@@ -0,0 +1,6 @@
+package com.tappayments.automation.utils;
+
+public class CommonConstants {
+
+ public static final String UTF_8 = "utf-8";
+}