diff --git a/ssh_shell_command_executor_using_sshj/pom.xml b/ssh_shell_command_executor_using_sshj/pom.xml new file mode 100644 index 00000000..08d3b809 --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/pom.xml @@ -0,0 +1,139 @@ + + + 4.0.0 + com.testsigma.addons + ssh_shell_command_executor_using_sshj + 1.0.1 + jar + + + UTF-8 + 11 + 11 + 1.2.24_cloud + 5.8.0-M1 + 1.0.0 + 3.2.1 + 1.18.30 + + + + + + com.testsigma + testsigma-java-sdk + ${testsigma.sdk.version} + + + org.projectlombok + lombok + ${lombok.version} + true + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + org.testng + testng + 6.14.3 + + + + org.seleniumhq.selenium + selenium-java + 4.33.0 + + + + io.appium + java-client + 9.4.0 + + + com.fasterxml.jackson.core + jackson-annotations + 2.13.0 + + + + + + com.hierynomus + sshj + 0.38.0 + + + org.apache.commons + commons-lang3 + 3.14.0 + + + + com.fasterxml.jackson.core + jackson-databind + 2.12.3 + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + org.apache.poi + poi + 5.2.4 + + + + + ssh_shell_command_executor_using_sshj + + + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven.source.plugin.version} + + + attach-sources + + jar + + + + + + + diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/android/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/android/SSHCommandExecutionSSHJ.java new file mode 100644 index 00000000..6b8933be --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/android/SSHCommandExecutionSSHJ.java @@ -0,0 +1,149 @@ +package com.testsigma.addons.android; + +import com.testsigma.addons.util.ImageComparisonUtils; +import com.testsigma.addons.util.StringToImageConverter; +import com.testsigma.sdk.ApplicationType; +import com.testsigma.sdk.AndroidAction; +import com.testsigma.sdk.Result; +import com.testsigma.sdk.annotation.Action; +import com.testsigma.sdk.annotation.RunTimeData; +import com.testsigma.sdk.annotation.TestData; +import com.testsigma.sdk.annotation.TestStepResult; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.Session.Command; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.openqa.selenium.NoSuchElementException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.concurrent.TimeUnit; + +@Data +@EqualsAndHashCode(callSuper = false) +@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name," + + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ," + + " Command Separator: Command-Separator , Store Output Variable: Variable-Name", + description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). " + + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable.", + applicationType = ApplicationType.ANDROID, + displayName = "SSHJ: Connect to SSH server and execute commands", + useCustomScreenshot = true) +public class SSHCommandExecutionSSHJ extends AndroidAction { + + @TestStepResult + private com.testsigma.sdk.TestStepResult testStepResult; + + @TestData(reference = "Host-Name") + private com.testsigma.sdk.TestData hostName; + @TestData(reference = "Port-Number") + private com.testsigma.sdk.TestData portNumber; + @TestData(reference = "User-Name") + private com.testsigma.sdk.TestData userName; + @TestData(reference = "User-Password") + private com.testsigma.sdk.TestData userPassword; + @TestData(reference = "Terminal-Commands") + private com.testsigma.sdk.TestData commands; + @TestData(reference = "Command-Separator") + private com.testsigma.sdk.TestData commandSeparator; + @TestData(reference = "Variable-Name") + private com.testsigma.sdk.TestData storeVariable; + + @RunTimeData + private com.testsigma.sdk.RunTimeData runTimeData; + + @Override + public Result execute() throws NoSuchElementException { + logger.info("Initiating execution (SSHJ) - Android"); + + String user = userName.getValue().toString(); + String host = hostName.getValue().toString(); + String password = userPassword.getValue().toString(); + int port = Integer.parseInt(portNumber.getValue().toString()); + String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty() + ? commandSeparator.getValue().toString() + : "&&"; + String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&"); + + String wrappedCommand = wrapInLoginShell(allCommands); + + SSHClient ssh = null; + Session session = null; + + try { + ssh = new SSHClient(); + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + ssh.connect(host, port); + ssh.authPassword(user, password); + logger.info("SSH session connected (SSHJ)."); + + session = ssh.startSession(); + Command cmd = session.exec(wrappedCommand); + StringBuilder output = new StringBuilder(); + try (Reader reader = new InputStreamReader(cmd.getInputStream())) { + char[] buf = new char[1024]; + int n; + while ((n = reader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + cmd.join(30, TimeUnit.SECONDS); + if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) { + try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) { + char[] buf = new char[1024]; + int n; + while ((n = errReader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + } + + runTimeData.setKey(storeVariable.getValue().toString()); + runTimeData.setValue(output.toString()); + + File screenshotFile = null; + try { + screenshotFile = StringToImageConverter.convertToFile(output); + String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null; + if (s3Url != null && !s3Url.isEmpty()) { + ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger); + boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath()); + if (!uploadResult) { + logger.debug("Error uploading custom screenshot to S3; step result may not show the output image."); + } else { + logger.debug("Custom screenshot (command output) uploaded successfully."); + } + } + } finally { + if (screenshotFile != null && screenshotFile.exists()) { + screenshotFile.deleteOnExit(); + } + } + + setSuccessMessage("Output is: " + output.toString()); + return Result.SUCCESS; + } catch (Exception e) { + String errorStack = ExceptionUtils.getStackTrace(e); + logger.info("Error occurred while executing the command (SSHJ): " + errorStack); + setErrorMessage("Error occurred while executing the command: " + e.getMessage()); + return Result.FAILED; + } finally { + try { + if (session != null) session.close(); + if (ssh != null) ssh.disconnect(); + } catch (IOException e) { + logger.info("Error closing SSHJ resources: " + e.getMessage()); + } + } + } + + private static String wrapInLoginShell(String command) { + String escaped = command.replace("'", "'\\''"); + return "bash -l -c '" + escaped + "'"; + } +} diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/ios/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/ios/SSHCommandExecutionSSHJ.java new file mode 100644 index 00000000..5afa03ed --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/ios/SSHCommandExecutionSSHJ.java @@ -0,0 +1,149 @@ +package com.testsigma.addons.ios; + +import com.testsigma.addons.util.ImageComparisonUtils; +import com.testsigma.addons.util.StringToImageConverter; +import com.testsigma.sdk.ApplicationType; +import com.testsigma.sdk.IOSAction; +import com.testsigma.sdk.Result; +import com.testsigma.sdk.annotation.Action; +import com.testsigma.sdk.annotation.RunTimeData; +import com.testsigma.sdk.annotation.TestData; +import com.testsigma.sdk.annotation.TestStepResult; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.Session.Command; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.openqa.selenium.NoSuchElementException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.concurrent.TimeUnit; + +@Data +@EqualsAndHashCode(callSuper = false) +@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name," + + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ," + + " Command Separator: Command-Separator , Store Output Variable: Variable-Name", + description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). " + + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable.", + applicationType = ApplicationType.IOS, + displayName = "SSHJ: Connect to SSH server and execute commands", + useCustomScreenshot = true) +public class SSHCommandExecutionSSHJ extends IOSAction { + + @TestStepResult + private com.testsigma.sdk.TestStepResult testStepResult; + + @TestData(reference = "Host-Name") + private com.testsigma.sdk.TestData hostName; + @TestData(reference = "Port-Number") + private com.testsigma.sdk.TestData portNumber; + @TestData(reference = "User-Name") + private com.testsigma.sdk.TestData userName; + @TestData(reference = "User-Password") + private com.testsigma.sdk.TestData userPassword; + @TestData(reference = "Terminal-Commands") + private com.testsigma.sdk.TestData commands; + @TestData(reference = "Command-Separator") + private com.testsigma.sdk.TestData commandSeparator; + @TestData(reference = "Variable-Name") + private com.testsigma.sdk.TestData storeVariable; + + @RunTimeData + private com.testsigma.sdk.RunTimeData runTimeData; + + @Override + public Result execute() throws NoSuchElementException { + logger.info("Initiating execution (SSHJ) - iOS"); + + String user = userName.getValue().toString(); + String host = hostName.getValue().toString(); + String password = userPassword.getValue().toString(); + int port = Integer.parseInt(portNumber.getValue().toString()); + String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty() + ? commandSeparator.getValue().toString() + : "&&"; + String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&"); + + String wrappedCommand = wrapInLoginShell(allCommands); + + SSHClient ssh = null; + Session session = null; + + try { + ssh = new SSHClient(); + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + ssh.connect(host, port); + ssh.authPassword(user, password); + logger.info("SSH session connected (SSHJ)."); + + session = ssh.startSession(); + Command cmd = session.exec(wrappedCommand); + StringBuilder output = new StringBuilder(); + try (Reader reader = new InputStreamReader(cmd.getInputStream())) { + char[] buf = new char[1024]; + int n; + while ((n = reader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + cmd.join(30, TimeUnit.SECONDS); + if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) { + try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) { + char[] buf = new char[1024]; + int n; + while ((n = errReader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + } + + runTimeData.setKey(storeVariable.getValue().toString()); + runTimeData.setValue(output.toString()); + + File screenshotFile = null; + try { + screenshotFile = StringToImageConverter.convertToFile(output); + String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null; + if (s3Url != null && !s3Url.isEmpty()) { + ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger); + boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath()); + if (!uploadResult) { + logger.debug("Error uploading custom screenshot to S3; step result may not show the output image."); + } else { + logger.debug("Custom screenshot (command output) uploaded successfully."); + } + } + } finally { + if (screenshotFile != null && screenshotFile.exists()) { + screenshotFile.deleteOnExit(); + } + } + + setSuccessMessage("Output is: " + output.toString()); + return Result.SUCCESS; + } catch (Exception e) { + String errorStack = ExceptionUtils.getStackTrace(e); + logger.info("Error occurred while executing the command (SSHJ): " + errorStack); + setErrorMessage("Error occurred while executing the command: " + e.getMessage()); + return Result.FAILED; + } finally { + try { + if (session != null) session.close(); + if (ssh != null) ssh.disconnect(); + } catch (IOException e) { + logger.info("Error closing SSHJ resources: " + e.getMessage()); + } + } + } + + private static String wrapInLoginShell(String command) { + String escaped = command.replace("'", "'\\''"); + return "bash -l -c '" + escaped + "'"; + } +} diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/mobileWeb/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/mobileWeb/SSHCommandExecutionSSHJ.java new file mode 100644 index 00000000..79670de0 --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/mobileWeb/SSHCommandExecutionSSHJ.java @@ -0,0 +1,149 @@ +package com.testsigma.addons.mobileWeb; + +import com.testsigma.addons.util.ImageComparisonUtils; +import com.testsigma.addons.util.StringToImageConverter; +import com.testsigma.sdk.ApplicationType; +import com.testsigma.sdk.Result; +import com.testsigma.sdk.WebAction; +import com.testsigma.sdk.annotation.Action; +import com.testsigma.sdk.annotation.RunTimeData; +import com.testsigma.sdk.annotation.TestData; +import com.testsigma.sdk.annotation.TestStepResult; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.Session.Command; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.openqa.selenium.NoSuchElementException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.concurrent.TimeUnit; + +@Data +@EqualsAndHashCode(callSuper = false) +@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name," + + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ," + + " Command Separator: Command-Separator , Store Output Variable: Variable-Name", + description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). " + + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable.", + applicationType = ApplicationType.MOBILE_WEB, + displayName = "SSHJ: Connect to SSH server and execute commands", + useCustomScreenshot = true) +public class SSHCommandExecutionSSHJ extends WebAction { + + @TestStepResult + private com.testsigma.sdk.TestStepResult testStepResult; + + @TestData(reference = "Host-Name") + private com.testsigma.sdk.TestData hostName; + @TestData(reference = "Port-Number") + private com.testsigma.sdk.TestData portNumber; + @TestData(reference = "User-Name") + private com.testsigma.sdk.TestData userName; + @TestData(reference = "User-Password") + private com.testsigma.sdk.TestData userPassword; + @TestData(reference = "Terminal-Commands") + private com.testsigma.sdk.TestData commands; + @TestData(reference = "Command-Separator") + private com.testsigma.sdk.TestData commandSeparator; + @TestData(reference = "Variable-Name") + private com.testsigma.sdk.TestData storeVariable; + + @RunTimeData + private com.testsigma.sdk.RunTimeData runTimeData; + + @Override + public Result execute() throws NoSuchElementException { + logger.info("Initiating execution (SSHJ) - Mobile Web"); + + String user = userName.getValue().toString(); + String host = hostName.getValue().toString(); + String password = userPassword.getValue().toString(); + int port = Integer.parseInt(portNumber.getValue().toString()); + String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty() + ? commandSeparator.getValue().toString() + : "&&"; + String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&"); + + String wrappedCommand = wrapInLoginShell(allCommands); + + SSHClient ssh = null; + Session session = null; + + try { + ssh = new SSHClient(); + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + ssh.connect(host, port); + ssh.authPassword(user, password); + logger.info("SSH session connected (SSHJ)."); + + session = ssh.startSession(); + Command cmd = session.exec(wrappedCommand); + StringBuilder output = new StringBuilder(); + try (Reader reader = new InputStreamReader(cmd.getInputStream())) { + char[] buf = new char[1024]; + int n; + while ((n = reader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + cmd.join(30, TimeUnit.SECONDS); + if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) { + try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) { + char[] buf = new char[1024]; + int n; + while ((n = errReader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + } + + runTimeData.setKey(storeVariable.getValue().toString()); + runTimeData.setValue(output.toString()); + + File screenshotFile = null; + try { + screenshotFile = StringToImageConverter.convertToFile(output); + String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null; + if (s3Url != null && !s3Url.isEmpty()) { + ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger); + boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath()); + if (!uploadResult) { + logger.debug("Error uploading custom screenshot to S3; step result may not show the output image."); + } else { + logger.debug("Custom screenshot (command output) uploaded successfully."); + } + } + } finally { + if (screenshotFile != null && screenshotFile.exists()) { + screenshotFile.deleteOnExit(); + } + } + + setSuccessMessage("Output is: " + output.toString()); + return Result.SUCCESS; + } catch (Exception e) { + String errorStack = ExceptionUtils.getStackTrace(e); + logger.info("Error occurred while executing the command (SSHJ): " + errorStack); + setErrorMessage("Error occurred while executing the command: " + e.getMessage()); + return Result.FAILED; + } finally { + try { + if (session != null) session.close(); + if (ssh != null) ssh.disconnect(); + } catch (IOException e) { + logger.info("Error closing SSHJ resources: " + e.getMessage()); + } + } + } + + private static String wrapInLoginShell(String command) { + String escaped = command.replace("'", "'\\''"); + return "bash -l -c '" + escaped + "'"; + } +} diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/salesforce/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/salesforce/SSHCommandExecutionSSHJ.java new file mode 100644 index 00000000..32712c8c --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/salesforce/SSHCommandExecutionSSHJ.java @@ -0,0 +1,150 @@ +package com.testsigma.addons.salesforce; + +import com.testsigma.addons.util.ImageComparisonUtils; +import com.testsigma.addons.util.StringToImageConverter; +import com.testsigma.sdk.ApplicationType; +import com.testsigma.sdk.Result; +import com.testsigma.sdk.WebAction; +import com.testsigma.sdk.annotation.Action; +import com.testsigma.sdk.annotation.RunTimeData; +import com.testsigma.sdk.annotation.TestData; +import com.testsigma.sdk.annotation.TestStepResult; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.Session.Command; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.openqa.selenium.NoSuchElementException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.concurrent.TimeUnit; + +@Data +@EqualsAndHashCode(callSuper = false) +@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name," + + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ," + + " Command Separator: Command-Separator , Store Output Variable: Variable-Name", + description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). " + + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable. " + + "For use in Salesforce test plans (applicationType WEB).", + applicationType = ApplicationType.WEB, + displayName = "SSHJ: Connect to SSH server and execute commands (Salesforce)", + useCustomScreenshot = true) +public class SSHCommandExecutionSSHJ extends WebAction { + + @TestStepResult + private com.testsigma.sdk.TestStepResult testStepResult; + + @TestData(reference = "Host-Name") + private com.testsigma.sdk.TestData hostName; + @TestData(reference = "Port-Number") + private com.testsigma.sdk.TestData portNumber; + @TestData(reference = "User-Name") + private com.testsigma.sdk.TestData userName; + @TestData(reference = "User-Password") + private com.testsigma.sdk.TestData userPassword; + @TestData(reference = "Terminal-Commands") + private com.testsigma.sdk.TestData commands; + @TestData(reference = "Command-Separator") + private com.testsigma.sdk.TestData commandSeparator; + @TestData(reference = "Variable-Name") + private com.testsigma.sdk.TestData storeVariable; + + @RunTimeData + private com.testsigma.sdk.RunTimeData runTimeData; + + @Override + public Result execute() throws NoSuchElementException { + logger.info("Initiating execution (SSHJ) - Salesforce"); + + String user = userName.getValue().toString(); + String host = hostName.getValue().toString(); + String password = userPassword.getValue().toString(); + int port = Integer.parseInt(portNumber.getValue().toString()); + String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty() + ? commandSeparator.getValue().toString() + : "&&"; + String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&"); + + String wrappedCommand = wrapInLoginShell(allCommands); + + SSHClient ssh = null; + Session session = null; + + try { + ssh = new SSHClient(); + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + ssh.connect(host, port); + ssh.authPassword(user, password); + logger.info("SSH session connected (SSHJ)."); + + session = ssh.startSession(); + Command cmd = session.exec(wrappedCommand); + StringBuilder output = new StringBuilder(); + try (Reader reader = new InputStreamReader(cmd.getInputStream())) { + char[] buf = new char[1024]; + int n; + while ((n = reader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + cmd.join(30, TimeUnit.SECONDS); + if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) { + try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) { + char[] buf = new char[1024]; + int n; + while ((n = errReader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + } + + runTimeData.setKey(storeVariable.getValue().toString()); + runTimeData.setValue(output.toString()); + + File screenshotFile = null; + try { + screenshotFile = StringToImageConverter.convertToFile(output); + String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null; + if (s3Url != null && !s3Url.isEmpty()) { + ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger); + boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath()); + if (!uploadResult) { + logger.debug("Error uploading custom screenshot to S3; step result may not show the output image."); + } else { + logger.debug("Custom screenshot (command output) uploaded successfully."); + } + } + } finally { + if (screenshotFile != null && screenshotFile.exists()) { + screenshotFile.deleteOnExit(); + } + } + + setSuccessMessage("Output is: " + output.toString()); + return Result.SUCCESS; + } catch (Exception e) { + String errorStack = ExceptionUtils.getStackTrace(e); + logger.info("Error occurred while executing the command (SSHJ): " + errorStack); + setErrorMessage("Error occurred while executing the command: " + e.getMessage()); + return Result.FAILED; + } finally { + try { + if (session != null) session.close(); + if (ssh != null) ssh.disconnect(); + } catch (IOException e) { + logger.info("Error closing SSHJ resources: " + e.getMessage()); + } + } + } + + private static String wrapInLoginShell(String command) { + String escaped = command.replace("'", "'\\''"); + return "bash -l -c '" + escaped + "'"; + } +} diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/ImageComparisonUtils.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/ImageComparisonUtils.java new file mode 100644 index 00000000..8e7e4f9f --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/ImageComparisonUtils.java @@ -0,0 +1,56 @@ +package com.testsigma.addons.util; + +import com.testsigma.sdk.Logger; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.entity.EntityBuilder; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.openqa.selenium.WebDriver; + +import java.io.File; + +public class ImageComparisonUtils { + WebDriver driver; + Logger logger; + + public ImageComparisonUtils(WebDriver driver, Logger logger) { + this.driver = driver; + this.logger = logger; + } + + RequestConfig config = RequestConfig.custom() + .setSocketTimeout(10 * 60 * 1000) + .setConnectionRequestTimeout(60 * 1000) + .setConnectTimeout(60 * 1000) + .build(); + + public boolean uploadFile(String s3SignedURL, String localPath) { + logger.debug("s3SignedURL - " + s3SignedURL); + logger.debug("localPath - " + localPath); + boolean localUrlExists = new File(localPath).exists(); + if (localUrlExists) { + logger.info(String.format("Uploading test asset to storage, presigned-URL:%s, localFilePath:%s", s3SignedURL, localPath)); + try (CloseableHttpClient httpclient = HttpClients.custom().setDefaultRequestConfig(config).build()) { + HttpPut httpPut = new HttpPut(s3SignedURL); + + File file = new File(localPath); + HttpEntity entity = EntityBuilder.create().setFile(file).build(); + httpPut.setEntity(entity); + HttpResponse response = httpclient.execute(httpPut); + logger.info("Upload completed"); + return true; + } catch (Exception e) { + logger.info("Exception while uploading custom screenshot to s3: " + ExceptionUtils.getStackTrace(e)); + return false; + } + } else { + logger.info("Local path does not exist"); + return false; + } + } + +} diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/StringToImageConverter.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/StringToImageConverter.java new file mode 100644 index 00000000..bde8eeee --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/StringToImageConverter.java @@ -0,0 +1,153 @@ +package com.testsigma.addons.util; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Converts a string into an image with white background and the text drawn on it. + * Font size is chosen based on the input text length (shorter text = larger font). + */ +public final class StringToImageConverter { + + private static final int MIN_FONT_SIZE = 12; + private static final int MAX_FONT_SIZE = 72; + private static final int PADDING = 40; + private static final int MIN_IMAGE_WIDTH = 200; + private static final int MAX_IMAGE_WIDTH = 1200; + private static final String FONT_NAME = Font.SANS_SERIF; + + private StringToImageConverter() { + } + + /** + * Converts the given text into a BufferedImage with white background and black text. + * Font size is derived from text length. + * + * @param text the string to render (can be null or empty; empty yields a small placeholder image) + * @return BufferedImage with white background and text + */ + public static BufferedImage convert(String text) { + String safeText = text == null ? "" : text; + int fontSize = computeFontSize(safeText.length()); + Font font = new Font(FONT_NAME, Font.PLAIN, fontSize); + + FontMetrics fm = getFontMetrics(font); + List lines = wrapLines(safeText, fm, MAX_IMAGE_WIDTH - 2 * PADDING); + int lineHeight = fm.getHeight(); + int ascent = fm.getAscent(); + int imageWidth = Math.max(MIN_IMAGE_WIDTH, computeTextWidth(lines, fm) + 2 * PADDING); + int imageHeight = Math.max(40, lines.size() * lineHeight + 2 * PADDING); + + BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setColor(Color.WHITE); + g.fillRect(0, 0, imageWidth, imageHeight); + g.setColor(Color.BLACK); + g.setFont(font); + + int y = PADDING + ascent; + for (String line : lines) { + g.drawString(line, PADDING, y); + y += lineHeight; + } + } finally { + g.dispose(); + } + return image; + } + + /** + * Converts the given text to an image and writes it to a temporary PNG file. + * + * @param text the string to render (can be CharSequence e.g. StringBuilder to avoid extra copy) + * @return temporary File containing the PNG image + * @throws IOException if writing the file fails + */ + public static File convertToFile(CharSequence text) throws IOException { + BufferedImage image = convert(text == null ? null : text.toString()); + File tempFile = File.createTempFile("testsigma-text-image-", ".png"); + ImageIO.write(image, "png", tempFile); + return tempFile; + } + + private static int computeFontSize(int textLength) { + if (textLength <= 0) return MAX_FONT_SIZE; + if (textLength <= 15) return MAX_FONT_SIZE; + if (textLength <= 50) return 48; + if (textLength <= 150) return 32; + if (textLength <= 400) return 24; + if (textLength <= 800) return 18; + return Math.max(MIN_FONT_SIZE, 14); + } + + private static FontMetrics getFontMetrics(Font font) { + BufferedImage dummy = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + Graphics2D g = dummy.createGraphics(); + g.setFont(font); + FontMetrics fm = g.getFontMetrics(); + g.dispose(); + return fm; + } + + private static List wrapLines(String text, FontMetrics fm, int maxWidth) { + List lines = new ArrayList<>(); + if (text.isEmpty()) { + lines.add(" "); + return lines; + } + String[] paragraphs = text.split("\\n", -1); + for (String para : paragraphs) { + if (para.isEmpty()) { + lines.add(" "); + continue; + } + StringBuilder line = new StringBuilder(); + for (String word : para.split(" ", -1)) { + String candidate = line.length() == 0 ? word : line + " " + word; + if (fm.stringWidth(candidate) <= maxWidth) { + line.setLength(0); + line.append(candidate); + } else { + if (line.length() > 0) { + lines.add(line.toString()); + line.setLength(0); + } + if (fm.stringWidth(word) <= maxWidth) { + line.append(word); + } else { + for (int i = 0; i < word.length(); ) { + int fit = 0; + int maxFit = word.length() - i; + while (fit < maxFit && fm.stringWidth(word.substring(i, i + fit + 1)) <= maxWidth) { + fit++; + } + if (fit == 0) fit = 1; + lines.add(word.substring(i, i + fit)); + i += fit; + } + } + } + } + if (line.length() > 0) { + lines.add(line.toString()); + } + } + return lines.isEmpty() ? List.of(" ") : lines; + } + + private static int computeTextWidth(List lines, FontMetrics fm) { + int max = 0; + for (String line : lines) { + max = Math.max(max, fm.stringWidth(line)); + } + return max; + } +} diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/web/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/web/SSHCommandExecutionSSHJ.java new file mode 100644 index 00000000..ee16a69d --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/web/SSHCommandExecutionSSHJ.java @@ -0,0 +1,154 @@ +package com.testsigma.addons.web; + +import com.testsigma.addons.util.ImageComparisonUtils; +import com.testsigma.addons.util.StringToImageConverter; +import com.testsigma.sdk.ApplicationType; +import com.testsigma.sdk.Result; +import com.testsigma.sdk.WebAction; +import com.testsigma.sdk.annotation.Action; +import com.testsigma.sdk.annotation.RunTimeData; +import com.testsigma.sdk.annotation.TestData; +import com.testsigma.sdk.annotation.TestStepResult; +import lombok.Data; + +import java.io.File; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; +import net.schmizz.sshj.connection.channel.direct.Session.Command; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.openqa.selenium.NoSuchElementException; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.concurrent.TimeUnit; + +@Data +@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name," + + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ," + + " Command Separator: Command-Separator , Store Output Variable: Variable-Name", + description = "Executes commands on an SSH server using the SSHJ library (alternative implementation)", + applicationType = ApplicationType.WEB, + useCustomScreenshot = true) +public class SSHCommandExecutionSSHJ extends WebAction { + + @TestStepResult + private com.testsigma.sdk.TestStepResult testStepResult; + + @TestData(reference = "Host-Name") + private com.testsigma.sdk.TestData hostName; + @TestData(reference = "Port-Number") + private com.testsigma.sdk.TestData portNumber; + @TestData(reference = "User-Name") + private com.testsigma.sdk.TestData userName; + @TestData(reference = "User-Password") + private com.testsigma.sdk.TestData userPassword; + @TestData(reference = "Terminal-Commands") + private com.testsigma.sdk.TestData commands; + @TestData(reference = "Command-Separator") + private com.testsigma.sdk.TestData commandSeparator; + @TestData(reference = "Variable-Name") + private com.testsigma.sdk.TestData storeVariable; + + @RunTimeData + private com.testsigma.sdk.RunTimeData runTimeData; + + @Override + public com.testsigma.sdk.Result execute() throws NoSuchElementException { + logger.info("Initiating execution (SSHJ)"); + + String user = userName.getValue().toString(); + String host = hostName.getValue().toString(); + String password = userPassword.getValue().toString(); + int port = Integer.parseInt(portNumber.getValue().toString()); + String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty() + ? commandSeparator.getValue().toString() + : "&&"; + String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&"); + + // Run in a login shell so PATH and env from /etc/profile, ~/.profile are loaded. + // Otherwise exec() uses a minimal shell (e.g. sh) and commands like "smsd" are not found. + String wrappedCommand = wrapInLoginShell(allCommands); + + SSHClient ssh = null; + Session session = null; + + try { + ssh = new SSHClient(); + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + ssh.connect(host, port); + ssh.authPassword(user, password); + logger.info("SSH session connected (SSHJ)."); + + session = ssh.startSession(); + Command cmd = session.exec(wrappedCommand); + StringBuilder output = new StringBuilder(); + try (Reader reader = new InputStreamReader(cmd.getInputStream())) { + char[] buf = new char[1024]; + int n; + while ((n = reader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + cmd.join(30, TimeUnit.SECONDS); + if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) { + try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) { + char[] buf = new char[1024]; + int n; + while ((n = errReader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + } + + runTimeData.setKey(storeVariable.getValue().toString()); + runTimeData.setValue(output.toString()); + + // Display output as custom screenshot in step result (same as DisplayTestDataAsStepResultScreenShot) + File screenshotFile = null; + try { + screenshotFile = StringToImageConverter.convertToFile(output); + String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null; + if (s3Url != null && !s3Url.isEmpty()) { + ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger); + boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath()); + if (!uploadResult) { + logger.debug("Error uploading custom screenshot to S3; step result may not show the output image."); + } else { + logger.debug("Custom screenshot (command output) uploaded successfully."); + } + } + } finally { + if (screenshotFile != null && screenshotFile.exists()) { + screenshotFile.deleteOnExit(); + } + } + + setSuccessMessage("Output is: " + output.toString()); + return Result.SUCCESS; + } catch (Exception e) { + String errorStack = ExceptionUtils.getStackTrace(e); + logger.info("Error occurred while executing the command (SSHJ): " + errorStack); + setErrorMessage("Error occurred while executing the command: " + e.getMessage()); + return Result.FAILED; + } finally { + try { + if (session != null) session.close(); + if (ssh != null) ssh.disconnect(); + } catch (IOException e) { + logger.info("Error closing SSHJ resources: " + e.getMessage()); + } + } + } + + /** + * Wraps the command in a login shell (bash -l -c '...') so that profile scripts + * are sourced and PATH includes /usr/local/bin, custom paths, etc. Without this, + * session.exec() runs in a minimal non-login shell where commands like "smsd" may not be found. + */ + private static String wrapInLoginShell(String command) { + String escaped = command.replace("'", "'\\''"); + return "bash -l -c '" + escaped + "'"; + } +} diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/windowsAdvanced/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/windowsAdvanced/SSHCommandExecutionSSHJ.java new file mode 100644 index 00000000..8207975a --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/windowsAdvanced/SSHCommandExecutionSSHJ.java @@ -0,0 +1,152 @@ +package com.testsigma.addons.windowsAdvanced; + +import com.testsigma.addons.util.ImageComparisonUtils; +import com.testsigma.addons.util.StringToImageConverter; +import com.testsigma.sdk.Result; +import com.testsigma.sdk.WindowsAdvancedAction; +import com.testsigma.sdk.annotation.Action; +import com.testsigma.sdk.annotation.RunTimeData; +import com.testsigma.sdk.annotation.TestData; +import com.testsigma.sdk.annotation.TestStepResult; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.Session.Command; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.concurrent.TimeUnit; + +@Data +@EqualsAndHashCode(callSuper = false) +@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name," + + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ," + + " Command Separator: Command-Separator , Store Output Variable: Variable-Name", + description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). " + + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable.", + applicationType = com.testsigma.sdk.ApplicationType.WINDOWS_ADVANCED, + displayName = "SSHJ: Connect to SSH server and execute commands", + useCustomScreenshot = true) +public class SSHCommandExecutionSSHJ extends WindowsAdvancedAction { + + @TestStepResult + private com.testsigma.sdk.TestStepResult testStepResult; + + @TestData(reference = "Host-Name") + private com.testsigma.sdk.TestData hostName; + @TestData(reference = "Port-Number") + private com.testsigma.sdk.TestData portNumber; + @TestData(reference = "User-Name") + private com.testsigma.sdk.TestData userName; + @TestData(reference = "User-Password") + private com.testsigma.sdk.TestData userPassword; + @TestData(reference = "Terminal-Commands") + private com.testsigma.sdk.TestData commands; + @TestData(reference = "Command-Separator") + private com.testsigma.sdk.TestData commandSeparator; + @TestData(reference = "Variable-Name") + private com.testsigma.sdk.TestData storeVariable; + + @RunTimeData + private com.testsigma.sdk.RunTimeData runTimeData; + + @Override + protected Result execute() { + logger.info("Initiating execution (SSHJ) - Windows Advanced"); + + String user = userName.getValue().toString(); + String host = hostName.getValue().toString(); + String password = userPassword.getValue().toString(); + int port = Integer.parseInt(portNumber.getValue().toString()); + String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty() + ? commandSeparator.getValue().toString() + : "&&"; + String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&"); + + String wrappedCommand = wrapInLoginShell(allCommands); + + SSHClient ssh = null; + Session session = null; + + try { + ssh = new SSHClient(); + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + ssh.connect(host, port); + ssh.authPassword(user, password); + logger.info("SSH session connected (SSHJ)."); + + session = ssh.startSession(); + Command cmd = session.exec(wrappedCommand); + StringBuilder output = new StringBuilder(); + try (Reader reader = new InputStreamReader(cmd.getInputStream())) { + char[] buf = new char[1024]; + int n; + while ((n = reader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + cmd.join(30, TimeUnit.SECONDS); + if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) { + try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) { + char[] buf = new char[1024]; + int n; + while ((n = errReader.read(buf)) != -1) { + output.append(buf, 0, n); + } + } + } + + runTimeData.setKey(storeVariable.getValue().toString()); + runTimeData.setValue(output.toString()); + + // Display output as custom screenshot in step result (same as web action) + File screenshotFile = null; + try { + screenshotFile = StringToImageConverter.convertToFile(output); + String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null; + if (s3Url != null && !s3Url.isEmpty()) { + ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(null, logger); + boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath()); + if (!uploadResult) { + logger.debug("Error uploading custom screenshot to S3; step result may not show the output image."); + } else { + logger.debug("Custom screenshot (command output) uploaded successfully."); + } + } + } finally { + if (screenshotFile != null && screenshotFile.exists()) { + screenshotFile.deleteOnExit(); + } + } + + setSuccessMessage("Output is: " + output.toString()); + return Result.SUCCESS; + } catch (Exception e) { + String errorStack = ExceptionUtils.getStackTrace(e); + logger.info("Error occurred while executing the command (SSHJ): " + errorStack); + setErrorMessage("Error occurred while executing the command: " + e.getMessage()); + return Result.FAILED; + } finally { + try { + if (session != null) session.close(); + if (ssh != null) ssh.disconnect(); + } catch (IOException e) { + logger.info("Error closing SSHJ resources: " + e.getMessage()); + } + } + } + + /** + * Wraps the command in a login shell (bash -l -c '...') so that profile scripts + * are sourced and PATH includes /usr/local/bin, custom paths, etc. + */ + private static String wrapInLoginShell(String command) { + String escaped = command.replace("'", "'\\''"); + return "bash -l -c '" + escaped + "'"; + } +} diff --git a/ssh_shell_command_executor_using_sshj/src/main/resources/testsigma-sdk.properties b/ssh_shell_command_executor_using_sshj/src/main/resources/testsigma-sdk.properties new file mode 100644 index 00000000..99da96ff --- /dev/null +++ b/ssh_shell_command_executor_using_sshj/src/main/resources/testsigma-sdk.properties @@ -0,0 +1 @@ +testsigma-sdk.api.key=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIyNDBkMjhiNS0xNmUwLThlNmYtOWQ0ZS05MjYxMGNiZTcyYzciLCJ1bmlxdWVJZCI6IjU5NzgiLCJpZGVudGl0eUFjY291bnRVVUlkIjoiNDMifQ.eX8QdtK6wAKQJ2rI3Em8VbztUA2m06VFbSLJ35vsNsiljS7yrqGjIZ3TjxdXL3dvscC-ZcU21ugKIjqyWmstkQ \ No newline at end of file