FILTERED_PACKAGES_TO_APPEND_SET = new ConcurrentSkipListSet<>();
+
+ private AllureLogsToStepAppender(
+ final String name,
+ final Filter filter,
+ final Layout extends Serializable> layout,
+ final boolean ignoreExceptions) {
+ super(name, filter, layout, ignoreExceptions, null);
+ }
+
+ /**
+ * Creates an AllureLogsToStepAppender.
+ *
+ * @param name The name of the appender.
+ * @param layout The layout to use for the appender.
+ * @param filter The filter to use for the appender.
+ * @param filteredPackagesToAppend A comma-separated list of packages to filter.
+ * @return A new AllureLogsToStepAppender instance, or null if the name is null.
+ */
+ @PluginFactory
+ public static AllureLogsToStepAppender createAppender(
+ @PluginAttribute("name") final String name,
+ @PluginElement("Layout") final Layout extends Serializable> layout,
+ @PluginElement("Filter") final Filter filter,
+ @PluginAttribute("filteredPackagesToAppend") final String filteredPackagesToAppend) {
+
+ if (name == null) {
+ return null;
+ }
+
+ final var usedLayout = Objects.requireNonNullElse(layout, PatternLayout.createDefaultLayout());
+
+ if (filteredPackagesToAppend != null) {
+ FILTERED_PACKAGES_TO_APPEND_SET.addAll(Arrays.asList(filteredPackagesToAppend.split(COMMA)));
+ }
+
+ return new AllureLogsToStepAppender(name, filter, usedLayout, true);
+ }
+
+ /**
+ * Appends a log event to Allure step.
+ *
+ * @param event The log event to append.
+ */
+ @Override
+ public void append(final LogEvent event) {
+ if (isSourcePackageValidForAppender(event)) {
+ Allure.step(event.getMessage().getFormattedMessage());
+ }
+ }
+
+ private boolean isSourcePackageValidForAppender(final LogEvent event) {
+ if (FILTERED_PACKAGES_TO_APPEND_SET.isEmpty()) {
+ return true;
+ }
+
+ final var sourceClassName = event.getSource().getClassName();
+
+ return FILTERED_PACKAGES_TO_APPEND_SET.stream().anyMatch(sourceClassName::contains);
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/analytics/AnalyticsEntry.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/analytics/AnalyticsEntry.java
index 6b7e86e..b161753 100644
--- a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/analytics/AnalyticsEntry.java
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/analytics/AnalyticsEntry.java
@@ -36,11 +36,23 @@
public class AnalyticsEntry extends LogEntry {
private final JsonObject parsedMessage;
+ /**
+ * Constructor for AnalyticsEntry.
+ *
+ * @param level The log level.
+ * @param timestamp The timestamp of the log entry.
+ * @param message The log message.
+ */
public AnalyticsEntry(final Level level, final long timestamp, final String message) {
super(level, timestamp, message);
this.parsedMessage = new Gson().fromJson(message, JsonObject.class);
}
+ /**
+ * Gets the webview GUID.
+ *
+ * @return The webview GUID, or null if not found.
+ */
public String getWebView() {
return Optional.ofNullable(parsedMessage)
.map(body -> body.get("webview"))
@@ -48,6 +60,11 @@ public String getWebView() {
.orElse(null);
}
+ /**
+ * Gets the method of the analytics call.
+ *
+ * @return The method, or null if not found.
+ */
public String getMethod() {
return Optional.ofNullable(parsedMessage)
.map(body -> body.getAsJsonObject("message"))
@@ -56,6 +73,11 @@ public String getMethod() {
.orElse(null);
}
+ /**
+ * Gets the parameters of the analytics call.
+ *
+ * @return The parameters as a JsonObject, or null if not found.
+ */
public JsonObject getParams() {
return Optional.ofNullable(parsedMessage)
.map(body -> body.getAsJsonObject("message"))
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/analytics/AnalyticsInterceptor.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/analytics/AnalyticsInterceptor.java
index 2faf3b2..6ed2b50 100644
--- a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/analytics/AnalyticsInterceptor.java
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/analytics/AnalyticsInterceptor.java
@@ -21,7 +21,6 @@
import com.applause.auto.pageobjectmodel.base.ComponentInterceptor;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
-import lombok.AllArgsConstructor;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
@@ -34,10 +33,18 @@
* This interceptor is added to classes in the PageObjectFactory to facilitate running code before
* and after methods. Currently, the only use case is for the @AnalyticsCall annotation.
*/
-@AllArgsConstructor
@SuppressWarnings("PMD.SignatureDeclareThrowsException") // since we're intercepting this is okay
public class AnalyticsInterceptor extends ComponentInterceptor {
- private AnalyticsHelper analyticsHelper;
+ private final AnalyticsHelper analyticsHelper;
+
+ /**
+ * Constructs a new AnalyticsInterceptor.
+ *
+ * @param analyticsHelper The AnalyticsHelper instance to use for analytics reporting.
+ */
+ public AnalyticsInterceptor(final AnalyticsHelper analyticsHelper) {
+ this.analyticsHelper = analyticsHelper;
+ }
/**
* Methods in a page object class that meet the criteria in match() are subject to the extra logic
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/control/BrowserControl.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/control/BrowserControl.java
index 611745d..7552af4 100644
--- a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/control/BrowserControl.java
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/control/BrowserControl.java
@@ -24,7 +24,6 @@
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
-import lombok.AllArgsConstructor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.openqa.selenium.Dimension;
@@ -35,11 +34,19 @@
* mouse-downs.
*/
@SuppressWarnings({"checkstyle:ParameterName", "checkstyle:AbbreviationAsWordInName"})
-@AllArgsConstructor
public class BrowserControl implements IPageObjectExtension {
private static final Logger logger = LogManager.getLogger();
private final IPageObjectContext context;
+ /**
+ * Constructor for BrowserControl.
+ *
+ * @param context The {@link IPageObjectContext} to use.
+ */
+ public BrowserControl(final IPageObjectContext context) {
+ this.context = context;
+ }
+
/**
* Gets the current driver as a JavascriptExecutor.
*
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/control/DeviceControl.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/control/DeviceControl.java
index 06891e4..2866139 100644
--- a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/control/DeviceControl.java
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/control/DeviceControl.java
@@ -32,7 +32,6 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
-import lombok.AllArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -53,11 +52,19 @@
"checkstyle:MultipleStringLiterals",
"checkstyle:LocalVariableName"
})
-@AllArgsConstructor
public class DeviceControl implements IPageObjectExtension {
private static final Logger logger = LogManager.getLogger();
private final IPageObjectContext context;
+ /**
+ * Constructor for DeviceControl.
+ *
+ * @param context The {@link IPageObjectContext} to use.
+ */
+ public DeviceControl(final IPageObjectContext context) {
+ this.context = context;
+ }
+
/**
* Checks if the current driver is an Android driver.
*
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/CommonMailClient.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/CommonMailClient.java
new file mode 100644
index 0000000..33e27c7
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/CommonMailClient.java
@@ -0,0 +1,332 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.email;
+
+import jakarta.mail.*;
+import jakarta.mail.Flags.Flag;
+import jakarta.mail.search.FlagTerm;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import lombok.NonNull;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/** Provides some common methods between most email clients. */
+public class CommonMailClient {
+ private String host;
+ private final Properties props = new Properties();
+ private final Store store;
+ private String protocol;
+ private String userName;
+ private String password;
+ private static final Logger logger = LogManager.getLogger(CommonMailClient.class);
+
+ /**
+ * Constructs a new CommonMailClient with the specified username, password, protocol, and host.
+ * Example: username = some@email.com, password = xxxx, protocol = imaps, host = imap.gmail.com
+ *
+ * @param userName The username for the mail account.
+ * @param password The password for the mail account.
+ * @param protocol The protocol to use for connecting to the mail server.
+ * @param host The hostname of the mail server.
+ * @throws MessagingException If an error occurs during the setup of mail credentials.
+ */
+ public CommonMailClient(
+ final String userName, final String password, final Protocol protocol, final String host)
+ throws MessagingException {
+ this.userName = userName.trim();
+ this.password = password;
+ this.protocol = protocol.getValue();
+ this.host = host.trim();
+ store = setMailCredentials();
+ }
+
+ /**
+ * Extracts from a String using Regular expressions
+ *
+ * @param text The text to extract from
+ * @param regex The regular expression to use
+ * @return The extracted text after applying the regex
+ */
+ public String extractWithRegexFromString(final String text, @NonNull final String regex) {
+ if (text.isEmpty()) {
+ logger.info("Text is empty");
+ return null;
+ }
+ logger.info("Extracting with regex = [{}...", regex);
+ Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
+ Matcher matcher = pattern.matcher(text);
+
+ if (matcher.find()) {
+ logger.info("Extracted text is:: {}", matcher.group(1).trim());
+ return matcher.group(1).trim();
+ }
+ throw new RuntimeException("No matches were found in the text.");
+ }
+
+ /**
+ * input stream with properties for email client, protocol, username, password, host example of
+ * property file content: mail.store.protocol = imaps username = some@email.com password = xxxx
+ * host = imap.gmail.com (for example).
+ *
+ * @param resourceFile an InputStream that represents a .properties files that includes all the
+ * above-mentioned properties.
+ * @throws MessagingException If an error occurs during mail session setup.
+ */
+ public CommonMailClient(@NonNull final InputStream resourceFile) throws MessagingException {
+ store = setMailCredentials(resourceFile);
+ }
+
+ /**
+ * Deletes all the read emails from the mentioned folder of the email account.
+ *
+ * @param emailFolder The email folder to use.
+ */
+ public void emptyAllReadEmailsFromInbox(@NonNull final Folder emailFolder) {
+ deleteReadEmails(emailFolder);
+ }
+
+ /**
+ * Marks all emails on the email account as read (it only does that for the 'Inbox' folder).
+ *
+ * @return A reference to the InboxFolder after marking all the emails in it as read.
+ */
+ public Folder markAllEmailsAsRead() {
+ Folder inboxFolder = null;
+ try {
+ inboxFolder = openSelectedFolderWithRights("Inbox", Folder.READ_WRITE);
+ Message[] emails = getUnreadEmails(inboxFolder);
+ for (Message email : emails) {
+ email.setFlag(Flag.SEEN, true);
+ }
+ } catch (Exception ex) {
+ logger.info("An exception was thrown. Message = " + ex.getMessage());
+ }
+
+ return inboxFolder;
+ }
+
+ /**
+ * Checks whether a certain email matches the provided search criteria or not.
+ *
+ * @param email The email object to check.
+ * @param criteria The search criteria to check against.
+ * @return True if the email is a match, false otherwise.
+ * @throws MessagingException Thrown in case an error happens when getting any details from the
+ * email.
+ */
+ protected boolean doesEmailMatchCriteria(
+ @NonNull final Message email, @NonNull final SearchCriteria criteria)
+ throws MessagingException {
+ if (criteria.emailSubject() != null && !criteria.emailSubject().isEmpty()) {
+ // check email subject
+ boolean subjectMatch = email.getSubject().matches(criteria.emailSubject());
+ // return if false, otherwise continue
+ if (!subjectMatch) {
+ return false;
+ }
+ }
+
+ if (criteria.sentFrom() != null && !criteria.sentFrom().isEmpty()) {
+ // check sentFrom
+ boolean sentFromMatch =
+ Arrays.stream(email.getFrom())
+ .sequential()
+ .anyMatch(
+ from ->
+ from.toString().contains(criteria.sentFrom().toLowerCase(Locale.ENGLISH)));
+ // return if false, otherwise continue
+ if (!sentFromMatch) {
+ return false;
+ }
+ }
+
+ if (criteria.sentTo() != null && !criteria.sentTo().isEmpty()) {
+ // check sentTo
+ // return if false, otherwise continue
+ return Arrays.stream(email.getAllRecipients())
+ .sequential()
+ .anyMatch(to -> to.toString().contains(criteria.sentTo().toLowerCase(Locale.ENGLISH)));
+ }
+
+ // email matches the whole criteria
+ return true;
+ }
+
+ /**
+ * input stream with properties for email client, protocol, username, password, host example of
+ * property file content: mail.store.protocol = imaps username = some@email.com password = xxxx
+ * host = imap.gmail.com (for example).
+ *
+ * @param resourceStream an InputStream that represents a .properties files that includes all the
+ * above-mentioned properties.
+ * @return mailProperties
+ */
+ private Properties setProperties(@NonNull final InputStream resourceStream) {
+ try {
+ props.load(resourceStream);
+ this.userName = props.getProperty("username");
+ this.password = props.getProperty("password");
+ this.protocol = props.getProperty("mail.store.protocol");
+ host = props.getProperty("host");
+ } catch (FileNotFoundException e) {
+ logger.error("FileNotFoundException on method setProperties\nMessage: {}", e.getMessage());
+ } catch (IOException e1) {
+ logger.error("IOException on method setProperties");
+ }
+ return props;
+ }
+
+ /**
+ * Opens a specified mail folder with the given access rights.
+ *
+ * @param folderName The name of the mail folder to open (e.g., "INBOX").
+ * @param accessMode The access mode to use when opening the folder. See {@link
+ * jakarta.mail.Folder} for available modes.
+ * @return The opened {@link jakarta.mail.Folder} object, or null if an error occurred.
+ * @throws MessagingException If an error occurs while opening the folder.
+ */
+ public Folder openSelectedFolderWithRights(@NonNull final String folderName, final int accessMode)
+ throws MessagingException {
+ Folder inbox = null;
+ try {
+ inbox = store.getFolder(folderName);
+ inbox.open(accessMode); // Open the folder immediately after retrieving it
+
+ } catch (MessagingException e1) {
+ logger.error(e1);
+ throw e1; // Re-throw the exception after logging it. This is crucial for proper error
+ // handling.
+ }
+ return inbox;
+ }
+
+ /**
+ * Initialize properties for current mailbox. Username, password, protocol, and host should be set
+ * in advance.
+ *
+ * @return The initialized Store object.
+ */
+ private Store setMailCredentials() throws MessagingException {
+ Session session = Session.getDefaultInstance(getProperties(), null);
+ return getStore(session);
+ }
+
+ /**
+ * Initialize properties for current mailbox example setMailCredentials(new
+ * InputStream("/xxx/xxx/propertyFile"))
+ *
+ * @param resourceStream an InputStream that represents a .properties files that includes all the
+ * above-mentioned properties.
+ * @return mail credential store
+ */
+ private Store setMailCredentials(@NonNull final InputStream resourceStream)
+ throws MessagingException {
+ if (props.isEmpty()) {
+ setProperties(resourceStream);
+ }
+ Session session = Session.getDefaultInstance(props, null);
+ return getStore(session);
+ }
+
+ private Store getStore(@NonNull final Session session) throws MessagingException {
+ Store storeLocal = null;
+ try {
+ storeLocal = session.getStore(protocol);
+ } catch (NoSuchProviderException e1) {
+ logger.error(e1);
+ throw e1;
+ }
+ try {
+ storeLocal.connect(host, userName, password);
+ } catch (MessagingException e1) {
+ logger.error(e1);
+ throw e1;
+ }
+ return storeLocal;
+ }
+
+ /**
+ * Creates a Properties instance with pre-set username, password, protocol, and host. Username,
+ * password, protocol, and host should be set in advance.
+ *
+ * @return A Properties instance containing the username, password, protocol, and host.
+ */
+ private Properties getProperties() {
+ Properties properties = new Properties();
+
+ properties.put("username", userName);
+ properties.put("password", password);
+ properties.put("mail.store.protocol", protocol);
+ properties.put("host", host);
+
+ return properties;
+ }
+
+ /**
+ * Get array with unread emails.
+ *
+ * @param folder The email folder to check.
+ * @return unread emails for selected folder
+ */
+ public Message[] getUnreadEmails(@NonNull final Folder folder) {
+ Message[] emails = new Message[0];
+ try {
+ Flags seen = new Flags(Flags.Flag.SEEN);
+ FlagTerm unseenFlagTerm = new FlagTerm(seen, false);
+ emails = folder.search(unseenFlagTerm);
+ } catch (MessagingException e1) {
+ logger.error(e1);
+ }
+
+ return emails;
+ }
+
+ /**
+ * Delete all read emails form selected folder.
+ *
+ * @param folder The email folder to check.
+ */
+ public void deleteReadEmails(@NonNull final Folder folder) {
+ Message[] emails;
+ try {
+ Flags seen = new Flags(Flags.Flag.SEEN);
+ FlagTerm seenFlagTerm = new FlagTerm(seen, true);
+ emails = folder.search(seenFlagTerm);
+ folder.setFlags(emails, new Flags(Flags.Flag.DELETED), true);
+ } catch (MessagingException e1) {
+ logger.error(e1);
+ }
+ }
+
+ /** Close the current connection store. */
+ public void closeConnection() {
+ try {
+ store.close();
+ } catch (MessagingException e) {
+ logger.error("Error occurred when closing connection.");
+ logger.error(e);
+ }
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/GmailHelper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/GmailHelper.java
new file mode 100644
index 0000000..5fd9bd1
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/GmailHelper.java
@@ -0,0 +1,277 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.email;
+
+import com.applause.auto.helpers.util.ThreadHelper;
+import jakarta.mail.BodyPart;
+import jakarta.mail.Flags.Flag;
+import jakarta.mail.Folder;
+import jakarta.mail.Message;
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.MimeMultipart;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+import lombok.NonNull;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.remote.RemoteWebDriver;
+
+/** A concrete email client implementation for Gmail. */
+public class GmailHelper extends CommonMailClient {
+
+ private static final Logger logger = LogManager.getLogger(GmailHelper.class);
+ private static final int TEN_MINUTES_TIMEOUT_WAIT_MILLI = 600000;
+ private static final int FIVE_SECOND_WAIT_MILLI = 5000;
+
+ /**
+ * Default constructor with params.
+ *
+ * PLEASE NOTE:
+ *
+ *
To make this work you should use a Gmail app password.
+ *
+ *
For more information on how to set it, please check
+ * https://support.google.com/accounts/answer/185833?hl=en
+ *
+ *
Example: username = some@email.com password = xxxx
+ *
+ * @param username The Gmail username (email address). Must not be null.
+ * @param appPassword The Gmail app password.
+ */
+ public GmailHelper(final String username, final String appPassword) throws MessagingException {
+ super(username.trim(), appPassword, Protocol.IMAP, "imap.gmail.com");
+ }
+
+ public GmailHelper(
+ final String username, final String appPassword, final Protocol protocol, final String host)
+ throws MessagingException {
+ super(username, appPassword, protocol, host);
+ }
+
+ /** Clears all read emails from the inbox folder of the email. */
+ public void emptyEmailBox() {
+ Folder inboxFolder = markAllEmailsAsRead();
+ emptyAllReadEmailsFromInbox(inboxFolder);
+ closeConnection();
+ }
+
+ /**
+ * Waits for a specific email to arrive.
+ *
+ * @param driver The currently active selenium WebDriver instance. It is kept alive as long as the
+ * email didn't arrive yet and the timeout value isn't reached.
+ * @param criteria The search criteria to use when searching for the email.
+ * @param checkOnlyUnreadEmails If true, then we would get fetch the unread emails. If false, * we
+ * would check all the emails in the folder (can be slow).
+ * @param markEmailAsSeen If true, the email would be marked as seen after checking it.
+ * @return A message array that contains all the emails that match the mentioned criteria.
+ */
+ public List waitForEmailToArrive(
+ @NonNull final WebDriver driver,
+ @NonNull final SearchCriteria criteria,
+ final boolean checkOnlyUnreadEmails,
+ final boolean markEmailAsSeen) {
+ return waitForEmailToArrive(
+ driver, criteria, checkOnlyUnreadEmails, markEmailAsSeen, TEN_MINUTES_TIMEOUT_WAIT_MILLI);
+ }
+
+ /**
+ * Waits for a specific email to arrive.
+ *
+ * @param driver The currently active selenium WebDriver instance. It is kept alive as long as the
+ * email didn't arrive yet and the timeout value isn't reached.
+ * @param criteria The search criteria to use when searching for the email.
+ * @param checkOnlyUnreadEmails If true, then we would get fetch the unread emails. If false, * we
+ * would check all the emails in the folder (can be slow).
+ * @param markEmailAsSeen If true, the email would be marked as seen after checking it.
+ * @param timeOutInMillis The time to wait in milli-seconds before timing out and giving up to
+ * find the email.
+ * @return A message array that contains all the emails that match the mentioned criteria.
+ */
+ public List waitForEmailToArrive(
+ @Nullable final WebDriver driver,
+ @NonNull final SearchCriteria criteria,
+ final boolean checkOnlyUnreadEmails,
+ final boolean markEmailAsSeen,
+ final long timeOutInMillis) {
+ logger.info("Waiting for email to arrive...");
+ int timeElapsed = 0;
+ List foundEmails = null;
+
+ while (timeElapsed < timeOutInMillis) {
+ foundEmails = isEmailReceived(criteria, checkOnlyUnreadEmails, markEmailAsSeen);
+ if (foundEmails.isEmpty()) {
+ logger.info(
+ "Email not received after {} seconds. Retrying after 5 seconds...", timeElapsed / 1000);
+ ThreadHelper.sleep(FIVE_SECOND_WAIT_MILLI);
+ timeElapsed += FIVE_SECOND_WAIT_MILLI;
+
+ /*
+ * Get the session ID to prevent remote Cloud provider from closing the connection for being
+ * idle (Usually timeouts after 90 seconds).
+ */
+ if (driver != null) {
+ logger.info("Session ID: {}", ((RemoteWebDriver) driver).getSessionId());
+ } else {
+ logger.info("No driver provided, moving on...");
+ }
+ } else {
+ break;
+ }
+ }
+
+ /*
+ * Shutdown hook to make sure the email server connection is terminated correctly before the
+ * application terminates.
+ */
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread(
+ () -> {
+ logger.info("Shutdown hook started. Terminating the GmailHelper email instance");
+ // close the email connection
+ closeConnection();
+ }));
+
+ return foundEmails;
+ }
+
+ /**
+ * Parses the content of the email one part at a time. Sometimes this might not work when the
+ * email is not formatted correctly, and you might need to use @parseEmailFromInputStream.
+ *
+ * @param email The email object to check
+ * @return A string representing the content of the concatenated parts of the email object.
+ */
+ public String parseEmailParts(@NonNull final Message email) {
+ String result = "";
+ try {
+ if (email.isMimeType("text/plain")) {
+ result = email.getContent().toString();
+ } else if (email.isMimeType("multipart/*")) {
+ MimeMultipart mimeMultipart = (MimeMultipart) email.getContent();
+ result = getTextFromMimeMultipart(mimeMultipart);
+ }
+ } catch (Exception ex) {
+ logger.info("An exception was thrown. Message = " + ex.getMessage());
+ }
+
+ return result;
+ }
+
+ /**
+ * Parses the email content form its input stream. This method will return an aggregated
+ * representation of all the email parts as text. Very useful in case parsing the email as parts
+ * fail.
+ *
+ * @param email The email object to check
+ * @return A string representation to the email's input stream.
+ */
+ public String parseEmailFromInputStream(@NonNull final Message email) {
+ try {
+ return new String(email.getInputStream().readAllBytes(), Charset.defaultCharset());
+ } catch (Exception e) {
+ logger.error(e);
+ }
+ return null;
+ }
+
+ /**
+ * Extracts the text from multipart emails.
+ *
+ * @param mimeMultipart The multipart email to get the content from.
+ * @return A string representing the email content.
+ * @throws MessagingException If a messaging exception occurs.
+ * @throws IOException If an I/O exception occurs.
+ */
+ private String getTextFromMimeMultipart(@NonNull final MimeMultipart mimeMultipart)
+ throws MessagingException, IOException {
+ StringBuilder result = new StringBuilder();
+ int count = mimeMultipart.getCount();
+ for (int i = 0; i < count; i++) {
+ BodyPart bodyPart = mimeMultipart.getBodyPart(i);
+ if (bodyPart.isMimeType("text/plain")) {
+ result.append('\n').append(bodyPart.getContent());
+ break; // without break same text appears twice in my tests
+ } else if (bodyPart.isMimeType("text/html")) {
+ String html = (String) bodyPart.getContent();
+ result.append('\n').append(org.jsoup.Jsoup.parse(html).text());
+ } else if (bodyPart.getContent() instanceof MimeMultipart) {
+ result.append(getTextFromMimeMultipart((MimeMultipart) bodyPart.getContent()));
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Checks whether a certain email exists in the inbox or not. It uses the provided criteria when
+ * searching. If multiple emails are found, it will return all of them.
+ *
+ * @param criteria The search criteria to use when searching for the email.
+ * @param checkOnlyUnreadEmails If true, then we would get fetch the unread emails. If false, we
+ * would check all the emails in the folder (can be slow).
+ * @param markEmailAsSeen If true, the email would be marked as seen after checking it.
+ * @return A message array that contains all the emails that match the mentioned criteria.
+ */
+ public List isEmailReceived(
+ @NonNull final SearchCriteria criteria,
+ final boolean checkOnlyUnreadEmails,
+ final boolean markEmailAsSeen) {
+ final var matchedEmails = new ArrayList();
+ try {
+ Message[] emails;
+ if (checkOnlyUnreadEmails) {
+ emails = getUnreadEmails(openSelectedFolderWithRights("Inbox", Folder.READ_WRITE));
+ } else {
+ emails = openSelectedFolderWithRights("Inbox", Folder.READ_WRITE).getMessages();
+ }
+
+ if (emails.length == 0) {
+ logger.info("No emails found, moving on...");
+ return List.of();
+ }
+
+ logger.info("Found emails: " + emails.length);
+ for (int i = 0; i < emails.length; i++) {
+ Message email = emails[i];
+ logger.info("---------------------------------");
+ logger.info("Email Number: [{}]", i + 1);
+ logger.info("Subject: [{}]", email.getSubject());
+ logger.info("From: [{}]", (Object) email.getFrom());
+
+ // check if the email matches the criteria
+ if (doesEmailMatchCriteria(email, criteria)) {
+ matchedEmails.add(email);
+ }
+
+ if (markEmailAsSeen) {
+ email.setFlag(Flag.SEEN, true);
+ }
+ }
+ return matchedEmails;
+ } catch (Exception e) {
+ logger.info("An exception was thrown.", e);
+ }
+
+ return List.of(new Message[0]);
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/Protocol.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/Protocol.java
new file mode 100644
index 0000000..e032f6a
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/Protocol.java
@@ -0,0 +1,30 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.email;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum Protocol {
+ POP3("pop3"),
+ IMAP("imaps"),
+ SMTP("smtp");
+ private final String value;
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/SearchCriteria.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/SearchCriteria.java
new file mode 100644
index 0000000..8e69861
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/email/SearchCriteria.java
@@ -0,0 +1,27 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.email;
+
+/**
+ * Simple record that represents the search criteria when searching for emails.
+ *
+ * @param emailSubject The subject of the email to search for.
+ * @param sentFrom The sender of the email to search for.
+ * @param sentTo The recipient of the email to search for.
+ */
+public record SearchCriteria(String emailSubject, String sentFrom, String sentTo) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/google/GoogleSheetParser.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/google/GoogleSheetParser.java
new file mode 100644
index 0000000..171f96f
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/google/GoogleSheetParser.java
@@ -0,0 +1,183 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.google;
+
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
+import com.google.api.client.http.HttpRequestInitializer;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.services.sheets.v4.Sheets;
+import com.google.api.services.sheets.v4.SheetsScopes;
+import com.google.api.services.sheets.v4.model.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.SneakyThrows;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Utility class for parsing Google Sheets. Requires configuration management on the Google Cloud
+ * side.
+ */
+@SuppressWarnings("PMD.LooseCoupling")
+public final class GoogleSheetParser {
+
+ private static final Logger logger = LogManager.getLogger(GoogleSheetParser.class);
+ private static final String KEY_PARAMETER = "key";
+
+ @Getter private final String sheetId;
+ @Getter private final String applicationName;
+ private final Sheets service;
+
+ /**
+ * Constructs a GoogleSheetParser using an API key.
+ *
+ * @param apiKey The API key.
+ * @param sheetId The Google Sheet ID.
+ * @param applicationName The application name.
+ */
+ public GoogleSheetParser(
+ @NonNull final String apiKey,
+ @NonNull final String sheetId,
+ @NonNull final String applicationName) {
+ this.sheetId = sheetId;
+ this.applicationName = applicationName;
+
+ final HttpTransport transport = new NetHttpTransport.Builder().build();
+ final HttpRequestInitializer httpRequestInitializer =
+ request ->
+ request.setInterceptor(intercepted -> intercepted.getUrl().set(KEY_PARAMETER, apiKey));
+
+ this.service =
+ new Sheets.Builder(transport, GsonFactory.getDefaultInstance(), httpRequestInitializer)
+ .setApplicationName(applicationName)
+ .build();
+ }
+
+ /**
+ * Constructs a GoogleSheetParser using a service account JSON file.
+ *
+ * @param serviceAccountJsonFileInputStream The input stream for the service account JSON file.
+ * @param sheetId The Google Sheet ID.
+ * @param applicationName The application name.
+ */
+ @SuppressWarnings("deprecation")
+ @SneakyThrows
+ public GoogleSheetParser(
+ @NonNull final InputStream serviceAccountJsonFileInputStream,
+ @NonNull final String sheetId,
+ @NonNull final String applicationName) {
+ this.sheetId = sheetId;
+ this.applicationName = applicationName;
+
+ final HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
+ final GoogleCredential credentials =
+ GoogleCredential.fromStream(serviceAccountJsonFileInputStream)
+ .createScoped(Collections.singletonList(SheetsScopes.SPREADSHEETS));
+
+ this.service =
+ new Sheets.Builder(httpTransport, GsonFactory.getDefaultInstance(), credentials)
+ .setApplicationName(applicationName)
+ .build();
+ }
+
+ /**
+ * Retrieves all cell records from a sheet.
+ *
+ * @param sheetTitle The title of the sheet.
+ * @return A list of lists representing the cell values.
+ * @throws IOException If an I/O error occurs.
+ * @throws GoogleSheetParserException If the sheet is not found or no values are present.
+ */
+ public List> getAllSheetCellRecords(@NonNull final String sheetTitle)
+ throws IOException {
+ final var sheetProperties = getSheetObjectBySheetTitle(sheetTitle).getProperties();
+ final var dataFilter = getAllSheetCellsDataFilter(sheetProperties);
+
+ final var request = new BatchGetValuesByDataFilterRequest();
+ request.setDataFilters(List.of(dataFilter));
+
+ final var response =
+ service.spreadsheets().values().batchGetByDataFilter(sheetId, request).execute();
+
+ final var valueRanges = response.getValueRanges();
+ final var values = getCellMatrixFromValueRanges(List.of(dataFilter), valueRanges);
+
+ logger.info("Lines loaded from Sheet: {}", values.size());
+ return values;
+ }
+
+ @SneakyThrows
+ private Sheet getSheetObjectBySheetTitle(@NonNull final String sheetName) {
+ final var spreadsheetDocument = service.spreadsheets().get(sheetId).execute();
+ return spreadsheetDocument.getSheets().stream()
+ .filter(sheet -> sheet.getProperties().getTitle().equals(sheetName))
+ .findFirst()
+ .orElseThrow(
+ () -> new GoogleSheetParserException("Sheet with title: " + sheetName + " not found"));
+ }
+
+ private DataFilter getAllSheetCellsDataFilter(@NonNull final SheetProperties sheetProperties) {
+ final var dataFilter = new DataFilter();
+ final var gridRange = new GridRange();
+ final var gridProperties = sheetProperties.getGridProperties();
+
+ gridRange.setSheetId(sheetProperties.getSheetId());
+ gridRange.setStartRowIndex(0);
+ gridRange.setEndRowIndex(gridProperties.getRowCount() - 1);
+ gridRange.setStartColumnIndex(0);
+ gridRange.setEndColumnIndex(gridProperties.getColumnCount() - 1);
+
+ logger.info(
+ "Creating data filter for sheet: {} with row range [{}, {}] and column range [{}, {}]",
+ sheetProperties.getTitle(),
+ gridRange.getStartRowIndex(),
+ gridRange.getEndRowIndex(),
+ gridRange.getStartColumnIndex(),
+ gridRange.getEndColumnIndex());
+
+ dataFilter.setGridRange(gridRange);
+ return dataFilter;
+ }
+
+ private List> getCellMatrixFromValueRanges(
+ @NonNull final List dataFilters,
+ @NonNull final List matchedValueRanges) {
+
+ if (matchedValueRanges.size() != dataFilters.size()) {
+ final String filters = dataFilters.stream().map(DataFilter::getGridRange).toList().toString();
+ throw new GoogleSheetParserException("No value ranges data present for " + filters);
+ }
+
+ return Collections.singletonList(
+ matchedValueRanges.getFirst().getValueRange().getValues().stream()
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new GoogleSheetParserException(
+ "No Collection value object found in value range result response")));
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/google/GoogleSheetParserException.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/google/GoogleSheetParserException.java
new file mode 100644
index 0000000..1de14e3
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/google/GoogleSheetParserException.java
@@ -0,0 +1,27 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.google;
+
+import lombok.NonNull;
+
+public class GoogleSheetParserException extends RuntimeException {
+
+ public GoogleSheetParserException(@NonNull final String message) {
+ super(message);
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/mapping/IRestObjectMapper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/mapping/IRestObjectMapper.java
new file mode 100644
index 0000000..bc6fb57
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/mapping/IRestObjectMapper.java
@@ -0,0 +1,25 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.http.mapping;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Functional Interface that is representing object mapper method for REST API */
+public interface IRestObjectMapper {
+ ObjectMapper restJsonObjectMapper();
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/mapping/JacksonJSONRestObjectMapping.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/mapping/JacksonJSONRestObjectMapping.java
new file mode 100644
index 0000000..a1dd762
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/mapping/JacksonJSONRestObjectMapping.java
@@ -0,0 +1,40 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.http.mapping;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Jackson library json REST API typical object mapper instance */
+public class JacksonJSONRestObjectMapping implements IRestObjectMapper {
+ private static final ObjectMapper objectMapper =
+ new ObjectMapper()
+ .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, false);
+
+ /**
+ * Object mapper for REST JSON Jackson mapping
+ *
+ * @return shared ObjectMapper
+ */
+ @Override
+ public ObjectMapper restJsonObjectMapper() {
+ return objectMapper;
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/RestApiDefaultRestAssuredApiHelper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/RestApiDefaultRestAssuredApiHelper.java
new file mode 100644
index 0000000..f7d2f8b
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/RestApiDefaultRestAssuredApiHelper.java
@@ -0,0 +1,48 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.http.restassured;
+
+import com.applause.auto.helpers.http.mapping.JacksonJSONRestObjectMapping;
+import com.applause.auto.helpers.http.restassured.client.RestApiDefaultRestAssuredApiClient;
+import com.applause.auto.helpers.http.restassured.client.RestAssuredApiClient;
+import io.restassured.specification.RequestSpecification;
+import java.time.Duration;
+import lombok.Getter;
+
+/**
+ * Default rest assured http client for REST API usage wrapper class For requests with different
+ * retry policies - AwaitilityWaitUtils could be used
+ */
+@Getter
+public class RestApiDefaultRestAssuredApiHelper {
+
+ private final RestAssuredApiClient restAssuredApiClient;
+
+ public RestApiDefaultRestAssuredApiHelper() {
+ restAssuredApiClient = new RestApiDefaultRestAssuredApiClient(Duration.ofSeconds(60));
+ }
+
+ /**
+ * 'with' describing default RestAssuredConfig
+ *
+ * @return request specification
+ */
+ public RequestSpecification withDefaultRestHttpClientConfigsSpecification() {
+ return restAssuredApiClient.restAssuredRequestSpecification(new JacksonJSONRestObjectMapping());
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/client/RestApiDefaultRestAssuredApiClient.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/client/RestApiDefaultRestAssuredApiClient.java
new file mode 100644
index 0000000..8620aba
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/client/RestApiDefaultRestAssuredApiClient.java
@@ -0,0 +1,75 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.http.restassured.client;
+
+import static io.restassured.config.ConnectionConfig.connectionConfig;
+
+import com.applause.auto.helpers.http.mapping.IRestObjectMapper;
+import io.restassured.config.ObjectMapperConfig;
+import io.restassured.config.RestAssuredConfig;
+import io.restassured.filter.log.LogDetail;
+import io.restassured.filter.log.RequestLoggingFilter;
+import io.restassured.specification.RequestSpecification;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import lombok.NonNull;
+
+/** Default rest assured REST API client impl. */
+public class RestApiDefaultRestAssuredApiClient extends RestAssuredApiClient {
+
+ public RestApiDefaultRestAssuredApiClient(final Duration apiRequestWaitTimeoutDuration) {
+ super(apiRequestWaitTimeoutDuration);
+ }
+
+ /**
+ * get rest assured request specification object for default client
+ *
+ * @return request specification
+ */
+ @Override
+ public RequestSpecification restAssuredRequestSpecification(
+ @NonNull final IRestObjectMapper restObjectMapper) {
+ return super.restAssuredRequestSpecification(restObjectMapper)
+ .filter(new RequestLoggingFilter(LogDetail.URI))
+ .response()
+ .logDetail(LogDetail.STATUS)
+ .request();
+ }
+
+ /**
+ * rest assured config with object mapper for default client
+ *
+ * @param restObjectMapper the object mapper
+ * @return configured RestAssured client
+ */
+ @Override
+ protected RestAssuredConfig restAssuredConfig(@NonNull final IRestObjectMapper restObjectMapper) {
+ RestAssuredConfig restAssuredConfig = new RestAssuredConfig();
+ restAssuredConfig =
+ restAssuredConfig
+ .objectMapperConfig(
+ new ObjectMapperConfig()
+ .jackson2ObjectMapperFactory(
+ (cls, charset) -> restObjectMapper.restJsonObjectMapper()))
+ .connectionConfig(
+ connectionConfig()
+ .closeIdleConnectionsAfterEachResponseAfter(
+ getApiRequestWaitTimeoutDuration().getSeconds(), TimeUnit.SECONDS));
+ return restAssuredConfig;
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/client/RestAssuredApiClient.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/client/RestAssuredApiClient.java
new file mode 100644
index 0000000..8f0b94c
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/http/restassured/client/RestAssuredApiClient.java
@@ -0,0 +1,62 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.http.restassured.client;
+
+import static io.restassured.RestAssured.given;
+
+import com.applause.auto.helpers.http.mapping.IRestObjectMapper;
+import io.restassured.config.RestAssuredConfig;
+import io.restassured.specification.RequestSpecification;
+import java.time.Duration;
+import lombok.Getter;
+import lombok.NonNull;
+
+/** Rest assured api client */
+@Getter
+public abstract class RestAssuredApiClient {
+
+ private final Duration apiRequestWaitTimeoutDuration;
+
+ /**
+ * Constructs a new RestAssuredApiClient.
+ *
+ * @param apiRequestWaitTimeoutDuration The timeout duration for API requests.
+ */
+ public RestAssuredApiClient(@NonNull final Duration apiRequestWaitTimeoutDuration) {
+ this.apiRequestWaitTimeoutDuration = apiRequestWaitTimeoutDuration;
+ }
+
+ /**
+ * Gets a Rest Assured request specification object.
+ *
+ * @param restObjectMapper The REST object mapper to use.
+ * @return A Rest Assured request specification.
+ */
+ public RequestSpecification restAssuredRequestSpecification(
+ final IRestObjectMapper restObjectMapper) {
+ return given().when().request().config(restAssuredConfig(restObjectMapper));
+ }
+
+ /**
+ * Creates a Rest Assured config object.
+ *
+ * @param restObjectMapper The REST object mapper to use.
+ * @return A Rest Assured config object.
+ */
+ protected abstract RestAssuredConfig restAssuredConfig(IRestObjectMapper restObjectMapper);
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/JiraDefect.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/JiraDefect.java
new file mode 100644
index 0000000..50f252d
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/JiraDefect.java
@@ -0,0 +1,30 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface JiraDefect {
+
+ String identifier() default "";
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/JiraID.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/JiraID.java
new file mode 100644
index 0000000..fecbbbf
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/JiraID.java
@@ -0,0 +1,30 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface JiraID {
+
+ String identifier() default "";
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/scanner/JiraAnnotationsScanner.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/scanner/JiraAnnotationsScanner.java
new file mode 100644
index 0000000..7d8943a
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/annotations/scanner/JiraAnnotationsScanner.java
@@ -0,0 +1,54 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.annotations.scanner;
+
+import com.applause.auto.helpers.jira.annotations.JiraDefect;
+import com.applause.auto.helpers.jira.annotations.JiraID;
+import com.applause.auto.helpers.jira.exceptions.JiraAnnotationException;
+import java.lang.reflect.Method;
+import lombok.NonNull;
+
+public final class JiraAnnotationsScanner {
+
+ private JiraAnnotationsScanner() {
+ // utility class
+ }
+
+ /**
+ * Scan test and get declared JiraId value Annotation is mandatory to be declared, otherwise
+ * exception is thrown
+ *
+ * @param result the test result
+ * @return JiraId identifier
+ * @throws JiraAnnotationException JIRA library error
+ */
+ public static String getJiraIdentifier(@NonNull final Method result)
+ throws JiraAnnotationException {
+ return result.getAnnotation(JiraID.class).identifier();
+ }
+
+ /**
+ * Scan test and get declared JiraDefect value
+ *
+ * @param result test result
+ * @return JiraDefect identifier
+ */
+ public static String getJiraDefect(@NonNull final Method result) {
+ return result.getAnnotation(JiraDefect.class).identifier();
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/JiraXrayClient.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/JiraXrayClient.java
new file mode 100644
index 0000000..3ec4783
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/JiraXrayClient.java
@@ -0,0 +1,86 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.clients;
+
+import com.applause.auto.helpers.jira.clients.modules.jira.JiraProjectAPI;
+import com.applause.auto.helpers.jira.clients.modules.jira.SearchAPI;
+import com.applause.auto.helpers.jira.clients.modules.xray.ExecutionsAPI;
+import com.applause.auto.helpers.jira.clients.modules.xray.IterationsAPI;
+import com.applause.auto.helpers.jira.clients.modules.xray.StepsAPI;
+import com.applause.auto.helpers.jira.clients.modules.xray.TestrunAPI;
+import java.util.Objects;
+
+/**
+ * Class for communicating with JIRA/X-Ray Server + DC instance API end-points.
+ *
+ * Cannot be used with Cloud instances, use GraphQL instead.
+ *
+ *
X-Ray documentation: ...
+ */
+@SuppressWarnings("PMD.DataClass")
+public final class JiraXrayClient {
+
+ private JiraProjectAPI jiraProjectAPI;
+ private SearchAPI searchAPI;
+ private ExecutionsAPI executionsAPI;
+ private IterationsAPI iterationsAPI;
+ private StepsAPI stepsAPI;
+ private TestrunAPI testrunAPI;
+
+ public JiraProjectAPI getJiraProjectApi() {
+ if (Objects.isNull(jiraProjectAPI)) {
+ jiraProjectAPI = new JiraProjectAPI();
+ }
+ return jiraProjectAPI;
+ }
+
+ public SearchAPI getSearchAPI() {
+ if (Objects.isNull(searchAPI)) {
+ searchAPI = new SearchAPI();
+ }
+ return searchAPI;
+ }
+
+ public ExecutionsAPI getExecutionsAPI() {
+ if (Objects.isNull(executionsAPI)) {
+ executionsAPI = new ExecutionsAPI();
+ }
+ return executionsAPI;
+ }
+
+ public IterationsAPI getIterationsAPI() {
+ if (Objects.isNull(iterationsAPI)) {
+ iterationsAPI = new IterationsAPI();
+ }
+ return iterationsAPI;
+ }
+
+ public StepsAPI getStepsAPI() {
+ if (Objects.isNull(stepsAPI)) {
+ stepsAPI = new StepsAPI();
+ }
+ return stepsAPI;
+ }
+
+ public TestrunAPI getTestrunAPI() {
+ if (Objects.isNull(testrunAPI)) {
+ testrunAPI = new TestrunAPI();
+ }
+ return testrunAPI;
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/jira/JiraProjectAPI.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/jira/JiraProjectAPI.java
new file mode 100644
index 0000000..a292a19
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/jira/JiraProjectAPI.java
@@ -0,0 +1,268 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.clients.modules.jira;
+
+import static com.applause.auto.helpers.jira.constants.XrayEndpoints.*;
+import static com.applause.auto.helpers.jira.helper.ResponseValidator.checkResponseInRange;
+import static com.applause.auto.helpers.jira.restclient.XrayRestAssuredClient.getRestClient;
+
+import com.applause.auto.helpers.jira.dto.requestmappers.JiraCreateTicketRequest;
+import com.applause.auto.helpers.jira.dto.responsemappers.AvailableIssueTypes;
+import com.applause.auto.helpers.jira.dto.responsemappers.AvailableProjects;
+import com.applause.auto.helpers.jira.dto.responsemappers.JiraCreateTicketResponse;
+import com.applause.auto.helpers.jira.dto.responsemappers.steps.Steps;
+import com.applause.auto.helpers.jira.dto.shared.Issuetype;
+import com.applause.auto.helpers.util.GenericObjectMapper;
+import com.applause.auto.helpers.util.XrayRequestHeaders;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.restassured.response.Response;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.Arrays;
+import java.util.List;
+import lombok.NonNull;
+import org.apache.commons.lang3.Range;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+@SuppressWarnings("checkstyle:MultipleStringLiterals")
+public class JiraProjectAPI {
+
+ private static final Logger logger = LogManager.getLogger(JiraProjectAPI.class);
+
+ /**
+ * Creates a simple JIRA ticket. Additional fields can be created as per project's needs. This
+ * method can be used to create a new test case, test plan, test execution, etc. ...
+ *
+ * @param jiraCreateTicketRequest The request object containing the ticket details.
+ * @return The ticket key extracted from the response (e.g., "PROJECT-123").
+ * @throws JsonProcessingException If an error occurs during JSON processing.
+ */
+ public String createTicket(@NonNull final JiraCreateTicketRequest jiraCreateTicketRequest)
+ throws JsonProcessingException {
+ logger.info("Creating Jira ticket: {}", jiraCreateTicketRequest.toString());
+ Response response = postTicket(jiraCreateTicketRequest);
+ checkResponseInRange(response, Range.of(200, 300), "Creating new Jira Ticket");
+ JiraCreateTicketResponse jiraCreateTicketResponse =
+ GenericObjectMapper.getObjectMapper()
+ .readValue(response.asString(), JiraCreateTicketResponse.class);
+ String createdJiraTicketId = jiraCreateTicketResponse.key();
+ logger.info("Created Jira ticket: {}", createdJiraTicketId);
+ return createdJiraTicketId;
+ }
+
+ /**
+ * Modify existing Jira Ticket by sending the short or full json data
+ *
+ * @param jiraTicketID the Jira ticket ID
+ * @param jsonAsString JIRA ticket JSON contents {@code @example:} { "fields" : { "labels": [
+ * "my_new_label" ] } }
+ */
+ public void updateTicket(@NonNull final String jiraTicketID, @NonNull final String jsonAsString) {
+ logger.info("Updating Jira ticket: {} with [ {} ]", jiraTicketID, jsonAsString);
+ Response response = putUpdateTicket(jiraTicketID, jsonAsString);
+ checkResponseInRange(response, Range.of(200, 300), "Updating Jira Ticket");
+ }
+
+ /**
+ * Delete existing Jira Ticket
+ *
+ * @param jiraTicketID JIRA ticket ID
+ */
+ public void deleteTicket(@NonNull final String jiraTicketID) {
+ logger.info("Deleting Jira ticket: {}", jiraTicketID);
+ Response response = deleteExistingTicket(jiraTicketID);
+ checkResponseInRange(response, Range.of(200, 300), "Deleting Jira Ticket");
+ }
+
+ /**
+ * Get Test Case steps
+ *
+ * @param jiraTicketID JIRA ticket ID
+ * @return Steps object
+ */
+ public Steps getTestCaseSteps(@NonNull final String jiraTicketID) throws JsonProcessingException {
+ Response response = getJiraTestCaseSteps(jiraTicketID);
+ Steps steps = GenericObjectMapper.getObjectMapper().readValue(response.asString(), Steps.class);
+ checkResponseInRange(response, Range.of(200, 300), "Get Test Case Steps");
+ return steps;
+ }
+
+ /**
+ * Upload file to specific jira ticket (issue, testcase, execution, plan etc). Checks performed:
+ *
+ *
+ * - If provided file exists
+ *
- If response contains file name and if status code is in 200 range
+ *
+ *
+ * @param jiraTicketID represents the jira ticket identifier.
+ * @param pathToFile full path to file with its extension.
+ * @throws FileNotFoundException If the file specified by {@code pathToFile} does not exist.
+ */
+ public void uploadAttachment(@NonNull final String jiraTicketID, @NonNull final String pathToFile)
+ throws FileNotFoundException {
+ Response response = postAttachment(jiraTicketID, pathToFile);
+ String fileName = new File(pathToFile).getName();
+ if (!response.getBody().asString().contains(fileName)) {
+ logger.error("Failed to upload attachment {}", fileName);
+ }
+ checkResponseInRange(response, Range.of(200, 300), "Adding attachment");
+ }
+
+ /**
+ * Retrieves the project ID associated with a given project key.
+ *
+ * @param projectKey The Jira project identifier (e.g., "CARQA-1234", where "CARQA" is the project
+ * identifier). Must not be null.
+ * @return The project ID as a String.
+ * @throws JsonProcessingException If there is an error processing the JSON response.
+ * @throws NullPointerException If the provided projectKey is null.
+ */
+ public String getProjectId(@NonNull final String projectKey) throws JsonProcessingException {
+ Response response = getProjectCode(projectKey);
+ checkResponseInRange(response, Range.of(200, 300), "Determine project Id");
+ AvailableProjects[] availableProjects =
+ GenericObjectMapper.getObjectMapper()
+ .readValue(response.asString(), AvailableProjects[].class);
+ return Arrays.stream(availableProjects)
+ .filter(project -> project.key().equalsIgnoreCase(projectKey))
+ .findFirst()
+ .get()
+ .id();
+ }
+
+ /**
+ * Retrieves a list of available issue types for a given project.
+ *
+ * @param projectId the identifier of the project.
+ * @return a list of Issuetype objects representing the available issue types.
+ * @throws JsonProcessingException if there is an error processing the JSON response.
+ */
+ public List getAvailableIssueTypes(@NonNull final String projectId)
+ throws JsonProcessingException {
+ Response response = getAvailableIssues(projectId);
+ checkResponseInRange(response, Range.of(200, 300), "Get project issue types");
+ return GenericObjectMapper.getObjectMapper()
+ .readValue(response.asString(), AvailableIssueTypes.class)
+ .values();
+ }
+
+ /**
+ * Attach label to Jira ticket
+ *
+ * @param jiraTicketID JIRA ticket ID
+ * @param labelName Ticket label name
+ */
+ public void addLabel(@NonNull final String jiraTicketID, @NonNull final String labelName) {
+ logger.info("Updating Jira ticket: {} with [ {} ] label", jiraTicketID, labelName);
+ Response response = putLabelToTicket(jiraTicketID, labelName);
+ checkResponseInRange(response, Range.of(200, 300), "Adding Jira Ticket label");
+ }
+
+ private Response postTicket(@NonNull final JiraCreateTicketRequest jiraCreateTicketRequestMapping)
+ throws JsonProcessingException {
+ logger.info("Creating Jira ticket: {}", jiraCreateTicketRequestMapping.toString());
+ return getRestClient()
+ .given()
+ .and()
+ .body(
+ GenericObjectMapper.getObjectMapper()
+ .writeValueAsString(jiraCreateTicketRequestMapping))
+ .when()
+ .post(ISSUE_PATH)
+ .then()
+ .extract()
+ .response();
+ }
+
+ private Response putUpdateTicket(
+ @NonNull final String jiraTicketID, @NonNull final String jsonAsString) {
+ logger.info("Updating Jira Ticket {} with [ {} ]", jiraTicketID, jsonAsString);
+ final var apiEndpoint = ISSUE_PATH + "/" + jiraTicketID;
+ return getRestClient()
+ .given()
+ .and()
+ .body(jsonAsString)
+ .when()
+ .put(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+
+ private Response getJiraTestCaseSteps(@NonNull final String jiraTicketID) {
+ logger.info("Getting X-Ray Test Case Steps response for case: {}", jiraTicketID);
+ String apiEndpoint = XRAY_PATH + TEST + "/" + jiraTicketID + "/" + STEPS;
+ return getRestClient().given().when().get(apiEndpoint).then().extract().response();
+ }
+
+ private Response postAttachment(
+ @NonNull final String jiraTicketID, @NonNull final String pathToFile)
+ throws FileNotFoundException {
+ logger.info("Uploading attachment {} to Jira Ticket {}", pathToFile, jiraTicketID);
+ File attachment = new File(pathToFile);
+ if (!attachment.exists()) {
+ throw new FileNotFoundException(String.format("Unable to find file %s", pathToFile));
+ }
+ String apiEndpoint = XRAY_PATH + ISSUE_PATH + "/" + jiraTicketID + "/" + ATTACHMENTS;
+ return getRestClient()
+ .given()
+ .header(XrayRequestHeaders.getContentTypeMultipartFormDataHeader())
+ .header(XrayRequestHeaders.getAtlassianNoCheckHeader())
+ .multiPart("file", attachment)
+ .and()
+ .when()
+ .post(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+
+ private Response getProjectCode(@NonNull final String projectKey) {
+ logger.info("Returning project code for project key {}", projectKey);
+ String apiEndpoint = LATEST_API + "/" + PROJECT;
+ return getRestClient().given().when().get(apiEndpoint).then().extract().response();
+ }
+
+ private Response getAvailableIssues(@NonNull final String projectId) {
+ logger.info("Returning available issues for project {}", projectId);
+ String apiEndpoint = ISSUE_PATH + "/" + CREATEMETA + "/" + projectId + "/" + ISSUE_TYPES;
+ return getRestClient().given().when().get(apiEndpoint).then().extract().response();
+ }
+
+ private Response deleteExistingTicket(@NonNull final String jiraTicketID) {
+ logger.info("Deleting issue {}", jiraTicketID);
+ String apiEndpoint = ISSUE_PATH + "/" + jiraTicketID;
+ return getRestClient().given().when().delete(apiEndpoint).then().extract().response();
+ }
+
+ private Response putLabelToTicket(@NonNull final String jiraTicketID, final String labelName) {
+ String apiEndpoint = ISSUE_PATH + "/" + jiraTicketID;
+ return getRestClient()
+ .given()
+ .and()
+ .body(String.format("{\"update\":{\"labels\":[{\"add\":\"%s\"}]}}", labelName))
+ .when()
+ .put(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/jira/SearchAPI.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/jira/SearchAPI.java
new file mode 100644
index 0000000..d08c1cb
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/jira/SearchAPI.java
@@ -0,0 +1,78 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.clients.modules.jira;
+
+import static com.applause.auto.helpers.jira.constants.XrayEndpoints.*;
+import static com.applause.auto.helpers.jira.helper.ResponseValidator.checkResponseInRange;
+import static com.applause.auto.helpers.jira.restclient.XrayRestAssuredClient.getRestClient;
+
+import com.applause.auto.helpers.jira.dto.jql.JqlFilteredResults;
+import com.applause.auto.helpers.util.GenericObjectMapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.restassured.response.Response;
+import java.util.Objects;
+import lombok.NonNull;
+import org.apache.commons.lang3.Range;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class SearchAPI {
+
+ private static final Logger logger = LogManager.getLogger(SearchAPI.class);
+
+ /**
+ * Filters Jira issues based on a provided JQL query.
+ *
+ * @param jqlQuery The JQL query used to filter issues. For example: \"labels IN
+ * (\"xray_automation\") AND issuetype=12106 AND summary~\"Test Description\""
+ * @param maxResults The maximum number of results to return. If null, the Jira default limit
+ * (usually 50) is used.
+ * @return A {@link JqlFilteredResults} object containing the total number of issues found and a
+ * list of the retrieved issues.
+ * @throws JsonProcessingException If an error occurs during JSON processing of the Jira response.
+ */
+ public JqlFilteredResults filterIssues(@NonNull final String jqlQuery, final Integer maxResults)
+ throws JsonProcessingException {
+ Response response = getIssuesByJqlFiltering(jqlQuery, maxResults);
+ checkResponseInRange(response, Range.of(200, 300), "Get issue by jql filter");
+ JqlFilteredResults results =
+ GenericObjectMapper.getObjectMapper()
+ .readValue(response.asString(), JqlFilteredResults.class);
+ if (results.total() == 0) {
+ logger.warn("JQL search returned 0 results");
+ }
+ return results;
+ }
+
+ public Response getIssuesByJqlFiltering(
+ @NonNull final String jqlQuery, final Integer maxResults) {
+ logger.info("Searching issues by JQL query [ {} ] ", jqlQuery);
+ StringBuilder apiEndpoint = new StringBuilder();
+ apiEndpoint.append(LATEST_API).append('/').append(SEARCH).append("?jql=").append(jqlQuery);
+ if (Objects.nonNull(maxResults)) {
+ apiEndpoint.append("&maxResults=").append(maxResults);
+ }
+ return getRestClient().given().when().get(apiEndpoint.toString()).then().extract().response();
+ }
+
+ public Response getIssueResponseObject(@NonNull final String jiraTicketID) {
+ logger.info("Returning issue response for {}", jiraTicketID);
+ String apiEndpoint = ISSUE_PATH + "/" + jiraTicketID;
+ return getRestClient().given().when().get(apiEndpoint).then().extract().response();
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/ExecutionsAPI.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/ExecutionsAPI.java
new file mode 100644
index 0000000..41798c2
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/ExecutionsAPI.java
@@ -0,0 +1,114 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.clients.modules.xray;
+
+import static com.applause.auto.helpers.jira.constants.XrayEndpoints.*;
+import static com.applause.auto.helpers.jira.helper.ResponseValidator.checkResponseInRange;
+import static com.applause.auto.helpers.jira.restclient.XrayRestAssuredClient.getRestClient;
+
+import com.applause.auto.helpers.jira.dto.requestmappers.XrayAddTo;
+import com.applause.auto.helpers.jira.dto.responsemappers.JiraCreateTicketResponse;
+import com.applause.auto.helpers.util.GenericObjectMapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.restassured.response.Response;
+import lombok.NonNull;
+import org.apache.commons.lang3.Range;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+@SuppressWarnings("checkstyle:MultipleStringLiterals")
+public class ExecutionsAPI {
+
+ private static final Logger logger = LogManager.getLogger(ExecutionsAPI.class);
+
+ /**
+ * Adds Test Execution to existing Test Plan.
+ *
+ * Request example: `{"add":["testExecKey"]}`
+ *
+ *
Response: 200 OK, if Test Execution was added successfully
+ *
+ * @param xrayAddToMapping The Test Execution to associate with the specified Test Plan.
+ * @param testPlanKey The key of the Test Plan to which the Test Execution should be added.
+ * @throws JsonProcessingException If an error occurs during JSON processing.
+ */
+ public void addExecutionToTestPlan(
+ @NonNull final XrayAddTo xrayAddToMapping, @NonNull final String testPlanKey)
+ throws JsonProcessingException {
+ Response response = postTestExecutionToTestPlan(xrayAddToMapping, testPlanKey);
+ checkResponseInRange(response, Range.of(200, 300), "Add test execution to test plan");
+ }
+
+ /**
+ * Adds Test to an existing Test Execution. Request example: "{"add":["testKey"]}". Response: 200
+ * OK, if test was added successfully.
+ *
+ * @param jiraCreateTicketResponseMapping The response containing the Test Execution key.
+ * @param xrayAddToMapping The Test key(s) to associate with the Test Execution.
+ * @throws JsonProcessingException If a JSON processing error occurs.
+ */
+ public void addTestToTestExecution(
+ @NonNull final JiraCreateTicketResponse jiraCreateTicketResponseMapping,
+ @NonNull final XrayAddTo xrayAddToMapping)
+ throws JsonProcessingException {
+ Response response = postTestToTestExecution(jiraCreateTicketResponseMapping, xrayAddToMapping);
+ checkResponseInRange(response, Range.of(200, 300), "Add test to test execution");
+ }
+
+ private Response postTestExecutionToTestPlan(
+ @NonNull final XrayAddTo xrayAddToMapping, @NonNull final String testPlanKey)
+ throws JsonProcessingException {
+ logger.info(
+ "Adding X-Ray Test Execution {} to X-Ray Test Plan {}", xrayAddToMapping, testPlanKey);
+
+ String apiEndpoint = XRAY_PATH + TEST_PLAN + "/" + testPlanKey + "/" + TEST_EXECUTION;
+
+ return getRestClient()
+ .given()
+ .and()
+ .body(GenericObjectMapper.getObjectMapper().writeValueAsString(xrayAddToMapping))
+ .when()
+ .post(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+
+ private Response postTestToTestExecution(
+ @NonNull final JiraCreateTicketResponse jiraCreateTicketResponseMapping,
+ @NonNull final XrayAddTo xrayAddToMapping)
+ throws JsonProcessingException {
+ logger.info(
+ "Adding X-Ray Test(s) [ {} ] to X-Ray Test Execution [ {} ]",
+ xrayAddToMapping.toString(),
+ jiraCreateTicketResponseMapping.key());
+
+ String apiEndpoint =
+ XRAY_PATH + TEST_EXEC + "/" + jiraCreateTicketResponseMapping.key() + "/" + TEST;
+
+ return getRestClient()
+ .given()
+ .and()
+ .body(GenericObjectMapper.getObjectMapper().writeValueAsString(xrayAddToMapping))
+ .when()
+ .post(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/IterationsAPI.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/IterationsAPI.java
new file mode 100644
index 0000000..1133d89
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/IterationsAPI.java
@@ -0,0 +1,62 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.clients.modules.xray;
+
+import static com.applause.auto.helpers.jira.constants.XrayEndpoints.*;
+import static com.applause.auto.helpers.jira.helper.ResponseValidator.checkResponseInRange;
+import static com.applause.auto.helpers.jira.restclient.XrayRestAssuredClient.getRestClient;
+
+import com.applause.auto.helpers.jira.dto.responsemappers.steps.Step;
+import com.applause.auto.helpers.util.GenericObjectMapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.restassured.response.Response;
+import org.apache.commons.lang3.Range;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+@SuppressWarnings("checkstyle:MultipleStringLiterals")
+public class IterationsAPI {
+
+ private static final Logger logger = LogManager.getLogger(IterationsAPI.class);
+
+ /**
+ * Get Test Run Iteration Steps information. Returned object will contain also each step ID which
+ * is needed for further step update request.
+ *
+ * @param testRunId The ID of the test run.
+ * @param iterationId The ID of the iteration.
+ * @return An array of Step objects.
+ * @throws JsonProcessingException If there is an error processing the JSON response.
+ */
+ public Step[] getTestRunIterationStepsData(final int testRunId, final int iterationId)
+ throws JsonProcessingException {
+ Response response = getTestRunIterationSteps(testRunId, iterationId);
+ Step[] steps =
+ GenericObjectMapper.getObjectMapper().readValue(response.asString(), Step[].class);
+ checkResponseInRange(response, Range.of(200, 300), "Get Test Run Iteration Steps Data");
+ return steps;
+ }
+
+ private Response getTestRunIterationSteps(final int testRunId, final int iterationId) {
+ logger.info(
+ "Getting X-Ray Test Run {} iteration steps response for ID: {}", testRunId, iterationId);
+ String apiEndpoint =
+ XRAY_PATH + TEST_RUN + "/" + testRunId + "/" + ITERATION + "/" + iterationId + "/" + STEP;
+ return getRestClient().given().when().get(apiEndpoint).then().extract().response();
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/StepsAPI.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/StepsAPI.java
new file mode 100644
index 0000000..509bce3
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/StepsAPI.java
@@ -0,0 +1,217 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.clients.modules.xray;
+
+import static com.applause.auto.helpers.jira.constants.XrayEndpoints.*;
+import static com.applause.auto.helpers.jira.helper.FilesHelper.*;
+import static com.applause.auto.helpers.jira.helper.ResponseValidator.checkResponseInRange;
+import static com.applause.auto.helpers.jira.restclient.XrayRestAssuredClient.getRestClient;
+
+import com.applause.auto.helpers.jira.dto.requestmappers.StepFieldsUpdate;
+import com.applause.auto.helpers.jira.dto.requestmappers.StepIterationAttachment;
+import com.applause.auto.helpers.util.GenericObjectMapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.restassured.response.Response;
+import java.io.IOException;
+import lombok.NonNull;
+import org.apache.commons.lang3.Range;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+@SuppressWarnings("checkstyle:MultipleStringLiterals")
+public class StepsAPI {
+
+ private static final Logger logger = LogManager.getLogger(StepsAPI.class);
+
+ /**
+ * Modify Test Run Step by using PUT API This method is used to update: status, comment, actual
+ * result Should be used when test is NOT parametrised and dataset is NOT present
+ *
+ * @param testRunId the test run ID
+ * @param stepId test step ID
+ * @param fields - fields which will be updated
+ */
+ public void updateTestRunStep(
+ final int testRunId, final int stepId, @NonNull final StepFieldsUpdate fields)
+ throws JsonProcessingException {
+ Response response = putTestRunStep(testRunId, stepId, fields);
+ checkResponseInRange(response, Range.of(200, 300), "Update Test Run Step");
+ }
+
+ /**
+ * Modify Test Run Iteration Step by using PUT API This method is used to update: status, comment,
+ * actual result Should be used when test is parametrised and dataset is present
+ *
+ * @param testRunId the test run ID
+ * @param iterationId test run iteration step
+ * @param stepId test step ID
+ * @param fields - fields which will be updated
+ */
+ public void updateTestRunIterationStep(
+ int testRunId, int iterationId, int stepId, @NonNull final StepFieldsUpdate fields)
+ throws JsonProcessingException {
+ Response response = putTestRunIterationStep(testRunId, iterationId, stepId, fields);
+ checkResponseInRange(response, Range.of(200, 300), "Update Test Run Iteration Step");
+ }
+
+ /**
+ * Upload Test Run Step attachment
+ *
+ * @param testRunId test run ID
+ * @param stepId test step ID
+ * @param filePath - path to file
+ */
+ public void uploadTestRunStepAttachment(
+ final int testRunId, final int stepId, @NonNull final String filePath) throws IOException {
+ Response response = postTestRunStepAttachment(testRunId, stepId, filePath);
+ checkResponseInRange(response, Range.of(200, 300), "Upload Test Run Step attachment");
+ }
+
+ /**
+ * Upload Test Run Iteration Step attachment
+ *
+ * @param testRunId test run ID
+ * @param iterationId test step iteration ID
+ * @param stepId test step ID
+ * @param filePath - path to file
+ */
+ public void uploadTestRunIterationStepAttachment(
+ final int testRunId, final int iterationId, final int stepId, @NonNull final String filePath)
+ throws IOException {
+ Response response =
+ postTestRunIterationStepAttachment(testRunId, iterationId, stepId, filePath);
+ checkResponseInRange(response, Range.of(200, 300), "Upload Test Run Iteration Step attachment");
+ }
+
+ private Response postTestRunStepAttachment(
+ final int testRunId, final int stepId, @NonNull final String filePath) throws IOException {
+ logger.info("Attaching {} to X-Ray Test Run {} step {}", filePath, testRunId, stepId);
+ StepIterationAttachment stepIterationAttachment =
+ new StepIterationAttachment(
+ encodeBase64File(filePath), getFileNameFromPath(filePath), getFileType(filePath));
+ final var apiEndpoint =
+ XRAY_PATH + TEST_RUN + "/" + testRunId + "/" + STEP + "/" + stepId + "/" + ATTACHMENT;
+
+ return getRestClient()
+ .given()
+ .and()
+ .body(GenericObjectMapper.getObjectMapper().writeValueAsString(stepIterationAttachment))
+ .when()
+ .post(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+
+ private Response postTestRunIterationStepAttachment(
+ final int testRunId, final int iterationId, final int stepId, @NonNull final String filePath)
+ throws IOException {
+ logger.info(
+ "Attaching {} to X-Ray Test Run {} iteration {} step {}",
+ filePath,
+ testRunId,
+ iterationId,
+ stepId);
+ StepIterationAttachment stepIterationAttachment =
+ new StepIterationAttachment(
+ encodeBase64File(filePath), getFileNameFromPath(filePath), getFileType(filePath));
+ String apiEndpoint =
+ XRAY_PATH
+ + TEST_RUN
+ + "/"
+ + testRunId
+ + "/"
+ + ITERATION
+ + "/"
+ + iterationId
+ + "/"
+ + STEP
+ + "/"
+ + stepId
+ + "/"
+ + ATTACHMENT;
+
+ return getRestClient()
+ .given()
+ .and()
+ .body(GenericObjectMapper.getObjectMapper().writeValueAsString(stepIterationAttachment))
+ .when()
+ .post(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+
+ private Response putTestRunStep(
+ final int testRunId, final int stepId, @NonNull final StepFieldsUpdate stepFieldsUpdate)
+ throws JsonProcessingException {
+ logger.info(
+ "Updating X-Ray Test Run {} step {} with {}",
+ testRunId,
+ stepId,
+ stepFieldsUpdate.toString());
+ String apiEndpoint = XRAY_PATH + TEST_RUN + "/" + testRunId + "/" + STEP + "/" + stepId;
+
+ return getRestClient()
+ .given()
+ .and()
+ .body(GenericObjectMapper.getObjectMapper().writeValueAsString(stepFieldsUpdate))
+ .when()
+ .put(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+
+ private Response putTestRunIterationStep(
+ final int testRunId,
+ final int iterationId,
+ final int stepId,
+ @NonNull final StepFieldsUpdate stepFieldsUpdate)
+ throws JsonProcessingException {
+ logger.info(
+ "Updating X-Ray Test Run {} iteration {} step {} with {}",
+ testRunId,
+ iterationId,
+ stepId,
+ stepFieldsUpdate.toString());
+ String apiEndpoint =
+ XRAY_PATH
+ + TEST_RUN
+ + "/"
+ + testRunId
+ + "/"
+ + ITERATION
+ + "/"
+ + iterationId
+ + "/"
+ + STEP
+ + "/"
+ + stepId;
+
+ return getRestClient()
+ .given()
+ .and()
+ .body(GenericObjectMapper.getObjectMapper().writeValueAsString(stepFieldsUpdate))
+ .when()
+ .put(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/TestrunAPI.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/TestrunAPI.java
new file mode 100644
index 0000000..be41c5b
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/clients/modules/xray/TestrunAPI.java
@@ -0,0 +1,207 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.clients.modules.xray;
+
+import static com.applause.auto.helpers.jira.constants.XrayEndpoints.*;
+import static com.applause.auto.helpers.jira.helper.FilesHelper.*;
+import static com.applause.auto.helpers.jira.helper.ResponseValidator.checkResponseInRange;
+import static com.applause.auto.helpers.jira.restclient.XrayRestAssuredClient.getRestClient;
+
+import com.applause.auto.helpers.jira.dto.requestmappers.StepIterationAttachment;
+import com.applause.auto.helpers.jira.dto.responsemappers.JiraCreateTicketResponse;
+import com.applause.auto.helpers.jira.dto.responsemappers.XrayTestRunDetails;
+import com.applause.auto.helpers.jira.dto.responsemappers.iteration.TestRunIteration;
+import com.applause.auto.helpers.util.GenericObjectMapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.restassured.response.Response;
+import java.io.IOException;
+import lombok.NonNull;
+import org.apache.commons.lang3.Range;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+@SuppressWarnings("checkstyle:MultipleStringLiterals")
+public class TestrunAPI {
+
+ private static final Logger logger = LogManager.getLogger(TestrunAPI.class);
+
+ /**
+ * Get Test Run ID which can be used in updating Test status in a certain Test Execution.
+ *
+ * @param jiraCreateTicketResponseMapping The Jira create ticket response mapping, used to get the
+ * testExecKey.
+ * @param testKey The test issue key.
+ * @return The testRunID.
+ * @throws JsonProcessingException If the JSON response is invalid.
+ */
+ public int getTestRunID(
+ @NonNull final JiraCreateTicketResponse jiraCreateTicketResponseMapping,
+ @NonNull final String testKey)
+ throws JsonProcessingException {
+ Response response = getTestRunIdOfTestFromExecution(jiraCreateTicketResponseMapping, testKey);
+ XrayTestRunDetails xrayTestRunDetailsMapping =
+ GenericObjectMapper.getObjectMapper()
+ .readValue(response.asString(), XrayTestRunDetails.class);
+ checkResponseInRange(response, Range.of(200, 300), "Collecting Test Run ID");
+ return xrayTestRunDetailsMapping.id();
+ }
+
+ /**
+ * Updates Test Run status.
+ *
+ * @param testRunId the test run ID
+ * @param statusToUpdate the status to update. Can be extracted after test execution from
+ * ITestResult using its getStatus() and then converted into one of the following values
+ * accepted by X-Ray as per project needs:
+ *
+ * - EXECUTING – Test is being executed; this is a non-final status;
+ *
- FAIL – Test failed
+ *
- ABORTED – Test was aborted
+ *
- PASS – Test passed successfully
+ *
+ *
+ * @throws NullPointerException if statusToUpdate is null
+ */
+ public void updateTestRun(final int testRunId, @NonNull final String statusToUpdate) {
+ Response response = putTestRunStatus(testRunId, statusToUpdate);
+ checkResponseInRange(response, Range.of(200, 300), "Update test run");
+ }
+
+ /**
+ * Get Test Run information
+ *
+ * @param testRunId test run ID
+ * @return XrayTestRunDetails object
+ */
+ public XrayTestRunDetails getTestRunData(final int testRunId) throws JsonProcessingException {
+ Response response = getTestRunBasedOnID(testRunId);
+ XrayTestRunDetails xrayTestRunDetailsMapping =
+ GenericObjectMapper.getObjectMapper()
+ .readValue(response.asString(), XrayTestRunDetails.class);
+ checkResponseInRange(response, Range.of(200, 300), "Get Test Run Data");
+ return xrayTestRunDetailsMapping;
+ }
+
+ /**
+ * Get Test Run Iteration information
+ *
+ * @param testRunId test run ID
+ * @param iterationId test run iteration ID
+ * @return TestRunIteration object
+ */
+ public TestRunIteration getTestRunIterationData(final int testRunId, final int iterationId)
+ throws JsonProcessingException {
+ Response response = getTestRunIterationBasedOnID(testRunId, iterationId);
+ TestRunIteration testRunIteration =
+ GenericObjectMapper.getObjectMapper()
+ .readValue(response.asString(), TestRunIteration.class);
+ checkResponseInRange(response, Range.of(200, 300), "Get Test Run Iteration Data");
+ return testRunIteration;
+ }
+
+ /**
+ * Upload Test Run attachment
+ *
+ * @param testRunId test run ID
+ * @param filePath - path to file
+ */
+ public void uploadTestRunAttachment(final int testRunId, @NonNull final String filePath)
+ throws IOException {
+ Response response = postTestRunAttachment(testRunId, filePath);
+ checkResponseInRange(response, Range.of(200, 300), "Upload Test Run attachment");
+ }
+
+ /**
+ * Post comment to Test Run
+ *
+ * @param testRunId test run ID
+ * @param comment test run comment
+ */
+ public void postTestRunComment(final int testRunId, @NonNull final String comment) {
+ Response response = postComment(testRunId, comment);
+ checkResponseInRange(response, Range.of(200, 300), "Posting Test Run comment");
+ }
+
+ private Response getTestRunIdOfTestFromExecution(
+ @NonNull final JiraCreateTicketResponse jiraCreateTicketResponseMapping,
+ @NonNull final String testKey) {
+ logger.info("Getting X-Ray Test Run ID for test: {}", testKey);
+ String apiEndpoint =
+ XRAY_PATH
+ + TEST_RUN
+ + "?"
+ + testExecIssueKeyParam
+ + jiraCreateTicketResponseMapping.key()
+ + "&"
+ + testIssueKeyParam
+ + testKey;
+ return getRestClient().given().when().get(apiEndpoint).then().extract().response();
+ }
+
+ private Response putTestRunStatus(final int testRunId, @NonNull final String statusToUpdate) {
+ logger.info("Updating X-Ray Test Run: {} with status: {}", testRunId, statusToUpdate);
+ String apiEndpoint =
+ XRAY_PATH + TEST_RUN + "/" + testRunId + "/" + STATUS + "?" + statusParam + statusToUpdate;
+ return getRestClient().given().when().put(apiEndpoint).then().extract().response();
+ }
+
+ private Response getTestRunBasedOnID(final int testRunId) {
+ logger.info("Getting X-Ray Test Run response for ID: {}", testRunId);
+ String apiEndpoint = XRAY_PATH + TEST_RUN + "/" + testRunId;
+ return getRestClient().given().when().get(apiEndpoint).then().extract().response();
+ }
+
+ private Response getTestRunIterationBasedOnID(final int testRunId, final int iterationId) {
+ logger.info("Getting X-Ray Test Run {} iteration response for ID: {}", testRunId, iterationId);
+ String apiEndpoint =
+ XRAY_PATH + TEST_RUN + "/" + testRunId + "/" + ITERATION + "/" + iterationId;
+ return getRestClient().given().when().get(apiEndpoint).then().extract().response();
+ }
+
+ private Response postTestRunAttachment(final int testRunId, @NonNull final String filePath)
+ throws IOException {
+ logger.info("Attaching {} to X-Ray Test Run {}", filePath, testRunId);
+ StepIterationAttachment stepIterationAttachment =
+ new StepIterationAttachment(
+ encodeBase64File(filePath), getFileNameFromPath(filePath), getFileType(filePath));
+ String apiEndpoint = XRAY_PATH + TEST_RUN + "/" + testRunId + "/" + ATTACHMENT;
+ return getRestClient()
+ .given()
+ .and()
+ .body(GenericObjectMapper.getObjectMapper().writeValueAsString(stepIterationAttachment))
+ .when()
+ .post(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+
+ private Response postComment(final int testRunId, @NonNull final String comment) {
+ logger.info("Posting comment [{}] to X-Ray Test Run {}", comment, testRunId);
+ String apiEndpoint = XRAY_PATH + TEST_RUN + "/" + testRunId + "/" + COMMENT;
+ return getRestClient()
+ .given()
+ .and()
+ .body(comment)
+ .when()
+ .put(apiEndpoint)
+ .then()
+ .extract()
+ .response();
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/constants/Statuses.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/constants/Statuses.java
new file mode 100644
index 0000000..3351858
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/constants/Statuses.java
@@ -0,0 +1,32 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.constants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+public enum Statuses {
+ PASS("PASS"),
+ FAIL("FAIL"),
+ ABORTED("ABORTED"),
+ BLOCKED("BLOCKED"),
+ TODO("TODO");
+
+ @Getter private final String value;
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/constants/XrayEndpoints.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/constants/XrayEndpoints.java
new file mode 100644
index 0000000..c59c4c5
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/constants/XrayEndpoints.java
@@ -0,0 +1,53 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.constants;
+
+@SuppressWarnings("PMD.DataClass")
+public final class XrayEndpoints {
+ public static final String LATEST_API = "/api/latest";
+
+ public static final String XRAY_PATH = "/raven/latest/api/";
+ public static final String ISSUE_PATH = LATEST_API + "/issue";
+
+ public static final String PROJECT = "project";
+ public static final String TEST_PLAN = "testplan";
+ public static final String TEST = "test";
+ public static final String STEP = "step";
+ public static final String STEPS = "steps";
+ public static final String TEST_EXECUTION = "testexecution";
+ public static final String TEST_EXEC = "testexec";
+ public static final String TEST_RUN = "testrun";
+ public static final String ITERATION = "iteration";
+ public static final String STATUS = "status";
+ public static final String ATTACHMENT = "attachment";
+ public static final String ATTACHMENTS = "attachments";
+ public static final String ISSUE_TYPES = "issuetypes";
+ public static final String CREATEMETA = "createmeta";
+ public static final String SEARCH = "search";
+ public static final String COMMENT = "comment";
+
+ /** Query parameters */
+ public static final String testExecIssueKeyParam = "testExecIssueKey=";
+
+ public static final String testIssueKeyParam = "testIssueKey=";
+ public static final String statusParam = "status=";
+
+ private XrayEndpoints() {
+ // utility class
+ }
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/Fields.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/Fields.java
new file mode 100644
index 0000000..eb6a7ac
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/Fields.java
@@ -0,0 +1,25 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.jql;
+
+import com.applause.auto.helpers.jira.dto.shared.Issuetype;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record Fields(
+ String summary, String labels, String created, Issuetype issuetype, String environment) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/Issues.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/Issues.java
new file mode 100644
index 0000000..f383430
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/Issues.java
@@ -0,0 +1,23 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.jql;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record Issues(String id, String key, Fields fields) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/JqlFilteredResults.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/JqlFilteredResults.java
new file mode 100644
index 0000000..06c4d5a
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/jql/JqlFilteredResults.java
@@ -0,0 +1,24 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.jql;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record JqlFilteredResults(int maxResults, int total, List issues) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/Fields.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/Fields.java
new file mode 100644
index 0000000..9caaeac
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/Fields.java
@@ -0,0 +1,23 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.requestmappers;
+
+import com.applause.auto.helpers.jira.dto.shared.Issuetype;
+import com.applause.auto.helpers.jira.dto.shared.Project;
+
+public record Fields(String summary, Issuetype issuetype, Project project) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/JiraCreateTicketRequest.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/JiraCreateTicketRequest.java
new file mode 100644
index 0000000..f658e58
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/JiraCreateTicketRequest.java
@@ -0,0 +1,20 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.requestmappers;
+
+public record JiraCreateTicketRequest(Fields fields) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/JiraFields.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/JiraFields.java
new file mode 100644
index 0000000..1c529a0
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/JiraFields.java
@@ -0,0 +1,36 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.requestmappers;
+
+import com.applause.auto.helpers.jira.dto.shared.Issuetype;
+import com.applause.auto.helpers.jira.dto.shared.Project;
+
+public class JiraFields {
+
+ private final Fields fields;
+
+ public JiraFields(final String issueType, final String projectId, final String summary) {
+ fields = new Fields(new Issuetype(issueType, null), new Project(projectId), summary);
+ }
+
+ public Fields getFields() {
+ return fields;
+ }
+
+ public record Fields(Issuetype issuetype, Project project, String summary) {}
+}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/StepFieldsUpdate.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/StepFieldsUpdate.java
new file mode 100644
index 0000000..80bd2bf
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/StepFieldsUpdate.java
@@ -0,0 +1,20 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.requestmappers;
+
+public record StepFieldsUpdate(String status, String comment, String actualResult) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/StepIterationAttachment.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/StepIterationAttachment.java
new file mode 100644
index 0000000..32d106a
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/StepIterationAttachment.java
@@ -0,0 +1,20 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.requestmappers;
+
+public record StepIterationAttachment(String data, String filename, String contentType) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/XrayAddTo.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/XrayAddTo.java
new file mode 100644
index 0000000..2384d72
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/requestmappers/XrayAddTo.java
@@ -0,0 +1,22 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.requestmappers;
+
+import java.util.List;
+
+public record XrayAddTo(List add) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/AvailableIssueTypes.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/AvailableIssueTypes.java
new file mode 100644
index 0000000..42e2b77
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/AvailableIssueTypes.java
@@ -0,0 +1,30 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.responsemappers;
+
+import com.applause.auto.helpers.jira.dto.shared.Issuetype;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import java.util.List;
+
+/**
+ * Represents a list of available issue types.
+ *
+ * @param values The list of {@link Issuetype} objects.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record AvailableIssueTypes(List values) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/AvailableProjects.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/AvailableProjects.java
new file mode 100644
index 0000000..c1ee0ee
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/AvailableProjects.java
@@ -0,0 +1,31 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.responsemappers;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * A record representing the available projects in Jira.
+ *
+ * @param self The URL of the project.
+ * @param id The ID of the project.
+ * @param key The key of the project.
+ * @param name The name of the project.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record AvailableProjects(String self, String id, String key, String name) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/JiraCreateTicketResponse.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/JiraCreateTicketResponse.java
new file mode 100644
index 0000000..5e24173
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/JiraCreateTicketResponse.java
@@ -0,0 +1,20 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.responsemappers;
+
+public record JiraCreateTicketResponse(String id, String key, String self) {}
diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/XrayTestRunDetails.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/XrayTestRunDetails.java
new file mode 100644
index 0000000..b4f7e50
--- /dev/null
+++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/XrayTestRunDetails.java
@@ -0,0 +1,43 @@
+/*
+ *
+ * Copyright © 2025 Applause App Quality, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.applause.auto.helpers.jira.dto.responsemappers;
+
+import com.applause.auto.helpers.jira.dto.responsemappers.iteration.Iteration;
+import com.applause.auto.helpers.jira.dto.responsemappers.steps.Step;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record XrayTestRunDetails(
+ int id,
+ String status,
+ String color,
+ String testKey,
+ String testExecKey,
+ String executedBy,
+ String startedOn,
+ String finishedOn,
+ String startedOnIso,
+ String finishedOnIso,
+ int duration,
+ List iterations,
+ List