diff --git a/.gitignore b/.gitignore index 24a4cb4..fa71811 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,7 @@ Temporary Items !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json + + +# =============================== Allure reporting =============================== +allure-results/ \ No newline at end of file diff --git a/auto-sdk-java-helpers/pom.xml b/auto-sdk-java-helpers/pom.xml index 96e116f..2b92298 100644 --- a/auto-sdk-java-helpers/pom.xml +++ b/auto-sdk-java-helpers/pom.xml @@ -54,10 +54,53 @@ org.apache.commons commons-collections4 + + io.rest-assured + rest-assured + + + org.awaitility + awaitility + + + org.jsoup + jsoup + + + us.codecraft + xsoup + + + com.github.javafaker + javafaker + + + io.qameta.allure + allure-testng + + + org.json + json + + + io.github.yaml-path + yaml-path + + + com.google.api-client + google-api-client + + + com.google.apis + google-api-services-sheets + + + jakarta.mail + jakarta.mail-api + org.testng testng - test diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/AnalyticsHelper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/AnalyticsHelper.java index 00b6282..a60ca7f 100644 --- a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/AnalyticsHelper.java +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/AnalyticsHelper.java @@ -781,6 +781,11 @@ public List getRequestsWithMultipleFilt return filtered; } + /** + * Returns a new instance of {@link AnalyticsInterceptor} associated with this object. + * + * @return A new {@link AnalyticsInterceptor} instance. + */ public AnalyticsInterceptor getInterceptor() { return new AnalyticsInterceptor(this); } diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/EnvironmentHelper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/EnvironmentHelper.java index d5e3f19..1a5e8f8 100644 --- a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/EnvironmentHelper.java +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/EnvironmentHelper.java @@ -21,7 +21,6 @@ import com.applause.auto.context.IPageObjectExtension; import com.applause.auto.data.enums.DriverType; import java.util.Locale; -import lombok.AllArgsConstructor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.openqa.selenium.remote.RemoteWebDriver; @@ -31,10 +30,18 @@ * driver type, which browser, which version. */ @SuppressWarnings("checkstyle:AbbreviationAsWordInName") -@AllArgsConstructor public class EnvironmentHelper implements IPageObjectExtension { private static final Logger logger = LogManager.getLogger(EnvironmentHelper.class); - private IPageObjectContext pageObjectContext; + private final IPageObjectContext pageObjectContext; + + /** + * Constructor for EnvironmentHelper. + * + * @param pageObjectContext The {@link IPageObjectContext} to use. + */ + public EnvironmentHelper(final IPageObjectContext pageObjectContext) { + this.pageObjectContext = pageObjectContext; + } /** * determine the cast class type of the created driver object and set the DriverType into the diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/AllureDriverUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/AllureDriverUtils.java new file mode 100644 index 0000000..388eb63 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/AllureDriverUtils.java @@ -0,0 +1,87 @@ +/* + * + * 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.allure; + +import io.qameta.allure.Allure; +import java.io.ByteArrayInputStream; +import lombok.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; + +/** Provides utility methods for attaching information to Allure reports. */ +public final class AllureDriverUtils { + + private static final Logger LOGGER = LogManager.getLogger(AllureDriverUtils.class); + private static final String SCREENSHOT_ATTACHMENT = "Screenshot attachment"; + private static final String IMAGE_PNG = "image/png"; + private static final String CURRENT_URL = "Current URL"; + private static final String TEXT_PLAIN = "text/plain"; + private static final String CURRENT_PAGE_SOURCE = "Current page source"; + private static final String LOG_EXTENSION = ".log"; + + private AllureDriverUtils() { + // Utility class - no public constructor + } + + /** + * Attaches a screenshot to the Allure report. + * + * @param driver The WebDriver instance to capture the screenshot from. + */ + public static void attachScreenshot(@NonNull final WebDriver driver) { + LOGGER.info("Taking screenshot on test failure"); + try { + var screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); + Allure.addAttachment( + SCREENSHOT_ATTACHMENT, IMAGE_PNG, new ByteArrayInputStream(screenshot), "png"); + } catch (Exception e) { + LOGGER.error("Error taking screenshot: {}", e.getMessage()); + } + } + + /** + * Attaches the current URL to the Allure report. + * + * @param driver The WebDriver instance to get the current URL from. + */ + public static void attachCurrentURL(@NonNull final WebDriver driver) { + LOGGER.info("Attaching current URL"); + try { + Allure.addAttachment(CURRENT_URL, TEXT_PLAIN, driver.getCurrentUrl(), LOG_EXTENSION); + } catch (Exception e) { + LOGGER.error("Error taking current URL: {}", e.getMessage()); + } + } + + /** + * Attaches the current page source to the Allure report. + * + * @param driver The WebDriver instance to get the page source from. + */ + public static void attachCurrentPageSourceOnFailure(@NonNull final WebDriver driver) { + LOGGER.info("Attaching page source"); + try { + Allure.addAttachment(CURRENT_PAGE_SOURCE, TEXT_PLAIN, driver.getPageSource(), LOG_EXTENSION); + } catch (Exception e) { + LOGGER.error("Error taking current page source: {}", e.getMessage()); + } + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/AllureUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/AllureUtils.java new file mode 100644 index 0000000..0b9bf50 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/AllureUtils.java @@ -0,0 +1,58 @@ +/* + * + * 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.allure; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import lombok.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Utility class for Allure reporting. */ +public final class AllureUtils { + + private static final Logger logger = LogManager.getLogger(AllureUtils.class); + private static final String CATEGORIES_JSON = "categories.json"; + + private AllureUtils() {} + + /** + * Copies the Allure defects categorization JSON file to the Allure report directory. + * + * @param allureReportPath The path to the Allure report directory. + * @param allureDefectsCategorisationFilePath The path to the Allure defects categorization JSON + * file. + * @throws IOException If an I/O error occurs. + */ + public static void addAllureDefectsCategoriesConfiguration( + @NonNull final String allureReportPath, + @NonNull final String allureDefectsCategorisationFilePath) + throws IOException { + + logger.info("Copying defects configs to {} file for Allure", CATEGORIES_JSON); + + final Path categoriesAllureFile = Path.of(allureReportPath, CATEGORIES_JSON); + + if (!Files.exists(categoriesAllureFile)) { + Files.createFile(categoriesAllureFile); + } + + Files.copy(Path.of(allureDefectsCategorisationFilePath), categoriesAllureFile); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/appenders/AllureLogsToStepAppender.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/appenders/AllureLogsToStepAppender.java new file mode 100644 index 0000000..57cec38 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/allure/appenders/AllureLogsToStepAppender.java @@ -0,0 +1,103 @@ +/* + * + * 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.allure.appenders; + +import io.qameta.allure.Allure; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import org.apache.logging.log4j.core.*; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.layout.PatternLayout; + +/** Appender for logging to Allure steps. */ +@Plugin( + name = "AllureLogsToStepAppender", + category = Node.CATEGORY, + elementType = Appender.ELEMENT_TYPE) +public final class AllureLogsToStepAppender extends AbstractAppender { + + private static final String COMMA = ","; + private static final Set FILTERED_PACKAGES_TO_APPEND_SET = new ConcurrentSkipListSet<>(); + + private AllureLogsToStepAppender( + final String name, + final Filter filter, + final Layout 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 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: + * + *

    + *
  1. If provided file exists + *
  2. 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 defects, + List evidences, + List testEnvironments, + List fixVersions, + List steps) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/Iteration.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/Iteration.java new file mode 100644 index 0000000..f519c2f --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/Iteration.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.responsemappers.iteration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record Iteration(int id, String status, List parameters) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/IterationParameters.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/IterationParameters.java new file mode 100644 index 0000000..1249206 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/IterationParameters.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.responsemappers.iteration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record IterationParameters(String name, String value) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/TestRunIteration.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/TestRunIteration.java new file mode 100644 index 0000000..1330386 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/iteration/TestRunIteration.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.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 TestRunIteration( + int id, + String testRunId, + String status, + List parameters, + List steps) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Action.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Action.java new file mode 100644 index 0000000..d7de7b6 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Action.java @@ -0,0 +1,28 @@ +/* + * + * 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.steps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents an action. + * + * @param value The value associated with the action. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Action(Value value) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Attachments.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Attachments.java new file mode 100644 index 0000000..cc5ffed --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Attachments.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.steps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A record representing an attachment. + * + * @param id The ID of the attachment. + * @param fileName The name of the file. + * @param fileURL The URL of the file. + * @param filePath The path to the file. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Attachments(int id, String fileName, String fileURL, String filePath) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Comment.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Comment.java new file mode 100644 index 0000000..9fb0201 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Comment.java @@ -0,0 +1,28 @@ +/* + * + * 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.steps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A record representing a comment in Jira. + * + * @param rendered The rendered content of the comment. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Comment(String rendered) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Data.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Data.java new file mode 100644 index 0000000..4727a87 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Data.java @@ -0,0 +1,29 @@ +/* + * + * 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.steps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents data with a type and value. + * + * @param type The type of the data. + * @param value The value of the data. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Data(String type, Value value) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Fields.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Fields.java new file mode 100644 index 0000000..f04f89c --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Fields.java @@ -0,0 +1,28 @@ +/* + * + * 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.steps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents the fields of a Jira issue. + * + * @param action The action associated with the fields. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Fields(Action action) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Step.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Step.java new file mode 100644 index 0000000..c025928 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Step.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.dto.responsemappers.steps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record Step( + int id, + int index, + String status, + Fields fields, + List attachments, + Comment comment, + List evidences, + Comment actualResult) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Steps.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Steps.java new file mode 100644 index 0000000..ffccf54 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Steps.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.responsemappers.steps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record Steps(List steps) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Value.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Value.java new file mode 100644 index 0000000..8ce6c0e --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/responsemappers/steps/Value.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.responsemappers.steps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record Value(String raw) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/shared/Issuetype.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/shared/Issuetype.java new file mode 100644 index 0000000..49f57a5 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/shared/Issuetype.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.shared; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record Issuetype(String id, String name) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/shared/Project.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/shared/Project.java new file mode 100644 index 0000000..3146f07 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/dto/shared/Project.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.shared; + +public record Project(String id) {} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/JiraAnnotationException.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/JiraAnnotationException.java new file mode 100644 index 0000000..ff5a9bf --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/JiraAnnotationException.java @@ -0,0 +1,26 @@ +/* + * + * 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.exceptions; + +public class JiraAnnotationException extends RuntimeException { + + public JiraAnnotationException( + final String message, final NullPointerException nullPointerException) { + super(message, nullPointerException); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/JiraPropertiesFileException.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/JiraPropertiesFileException.java new file mode 100644 index 0000000..e57620e --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/JiraPropertiesFileException.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.exceptions; + +public class JiraPropertiesFileException extends RuntimeException { + + public JiraPropertiesFileException(final String message) { + super(message); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/UnidentifiedExecutionStatusException.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/UnidentifiedExecutionStatusException.java new file mode 100644 index 0000000..cbc54aa --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/exceptions/UnidentifiedExecutionStatusException.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.exceptions; + +public class UnidentifiedExecutionStatusException extends RuntimeException { + + public UnidentifiedExecutionStatusException(final String message) { + super(message); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/helper/FilesHelper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/helper/FilesHelper.java new file mode 100644 index 0000000..7cc463c --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/helper/FilesHelper.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.helper; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; +import lombok.NonNull; +import lombok.SneakyThrows; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class FilesHelper { + private static final Logger logger = LogManager.getLogger(FilesHelper.class); + + private FilesHelper() { + // utility class + } + + /** + * Encodes a file to Base64. + * + * @param filePath The path to the file to encode. + * @return The Base64 encoded string of the file. + * @throws IOException If an I/O error occurs while reading the file. + * @throws NullPointerException If the provided filePath is null. + */ + public static String encodeBase64File(@NonNull final String filePath) throws IOException { + logger.info("Encoding file: {} to Base64", filePath); + byte[] fileContent = Files.readAllBytes(getFile(filePath).toPath()); + return Base64.getEncoder().encodeToString(fileContent); + } + + /** + * Gets the file name from a file path. + * + * @param filePath The path to the file. + * @return The name of the file. + * @throws NullPointerException If the provided filePath is null. + */ + public static String getFileNameFromPath(@NonNull final String filePath) { + return getFile(filePath).getName(); + } + + /** + * Gets the file type from a file path. + * + * @param filePath The path to the file. + * @return The type of the file, or null if it cannot be determined. + * @throws IOException If an I/O error occurs while determining the file type. + * @throws NullPointerException If the provided filePath is null. + */ + public static String getFileType(@NonNull final String filePath) throws IOException { + return Files.probeContentType(getFile(filePath).toPath()); + } + + @SneakyThrows + private static File getFile(@NonNull final String filePath) { + logger.info("Returning file from path: {}", filePath); + return new File(filePath); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/helper/ResponseValidator.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/helper/ResponseValidator.java new file mode 100644 index 0000000..c80ae17 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/helper/ResponseValidator.java @@ -0,0 +1,49 @@ +/* + * + * 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.helper; + +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; + +public final class ResponseValidator { + + private static final Logger logger = LogManager.getLogger(ResponseValidator.class); + + private ResponseValidator() { + // utility class + } + + public static void checkResponseInRange( + @NonNull final Response response, + @NonNull final Range expectedRange, + final String action) { + int statusCode = response.statusCode(); + if (expectedRange.contains(statusCode)) { + logger.info("{} was successfully performed", action); + logger.info(response.getBody().asString()); + } else { + logger.error("{} failed with status code {}", action, statusCode); + if (response.getBody() != null) { + logger.error(response.getBody().asString()); + } + } + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/restclient/IJiraAppConfig.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/restclient/IJiraAppConfig.java new file mode 100644 index 0000000..b1c1cdc --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/restclient/IJiraAppConfig.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.restclient; + +import org.aeonbits.owner.Config; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; + +@LoadPolicy(Config.LoadType.MERGE) +@Sources({"classpath:jira.properties"}) +interface IJiraAppConfig extends Config { + @Key("jira.url") + String jiraUrl(); + + @Key("jira.xray.token") + String jiraToken(); +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/restclient/XrayRestAssuredClient.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/restclient/XrayRestAssuredClient.java new file mode 100644 index 0000000..00ffe61 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/jira/restclient/XrayRestAssuredClient.java @@ -0,0 +1,70 @@ +/* + * + * 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.restclient; + +import com.applause.auto.helpers.http.restassured.RestApiDefaultRestAssuredApiHelper; +import com.applause.auto.helpers.jira.exceptions.JiraPropertiesFileException; +import com.applause.auto.helpers.util.XrayRequestHeaders; +import io.restassured.specification.RequestSpecification; +import java.io.File; +import lombok.NonNull; +import org.aeonbits.owner.ConfigFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class XrayRestAssuredClient { + + private static final Logger logger = LogManager.getLogger(XrayRestAssuredClient.class); + private static final String JIRA_PROPERTIES_FILE_PATH = "src/main/resources/jira.properties"; + + private static String jiraUrl; + private static String token; + private static final IJiraAppConfig configApp = ConfigFactory.create(IJiraAppConfig.class); + + private XrayRestAssuredClient() { + // utility class + } + + private static void loadProperties() { + if (!new File(JIRA_PROPERTIES_FILE_PATH).exists()) { + throw new JiraPropertiesFileException( + "Couldn't find jira.properties file in resources folder!"); + } + token = configApp.jiraToken(); + jiraUrl = configApp.jiraUrl(); + checkJiraUrl(jiraUrl); + } + + public static RequestSpecification getRestClient() { + loadProperties(); + RestApiDefaultRestAssuredApiHelper restApiDefaultRestAssuredApiHelper = + new RestApiDefaultRestAssuredApiHelper(); + return restApiDefaultRestAssuredApiHelper + .withDefaultRestHttpClientConfigsSpecification() + .baseUri(jiraUrl) + .header(XrayRequestHeaders.getBearerAuthorizationHeader(token)) + .header(XrayRequestHeaders.getAcceptApplicationJsonHeader()) + .header(XrayRequestHeaders.getJSONContentTypeHeader()); + } + + private static void checkJiraUrl(@NonNull final String jiraUrl2) { + if (!jiraUrl2.startsWith("https")) { + logger.warn("API calls might return 415 because provided Jira url is not https!"); + } + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileContextsUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileContextsUtils.java new file mode 100644 index 0000000..ae053af --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileContextsUtils.java @@ -0,0 +1,130 @@ +/* + * + * 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.mobile; + +import static com.applause.auto.helpers.util.AwaitilityWaitUtils.waitForCondition; + +import io.appium.java_client.remote.SupportsContextSwitching; +import java.util.Set; +import java.util.concurrent.Callable; +import lombok.NonNull; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Helper class to work with mobile web and native contexts */ +public final class MobileContextsUtils { + + private static final Logger logger = LogManager.getLogger(MobileContextsUtils.class); + + private static final String WEB_CONTEXT_PART_NAME = "web"; + private static final String NATIVE_CONTEXT_PART_NAME = "native"; + + private MobileContextsUtils() { + // utility class + } + + /** + * Perform actions in a web context, return result and switch back to native context + * + * @param driver - automation driver + * @param actions - callable to actions to perform inside switched context + * @param waitForContextTimeout - wait timeout to wait for context to appear + * @param waitPollingInterval - polling interval timeout for wait for context to appear + * @param Generic class that is used. + * @return An object instance from callable actions performed in context + */ + @SneakyThrows + public static A performActionsInWebView( + @NonNull final SupportsContextSwitching driver, + @NonNull final Callable actions, + final int waitForContextTimeout, + final int waitPollingInterval) { + switchToFirstAvailableWebContext(driver, waitForContextTimeout, waitPollingInterval); + try { + return actions.call(); + } finally { + switchToFirstAvailableNativeContext(driver, waitForContextTimeout, waitPollingInterval); + } + } + + /** + * Switching to web context + * + * @param driver - automation driver + * @param waitForContextTimeout - wait timeout to wait for context to appear + * @param waitPollingInterval - polling interval timeout for wait for context to appear + */ + public static void switchToFirstAvailableWebContext( + @NonNull final SupportsContextSwitching driver, + final int waitForContextTimeout, + final int waitPollingInterval) { + waitForContextAndSwitchToIt( + driver, WEB_CONTEXT_PART_NAME, waitForContextTimeout, waitPollingInterval); + } + + /** + * Switching to native context + * + * @param driver - automation driver + * @param waitForContextTimeout - wait timeout to wait for context to appear + * @param waitPollingInterval - polling interval timeout for wait for context to appear + */ + public static void switchToFirstAvailableNativeContext( + @NonNull final SupportsContextSwitching driver, + final int waitForContextTimeout, + final int waitPollingInterval) { + waitForContextAndSwitchToIt( + driver, NATIVE_CONTEXT_PART_NAME, waitForContextTimeout, waitPollingInterval); + } + + /** + * Wait for mobile app context by string part and switch to it + * + * @param driver - automation driver + * @param contextStringPart - part of expected context name on mobile view + * @param waitForContextTimeout - wait timeout to wait for context to appear + * @param waitPollingInterval - polling interval timeout for wait for context to appear + */ + public static void waitForContextAndSwitchToIt( + @NonNull final SupportsContextSwitching driver, + @NonNull final String contextStringPart, + final int waitForContextTimeout, + final int waitPollingInterval) { + logger.info("Looking for context containing: " + contextStringPart); + waitForCondition( + () -> { + Set contexts = driver.getContextHandles(); + logger.info("Available contexts: " + contexts); + return contexts.stream() + .anyMatch(context -> StringUtils.containsIgnoreCase(context, contextStringPart)); + }, + waitForContextTimeout, + waitPollingInterval, + "Waiting for context string part: " + contextStringPart); + Set contexts = driver.getContextHandles(); + String neededContext = + contexts.stream() + .filter(contextItem -> StringUtils.containsIgnoreCase(contextItem, contextStringPart)) + .findFirst() + .get(); + logger.info("Switching to context with name: " + neededContext); + driver.context(neededContext); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileElementUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileElementUtils.java new file mode 100644 index 0000000..4d47937 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileElementUtils.java @@ -0,0 +1,144 @@ +/* + * + * 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.mobile; + +import static com.applause.auto.helpers.mobile.MobileUtils.getMobileDriver; + +import io.appium.java_client.AppiumDriver; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.Point; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.PointerInput; +import org.openqa.selenium.interactions.Sequence; + +/** Helper for mobile elements on native mobile */ +public final class MobileElementUtils { + + private static final Logger logger = LogManager.getLogger(MobileElementUtils.class); + + private MobileElementUtils() { + // utility class + } + + /** + * Tap element center + * + * @param driver - automation driver + * @param element - mobile element for tap on + */ + public static void tapElementCenter( + @NonNull final AppiumDriver driver, @NonNull final WebElement element) { + Point centerOfElement = getCenter(element); + tapElementByCoordinates(driver, centerOfElement); + } + + /** + * Tap element by coordinates + * + * @param driver - automation driver + * @param elementCoordinates - coordinates point for tap + */ + public static void tapElementByCoordinates( + @NonNull final AppiumDriver driver, @NonNull final Point elementCoordinates) { + PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger"); + Sequence tap = new Sequence(finger, 1); + tap.addAction( + finger.createPointerMove( + Duration.ofMillis(500), + PointerInput.Origin.viewport(), + elementCoordinates.getX(), + elementCoordinates.getY())); + tap.addAction(finger.createPointerDown(0)); // 0 represents the left mouse button + tap.addAction(finger.createPointerUp(0)); + driver.perform(List.of(tap)); + } + + /** + * Long press on element center (Updated for Appium v8) + * + * @param driver - automation driver + * @param element - mobile element for long press on + */ + public static void longPressOnElementCenter( + @NonNull final AppiumDriver driver, @NonNull final WebElement element) { + Point centerOfElement = getCenter(element); + + PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger"); + Sequence longPress = new Sequence(finger, 1); + + longPress.addAction( + finger.createPointerMove( + Duration.ofMillis(500), + PointerInput.Origin.viewport(), + centerOfElement.getX(), + centerOfElement.getY())); + longPress.addAction(finger.createPointerDown(0)); // Press down + longPress.addAction( + finger.createPointerMove( + Duration.ofMillis(1000), + PointerInput.Origin.viewport(), + centerOfElement.getX(), + centerOfElement.getY())); // Hold for duration. Appium longpress is default 1000 ms + longPress.addAction(finger.createPointerUp(0)); // Release + + driver.perform(List.of(longPress)); + } + + /** + * Click element with execute script, this is new Appium mobile approach of elements and device + * interactions through .executeScript(...) + * + * @param driver - automation driver + * @param element - mobile element for click on + * @param tapCount - tap count + * @param touchCount - tap count + * @param duration - duration for tap + */ + public static void clickElementWithExecuteScript( + @NonNull final WebDriver driver, + @NonNull final WebElement element, + final int tapCount, + final int touchCount, + final int duration) { + Point elementCenter = getCenter(element); + logger.info("Element coords: " + elementCenter); + Map tap = new HashMap<>(); + tap.put("tapCount", (double) tapCount); + tap.put("touchCount", (double) touchCount); + tap.put("duration", (double) duration); + tap.put("x", (double) elementCenter.getX()); + tap.put("y", (double) elementCenter.getY()); + getMobileDriver(driver).executeScript("mobile: tap", tap); + } + + public static Point getCenter(@NonNull final WebElement element) { + Dimension size = element.getSize(); + Point location = element.getLocation(); + int centerX = location.getX() + size.getWidth() / 2; + int centerY = location.getY() + size.getHeight() / 2; + return new Point(centerX, centerY); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileUtils.java new file mode 100644 index 0000000..7ce67e1 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/MobileUtils.java @@ -0,0 +1,263 @@ +/* + * + * 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.mobile; + +import com.applause.auto.helpers.util.ThreadHelper; +import io.appium.java_client.AppiumDriver; +import io.appium.java_client.HidesKeyboard; +import io.appium.java_client.InteractsWithApps; +import io.appium.java_client.android.StartsActivity; +import io.appium.java_client.ios.IOSDriver; +import java.time.Duration; +import java.util.List; +import java.util.stream.IntStream; +import lombok.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.interactions.PointerInput; +import org.openqa.selenium.interactions.Sequence; +import org.openqa.selenium.remote.RemoteWebDriver; + +/** Common mobile native utils */ +public final class MobileUtils { + + private static final Logger logger = LogManager.getLogger(MobileUtils.class); + + private static final String CF_BUNDLE_IDENTIFIER = "CFBundleIdentifier"; + + private MobileUtils() { + // utility class + } + + /** + * Scroll down scroll view X times + * + * @param driver - automation driver + * @param pStartXCoef - start x coef. ( pStartXCoef * x screen width) + * @param pStartYCoef - start y coef. ( pStartYCoef * Y screen height) + * @param pEndYCoef - end y coef. ( pEndYCoef * Y screen height) + * @param waitOption1 - wait action after first press + * @param waitOption2 - wait action after move to + * @param scrollTimes - how many times to scroll + */ + public static void scrollVerticalSeveralTimes( + @NonNull final AppiumDriver driver, + final double pStartXCoef, + final double pStartYCoef, + final double pEndYCoef, + final int waitOption1, + final int waitOption2, + final int scrollTimes) { + IntStream.range(0, scrollTimes) + .forEach( + action -> + scrollVertical( + driver, pStartXCoef, pStartYCoef, pEndYCoef, waitOption1, waitOption2)); + } + + /** + * Scroll down scroll view + * + * @param driver - automation driver + * @param pStartXCoef - start x coef. ( pStartXCoef * x screen width) + * @param pStartYCoef - start y coef. ( pStartYCoef * Y screen height) + * @param pEndYCoef - end y coef. ( pEndYCoef * Y screen height) + * @param waitOption1 - wait action after first press + * @param waitOption2 - wait action after move to + */ + public static void scrollVertical( + @NonNull final AppiumDriver driver, + final double pStartXCoef, + final double pStartYCoef, + final double pEndYCoef, + final int waitOption1, + final int waitOption2) { + + Dimension size = driver.manage().window().getSize(); + + int startY = (int) (size.getHeight() * pStartYCoef); + int endY = (int) (size.getHeight() * pEndYCoef); + int startX = (int) (size.getWidth() * pStartXCoef); // Make sure startX is an int + + logger.info( + "Swiping Down: [startX]: " + startX + " , [startY]: " + startY + " , [endY]: " + endY); + + try { + PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger"); + Sequence scroll = new Sequence(finger, 1); + + scroll.addAction( + finger.createPointerMove( + Duration.ofMillis(0), PointerInput.Origin.viewport(), startX, startY)); + scroll.addAction(finger.createPointerDown(0)); + scroll.addAction( + finger.createPointerMove( + Duration.ofMillis(waitOption1), PointerInput.Origin.viewport(), startX, startY)); + scroll.addAction( + finger.createPointerMove( + Duration.ofMillis(waitOption2), PointerInput.Origin.viewport(), startX, endY)); + scroll.addAction(finger.createPointerUp(0)); + + driver.perform(List.of(scroll)); + + } catch (Exception wex) { + logger.warn( + "Swipe cause error, probably nothing to swipe or driver issue: " + wex.getMessage()); + } + } + + /** Clear chrome cache locally through ADB shell */ + public static void clearAndroidChromeCacheForLocalExecution() { + logger.info("Clearing android chrome cache"); + new ProcessBuilder().command("db", "shell", "pm", "clear", "com.android.chrome"); + } + + /** + * Hide mobile keyboard + * + * @param driver - automation driver + */ + public static void hideKeyboard(@NonNull final HidesKeyboard driver) { + try { + driver.hideKeyboard(); + } catch (Exception e) { + logger.error("Error occured while hiding a keyboard"); + } + } + + /** + * Press screen by coordinates + * + * @param driver - automation driver + * @param x - x coordinate press + * @param y - y coordinate press + * @param waitOption - wait action option value in millis + */ + public static void pressByCoordinates( + @NonNull final AppiumDriver driver, final int x, final int y, final long waitOption) { + logger.info("Pressing natively by coordinates: " + x + " " + y); + + PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger"); + Sequence press = new Sequence(finger, 1); + + press.addAction( + finger.createPointerMove( + Duration.ofMillis(0), PointerInput.Origin.viewport(), x, y)); // Move to coordinates + press.addAction(finger.createPointerDown(0)); // Press down + if (waitOption > 0) { // Add wait if specified + press.addAction( + finger.createPointerMove( + Duration.ofMillis(waitOption), PointerInput.Origin.viewport(), x, y)); // Hold + } + press.addAction(finger.createPointerUp(0)); // Release + + driver.perform(List.of(press)); + } + + /** + * Refresh page source (for mobile UIs with big page layout xml) + * + * @param driver - automation driver + * @param waitBeforePageSourceRefresh - wait before page source refresh in millis + * @param waitAfterPageSourceRefresh - wait after page source refresh in millis + */ + public static void refreshPageSource( + @NonNull final WebDriver driver, + final int waitBeforePageSourceRefresh, + final int waitAfterPageSourceRefresh) { + logger.info("Refresh page source"); + ThreadHelper.sleep(waitBeforePageSourceRefresh); + getMobileDriver(driver).getPageSource(); + ThreadHelper.sleep(waitAfterPageSourceRefresh); + } + + /** + * Get mobile appium driver + * + * @param driver - automation driver + * @return casted mobile driver or exception if driver casting to mobile one is not supported + */ + public static AppiumDriver getMobileDriver(@NonNull final WebDriver driver) { + if (driver instanceof AppiumDriver) { + return (AppiumDriver) driver; + } else { + throw new IllegalStateException("No mobile driver found"); + } + } + + /** + * Get App Bundle Identifier for android device + * + * @param driver The StartsActivity driver instance. + * @return The app bundle identifier. + */ + public static String getBundleIdentifierAndroid(@NonNull final StartsActivity driver) { + String bundleId = driver.getCurrentPackage(); + logger.info(String.format("App bundle ID is [%s]", bundleId)); + return bundleId; + } + + /** + * Get App Bundle Identifier iOS + * + * @param driver The RemoteWebDriver instance. + * @return The app bundle identifier. + */ + public static String getBundleIdentifierIos(@NonNull final RemoteWebDriver driver) { + String bundleId = + getMobileDriver(driver).getCapabilities().getCapability(CF_BUNDLE_IDENTIFIER).toString(); + logger.info("App bundle ID is [{}]", bundleId); + return bundleId; + } + + /** + * Get App Bundle Identifier + * + * @param driver The RemoteWebDriver instance. + * @return The app bundle identifier. + */ + public static String getBundleIdentifier(@NonNull final RemoteWebDriver driver) { + return driver instanceof IOSDriver + ? getBundleIdentifierIos(driver) + : getBundleIdentifierAndroid((StartsActivity) driver); + } + + /** + * Move App In Background + * + * @param driver The InteractsWithApps driver instance. + */ + public static void moveAppToBackground(@NonNull final InteractsWithApps driver) { + logger.info("Move App To Background"); + driver.runAppInBackground(Duration.ofSeconds(-1)); + } + + /** + * Activate App using bundleId and Return to AUT + * + * @param driver The InteractsWithApps driver instance. + * @param bundleId The Bundle ID of the app to activate. + */ + public static void activateApp( + @NonNull final InteractsWithApps driver, @NonNull final String bundleId) { + logger.info("Return to AUT."); + driver.activateApp(bundleId); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/deeplinks/MobileDeepLinksUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/deeplinks/MobileDeepLinksUtils.java new file mode 100644 index 0000000..1b8bd1c --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/deeplinks/MobileDeepLinksUtils.java @@ -0,0 +1,120 @@ +/* + * + * 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.mobile.deeplinks; + +import com.applause.auto.helpers.mobile.MobileUtils; +import com.applause.auto.helpers.util.ThreadHelper; +import io.appium.java_client.AppiumBy; +import io.appium.java_client.AppiumDriver; +import io.appium.java_client.HidesKeyboard; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import lombok.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.By; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.FluentWait; + +/** Provides some common mobile deeplinks navigation methods */ +public final class MobileDeepLinksUtils { + + private static final Logger logger = LogManager.getLogger(MobileDeepLinksUtils.class); + + private MobileDeepLinksUtils() { + // utility class + } + + /** + * Open deeplink on Android + * + * @param driver - mobile driver + * @param androidDeepLink - Andrdoid deeplink DTO object + */ + public static void openDeepLinkOnAndroid( + @NonNull final AppiumDriver driver, + @NonNull final NativeMobileAppCommonDeeplink.AndroidDeepLink androidDeepLink) { + logger.info("Opening a Android deeplink: " + androidDeepLink.deepLinkUrl()); + Map parameters = new HashMap<>(); + parameters.put("url", androidDeepLink.deepLinkUrl()); + parameters.put("package", androidDeepLink.deepLinkPackage()); + driver.executeScript("mobile:deepLink", parameters); + // another possible approach + // getMobileDriver().get(androidDeepLink.getDeepLinkUrl()); + } + + /** + * Open deeplink on iOS + * + * @param driver - mobile driver + * @param iosDeepLink - iOS deeplink DTO object + * @param waitForSafariURLBarTimeoutInSec - wait for Safari URL bar to appear during opening a + * deeplink + * @param waitForSafariURLBarPollingInSec - polling timeout for Safari URL bar to appear during + * opening a deeplink + * @param the driver + */ + public static void openDeepLinkOniOS( + @NonNull final T driver, + @NonNull final NativeMobileAppCommonDeeplink.IOSDeepLink iosDeepLink, + final int waitForSafariURLBarTimeoutInSec, + final int waitForSafariURLBarPollingInSec) { + logger.info("Launch Safari and enter the deep link in the address bar"); + Map parameters = new HashMap<>(); + parameters.put("bundleId", "com.apple.mobilesafari"); + driver.executeScript("mobile:launchApp", parameters); + + By urlButtonSelector = + AppiumBy.iOSNsPredicateString("type == 'XCUIElementTypeButton' && name CONTAINS 'URL'"); + + // Wait for the url button to appear and click on it so the text field will appear + // iOS 13 now has the keyboard open by default because the URL field has focus when opening + // the Safari browser + final var wait = + new FluentWait<>(driver) + .withTimeout(Duration.ofSeconds(waitForSafariURLBarTimeoutInSec)) + .pollingEvery(Duration.ofSeconds(waitForSafariURLBarPollingInSec)) + .ignoring(Exception.class); + + wait.until(ExpectedConditions.presenceOfElementLocated(urlButtonSelector)); + + MobileUtils.hideKeyboard(driver); + + driver.findElement(urlButtonSelector).click(); + // URL bar is 'jumping' here on the left side, custom wait + ThreadHelper.sleep(1000); + + By urlFieldSelector = + AppiumBy.iOSNsPredicateString("type == 'XCUIElementTypeTextField' && name CONTAINS 'URL'"); + driver.findElement(urlFieldSelector).sendKeys(iosDeepLink.deepLinkUrl()); + By goSelector = + AppiumBy.iOSNsPredicateString("type == 'XCUIElementTypeButton' && label CONTAINS 'go'"); + try { + wait.until(ExpectedConditions.presenceOfElementLocated(goSelector)); + driver.findElement(goSelector).click(); + } catch (Exception e) { + logger.error("iOS deeplink navigation confirmation button click issue"); + } + By openSelector = + AppiumBy.iOSNsPredicateString( + "type == 'XCUIElementTypeButton' && (name CONTAINS 'Open' OR name == 'Go')"); + wait.until(ExpectedConditions.presenceOfElementLocated(openSelector)); + driver.findElement(openSelector).click(); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/deeplinks/NativeMobileAppCommonDeeplink.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/deeplinks/NativeMobileAppCommonDeeplink.java new file mode 100644 index 0000000..b8739b6 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/deeplinks/NativeMobileAppCommonDeeplink.java @@ -0,0 +1,44 @@ +/* + * + * 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.mobile.deeplinks; + +/** + * Deeplink DTO to keep deeplink data for mobile deeplink navigation. + * + * @param androidDeepLink The Android deep link information. + * @param iOSDeepLink The iOS deep link information. + */ +public record NativeMobileAppCommonDeeplink( + AndroidDeepLink androidDeepLink, IOSDeepLink iOSDeepLink) { + + /** + * Android deep link record. + * + * @param deepLinkUrl The deep link URL. + * @param deepLinkPackage The deep link package. + */ + public record AndroidDeepLink(String deepLinkUrl, String deepLinkPackage) {} + + /** + * iOS deep link record. + * + * @param deepLinkUrl The deep link URL. + */ + @SuppressWarnings("checkstyle:TypeName") + public record IOSDeepLink(String deepLinkUrl) {} +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/fileuploading/saucelabs/FileUploadingHelper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/fileuploading/saucelabs/FileUploadingHelper.java new file mode 100644 index 0000000..9aa9e69 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/mobile/fileuploading/saucelabs/FileUploadingHelper.java @@ -0,0 +1,171 @@ +/* + * + * 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.mobile.fileuploading.saucelabs; + +import com.applause.auto.helpers.mobile.MobileUtils; +import com.applause.auto.helpers.util.ThreadHelper; +import com.google.common.collect.ImmutableMap; +import io.appium.java_client.AppiumDriver; +import io.appium.java_client.InteractsWithApps; +import io.appium.java_client.android.AndroidDriver; +import io.restassured.internal.util.IOUtils; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; + +/** The type File Uploading Helper. */ +public final class FileUploadingHelper { + + private static final Logger logger = LogManager.getLogger(FileUploadingHelper.class); + + public static final String DEVICE_IMAGES_LOCATION_ANDROID = "sdcard/Download/%s"; + + private FileUploadingHelper() { + // utility class + } + + /** + * Upload Images to the device. + * + * @param driver The AppiumDriver instance. + * @param imagesPath the image file path. + * @param timeOuts The timeout in milliseconds. + * @param The type of AppiumDriver, which must extend both AppiumDriver and InteractsWithApps. + * @throws RuntimeException If an exception occurs during file upload. + */ + public static void uploadImages( + @NonNull final T driver, @NonNull final String imagesPath, final long timeOuts) { + String bundleIdentifier = MobileUtils.getBundleIdentifier(driver); + MobileUtils.moveAppToBackground(driver); + /* + * we have static wait because there is no way to check if the app was moved to the Background. + * Sometimes, especially in the cloud, I found that image uploading started too early, and they + * were uploaded but the app didn't see them + */ + ThreadHelper.sleep(5000); + getFilesFromPaths(imagesPath) + .forEach( + image -> { + try { + uploadFile(driver, image); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + ThreadHelper.sleep(timeOuts); + MobileUtils.activateApp(driver, bundleIdentifier); + } + + /** + * Upload one file. + * + * @param driver the WebDriver instance to use. + * @param fileName the file to upload. + * @throws RuntimeException if an error occurs during file upload. + */ + public static void uploadFile(@NonNull final WebDriver driver, @NonNull final File fileName) { + try { + logger.info("Uploading [{}] file", fileName); + ((AndroidDriver) MobileUtils.getMobileDriver(driver)) + .pushFile(String.format(DEVICE_IMAGES_LOCATION_ANDROID, fileName.getName()), fileName); + } catch (Exception e) { + logger.error(e); + throw new RuntimeException(e); // Re-throw the exception after logging it. + } + } + + /** + * Get Files From Path + * + * @param pathToFolder the folder path + * @return A Set of Files found in the given path. Returns an empty set if an IOException occurs. + */ + private static Set getFilesFromPaths(@NonNull final String pathToFolder) { + try (Stream stringStream = + Files.walk(Paths.get(pathToFolder)).filter(Files::isRegularFile)) { + List pathList = stringStream.toList(); + return pathList.stream().map(path -> new File(path.toString())).collect(Collectors.toSet()); + } catch (IOException e) { + logger.info(e); + } + return Collections.emptySet(); + } + + /** + * Clears the Measurement images folder on the device. + * + *

This method clears the folder located at "sdcard/greendot/hijacking/strip". It uses an ADB + * shell command to remove all files and subdirectories within the specified folder. + * + * @param driver The WebDriver instance used to interact with the device. Must not be null. = + */ + @SuppressWarnings("PMD.DoNotHardCodeSDCard") + public static void clearImagesFolder(@NonNull final WebDriver driver) { + logger.info( + "Clearing Mocked Measurement images folder on device [sdcard/greendot/hijacking/strip]"); + try { + List removePicsArgs = Arrays.asList("-rf", "/sdcard/greendot/hijacking/strip/*.*"); + Map removePicsCmd = ImmutableMap.of("command", "rm", "args", removePicsArgs); + ((AndroidDriver) MobileUtils.getMobileDriver(driver)) + .executeScript("mobile: shell", removePicsCmd); + } catch (Exception e) { + logger.info(e.getMessage()); + logger.info( + "\n\nTo run any shell commands, we need to set the 'relaxed security' flag. Please start your Appium server with [appium --relaxed-security]\n"); + } + } + + /** + * Camera image injection or camera mocking Inject any image and then use device Camera with that + * image in front + * + *

Need to use this caps to Enable image-injection on RDC -> + * desiredCapabilities.setCapability("sauceLabsImageInjectionEnabled", true); + * + *

Link - ... + * + * @param driver the webdriver + * @param fileLocation file path + */ + public static void injectImageToSauce( + @NonNull final WebDriver driver, @NonNull final String fileLocation) { + try { + logger.info("Injecting [{}] file to Sauce device.", fileLocation); + FileInputStream in = new FileInputStream(fileLocation); + String qrCodeImage = Base64.getEncoder().encodeToString(IOUtils.toByteArray(in)); + + // Provide the transformed image to the device + ((JavascriptExecutor) driver).executeScript("sauce:inject-image=" + qrCodeImage); + } catch (Exception e) { + logger.error(e); + throw new RuntimeException("Image injection error.", e); + } + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/sync/all/AllMatchCondition.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/sync/all/AllMatchCondition.java index 8e35367..7606dfc 100644 --- a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/sync/all/AllMatchCondition.java +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/sync/all/AllMatchCondition.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.Optional; import java.util.function.Function; -import lombok.RequiredArgsConstructor; import lombok.Setter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -39,7 +38,6 @@ * @param a type extending UIElement, a List of which serves as both the input and output types * of this Condition */ -@RequiredArgsConstructor public class AllMatchCondition implements Condition, List> { private static final Logger logger = LogManager.getLogger(); @@ -49,6 +47,20 @@ public class AllMatchCondition implements Condition @Setter private Duration timeout; @Setter private Duration pollingInterval; + /** + * Constructor for AllMatchCondition. + * + * @param elements The list of elements to check. + * @param function The function to apply to each element. + * @param description The description of the condition. + */ + public AllMatchCondition( + final List elements, final Function function, final String description) { + this.elements = elements; + this.function = function; + this.description = description; + } + /** * Run the Condition against the whole List. * diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/sync/all/AllMatchConditionBuilder.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/sync/all/AllMatchConditionBuilder.java index eaddaa1..5608b5f 100644 --- a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/sync/all/AllMatchConditionBuilder.java +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/sync/all/AllMatchConditionBuilder.java @@ -22,7 +22,6 @@ import com.applause.auto.pageobjectmodel.base.UIElement; import java.util.List; import java.util.function.Function; -import lombok.AllArgsConstructor; import lombok.NonNull; /** @@ -32,11 +31,19 @@ * @param a type extending UIElement, a List of which serves as both the input and output types * of this Condition */ -@AllArgsConstructor public class AllMatchConditionBuilder implements ConditionBuilder, List> { private final List elements; + /** + * Constructor for AllMatchConditionBuilder. + * + * @param elements The list of UI elements to evaluate. + */ + public AllMatchConditionBuilder(final List elements) { + this.elements = elements; + } + /** * Check if the List items meet a custom condition. * diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/testdata/TestDataProvider.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/testdata/TestDataProvider.java new file mode 100644 index 0000000..9dfbf8a --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/testdata/TestDataProvider.java @@ -0,0 +1,41 @@ +/* + * + * 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.testdata; + +import java.util.Collection; + +public interface TestDataProvider { + + /** + * read single value from test data file + * + * @param dataPathSyntax file path + * @param test data type + * @return test data + */ + T readSingleValueFromTestDataFile(String dataPathSyntax); + + /** + * read collection of values from test data file + * + * @param dataPathSyntax file path + * @param test data type + * @return list of test data + */ + > C readValuesFromTestDataFile(String dataPathSyntax); +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/testdata/yaml/YamlTestDataProvider.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/testdata/yaml/YamlTestDataProvider.java new file mode 100644 index 0000000..ec38061 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/testdata/yaml/YamlTestDataProvider.java @@ -0,0 +1,55 @@ +/* + * + * 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.testdata.yaml; + +import com.applause.auto.helpers.testdata.TestDataProvider; +import io.github.yamlpath.YamlExpressionParser; +import io.github.yamlpath.YamlPath; +import java.io.FileInputStream; +import java.util.Collection; +import lombok.Getter; +import lombok.NonNull; +import lombok.SneakyThrows; + +/** Yaml test data reader based on ... */ +public class YamlTestDataProvider implements TestDataProvider { + + @Getter private final String yamlTestDataFilePath; + + private final YamlExpressionParser yamlExpressionParser; + + @SneakyThrows + public YamlTestDataProvider(@NonNull final String yamlTestDataFilePath) { + this.yamlTestDataFilePath = yamlTestDataFilePath; + try (var file = new FileInputStream(yamlTestDataFilePath)) { + this.yamlExpressionParser = YamlPath.from(file); + } + } + + @Override + public T readSingleValueFromTestDataFile(@NonNull final String yamlPathSyntax) { + return yamlExpressionParser.readSingle(yamlPathSyntax); + } + + @SuppressWarnings("unchecked") + @Override + public > C readValuesFromTestDataFile( + @NonNull final String dataPathSyntax) { + return (C) yamlExpressionParser.read(dataPathSyntax); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/AwaitilityWaitUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/AwaitilityWaitUtils.java new file mode 100644 index 0000000..783e314 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/AwaitilityWaitUtils.java @@ -0,0 +1,80 @@ +/* + * + * 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.util; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import lombok.NonNull; +import org.awaitility.Awaitility; + +/** Awaitility Wait Utils */ +public final class AwaitilityWaitUtils { + + private AwaitilityWaitUtils() { + // utility class + } + + /** + * Wait for callable state with predicate and return it's value + * + * @param - callable actions type parameter + * @param callable - actions to call during wait + * @param predicate - predicate to match for actions that will be called during wait + * @param waitInterval - wait interval in seconds + * @param pollingInterval - polling timeout for wait interval in seconds + * @param alias - text alias for this wait + * @return T - object instance that will be returned from callable actions during wait + */ + public static T waitForCondition( + @NonNull final Callable callable, + @NonNull final Predicate predicate, + final int waitInterval, + final int pollingInterval, + final String alias) { + return Awaitility.with() + .pollInterval(pollingInterval, TimeUnit.SECONDS) + .pollInSameThread() + .atMost(waitInterval, TimeUnit.SECONDS) + .ignoreExceptions() + .alias(alias) + .until(callable, predicate); + } + + /** + * Wait for callable state with predicate and return it's value + * + * @param callable - boolean callable actions state that will be checked during wait + * @param waitInterval - wait interval in seconds + * @param pollingInterval - polling timeout for wait interval in seconds + * @param alias - text alias for this wait + */ + public static void waitForCondition( + @NonNull final Callable callable, + int waitInterval, + int pollingInterval, + final String alias) { + Awaitility.with() + .pollInterval(pollingInterval, TimeUnit.SECONDS) + .pollInSameThread() + .atMost(waitInterval, TimeUnit.SECONDS) + .ignoreExceptions() + .alias(alias) + .until(callable); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/GenericObjectMapper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/GenericObjectMapper.java new file mode 100644 index 0000000..4407ee9 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/GenericObjectMapper.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.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; + +public class GenericObjectMapper { + @Getter private static final ObjectMapper objectMapper = new ObjectMapper(); +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/RandomUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/RandomUtils.java new file mode 100644 index 0000000..8b02fb1 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/RandomUtils.java @@ -0,0 +1,77 @@ +/* + * + * 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.util; + +import com.github.javafaker.Faker; +import java.util.Locale; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Random dat utils */ +public final class RandomUtils { + + private static final Logger logger = LogManager.getLogger(RandomUtils.class); + + private RandomUtils() { + // utility class + } + + /** + * Get Faker object for further fake data generation * / + * + * @return Faker - faker object from Faker library ... + */ + public static Faker getFaker() { + return new Faker(); + } + + /** + * Get random password + * + * @param upperCaseSymbolsCount - count of upper case symbols + * @param lowCaseSymbolsCount - count of lower case symbols + * @param numericSymbolsCount - numeric symbols count + * @param withSpecialCharacter - should include special character or not + * @return generated password + */ + public static String getRandomValidUserAccountPassword( + final int upperCaseSymbolsCount, + final int lowCaseSymbolsCount, + final int numericSymbolsCount, + final boolean withSpecialCharacter) { + StringBuilder randomPasswordStringBuilder = + new StringBuilder() + .append( + RandomStringUtils.secure() + .nextAlphabetic(upperCaseSymbolsCount) + .toUpperCase(Locale.ENGLISH)) + .append( + RandomStringUtils.secure() + .nextAlphabetic(lowCaseSymbolsCount) + .toLowerCase(Locale.ENGLISH)) + .append(RandomStringUtils.secure().nextNumeric(numericSymbolsCount)); + if (withSpecialCharacter) { + randomPasswordStringBuilder.append('$'); + } + String randomPassword = randomPasswordStringBuilder.toString(); + logger.info("Newly generated random password is: {}", randomPassword); + return randomPassword; + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/ThreadHelper.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/ThreadHelper.java new file mode 100644 index 0000000..9da39eb --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/ThreadHelper.java @@ -0,0 +1,41 @@ +/* + * + * 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.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Utility helper class that provides common actions needed to control the current Thread. */ +public final class ThreadHelper { + private static final Logger logger = LogManager.getLogger(ThreadHelper.class); + + private ThreadHelper() {} + + /** + * Suspends the current thread for a specified period of milliseconds. + * + * @param milliseconds A long value that represents the milliseconds. + */ + public static void sleep(long milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + logger.error(e); + } + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/XrayRequestHeaders.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/XrayRequestHeaders.java new file mode 100644 index 0000000..eb16a6c --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/util/XrayRequestHeaders.java @@ -0,0 +1,46 @@ +/* + * + * 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.util; + +import io.restassured.http.Header; + +public final class XrayRequestHeaders { + private XrayRequestHeaders() { + // utility class + } + + public static Header getAcceptApplicationJsonHeader() { + return new Header("Accept", "application/json"); + } + + public static Header getContentTypeMultipartFormDataHeader() { + return new Header("Content-Type", "multipart/form-data"); + } + + public static Header getBearerAuthorizationHeader(final String token) { + return new Header("Authorization", "Bearer " + token); + } + + public static Header getJSONContentTypeHeader() { + return new Header("Content-Type", "application/json; charset=utf-8"); + } + + public static Header getAtlassianNoCheckHeader() { + return new Header("X-Atlassian-Token", "no-check"); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/HtmlUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/HtmlUtils.java new file mode 100644 index 0000000..3b2e8d7 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/HtmlUtils.java @@ -0,0 +1,71 @@ +/* + * + * 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.web; + +import com.applause.auto.helpers.util.AwaitilityWaitUtils; +import java.util.List; +import lombok.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.openqa.selenium.WebDriver; +import us.codecraft.xsoup.Xsoup; + +/** Common methods for HTML-based content. */ +public final class HtmlUtils { + + private static final Logger logger = LogManager.getLogger(HtmlUtils.class); + + private HtmlUtils() { + // utility class + } + + /** + * Wait for page viewport to be not empty + * + * @param driver - automation driver + * @param xpathRootLocator - root Xpath locator for viewport DOM element + * @param waitInterval - wait interval to wait for viewport + * @param pollingInterval - polling interval for wait of viewport + */ + public static void waitForPageViewPortNotEmpty( + @NonNull final WebDriver driver, + @NonNull final String xpathRootLocator, + final int waitInterval, + final int pollingInterval) { + AwaitilityWaitUtils.waitForCondition( + () -> { + String currentPageSource = driver.getPageSource(); + boolean gsdHtmlInViewportLoaded = + wasHtmlInViewportLoadedByRootElementXpathLocator(currentPageSource, xpathRootLocator); + logger.info("Page viewport was loaded correctly: {}", gsdHtmlInViewportLoaded); + return gsdHtmlInViewportLoaded; + }, + waitInterval, + pollingInterval, + "Wait for html viewport to be loaded"); + } + + private static boolean wasHtmlInViewportLoadedByRootElementXpathLocator( + @NonNull final String currentHtml, @NonNull final String xpathRootLocator) { + Document doc = Jsoup.parse(currentHtml); + List list = Xsoup.compile(xpathRootLocator).evaluate(doc).list(); + return !list.isEmpty(); + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/WebElementUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/WebElementUtils.java new file mode 100644 index 0000000..1e151a2 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/WebElementUtils.java @@ -0,0 +1,384 @@ +/* + * + * 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.web; + +import com.applause.auto.helpers.util.ThreadHelper; +import io.appium.java_client.AppiumDriver; +import io.appium.java_client.android.AndroidDriver; +import io.appium.java_client.ios.IOSDriver; +import java.time.Duration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.*; +import org.openqa.selenium.interactions.PointerInput; +import org.openqa.selenium.interactions.Sequence; + +/** Common utils for web elements */ +public final class WebElementUtils { + + private static final Logger logger = LogManager.getLogger(WebElementUtils.class); + + private WebElementUtils() { + // utility class + } + + /** + * Get element Y offset + * + * @param driver - automation driver + * @param element - web element + * @return element Y offset value + */ + public static int getElementYOffset( + @NonNull final WebDriver driver, @NonNull final WebElement element) { + int offsetY = + Integer.parseInt( + ((JavascriptExecutor) driver) + .executeScript("return arguments[0].offsetTop", element) + .toString()); + logger.info("Current element{} offset y = {}", element, offsetY); + return offsetY; + } + + /** + * Get element text content with js + * + * @param driver - automation driver + * @param webElementWithTextContent - web element + * @return element text content + */ + public static String getElementTextContentWithJS( + @NonNull final WebDriver driver, @NonNull final WebElement webElementWithTextContent) { + JavascriptExecutor js = (JavascriptExecutor) driver; + try { + String textContent = + (String) js.executeScript("return arguments[0].textContent", webElementWithTextContent); + logger.info("Text content is: {}", textContent); + return textContent; + } catch (Exception e) { + return StringUtils.EMPTY; + } + } + + /** + * Custom sendkeys by symbol with delay + * + * @param textBoxElement - web element + * @param text - text to input + * @param waitBetweenEachInput - wait between input in millis + */ + public static void customSendKeysBySymbolWithDelay( + @NonNull final WebElement textBoxElement, + @NonNull final String text, + final int waitBetweenEachInput) { + for (int symbolIndex = 0; symbolIndex < text.length(); symbolIndex++) { + textBoxElement.sendKeys(Character.toString(text.charAt(symbolIndex))); + ThreadHelper.sleep(waitBetweenEachInput); + } + } + + /** + * Clear input field value with backspace with delay + * + * @param inputWebElement - web element + * @param waitBetweenEachDelete - wait between each backspace deletion + */ + public static void clearFieldValueWithBackspaceWithDelay( + @NonNull final WebElement inputWebElement, final int waitBetweenEachDelete) { + int valueCharacters = inputWebElement.getAttribute("value").length(); + for (int i = 0; i < valueCharacters + 1; i++) { + inputWebElement.sendKeys(Keys.BACK_SPACE); + ThreadHelper.sleep(waitBetweenEachDelete); + } + } + + /** + * Check whether mobile web execution is being performed + * + * @param driver - automation driver + * @return boolean state whether this kind of driver is mobile one of not + */ + public static boolean isMobileWebExecutionDriver(@NonNull final WebDriver driver) { + return driver instanceof AppiumDriver; + } + + /** + * Clicks an element at an accurate point on devices, with native tap. This method is to mitigate + * issues where the different device sizes cause the element locations to differ. + * + * @param driver The WebDriver instance to use. + * @param element The web element to click. + * @param xOffset The x-offset from the element's center to click. + * @param yOffset The y-offset from the element's center to click. + * @param isTablet A boolean indicating whether the device is a tablet. This is used in + * calculating the accurate Y coordinate. + */ + public static void clickElementWithNativeTapWithOffset( + @NonNull final AppiumDriver driver, // Use AppiumDriver + @NonNull final WebElement element, + final int xOffset, + final int yOffset, + final boolean isTablet) { + + int x = getAccuratePointX(driver, element) + xOffset; + int y = getAccuratePointY(driver, element, isTablet) + yOffset; + logger.info("Clicking element with native tap at ({}, {}).", x, y); + + PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger"); + Sequence tap = new Sequence(finger, 1); + + tap.addAction( + finger.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), x, y)); + tap.addAction(finger.createPointerDown(0)); + tap.addAction(finger.createPointerUp(0)); + + driver.perform(List.of(tap)); + } + + /** + * Gets the accurate X coordinate of the center of a web element, accounting for window scaling. + * This method calculates the element's position and width, then adjusts for any difference + * between the browser's reported window size and the actual rendered viewport width. + * + * @param driver The WebDriver instance used to interact with the browser. Must not be null. + * @param element The WebElement whose X coordinate is to be retrieved. Must not be null. + * @return The accurate X coordinate of the center of the element, adjusted for window scaling. + * Returns an integer representing the pixel value. + * @throws NullPointerException If either the driver or the element is null. + */ + public static int getAccuratePointX( + @NonNull final WebDriver driver, @NonNull final WebElement element) { + double widthRatio = + (double) driver.manage().window().getSize().width / getJavascriptWindowWidth(driver); + return (int) (getLocation(driver, element).getX() * widthRatio) + + (element.getSize().getWidth() / 2); + } + + /** + * Retrieves the width of the browser window as reported by JavaScript. + * + * @param driver The WebDriver instance to use for executing JavaScript. Must not be null. + * @return The width of the browser window in pixels. + * @throws NullPointerException If the provided WebDriver is null. + */ + public static int getJavascriptWindowWidth(@NonNull final WebDriver driver) { + int windowWidth = + ((Long) + ((JavascriptExecutor) driver) + .executeScript("return window.innerWidth || document.body.clientWidth")) + .intValue(); + logger.info("Current window width is: {}", windowWidth); + return windowWidth; + } + + /** + * Gets the element location on the screen using JS. This method is currently used as a workaround + * as the default getLocation is not working in W3C mode. + * + * @param driver The WebDriver instance. + * @param element The element to retrieve the location from. + * @return A point representing the location of the top-left corner of the element. + */ + public static Point getLocation( + @NonNull final WebDriver driver, @NonNull final WebElement element) { + return getElementRect(driver, element).getPoint(); + } + + /** + * Gets element rect. + * + * @param driver The WebDriver instance. + * @param element The WebElement to get the rectangle of. + * @return The Rectangle representing the element's dimensions and position. + */ + public static Rectangle getElementRect( + @NonNull final WebDriver driver, @NonNull final WebElement element) { + Map result = + (Map) + ((JavascriptExecutor) driver) + .executeScript("return arguments[0].getBoundingClientRect()", element); + logger.info(result.toString()); + return new Rectangle( + (int) Double.parseDouble(result.get("x").toString()), + (int) Double.parseDouble(result.get("y").toString()), + (int) Double.parseDouble(result.get("height").toString()), + (int) Double.parseDouble(result.get("width").toString())); + } + + /** + * Gets accurate Y point for element. This method calculates the accurate Y coordinate of a web + * element, taking into account differences between Appium and Selenium's reported window heights, + * and whether the device is a tablet or not. + * + * @param driver The WebDriver instance used to interact with the browser. + * @param element The WebElement for which to calculate the Y coordinate. + * @param isTablet A boolean indicating whether the device is a tablet (true) or a phone (false). + * @return The accurate Y coordinate of the element. + */ + public static int getAccuratePointY( + @NonNull final WebDriver driver, @NonNull final WebElement element, final boolean isTablet) { + int windowDiff = + isTablet + ? getWindowHeightDiffBetweenAppiumAndSelenium(driver) + : (int) (getWindowHeightDiffBetweenAppiumAndSelenium(driver) / 1.5); + int seleniumWindowHeight = driver.manage().window().getSize().getHeight(); + double heightRatio = (double) seleniumWindowHeight / getJavascriptWindowHeight(driver); + return (int) ((heightRatio * getLocation(driver, element).getY()) + windowDiff) + + element.getSize().getHeight() / 2; + } + + /** + * Calculates the difference in window height between what Appium reports and what Selenium + * reports. This is often necessary due to differences in how Appium and Selenium handle window + * sizes, especially on mobile devices. + * + * @param driver The WebDriver instance to use for retrieving window dimensions. Must not be null. + * @return The difference in window height (Appium height - Selenium height). A positive value + * indicates that Appium reports a larger height than Selenium. A negative value indicates the + * opposite. + * @throws NullPointerException If the provided {@code driver} is {@code null}. + */ + public static int getWindowHeightDiffBetweenAppiumAndSelenium(@NonNull final WebDriver driver) { + int seleniumWindowHeight = driver.manage().window().getSize().getHeight(); + int appiumHeight = getAppiumWindowHeight(driver); + return appiumHeight - seleniumWindowHeight; + } + + /** + * Returns the JavaScript window height. + * + * @param driver The WebDriver instance to use for executing JavaScript. + * @return The height of the JavaScript window in pixels. + */ + public static int getJavascriptWindowHeight(@NonNull final WebDriver driver) { + return ((Long) + ((JavascriptExecutor) driver) + .executeScript("return window.innerHeight || document.body.clientHeight")) + .intValue(); + } + + /** + * Retrieves the height of the Appium window. + * + * @param driver The WebDriver instance. + * @return The height of the Appium window in pixels. + */ + public static int getAppiumWindowHeight(@NonNull final WebDriver driver) { + String currentContext = getContext(driver); + changeContext(driver, "NATIVE_APP"); + int appiumHeight = + isAndroid(driver) + ? getAndroidDriver(driver).manage().window().getSize().getHeight() + : getIOSDriver(driver).manage().window().getSize().getHeight(); + changeContext(driver, currentContext); + return appiumHeight; + } + + /** + * Switches the WebDriver's context to the desired context. + * + * @param driver The WebDriver instance. + * @param desiredContext The desired context to switch to. + * @return {@code true} if the context was successfully switched, {@code false} otherwise. + */ + public static boolean changeContext( + @NonNull final WebDriver driver, @NonNull final String desiredContext) { + return isAndroid(driver) + ? changeContextAndroid(driver, desiredContext) + : changeContextIos(driver, desiredContext); + } + + private static boolean changeContextAndroid( + @NonNull final WebDriver driver, @NonNull final String desiredContext) { + try { + Set contextNames = getAndroidDriver(driver).getContextHandles(); + Iterator contextNameIterator = contextNames.iterator(); + + String contextName; + do { + if (!contextNameIterator.hasNext()) { + return false; + } + + contextName = contextNameIterator.next(); + } while (!contextName.contains(desiredContext)); + + logger.debug("Switching to context [{}].", contextName); + getAndroidDriver(driver).context(contextName); + return true; + } catch (Exception var6) { + logger.error("Unable to switch to context [{}].", desiredContext, var6); + return false; + } + } + + private static boolean changeContextIos( + @NonNull final WebDriver driver, @NonNull final String desiredContext) { + try { + Set contextNames = getIOSDriver(driver).getContextHandles(); + Iterator contextNameIterator = contextNames.iterator(); + + String contextName; + do { + if (!contextNameIterator.hasNext()) { + return false; + } + + contextName = contextNameIterator.next(); + } while (!contextName.contains(desiredContext)); + + logger.debug("Switching to context [{}].", contextName); + getIOSDriver(driver).context(contextName); + return true; + } catch (Exception var6) { + logger.error("Unable to switch to context [{}].", desiredContext, var6); + return false; + } + } + + /** + * Retrieves the current context of the WebDriver. + * + * @param driver The WebDriver instance. + * @return The current context as a String. + * @throws NullPointerException If the driver is null. + */ + public static String getContext(@NonNull final WebDriver driver) { + return isAndroid(driver) + ? getAndroidDriver(driver).getContext() + : getIOSDriver(driver).getContext(); + } + + private static boolean isAndroid(@NonNull final WebDriver driver) { + return driver instanceof AndroidDriver; + } + + private static AndroidDriver getAndroidDriver(@NonNull final WebDriver driver) { + return (AndroidDriver) driver; + } + + private static IOSDriver getIOSDriver(@NonNull final WebDriver driver) { + return (IOSDriver) driver; + } +} diff --git a/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/WebUtils.java b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/WebUtils.java new file mode 100644 index 0000000..26ba9a5 --- /dev/null +++ b/auto-sdk-java-helpers/src/main/java/com/applause/auto/helpers/web/WebUtils.java @@ -0,0 +1,52 @@ +/* + * + * 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.web; + +import lombok.NonNull; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; + +/** Web common utils */ +public final class WebUtils { + + private WebUtils() { + // utility class + } + + /** + * JS scroll down + * + * @param driver - automation driver + * @param yValue - y value scroll to + */ + public static void jsScrollDown(@NonNull final WebDriver driver, final int yValue) { + ((JavascriptExecutor) driver).executeScript("window.scrollBy(0, " + yValue + ");"); + } + + /** + * Get page Y position + * + * @param driver - automation driver + * @return Y page position value + */ + public static int getPagePositionY(@NonNull final WebDriver driver) { + return (int) + Float.parseFloat( + String.valueOf(((JavascriptExecutor) driver).executeScript("return window.scrollY;"))); + } +} diff --git a/auto-sdk-java-integrations/pom.xml b/auto-sdk-java-integrations/pom.xml index 3f1de43..418db8b 100644 --- a/auto-sdk-java-integrations/pom.xml +++ b/auto-sdk-java-integrations/pom.xml @@ -56,8 +56,12 @@ test - com.sun.mail - jakarta.mail + jakarta.mail + jakarta.mail-api + + + org.eclipse.angus + angus-mail diff --git a/auto-sdk-java-testng/src/main/java/com/applause/auto/testng/TestNgContextUtils.java b/auto-sdk-java-testng/src/main/java/com/applause/auto/testng/TestNgContextUtils.java index 9ac0175..77601d9 100644 --- a/auto-sdk-java-testng/src/main/java/com/applause/auto/testng/TestNgContextUtils.java +++ b/auto-sdk-java-testng/src/main/java/com/applause/auto/testng/TestNgContextUtils.java @@ -80,7 +80,7 @@ public static ContextType getContextType(final @NonNull ITestResult testResult) * @param testResult The TestNG result * @return A Tuple containing the Class and Method used for the result. */ - private static Pair, Method> getUnderlyingClassAndMethod( + public static Pair, Method> getUnderlyingClassAndMethod( final @NonNull ITestResult testResult) { final ITestNGMethod testMethod = testResult.getMethod(); final String methodName = testMethod.getMethodName(); diff --git a/auto-sdk-java-testng/src/main/java/com/applause/auto/testng/listeners/XrayListener.java b/auto-sdk-java-testng/src/main/java/com/applause/auto/testng/listeners/XrayListener.java new file mode 100644 index 0000000..7fcff3f --- /dev/null +++ b/auto-sdk-java-testng/src/main/java/com/applause/auto/testng/listeners/XrayListener.java @@ -0,0 +1,129 @@ +/* + * + * 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.testng.listeners; + +import com.applause.auto.helpers.jira.annotations.scanner.JiraAnnotationsScanner; +import com.applause.auto.helpers.jira.clients.JiraXrayClient; +import com.applause.auto.helpers.jira.dto.requestmappers.XrayAddTo; +import com.applause.auto.helpers.jira.dto.responsemappers.JiraCreateTicketResponse; +import com.applause.auto.helpers.jira.exceptions.JiraAnnotationException; +import com.applause.auto.helpers.jira.exceptions.UnidentifiedExecutionStatusException; +import com.applause.auto.testng.TestNgContextUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import java.lang.reflect.Method; +import java.util.Collections; +import lombok.NonNull; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +public class XrayListener extends TestListenerAdapter { + + private static final Logger logger = LogManager.getLogger(XrayListener.class); + private final JiraXrayClient jiraXrayClient = new JiraXrayClient(); + + @Override + public void onTestFailure(@NonNull final ITestResult result) { + logger.info("Setting status for FAILED test: [{}]", result.getTestName()); + super.onTestFailure(result); + updateJiraXray(result); + } + + @Override + public void onTestSuccess(@NonNull final ITestResult result) { + logger.info("Setting status for PASSED test: [{}]", result.getTestName()); + super.onTestSuccess(result); + updateJiraXray(result); + } + + @Override + public void onTestSkipped(@NonNull final ITestResult result) { + logger.info("Setting status for SKIPPED test: [{}]", result.getTestName()); + super.onTestSkipped(result); + updateJiraXray(result); + } + + /** + * Updates Jira X-Ray with execution results + * + * @param result test result + */ + private void updateJiraXray(@NonNull final ITestResult result) { + try { + int testRunId = addTestToTestExecution(result); + jiraXrayClient.getTestrunAPI().updateTestRun(testRunId, getExecutionStatus(result)); + } catch (JsonProcessingException | JiraAnnotationException e) { + logger.error("Failed to add Test to Test Execution and update its execution status.", e); + } + } + + /** + * Adds test to X-Ray Test Execution based on data passed as @JiraID annotation + * + * @param result test result + * @return testRunId, String value that represents identifier of the test case executed under a + * Test Execution + * @throws JsonProcessingException when response JSON is invalid + */ + private int addTestToTestExecution(@NonNull final ITestResult result) + throws JsonProcessingException, JiraAnnotationException { + ITestContext context = result.getTestContext(); + final Pair, Method> classAndMethod = + TestNgContextUtils.getUnderlyingClassAndMethod(result); + final Method underlyingMethod = classAndMethod.getRight(); + String jiraTestIdentifier = JiraAnnotationsScanner.getJiraIdentifier(underlyingMethod); + // Assuming testExecKey refers to Test Execution key (ticket ID) the current test is using which + // must be created before execution of the test + String testExecutionKey = context.getAttribute("testExecKey").toString(); + JiraCreateTicketResponse jiraCreateTicketResponseMapping = + new JiraCreateTicketResponse(null, testExecutionKey, null); + XrayAddTo xrayAddToMapping = new XrayAddTo(Collections.singletonList(jiraTestIdentifier)); + jiraXrayClient + .getExecutionsAPI() + .addTestToTestExecution(jiraCreateTicketResponseMapping, xrayAddToMapping); + return jiraXrayClient + .getTestrunAPI() + .getTestRunID(jiraCreateTicketResponseMapping, jiraTestIdentifier); + } + + /** + * Gets test execution status based on the ITestResult and maps it to accepted X-Ray values + * + * @param result test result + * @return status + */ + private String getExecutionStatus(@NonNull final ITestResult result) { + String testKey = result.getMethod().getMethodName(); + String status = ""; + logger.info("Getting execution status for test key: {}", testKey); + int statusCode = result.getStatus(); + status = + switch (statusCode) { + case 1 -> "PASS"; + case 2 -> "FAIL"; + case 3 -> "BLOCKED"; + default -> + throw new UnidentifiedExecutionStatusException( + String.format("Unable to determine status for code %s", statusCode)); + }; + return status; + } +} diff --git a/build-tools/checkstyle.xml b/build-tools/checkstyle.xml index 5e2379b..7376f0d 100644 --- a/build-tools/checkstyle.xml +++ b/build-tools/checkstyle.xml @@ -155,10 +155,6 @@ - - - - @@ -189,7 +185,7 @@ - + diff --git a/pom.xml b/pom.xml index 3b39690..4d82e7a 100644 --- a/pom.xml +++ b/pom.xml @@ -65,11 +65,22 @@ 3.3.2 1.0.12 1.18.34 - 2.1.2 + 2.1.3 + 2.0.3 2.3.31 6.0.0 4.4 + 5.5.0 + 4.2.2 + 1.18.3 + 0.3.7 + 1.0.2 + 2.29.1 + 20250107 + 0.0.12 + 2.7.2 + v4-rev612-1.25.0 5.13.0 5.2.0 @@ -228,9 +239,14 @@ ${javax.ws.rs-api.version} - com.sun.mail - jakarta.mail - 2.0.1 + jakarta.mail + jakarta.mail-api + ${jakarta.mail-api.version} + + + org.eclipse.angus + angus-mail + ${jakarta.angus.mail.version} com.github.spotbugs @@ -248,6 +264,56 @@ freemarker ${freemarker.version} + + io.rest-assured + rest-assured + ${rest-assured.version} + + + org.awaitility + awaitility + ${awaitility.version} + + + org.jsoup + jsoup + ${jsoup.version} + + + us.codecraft + xsoup + ${xsoup.version} + + + com.github.javafaker + javafaker + ${faker.version} + + + io.qameta.allure + allure-testng + ${allure.version} + + + org.json + json + ${json.version} + + + io.github.yaml-path + yaml-path + ${yaml-path.version} + + + com.google.api-client + google-api-client + ${google-api-client.version} + + + com.google.apis + google-api-services-sheets + ${google-api-services-sheets.version} +